From de6a4944e6134c6758745fc54c8cb8e0eab28ebf Mon Sep 17 00:00:00 2001 From: MrHua269 Date: Thu, 9 Jul 2026 00:03:09 +0800 Subject: [PATCH] Initial Commit --- .gitattributes | 11 + .gitignore | 17 + build.gradle.kts | 108 + camellia-api/build.gradle.kts.patch | 40 + .../features/0001-Rebrand-to-Camellia.patch | 28 + ...isable-timings-warn-msg-and-commands.patch | 45 + ...Force-disable-reload-command-and-api.patch | 23 + .../features/0004-KioCG-Chunk-API.patch | 28 + ...05-Leaves-Fix-SculkCatalyst-exp-skip.patch | 86 + .../0006-Pufferfish-SIMD-Utilities.patch | 33 + ...07-Purpur-Lobotomize-stuck-villagers.patch | 27 + .../pufferfish/simd/SIMDChecker.java | 53 + .../pufferfish/simd/SIMDDetection.java | 53 + .../pufferfish/simd/VectorMapPalette.java | 101 + .../api/config/CamelliaConfigBuilder.java | 82 + .../api/config/CamelliaConfigsInstance.java | 203 ++ .../camellia/api/config/ConfigDataPair.java | 11 + camellia-server/build.gradle.kts.patch | 148 + .../0001-Config-system-framework.patch | 42 + ...Configurable-region-format-framework.patch | 388 +++ .../0003-Correct-player-respawn-place.patch | 54 + ...rrect-CAS-get-in-RegionizedTaskQueue.patch | 19 + ...Correct-thread-unsafe-random-sources.patch | 32 + ...ogic-of-some-projectile-entities-Sho.patch | 59 + ...o-not-enable-any-debug-subscriptions.patch | 20 + ...oo-far-poi-chunk-in-poi-compete-scan.patch | 32 + ...009-Entity-portal-teleport-speed-fix.patch | 67 + ...threading-with-entity-ai-data-access.patch | 289 ++ .../0011-Fix-off-region-leashing.patch | 35 + ...sk-returning-of-ServerConfigurationP.patch | 30 + .../0013-Fix-riding-statistics-desync.patch | 64 + ...-player-auto-saving-ignores-interval.patch | 19 + .../0015-Fix-dragon-part-desync.patch | 47 + ...nder-pearls-when-player-switched-dim.patch | 19 + .../0017-Fix-creative-item-picking.patch | 19 + .../0018-Fix-high-velocity-moving-issue.patch | 63 + ...-and-pitch-were-ignored-during-telep.patch | 22 + ...0-Force-disable-builtin-spark-plugin.patch | 98 + ...animals-check-can-teleport-in-an-unl.patch | 24 + ...iiju-Entity-tick-and-removal-limiter.patch | 55 + ...iju-Vanilla-end-portal-teleportation.patch | 81 + ...sync-protocol-switching-optimization.patch | 288 ++ ...g-to-support-for-long-command-inputs.patch | 21 + .../0026-Add-config-for-server-mod-name.patch | 19 + .../0027-Add-config-for-cpu-affinity.patch | 19 + ...-Add-config-for-unsafe-teleportation.patch | 45 + ...0029-Add-config-for-watchdog-timeout.patch | 19 + ...nfig-to-disable-entity-tick-catchers.patch | 18 + ...031-Add-config-for-heightmap-warning.patch | 18 + .../0032-Add-config-gui-support.patch | 19 + ...he-data-command-to-be-enabled-config.patch | 21 + ...-config-for-command-block-command-ex.patch | 19 + ...d-config-to-modify-tripwire-behavior.patch | 116 + .../0036-Add-config-for-item-multitask.patch | 39 + ...37-Add-config-to-enable-tick-command.patch | 104 + ...g-for-whether-to-save-portal-tickets.patch | 19 + .../0039-Add-portal-rate-limiter.patch | 130 + .../features/0040-Add-status-bar.patch | 348 +++ .../features/0041-Leaves-Vanilla-Hopper.patch | 94 + ...Leaves-Lithium-Sleeping-Block-Entity.patch | 2739 +++++++++++++++++ .../0043-Petal-Reduce-sensor-work.patch | 28 + ...44-Purpur-Lobotomize-stuck-villagers.patch | 89 + ...fish-Reduce-projectile-chunk-loading.patch | 68 + ...tle-goal-selector-during-inactive-ti.patch | 30 + ...-Secure-seed-and-matter-seed-command.patch | 423 +++ .../0048-Secure-seed-V2-with-Blake3.patch | 70 + ...brain-maps-with-optimized-collection.patch | 34 + ...ess-creating-stats-json-bases-on-pla.patch | 54 + ...imize-PatchedDataComponentMap-equals.patch | 36 + ...er-checking-for-useless-move-packets.patch | 53 + .../0053-Leaf-fast-bit-radix-sort.patch | 70 + ...-Configurable-vanilla-username-check.patch | 203 ++ ...acket-limit-Camellia-Option-to-full-.patch | 61 + ...aves-Configurable-collision-behavior.patch | 56 + ...0057-Leaves-Optimized-dragon-respawn.patch | 95 + ...ale-Variable-entity-wake-up-duration.patch | 57 + ...attributes-with-optimized-collection.patch | 30 + ...Skip-entity-move-if-movement-is-zero.patch | 42 + ...61-Gale-Store-mob-counts-in-an-array.patch | 36 + ...timize-random-calls-in-chunk-ticking.patch | 113 + ...duce-enderman-teleport-chunk-lookups.patch | 58 + .../0064-Gale-Cache-ShapePairKey-hash.patch | 45 + ...on-check-has-physics-before-same-veh.patch | 35 + ...gible-planar-movement-multiplication.patch | 32 + ...7-Gale-Optimize-matching-item-checks.patch | 26 + ...da-and-Optional-allocation-in-Entity.patch | 42 + .../0069-Gale-Reduce-array-allocations.patch | 441 +++ ...0070-Some-optimizations-from-krypton.patch | 80 + .../features/0001-Rebrand-to-Camellia.patch | 115 + ...Correct-thread-unsafe-random-sources.patch | 19 + ...Force-disable-reload-command-and-api.patch | 18 + ...4-Force-disable-builtin-spark-plugin.patch | 59 + .../0005-Add-config-for-server-mod-name.patch | 19 + ...Add-config-to-disable-async-catchers.patch | 94 + ...ig-to-avoid-of-folia-supported-check.patch | 48 + .../0008-Add-config-for-cpu-affinity.patch | 28 + .../features/0009-Add-status-bar.patch | 39 + ...ockPos-transform-to-Position-support.patch | 23 + ...11-Purpur-Lobotomize-stuck-villagers.patch | 25 + ...0012-Leaf-Skip-event-if-no-listeners.patch | 33 + ...-Secure-seed-and-matter-seed-command.patch | 43 + ...-Configurable-vanilla-username-check.patch | 82 + .../java/abomination/LinearRegionFile.java | 621 ++++ .../src/main/java/com/kiocg/ChunkHot.java | 90 + .../kaiijumc/kaiiju/KaiijuEntityLimits.java | 158 + .../kaiiju/KaiijuEntityThrottler.java | 101 + .../java/me/titaniumtown/ArrayConstants.java | 21 + .../camellia/commands/CommandRegister.java | 14 + .../camellia/commands/bar/BarCommand.java | 52 + .../camellia/commands/bar/BarSubcommand.java | 35 + .../commands/bar/sub/ConfigEditCommand.java | 74 + .../commands/bar/sub/ToggleCommand.java | 127 + .../commands/config/ConfigCommand.java | 37 + .../commands/config/ConfigSubcommand.java | 24 + .../commands/config/sub/CleanCommand.java | 69 + .../commands/config/sub/OpenGuiCommand.java | 83 + .../commands/config/sub/ReloadCommand.java | 25 + .../commands/config/sub/ResetCommand.java | 59 + .../config/sub/ResetCommentsCommand.java | 68 + .../commands/config/sub/SetCommand.java | 123 + .../commands/config/sub/SubmitCommand.java | 37 + .../camellia/config/ConfigManager.java | 145 + .../camellia/config/ConfigsInstance.java | 1065 +++++++ .../config/ConfigsInstanceBuilder.java | 82 + .../config/DefaultTransformLogic.java | 7 + .../camellia/config/IConfigModule.java | 27 + ...alFormatConversionExceptionWithOrigin.java | 24 + .../config/flags/CommandSuggestions.java | 9 + .../config/flags/ConfigClassInfo.java | 17 + .../camellia/config/flags/ConfigInfo.java | 15 + .../camellia/config/flags/DoNotLoad.java | 8 + .../config/flags/HotReloadUnsupported.java | 8 + .../config/flags/TransformedConfig.java | 28 + .../modules/experiment/CommandConfig.java | 25 + .../experiment/DisableAsyncCatcherConfig.java | 17 + .../experiment/DisableEntityCatchConfig.java | 15 + .../fixes/CollisionBehaviorConfig.java | 22 + .../fixes/FoliaEntityMovingFixConfig.java | 21 + .../ForceCleanupEntityBrainMemoryConfig.java | 18 + .../modules/fixes/ItemMultitaskConfig.java | 14 + .../fixes/LongCommandSupportConfig.java | 14 + .../config/modules/fixes/POIRangeFixes.java | 15 + .../modules/fixes/PathfindingFixesConfig.java | 14 + .../fixes/UnsafeTeleportationConfig.java | 15 + .../config/modules/function/MembarConfig.java | 50 + .../function/PortalRateLimiterConfig.java | 66 + .../modules/function/RegionBarConfig.java | 50 + .../modules/function/RegionFormatConfig.java | 81 + .../modules/function/SecureSeedConfig.java | 41 + .../config/modules/function/TpsBarConfig.java | 58 + .../function/TripwireBehaviorConfig.java | 21 + .../modules/misc/DisableWarningConfig.java | 14 + .../modules/misc/FoliaWatchogConfig.java | 12 + .../misc/PaperPacketLimiterConfig.java | 15 + .../modules/misc/SavePortalTicketsConfig.java | 14 + .../modules/misc/ServerModNameConfig.java | 15 + .../modules/misc/UsernameCheckConfig.java | 63 + .../AsyncProtocolChangeConfig.java | 15 + .../optimizations/CpuAffinityConfig.java | 122 + .../EntityGoalSelectorInactiveTickConfig.java | 15 + .../GaleVariableEntityWakeupConfig.java | 17 + .../KaiijuEntityLimiterConfig.java | 18 + .../LeavesSleepingBlockEntityConfig.java | 17 + .../LobotomizeVillageConfig.java | 16 + .../OptimizedDragonRespawnConfig.java | 12 + .../PetalReduceSensorWorkConfig.java | 14 + .../ProjectileChunkReduceConfig.java | 14 + .../modules/optimizations/SIMDConfig.java | 45 + .../config/modules/removed/RemovedConfig.java | 14 + .../DisableCheckForFoliaSupported.java | 14 + .../data/BufferedLinearRegionFile.java | 1441 +++++++++ .../camellia/data/RegionFile.java | 44 + .../camellia/enums/EnumBarType.java | 92 + .../enums/EnumCollisionBehaviorMode.java | 7 + .../camellia/enums/EnumConfigCategory.java | 33 + .../camellia/enums/EnumRegionFormat.java | 29 + .../camellia/enums/EnumStatusBarDisplay.java | 21 + .../camellia/enums/EnumTripwireBehavior.java | 7 + .../functions/bars/TickableStatusBar.java | 194 ++ .../functions/bars/TickableStatusBarList.java | 171 + .../camellia/functions/bars/impl/Membar.java | 110 + .../functions/bars/impl/RegionBar.java | 129 + .../camellia/functions/bars/impl/TpsBar.java | 167 + .../utils/AffinityRunnableWrapper.java | 34 + .../BufferedLinearRegionFileFlusher.java | 133 + .../camellia/utils/ClassLoadUtil.java | 97 + .../camellia/utils/NullPlugin.java | 152 + .../camellia/utils/RateThrottler.java | 87 + .../camellia/utils/RegionCreatorInfo.java | 8 + .../camellia/utils/RegionFileFactory.java | 10 + .../utils/dialog/ConfigCommandDialog.java | 117 + .../utils/dialog/ConfigDialogUtil.java | 55 + .../camellia/utils/dialog/DialogUtil.java | 472 +++ .../EntityMoveOutOfRegionException.java | 30 + .../dreeam/leaf/util/FastBitRadixSort.java | 104 + .../world/biome/PositionalBiomeGetter.java | 41 + .../leavesmc/leaves/command/ArgumentNode.java | 83 + .../leaves/command/CommandContext.java | 142 + .../leavesmc/leaves/command/CommandNode.java | 100 + .../leavesmc/leaves/command/CommandUtils.java | 149 + .../leavesmc/leaves/command/LiteralNode.java | 34 + .../org/leavesmc/leaves/command/RootNode.java | 73 + .../leaves/command/WrappedArgument.java | 85 + .../LithiumCooldownReceivingInventory.java | 47 + .../api/inventory/LithiumDefaultedList.java | 33 + .../api/inventory/LithiumInventory.java | 78 + .../LithiumTransferConditionInventory.java | 39 + .../SetBlockStateHandlingBlockEntity.java | 23 + .../entity/SetChangedHandlingBlockEntity.java | 23 + .../ShapeUpdateHandlingBlockBehaviour.java | 29 + .../SleepUntilTimeBlockEntityTickInvoker.java | 77 + .../block/entity/SleepingBlockEntity.java | 102 + .../InventoryChangeEmitter.java | 47 + .../InventoryChangeListener.java | 32 + .../InventoryChangeTracker.java | 34 + .../ComparatorTracker.java | 26 + .../ComparatorTracking.java | 63 + .../hopper/BlockStateOnlyInventory.java | 4 + .../hopper/ComparatorUpdatePattern.java | 114 + .../common/hopper/HopperCachingState.java | 29 + .../lithium/common/hopper/HopperHelper.java | 168 + .../common/hopper/InventoryHelper.java | 52 + .../common/hopper/LithiumDoubleInventory.java | 190 ++ .../common/hopper/LithiumDoubleStackList.java | 162 + .../common/hopper/LithiumStackList.java | 304 ++ .../lithium/common/hopper/UpdateReceiver.java | 28 + .../ChunkSectionEntityMovementListener.java | 5 + .../ChunkSectionEntityMovementTracker.java | 75 + .../ChunkSectionInventoryEntityTracker.java | 73 + ...ChunkSectionItemEntityMovementTracker.java | 73 + .../common/util/DirectionConstants.java | 33 + .../util/change_tracking/ChangePublisher.java | 34 + .../change_tracking/ChangeSubscriber.java | 211 ++ .../common/util/tuples/WorldSectionBox.java | 37 + .../lithium/common/world/WorldHelper.java | 29 + .../leaves/util/BlockPatternHelper.java | 46 + .../leaves/util/ItemOverstackUtils.java | 31 + gradle.properties | 17 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 251 ++ gradlew.bat | 94 + settings.gradle.kts | 57 + 243 files changed, 21326 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 build.gradle.kts create mode 100644 camellia-api/build.gradle.kts.patch create mode 100644 camellia-api/paper-patches/features/0001-Rebrand-to-Camellia.patch create mode 100644 camellia-api/paper-patches/features/0002-Disable-timings-warn-msg-and-commands.patch create mode 100644 camellia-api/paper-patches/features/0003-Force-disable-reload-command-and-api.patch create mode 100644 camellia-api/paper-patches/features/0004-KioCG-Chunk-API.patch create mode 100644 camellia-api/paper-patches/features/0005-Leaves-Fix-SculkCatalyst-exp-skip.patch create mode 100644 camellia-api/paper-patches/features/0006-Pufferfish-SIMD-Utilities.patch create mode 100644 camellia-api/paper-patches/features/0007-Purpur-Lobotomize-stuck-villagers.patch create mode 100644 camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java create mode 100644 camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java create mode 100644 camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java create mode 100644 camellia-api/src/main/java/moe/monnairealms/camellia/api/config/CamelliaConfigBuilder.java create mode 100644 camellia-api/src/main/java/moe/monnairealms/camellia/api/config/CamelliaConfigsInstance.java create mode 100644 camellia-api/src/main/java/moe/monnairealms/camellia/api/config/ConfigDataPair.java create mode 100644 camellia-server/build.gradle.kts.patch create mode 100644 camellia-server/minecraft-patches/features/0001-Config-system-framework.patch create mode 100644 camellia-server/minecraft-patches/features/0002-Configurable-region-format-framework.patch create mode 100644 camellia-server/minecraft-patches/features/0003-Correct-player-respawn-place.patch create mode 100644 camellia-server/minecraft-patches/features/0004-Correct-CAS-get-in-RegionizedTaskQueue.patch create mode 100644 camellia-server/minecraft-patches/features/0005-Correct-thread-unsafe-random-sources.patch create mode 100644 camellia-server/minecraft-patches/features/0006-Correct-portal-logic-of-some-projectile-entities-Sho.patch create mode 100644 camellia-server/minecraft-patches/features/0007-Do-not-enable-any-debug-subscriptions.patch create mode 100644 camellia-server/minecraft-patches/features/0008-Do-not-load-too-far-poi-chunk-in-poi-compete-scan.patch create mode 100644 camellia-server/minecraft-patches/features/0009-Entity-portal-teleport-speed-fix.patch create mode 100644 camellia-server/minecraft-patches/features/0010-Fix-region-threading-with-entity-ai-data-access.patch create mode 100644 camellia-server/minecraft-patches/features/0011-Fix-off-region-leashing.patch create mode 100644 camellia-server/minecraft-patches/features/0012-Fix-unpatched-task-returning-of-ServerConfigurationP.patch create mode 100644 camellia-server/minecraft-patches/features/0013-Fix-riding-statistics-desync.patch create mode 100644 camellia-server/minecraft-patches/features/0014-Fix-player-auto-saving-ignores-interval.patch create mode 100644 camellia-server/minecraft-patches/features/0015-Fix-dragon-part-desync.patch create mode 100644 camellia-server/minecraft-patches/features/0016-Fix-misbehaved-ender-pearls-when-player-switched-dim.patch create mode 100644 camellia-server/minecraft-patches/features/0017-Fix-creative-item-picking.patch create mode 100644 camellia-server/minecraft-patches/features/0018-Fix-high-velocity-moving-issue.patch create mode 100644 camellia-server/minecraft-patches/features/0019-Fix-teleport-yam-and-pitch-were-ignored-during-telep.patch create mode 100644 camellia-server/minecraft-patches/features/0020-Force-disable-builtin-spark-plugin.patch create mode 100644 camellia-server/minecraft-patches/features/0021-Prevent-tamable-animals-check-can-teleport-in-an-unl.patch create mode 100644 camellia-server/minecraft-patches/features/0022-Kaiiju-Entity-tick-and-removal-limiter.patch create mode 100644 camellia-server/minecraft-patches/features/0023-Kaiiju-Vanilla-end-portal-teleportation.patch create mode 100644 camellia-server/minecraft-patches/features/0024-Async-protocol-switching-optimization.patch create mode 100644 camellia-server/minecraft-patches/features/0025-Add-config-to-support-for-long-command-inputs.patch create mode 100644 camellia-server/minecraft-patches/features/0026-Add-config-for-server-mod-name.patch create mode 100644 camellia-server/minecraft-patches/features/0027-Add-config-for-cpu-affinity.patch create mode 100644 camellia-server/minecraft-patches/features/0028-Add-config-for-unsafe-teleportation.patch create mode 100644 camellia-server/minecraft-patches/features/0029-Add-config-for-watchdog-timeout.patch create mode 100644 camellia-server/minecraft-patches/features/0030-Add-config-to-disable-entity-tick-catchers.patch create mode 100644 camellia-server/minecraft-patches/features/0031-Add-config-for-heightmap-warning.patch create mode 100644 camellia-server/minecraft-patches/features/0032-Add-config-gui-support.patch create mode 100644 camellia-server/minecraft-patches/features/0033-Add-force-the-data-command-to-be-enabled-config.patch create mode 100644 camellia-server/minecraft-patches/features/0034-Add-experimental-config-for-command-block-command-ex.patch create mode 100644 camellia-server/minecraft-patches/features/0035-Add-config-to-modify-tripwire-behavior.patch create mode 100644 camellia-server/minecraft-patches/features/0036-Add-config-for-item-multitask.patch create mode 100644 camellia-server/minecraft-patches/features/0037-Add-config-to-enable-tick-command.patch create mode 100644 camellia-server/minecraft-patches/features/0038-Add-config-for-whether-to-save-portal-tickets.patch create mode 100644 camellia-server/minecraft-patches/features/0039-Add-portal-rate-limiter.patch create mode 100644 camellia-server/minecraft-patches/features/0040-Add-status-bar.patch create mode 100644 camellia-server/minecraft-patches/features/0041-Leaves-Vanilla-Hopper.patch create mode 100644 camellia-server/minecraft-patches/features/0042-Leaves-Lithium-Sleeping-Block-Entity.patch create mode 100644 camellia-server/minecraft-patches/features/0043-Petal-Reduce-sensor-work.patch create mode 100644 camellia-server/minecraft-patches/features/0044-Purpur-Lobotomize-stuck-villagers.patch create mode 100644 camellia-server/minecraft-patches/features/0045-Pufferfish-Reduce-projectile-chunk-loading.patch create mode 100644 camellia-server/minecraft-patches/features/0046-Pufferfish-Throttle-goal-selector-during-inactive-ti.patch create mode 100644 camellia-server/minecraft-patches/features/0047-Leaf-Secure-seed-and-matter-seed-command.patch create mode 100644 camellia-server/minecraft-patches/features/0048-Secure-seed-V2-with-Blake3.patch create mode 100644 camellia-server/minecraft-patches/features/0049-Leaf-Replace-brain-maps-with-optimized-collection.patch create mode 100644 camellia-server/minecraft-patches/features/0050-Leaf-Remove-useless-creating-stats-json-bases-on-pla.patch create mode 100644 camellia-server/minecraft-patches/features/0051-Leaf-Optimize-PatchedDataComponentMap-equals.patch create mode 100644 camellia-server/minecraft-patches/features/0052-Leaf-Better-checking-for-useless-move-packets.patch create mode 100644 camellia-server/minecraft-patches/features/0053-Leaf-fast-bit-radix-sort.patch create mode 100644 camellia-server/minecraft-patches/features/0054-Leaf-Configurable-vanilla-username-check.patch create mode 100644 camellia-server/minecraft-patches/features/0055-Leaves-Disable-packet-limit-Camellia-Option-to-full-.patch create mode 100644 camellia-server/minecraft-patches/features/0056-Leaves-Configurable-collision-behavior.patch create mode 100644 camellia-server/minecraft-patches/features/0057-Leaves-Optimized-dragon-respawn.patch create mode 100644 camellia-server/minecraft-patches/features/0058-Gale-Variable-entity-wake-up-duration.patch create mode 100644 camellia-server/minecraft-patches/features/0059-Gale-Replace-AI-attributes-with-optimized-collection.patch create mode 100644 camellia-server/minecraft-patches/features/0060-Gale-Skip-entity-move-if-movement-is-zero.patch create mode 100644 camellia-server/minecraft-patches/features/0061-Gale-Store-mob-counts-in-an-array.patch create mode 100644 camellia-server/minecraft-patches/features/0062-Gale-Optimize-random-calls-in-chunk-ticking.patch create mode 100644 camellia-server/minecraft-patches/features/0063-Gale-Reduce-enderman-teleport-chunk-lookups.patch create mode 100644 camellia-server/minecraft-patches/features/0064-Gale-Cache-ShapePairKey-hash.patch create mode 100644 camellia-server/minecraft-patches/features/0065-Gale-For-collision-check-has-physics-before-same-veh.patch create mode 100644 camellia-server/minecraft-patches/features/0066-Gale-Skip-negligible-planar-movement-multiplication.patch create mode 100644 camellia-server/minecraft-patches/features/0067-Gale-Optimize-matching-item-checks.patch create mode 100644 camellia-server/minecraft-patches/features/0068-Gale-Reduce-lambda-and-Optional-allocation-in-Entity.patch create mode 100644 camellia-server/minecraft-patches/features/0069-Gale-Reduce-array-allocations.patch create mode 100644 camellia-server/minecraft-patches/features/0070-Some-optimizations-from-krypton.patch create mode 100644 camellia-server/paper-patches/features/0001-Rebrand-to-Camellia.patch create mode 100644 camellia-server/paper-patches/features/0002-Correct-thread-unsafe-random-sources.patch create mode 100644 camellia-server/paper-patches/features/0003-Force-disable-reload-command-and-api.patch create mode 100644 camellia-server/paper-patches/features/0004-Force-disable-builtin-spark-plugin.patch create mode 100644 camellia-server/paper-patches/features/0005-Add-config-for-server-mod-name.patch create mode 100644 camellia-server/paper-patches/features/0006-Add-config-to-disable-async-catchers.patch create mode 100644 camellia-server/paper-patches/features/0007-Add-config-to-avoid-of-folia-supported-check.patch create mode 100644 camellia-server/paper-patches/features/0008-Add-config-for-cpu-affinity.patch create mode 100644 camellia-server/paper-patches/features/0009-Add-status-bar.patch create mode 100644 camellia-server/paper-patches/features/0010-Add-back-BlockPos-transform-to-Position-support.patch create mode 100644 camellia-server/paper-patches/features/0011-Purpur-Lobotomize-stuck-villagers.patch create mode 100644 camellia-server/paper-patches/features/0012-Leaf-Skip-event-if-no-listeners.patch create mode 100644 camellia-server/paper-patches/features/0013-Leaf-Secure-seed-and-matter-seed-command.patch create mode 100644 camellia-server/paper-patches/features/0014-Leaf-Configurable-vanilla-username-check.patch create mode 100644 camellia-server/src/main/java/abomination/LinearRegionFile.java create mode 100644 camellia-server/src/main/java/com/kiocg/ChunkHot.java create mode 100644 camellia-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityLimits.java create mode 100644 camellia-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityThrottler.java create mode 100644 camellia-server/src/main/java/me/titaniumtown/ArrayConstants.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/CommandRegister.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/BarCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/BarSubcommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/sub/ConfigEditCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/sub/ToggleCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/ConfigCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/ConfigSubcommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/CleanCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/OpenGuiCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ReloadCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ResetCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ResetCommentsCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/SetCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/SubmitCommand.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigManager.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigsInstance.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigsInstanceBuilder.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/DefaultTransformLogic.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/IConfigModule.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/IllegalFormatConversionExceptionWithOrigin.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/CommandSuggestions.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/ConfigClassInfo.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/ConfigInfo.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/DoNotLoad.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/HotReloadUnsupported.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/TransformedConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/CommandConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/DisableAsyncCatcherConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/DisableEntityCatchConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/CollisionBehaviorConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/FoliaEntityMovingFixConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/ForceCleanupEntityBrainMemoryConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/ItemMultitaskConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/LongCommandSupportConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/POIRangeFixes.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/PathfindingFixesConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/UnsafeTeleportationConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/MembarConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/PortalRateLimiterConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/RegionBarConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/RegionFormatConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/SecureSeedConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/TpsBarConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/TripwireBehaviorConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/DisableWarningConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/FoliaWatchogConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/PaperPacketLimiterConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/SavePortalTicketsConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/ServerModNameConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/UsernameCheckConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/AsyncProtocolChangeConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/CpuAffinityConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/EntityGoalSelectorInactiveTickConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/GaleVariableEntityWakeupConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/KaiijuEntityLimiterConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/LeavesSleepingBlockEntityConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/LobotomizeVillageConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/OptimizedDragonRespawnConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/PetalReduceSensorWorkConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/ProjectileChunkReduceConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/SIMDConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/removed/RemovedConfig.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/unsupported/DisableCheckForFoliaSupported.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/data/BufferedLinearRegionFile.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/data/RegionFile.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumBarType.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumCollisionBehaviorMode.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumConfigCategory.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumRegionFormat.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumStatusBarDisplay.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumTripwireBehavior.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/TickableStatusBar.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/TickableStatusBarList.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/Membar.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/RegionBar.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/TpsBar.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/AffinityRunnableWrapper.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/BufferedLinearRegionFileFlusher.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/ClassLoadUtil.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/NullPlugin.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/RateThrottler.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/RegionCreatorInfo.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/RegionFileFactory.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/ConfigCommandDialog.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/ConfigDialogUtil.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/DialogUtil.java create mode 100644 camellia-server/src/main/java/moe/monnairealms/camellia/utils/entity/EntityMoveOutOfRegionException.java create mode 100644 camellia-server/src/main/java/org/dreeam/leaf/util/FastBitRadixSort.java create mode 100644 camellia-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/command/ArgumentNode.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/command/CommandContext.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/command/CommandNode.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/command/CommandUtils.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/command/LiteralNode.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/command/RootNode.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/command/WrappedArgument.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java create mode 100644 camellia-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ece71ae --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..388d15b --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build + +/run + +/luminol-server/build.gradle.kts +/luminol-api/build.gradle.kts +/folia-server/build.gradle.kts +/folia-server/src/minecraft +/folia-api/build.gradle.kts +/paper-server +/paper-api + +.idea/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..b594398 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,108 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +plugins { + java // TODO java launcher tasks + id("moe.luminolmc.hyacinthusweight.patcher") +} + +paperweight { + filterPatches = false + upstreams.register("folia") { + repo = github("PaperMC", "Folia") + ref = providers.gradleProperty("foliaRef") + + patchFile { + path = "folia-server/build.gradle.kts" + outputFile = file("camellia-server/build.gradle.kts") + patchFile = file("camellia-server/build.gradle.kts.patch") + } + + patchFile { + path = "folia-api/build.gradle.kts" + outputFile = file("camellia-api/build.gradle.kts") + patchFile = file("camellia-api/build.gradle.kts.patch") + } + + patchRepo("paperApi") { + upstreamPath = "paper-api" + patchesDir = file("camellia-api/paper-patches") + outputDir = file("paper-api") + } + + patchDir("foliaApi") { + upstreamPath = "folia-api" + excludes = listOf("build.gradle.kts", "build.gradle.kts.patch", "paper-patches") + patchesDir = file("camellia-api/folia-patches") + outputDir = file("folia-api") + } + } +} + +val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/" +val bacteriawaMavenPublicUrl = "https://repo.bacteriawa.com/repository/maven-public/"; + +subprojects { + apply(plugin = "java-library") + apply(plugin = "maven-publish") + + extensions.configure { + toolchain { + languageVersion = JavaLanguageVersion.of(25) + } + } + + repositories { + mavenCentral() + maven(paperMavenPublicUrl) + maven(bacteriawaMavenPublicUrl) + } + + dependencies { + "testRuntimeOnly"("org.junit.platform:junit-platform-launcher") + } + + tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + } + tasks.withType().configureEach { + options.encoding = Charsets.UTF_8.name() + options.release = 25 + options.isFork = true + } + tasks.withType().configureEach { + options.encoding = Charsets.UTF_8.name() + } + tasks.withType().configureEach { + filteringCharset = Charsets.UTF_8.name() + } + tasks.withType { + testLogging { + showStackTraces = true + exceptionFormat = TestExceptionFormat.FULL + events(TestLogEvent.STANDARD_OUT) + } + } + + extensions.configure { + repositories { + maven("https://repo.bacteriawa.com/repository/maven-releases/") { + name = "Bacteriawa" + credentials(PasswordCredentials::class) { + username = System.getenv("PRIVATE_MAVEN_REPO_USERNAME") + password = System.getenv("PRIVATE_MAVEN_REPO_PASSWORD") + } + } + } + } + + tasks.withType().configureEach { + options { + (this as StandardJavadocDocletOptions).apply { + addStringOption("-add-modules", "jdk.incubator.vector") + addStringOption("Xdoclint:none", "-quiet") + } + } + } +} diff --git a/camellia-api/build.gradle.kts.patch b/camellia-api/build.gradle.kts.patch new file mode 100644 index 0000000..ab57596 --- /dev/null +++ b/camellia-api/build.gradle.kts.patch @@ -0,0 +1,40 @@ +--- a/folia-api/build.gradle.kts ++++ b/folia-api/build.gradle.kts +@@ -105,17 +_,21 @@ + java { + srcDir(generatedDir) + srcDir(file("../paper-api/src/main/java")) ++ srcDir(file("../folia-api/src/main/java")) + } + resources { + srcDir(file("../paper-api/src/main/resources")) ++ srcDir(file("../folia-api/src/main/resources")) + } + } + test { + java { + srcDir(file("../paper-api/src/test/java")) ++ srcDir(file("../folia-api/src/test/java")) + } + resources { + srcDir(file("../paper-api/src/test/resources")) ++ srcDir(file("../folia-api/src/test/resources")) + } + } + } +@@ -258,4 +_,15 @@ + } + tasks.check { + dependsOn(scanJarForBadCalls) ++} ++ ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ // Pufferfish SIMD ++ compilerArgs.add("--add-modules=jdk.incubator.vector") ++ ++ // Disable compiler warns ++ compilerArgs.add("-Xlint:-module") ++ compilerArgs.add("-Xlint:-removal") ++ compilerArgs.add("-Xlint:-dep-ann") + } diff --git a/camellia-api/paper-patches/features/0001-Rebrand-to-Camellia.patch b/camellia-api/paper-patches/features/0001-Rebrand-to-Camellia.patch new file mode 100644 index 0000000..55e6835 --- /dev/null +++ b/camellia-api/paper-patches/features/0001-Rebrand-to-Camellia.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 18:15:21 +0800 +Subject: [PATCH] Rebrand to Camellia + + +diff --git a/src/main/java/io/papermc/paper/ServerBuildInfo.java b/src/main/java/io/papermc/paper/ServerBuildInfo.java +index 652ff54e7c50412503725d628bfe72ed03059790..2e10b3f7833960074b2d4c2a670d4d1f6e1fc123 100644 +--- a/src/main/java/io/papermc/paper/ServerBuildInfo.java ++++ b/src/main/java/io/papermc/paper/ServerBuildInfo.java +@@ -19,6 +19,17 @@ public interface ServerBuildInfo { + */ + Key BRAND_PAPER_ID = Key.key("papermc", "paper"); + ++ // Camellia start ++ /** ++ * The brand id for folia ++ */ ++ Key BRAND_FOLIA_ID = Key.key("papermc", "folia"); ++ /** ++ * The brand id for Camellia. ++ */ ++ Key BRAND_CAMELLIA_ID = Key.key("monnairealms", "camellia"); ++ // Camellia end ++ + /** + * Gets the {@code ServerBuildInfo}. + * diff --git a/camellia-api/paper-patches/features/0002-Disable-timings-warn-msg-and-commands.patch b/camellia-api/paper-patches/features/0002-Disable-timings-warn-msg-and-commands.patch new file mode 100644 index 0000000..bea35a3 --- /dev/null +++ b/camellia-api/paper-patches/features/0002-Disable-timings-warn-msg-and-commands.patch @@ -0,0 +1,45 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 09:50:39 +0800 +Subject: [PATCH] Disable timings warn msg and commands + + +diff --git a/src/main/java/co/aikar/timings/Timings.java b/src/main/java/co/aikar/timings/Timings.java +index 89017af09ce32e7a66014fc3aeb50155921252a5..2a6f4b82d4a83ede809372103dd5f731a32de6fd 100644 +--- a/src/main/java/co/aikar/timings/Timings.java ++++ b/src/main/java/co/aikar/timings/Timings.java +@@ -147,7 +147,7 @@ public final class Timings { + public static void setTimingsEnabled(boolean enabled) { + enabled = false; // Folia - region threading - disable timings + if (enabled && !warnedAboutDeprecationOnEnable) { +- Bukkit.getLogger().severe(PlainTextComponentSerializer.plainText().serialize(deprecationMessage())); ++ // Bukkit.getLogger().severe(PlainTextComponentSerializer.plainText().serialize(deprecationMessage())); // Camellia - Disable timings warn msg and commands + warnedAboutDeprecationOnEnable = true; + } + } +diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java +index ad1330d87223dfcf3c9da40aa5ece8c21141f0d6..58b9ff1141c62f5d35bfa5da9a02a043b84ec0d9 100644 +--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java ++++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java +@@ -34,7 +34,7 @@ public class SimpleCommandMap implements CommandMap { + final ReloadCommand reload = new ReloadCommand("reload"); + this.knownCommands.put("bukkit:reload", reload); + this.knownCommands.put("bukkit:rl", reload); +- register("bukkit", new co.aikar.timings.TimingsCommand("timings")); ++ //register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Camellia - Disable timings warn msg and commands + } + + public void setFallbackCommands() { +diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +index d1f1e5adbd18800aeb44bf71f4a002c674fc958d..765cb513e26dcd7a4cf66ecdbb4d29a00e7a9e50 100644 +--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java ++++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +@@ -966,7 +966,7 @@ public final class SimplePluginManager implements PluginManager { + */ + @Deprecated(forRemoval = true) + public void useTimings(boolean use) { +- co.aikar.timings.Timings.setTimingsEnabled(use); // Paper ++ // co.aikar.timings.Timings.setTimingsEnabled(use); // Paper // Camellia - Disable timings warn msg and commands + } + + // Paper start diff --git a/camellia-api/paper-patches/features/0003-Force-disable-reload-command-and-api.patch b/camellia-api/paper-patches/features/0003-Force-disable-reload-command-and-api.patch new file mode 100644 index 0000000..6fd9b78 --- /dev/null +++ b/camellia-api/paper-patches/features/0003-Force-disable-reload-command-and-api.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 09:53:09 +0800 +Subject: [PATCH] Force disable reload command and api + + +diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java +index 58b9ff1141c62f5d35bfa5da9a02a043b84ec0d9..afb81d3e2cc64bb49a0512cdf857bb44db31ed7a 100644 +--- a/src/main/java/org/bukkit/command/SimpleCommandMap.java ++++ b/src/main/java/org/bukkit/command/SimpleCommandMap.java +@@ -31,9 +31,9 @@ public class SimpleCommandMap implements CommandMap { + } + + private void setDefaultCommands() { +- final ReloadCommand reload = new ReloadCommand("reload"); +- this.knownCommands.put("bukkit:reload", reload); +- this.knownCommands.put("bukkit:rl", reload); ++ //final ReloadCommand reload = new ReloadCommand("reload"); // Camellia - Force disable reload command and api ++ //this.knownCommands.put("bukkit:reload", reload); // Camellia - Force disable reload command and api ++ //this.knownCommands.put("bukkit:rl", reload); // Camellia - Force disable reload command and api + //register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Camellia - Disable timings warn msg and commands + } + diff --git a/camellia-api/paper-patches/features/0004-KioCG-Chunk-API.patch b/camellia-api/paper-patches/features/0004-KioCG-Chunk-API.patch new file mode 100644 index 0000000..2940e43 --- /dev/null +++ b/camellia-api/paper-patches/features/0004-KioCG-Chunk-API.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 16:40:49 +0800 +Subject: [PATCH] KioCG Chunk API + + +diff --git a/src/main/java/org/bukkit/Chunk.java b/src/main/java/org/bukkit/Chunk.java +index d873b78f6935a23ff8f6092b80968267b182cc0f..b4323370859dd4d9e86614484cdb9d2e35c45b40 100644 +--- a/src/main/java/org/bukkit/Chunk.java ++++ b/src/main/java/org/bukkit/Chunk.java +@@ -392,4 +392,6 @@ public interface Chunk extends PersistentDataHolder { + */ + UNLOADED; + } ++ ++ long getChunkHotAvg(); // KioCG + } +diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java +index e5947f299937a8100fd8c6b1456da9f9003c6b53..2c770b257edc758d6b41956a841b94e32486945d 100644 +--- a/src/main/java/org/bukkit/entity/Player.java ++++ b/src/main/java/org/bukkit/entity/Player.java +@@ -4020,4 +4020,6 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM + default ObjectContents asObjectContents() { + return this.getPlayerProfile().asObjectContents(); + } ++ ++ long getNearbyChunkHot(); // KioCG + } diff --git a/camellia-api/paper-patches/features/0005-Leaves-Fix-SculkCatalyst-exp-skip.patch b/camellia-api/paper-patches/features/0005-Leaves-Fix-SculkCatalyst-exp-skip.patch new file mode 100644 index 0000000..61c952b --- /dev/null +++ b/camellia-api/paper-patches/features/0005-Leaves-Fix-SculkCatalyst-exp-skip.patch @@ -0,0 +1,86 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sat, 24 May 2025 01:25:10 +0800 +Subject: [PATCH] Leaves: Fix SculkCatalyst exp skip + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As part of: Leaves (https://github.com/LeavesMC/Leaves/blob/ea91106ae57fc4cc14e2e0225009cf9919072f7f/leaves-api/paper-patches/features/0007-Fix-SculkCatalyst-exp-skip.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/src/main/java/org/bukkit/event/entity/EntityDeathEvent.java b/src/main/java/org/bukkit/event/entity/EntityDeathEvent.java +index 76c08499b704a6cb0cb95ce69b9a9248d69cc127..d516595a8675a9f3d9c14658b66160ba4b3c4278 100644 +--- a/src/main/java/org/bukkit/event/entity/EntityDeathEvent.java ++++ b/src/main/java/org/bukkit/event/entity/EntityDeathEvent.java +@@ -27,6 +27,7 @@ public class EntityDeathEvent extends EntityEvent implements Cancellable { + @Nullable private org.bukkit.SoundCategory deathSoundCategory; + private float deathSoundVolume; + private float deathSoundPitch; ++ private int rewardExp; // Leaves - exp fix + + private boolean cancelled; + +@@ -35,13 +36,20 @@ public class EntityDeathEvent extends EntityEvent implements Cancellable { + this(livingEntity, damageSource, drops, 0); + } + +- @ApiStatus.Internal + public EntityDeathEvent(@NotNull final LivingEntity livingEntity, @NotNull DamageSource damageSource, @NotNull final List drops, final int droppedExp) { ++ // Leaves start - exp fix ++ this(livingEntity, damageSource, drops, droppedExp, droppedExp); ++ } ++ ++ @ApiStatus.Internal ++ public EntityDeathEvent(@NotNull final LivingEntity livingEntity, @NotNull DamageSource damageSource, @NotNull final List drops, final int droppedExp, final int rewardExp) { + super(livingEntity); + this.damageSource = damageSource; + this.drops = drops; + this.dropExp = droppedExp; ++ this.rewardExp = rewardExp; + } ++ // Leaves end - exp fix + + @NotNull + @Override +@@ -230,4 +238,14 @@ public class EntityDeathEvent extends EntityEvent implements Cancellable { + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } ++ ++ // Leaves start - exp fix ++ public int getRewardExp() { ++ return rewardExp; ++ } ++ ++ public void setRewardExp(int rewardExp) { ++ this.rewardExp = rewardExp; ++ } ++ // Leaves end - exp fix + } +diff --git a/src/main/java/org/bukkit/event/entity/PlayerDeathEvent.java b/src/main/java/org/bukkit/event/entity/PlayerDeathEvent.java +index 78497c423edaff9550315610ccc2f3d4d3614dce..187fb47ba43d300997f19ecb82ddc41fdd312ec1 100644 +--- a/src/main/java/org/bukkit/event/entity/PlayerDeathEvent.java ++++ b/src/main/java/org/bukkit/event/entity/PlayerDeathEvent.java +@@ -25,6 +25,7 @@ public class PlayerDeathEvent extends EntityDeathEvent { + private boolean doExpDrop; + private boolean keepLevel = false; + private boolean keepInventory = false; ++ private boolean useApiExpDropStatus = false; // Leaves - exp fix + @Deprecated + private final List itemsToKeep = new ArrayList<>(); + +@@ -82,8 +83,15 @@ public class PlayerDeathEvent extends EntityDeathEvent { + this.showDeathMessages = true; + this.deathMessage = LegacyComponentSerializer.legacySection().deserializeOrNull(deathMessage); + this.doExpDrop = doExpDrop; ++ this.useApiExpDropStatus = true; // Leaves - exp fix + } + ++ // Leaves start - exp fix ++ public boolean forceUseEventDropStatus() { ++ return this.useApiExpDropStatus; ++ } ++ // Leaves end - exp fix ++ + @NotNull + @Override + public Player getEntity() { diff --git a/camellia-api/paper-patches/features/0006-Pufferfish-SIMD-Utilities.patch b/camellia-api/paper-patches/features/0006-Pufferfish-SIMD-Utilities.patch new file mode 100644 index 0000000..67f1fe4 --- /dev/null +++ b/camellia-api/paper-patches/features/0006-Pufferfish-SIMD-Utilities.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 00:05:30 +0800 +Subject: [PATCH] Pufferfish: SIMD Utilities + +As part of: Pufferfish +Licensed under: GPL-3.0 (https://github.com/pufferfish-gg/Pufferfish/blob/04bc249f9eee6157338b6884113b6fa3192bf59b/PATCH-LICENSE) + +diff --git a/src/main/java/org/bukkit/map/MapPalette.java b/src/main/java/org/bukkit/map/MapPalette.java +index 76ae2750ff7b04a281d4b794dc1f48dd832db2b6..645ab1c9e008babd8f5604f156db2db14393693b 100644 +--- a/src/main/java/org/bukkit/map/MapPalette.java ++++ b/src/main/java/org/bukkit/map/MapPalette.java +@@ -35,7 +35,7 @@ public final class MapPalette { + } + + @NotNull +- static final Color[] colors = { ++ public static final Color[] colors = { // Pufferfish - package-private -> public + // Start generate - MapPalette#colors + new Color(0x00000000, true), + new Color(0x00000000, true), +@@ -394,9 +394,11 @@ public final class MapPalette { + temp.getRGB(0, 0, temp.getWidth(), temp.getHeight(), pixels, 0, temp.getWidth()); + + byte[] result = new byte[temp.getWidth() * temp.getHeight()]; ++ if ((mapColorCache != null && mapColorCache.isCached()) || !gg.pufferfish.pufferfish.simd.SIMDDetection.isEnabled) { // Pufferfish - vectorized map color conversion + for (int i = 0; i < pixels.length; i++) { + result[i] = matchColor(new Color(pixels[i], true)); + } ++ } else gg.pufferfish.pufferfish.simd.VectorMapPalette.matchColorVectorized(pixels, result); // Pufferfish - vectorized map color conversion + return result; + } + diff --git a/camellia-api/paper-patches/features/0007-Purpur-Lobotomize-stuck-villagers.patch b/camellia-api/paper-patches/features/0007-Purpur-Lobotomize-stuck-villagers.patch new file mode 100644 index 0000000..1683e9e --- /dev/null +++ b/camellia-api/paper-patches/features/0007-Purpur-Lobotomize-stuck-villagers.patch @@ -0,0 +1,27 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 12 Jun 2025 08:00:15 +0800 +Subject: [PATCH] Purpur: Lobotomize stuck villagers + +Co-authored by: William Blake Galbreath +As part of: Purpur (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/purpur-server/minecraft-patches/features/0001-Ridables.patch) +Licensed under: MIT (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/LICENSE) + +diff --git a/src/main/java/org/bukkit/entity/Villager.java b/src/main/java/org/bukkit/entity/Villager.java +index a2024b5dda030a7724c13b2446224903ae837339..71f34a40b136f7f08a4820056c00692b986f63bd 100644 +--- a/src/main/java/org/bukkit/entity/Villager.java ++++ b/src/main/java/org/bukkit/entity/Villager.java +@@ -405,4 +405,13 @@ public interface Villager extends AbstractVillager { + * Demand is still updated even if all events are canceled. + */ + public void restock(); ++ // Purpur start ++ ++ /** ++ * Check if villager is currently lobotomized ++ * ++ * @return True if lobotomized ++ */ ++ boolean isLobotomized(); ++ // Purpur end + } diff --git a/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java b/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java new file mode 100644 index 0000000..6a5c6ef --- /dev/null +++ b/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java @@ -0,0 +1,53 @@ +/* + * This file is part of Pufferfish (https://github.com/pufferfish-gg/Pufferfish) + * + * Pufferfish 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. + * + * Pufferfish 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 Pufferfish. If not, see . + */ + +package gg.pufferfish.pufferfish.simd; + +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.IntVector; +import jdk.incubator.vector.VectorSpecies; +import org.slf4j.Logger; + +/** + * Basically, java is annoying, and we have to push this out to its own class. + */ +@Deprecated +public class SIMDChecker { + + @Deprecated + public static boolean canEnable(Logger logger) { + try { + SIMDDetection.testRun = true; + + VectorSpecies ISPEC = IntVector.SPECIES_PREFERRED; + VectorSpecies FSPEC = FloatVector.SPECIES_PREFERRED; + + logger.info("Max SIMD vector size on this system is {} bits (int)", ISPEC.vectorBitSize()); + logger.info("Max SIMD vector size on this system is " + FSPEC.vectorBitSize() + " bits (float)"); + + if (ISPEC.elementSize() < 2 || FSPEC.elementSize() < 2) { + logger.warn("SIMD is not properly supported on this system!"); + return false; + } + + return true; + } catch (NoClassDefFoundError | Exception ignored) { + } // Basically, we don't do anything. This lets us detect if it's not functional and disable it. + return false; + } + +} diff --git a/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java b/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java new file mode 100644 index 0000000..aaa7799 --- /dev/null +++ b/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java @@ -0,0 +1,53 @@ +/* + * This file is part of Pufferfish (https://github.com/pufferfish-gg/Pufferfish) + * + * Pufferfish 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. + * + * Pufferfish 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 Pufferfish. If not, see . + */ + +package gg.pufferfish.pufferfish.simd; + +import org.slf4j.Logger; + +@Deprecated +public class SIMDDetection { + + public static boolean isEnabled = false; + public static boolean testRun = false; + + @Deprecated + public static boolean canEnable(Logger logger) { + try { + return SIMDChecker.canEnable(logger); + } catch (NoClassDefFoundError | Exception ignored) { + return false; + } + } + + @Deprecated + public static int getJavaVersion() { + // https://stackoverflow.com/a/2591122 + String version = System.getProperty("java.version"); + if (version.startsWith("1.")) { + version = version.substring(2, 3); + } else { + int dot = version.indexOf("."); + if (dot != -1) { + version = version.substring(0, dot); + } + } + version = version.split("-")[0]; // Azul is stupid + return Integer.parseInt(version); + } + +} diff --git a/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java b/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java new file mode 100644 index 0000000..ebb51f0 --- /dev/null +++ b/camellia-api/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java @@ -0,0 +1,101 @@ +/* + * This file is part of Pufferfish (https://github.com/pufferfish-gg/Pufferfish) + * + * Pufferfish 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. + * + * Pufferfish 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 Pufferfish. If not, see . + */ + +package gg.pufferfish.pufferfish.simd; + +import jdk.incubator.vector.FloatVector; +import jdk.incubator.vector.IntVector; +import jdk.incubator.vector.VectorMask; +import jdk.incubator.vector.VectorSpecies; +import org.bukkit.map.MapPalette; + +import java.awt.*; + +@Deprecated +public class VectorMapPalette { + + private static final VectorSpecies I_SPEC = IntVector.SPECIES_PREFERRED; + private static final VectorSpecies F_SPEC = FloatVector.SPECIES_PREFERRED; + + @Deprecated + public static void matchColorVectorized(int[] in, byte[] out) { + int speciesLength = I_SPEC.length(); + int i; + for (i = 0; i < in.length - speciesLength; i += speciesLength) { + float[] redsArr = new float[speciesLength]; + float[] bluesArr = new float[speciesLength]; + float[] greensArr = new float[speciesLength]; + int[] alphasArr = new int[speciesLength]; + + for (int j = 0; j < speciesLength; j++) { + alphasArr[j] = (in[i + j] >> 24) & 0xFF; + redsArr[j] = (in[i + j] >> 16) & 0xFF; + greensArr[j] = (in[i + j] >> 8) & 0xFF; + bluesArr[j] = (in[i + j] >> 0) & 0xFF; + } + + IntVector alphas = IntVector.fromArray(I_SPEC, alphasArr, 0); + FloatVector reds = FloatVector.fromArray(F_SPEC, redsArr, 0); + FloatVector greens = FloatVector.fromArray(F_SPEC, greensArr, 0); + FloatVector blues = FloatVector.fromArray(F_SPEC, bluesArr, 0); + IntVector resultIndex = IntVector.zero(I_SPEC); + VectorMask modificationMask = VectorMask.fromLong(I_SPEC, 0xffffffff); + + modificationMask = modificationMask.and(alphas.lt(128).not()); + FloatVector bestDistances = FloatVector.broadcast(F_SPEC, Float.MAX_VALUE); + + for (int c = 4; c < MapPalette.colors.length; c++) { + // We're using 32-bit floats here because it's 2x faster and nobody will know the difference. + // For correctness, the original algorithm uses 64-bit floats instead. Completely unnecessary. + FloatVector compReds = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getRed()); + FloatVector compGreens = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getGreen()); + FloatVector compBlues = FloatVector.broadcast(F_SPEC, MapPalette.colors[c].getBlue()); + + FloatVector rMean = reds.add(compReds).div(2.0f); + FloatVector rDiff = reds.sub(compReds); + FloatVector gDiff = greens.sub(compGreens); + FloatVector bDiff = blues.sub(compBlues); + + FloatVector weightR = rMean.div(256.0f).add(2); + FloatVector weightG = FloatVector.broadcast(F_SPEC, 4.0f); + FloatVector weightB = FloatVector.broadcast(F_SPEC, 255.0f).sub(rMean).div(256.0f).add(2.0f); + + FloatVector distance = weightR.mul(rDiff).mul(rDiff).add(weightG.mul(gDiff).mul(gDiff)).add(weightB.mul(bDiff).mul(bDiff)); + + // Now we compare to the best distance we've found. + // This mask contains a "1" if better, and a "0" otherwise. + VectorMask bestDistanceMask = distance.lt(bestDistances); + bestDistances = bestDistances.blend(distance, bestDistanceMask); // Update the best distances + + // Update the result array + // We also AND with the modification mask because we don't want to interfere if the alpha value isn't large enough. + resultIndex = resultIndex.blend(c, bestDistanceMask.cast(I_SPEC).and(modificationMask)); // Update the results + } + + for (int j = 0; j < speciesLength; j++) { + int index = resultIndex.lane(j); + out[i + j] = (byte) (index < 128 ? index : -129 + (index - 127)); + } + } + + // For the final ones, fall back to the regular method + for (; i < in.length; i++) { + out[i] = MapPalette.matchColor(new Color(in[i], true)); + } + } + +} diff --git a/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/CamelliaConfigBuilder.java b/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/CamelliaConfigBuilder.java new file mode 100644 index 0000000..f8fea20 --- /dev/null +++ b/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/CamelliaConfigBuilder.java @@ -0,0 +1,82 @@ +package moe.monnairealms.camellia.api.config; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +/** + * Builder interface for creating {@link CamelliaConfigsInstance} instances. + *

+ * Provides multiple overloaded factory methods to construct configuration instances + * with varying levels of customization, including custom base directories, + * file names, and command names. + */ +public interface CamelliaConfigBuilder { + /** + * Creates a configuration instance using the default base directory and file name. + * + * @param loader the class loader used to load configuration resources + * @param name the configuration name identifier + * @param pack the package path where configuration classes are located + * @return a new {@link CamelliaConfigsInstance} + */ + CamelliaConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull String name, + @NotNull String pack + ); + + /** + * Creates a configuration instance with a custom base directory. + * + * @param loader the class loader used to load configuration resources + * @param base the base directory where the configuration file is stored + * @param name the configuration name identifier + * @param pack the package path where configuration classes are located + * @return a new {@link CamelliaConfigsInstance} + */ + CamelliaConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull File base, + @NotNull String name, + @NotNull String pack + ); + + /** + * Creates a configuration instance with a custom base directory and file name. + * + * @param loader the class loader used to load configuration resources + * @param base the base directory where the configuration file is stored + * @param name the configuration name identifier + * @param file_name the configuration file name (without extension) + * @param pack the package path where configuration classes are located + * @return a new {@link CamelliaConfigsInstance} + */ + CamelliaConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull File base, + @NotNull String name, + @NotNull String file_name, + @NotNull String pack + ); + + /** + * Creates a configuration instance with full customization options. + * + * @param loader the class loader used to load configuration resources + * @param base the base directory where the configuration file is stored + * @param name the configuration name identifier + * @param file_name the configuration file name (without extension) + * @param command_name the command name associated with this configuration + * @param pack the package path where configuration classes are located + * @return a new {@link CamelliaConfigsInstance} + */ + CamelliaConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull File base, + @NotNull String name, + @NotNull String file_name, + @NotNull String command_name, + @NotNull String pack + ); +} diff --git a/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/CamelliaConfigsInstance.java b/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/CamelliaConfigsInstance.java new file mode 100644 index 0000000..fec4c67 --- /dev/null +++ b/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/CamelliaConfigsInstance.java @@ -0,0 +1,203 @@ +package moe.monnairealms.camellia.api.config; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CompletableFuture; + +/** + * Interface for managing configuration instances and operations. + * Provides methods for loading, saving, modifying, and querying configuration values. + */ +public interface CamelliaConfigsInstance { + /** + * Initialize the configuration subsystem for this instance. + * Implementations should load configuration files, apply defaults and + * prepare any internal caches or state needed for subsequent operations. + * This method may perform I/O and therefore can throw an {@link IOException} + * if initialization fails. + * + * @throws IOException if an I/O error occurs while initializing + */ + void initialize() throws IOException; + + /** + * Reloads all configurations asynchronously + * + * @param keepComments whether to preserve existing comments in the config file + * @return CompletableFuture that completes when reload is finished + */ + @NotNull CompletableFuture reloadAsync(boolean keepComments); + + /** + * Sets a configuration value by key + * + * @param key the configuration key (dot-separated path) + * @param value the value to set + * @return true if the key exists and value was set, false otherwise + */ + boolean setConfig(String key, Object value); + + /** + * Saves all pending configuration changes to disk + */ + void saveConfigs(); + + /** + * Resets a configuration value to its default + * + * @param key the configuration key to reset + */ + void resetConfig(String key); + + /** + * Gets the default value of a configuration as string + * + * @param key the configuration key + * @return the default value as string + */ + String getDefaultConfig(String key); + + /** + * Gets the current value of a configuration as string + * + * @param key the configuration key + * @return the current value as string + */ + String getConfig(String key); + + /** + * Gets the original (untransformed) value of a configuration + * + * @param key the configuration key + * @param the expected type of the value + * @return the original configuration value + */ + T getConfigOrigin(String key); + + /** + * Gets available suggestions for a configuration key + * + * @param key the configuration key + * @return array of suggestion strings, or null if no suggestions available + */ + String[] getConfigSuggestions(String key); + + /** + * Completes a partial configuration path by finding matching keys + * + * @param partialPath the partial path to complete + * @return list of possible completions + */ + List completeConfigPath(String partialPath); + + /** + * Completes a partial configuration path with specific depth + * + * @param partialPath the partial path to complete + * @param dotIndex the maximum number of dots (depth) in the result + * @return list of possible completions + */ + List completeConfigPath(String partialPath, int dotIndex); + + /** + * Gets all configuration paths that start with the given prefix + * + * @param currentPath the prefix to search for + * @return list of matching configuration paths + */ + List getAllConfigPaths(String currentPath); + + /** + * Gets all configuration data pairs without prefix filter + * + * @return set of all configuration data + */ + Set getAllData(); + + /** + * Gets configuration data pairs filtered by prefix + * + * @param prefix the prefix to filter by + * @return set of matching configuration data + */ + Set getData(String prefix); + + /** + * Gets all configuration data pairs with full information (comments and suggestions) + * + * @return set of all configuration data with full details + */ + Set getAllDataFull(); + + /** + * Gets configuration data pairs with full information filtered by prefix + * + * @param prefix the prefix to filter by + * @return set of matching configuration data with full details + */ + Set getDataFull(String prefix); + + /** + * Gets configuration data pairs with optional comments and suggestions + * + * @param prefix the prefix to filter by + * @param _comment whether to include comments + * @param _withSuggestions whether to include suggestions + * @return set of matching configuration data + */ + Set getData(String prefix, boolean _comment, boolean _withSuggestions); + + /** + * Gets configuration data pairs for specified keys + * + * @param list list of configuration keys + * @return set of configuration data for the specified keys + */ + Set getData(List list); + + /** + * Gets configuration data pairs with comments for specified keys + * + * @param list list of configuration keys + * @return set of configuration data with comments + */ + Set getDataWithComment(List list); + + /** + * Gets configuration data pairs with full information for specified keys + * + * @param list list of configuration keys + * @return set of configuration data with full details + */ + Set getDataFull(List list); + + /** + * Gets configuration data pairs for specified keys with optional features + * + * @param list list of configuration keys + * @param _comment whether to include comments + * @param _withSuggestions whether to include suggestions + * @return set of configuration data + */ + Set getData(List list, boolean _comment, boolean _withSuggestions); + + /** + * Remove a configuration entry by its key. + * If the key does not exist this should be a no-op. + * + * @param key the configuration key to remove + */ + void removeConfig(String key); + + /** + * Remove multiple configuration entries by their keys. + * Implementations should attempt to remove each key provided. If some + * keys do not exist they may be ignored. + * + * @param keys an array of configuration keys to remove + */ + void removeConfig(String[] keys); +} diff --git a/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/ConfigDataPair.java b/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/ConfigDataPair.java new file mode 100644 index 0000000..116b4da --- /dev/null +++ b/camellia-api/src/main/java/moe/monnairealms/camellia/api/config/ConfigDataPair.java @@ -0,0 +1,11 @@ +package moe.monnairealms.camellia.api.config; + +import org.jspecify.annotations.Nullable; + +public record ConfigDataPair( + String key, + Object value, + @Nullable String comment, + @Nullable String[] suggestions +) { +} diff --git a/camellia-server/build.gradle.kts.patch b/camellia-server/build.gradle.kts.patch new file mode 100644 index 0000000..154cf43 --- /dev/null +++ b/camellia-server/build.gradle.kts.patch @@ -0,0 +1,148 @@ +--- a/folia-server/build.gradle.kts ++++ b/folia-server/build.gradle.kts +@@ -7,15 +_,16 @@ + `java-library` + `maven-publish` + idea +- id("io.papermc.paperweight.core") ++ id("moe.luminolmc.hyacinthusweight.core") + id("io.papermc.fill.gradle") version "1.0.12" + } + + val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/" ++val bacteriawaMavenPublicUrl = "https://repo.bacteriawa.com/repository/maven-public/" + + dependencies { + mache("io.papermc:mache:26.2+build.1") +- paperclip("io.papermc:paperclip:3.0.4") ++ hyacinthusclip("moe.luminolmc:hyacinthusclip:${providers.gradleProperty("clipVersion").get()}") + } + + paperweight { +@@ -24,6 +_,7 @@ + gitFilePatches = false + + val fork = forks.register("folia") { ++ rootDirectory = upstreamsDirectory().map { it.dir("folia") } + upstream.patchDir("paperServer") { + upstreamPath = "paper-server" + excludes = setOf("src/minecraft", "patches", "build.gradle.kts") +@@ -32,7 +_,23 @@ + } + } + +- activeFork = fork ++ val camellia = forks.register("camellia") { ++ forks = fork ++ upstream.patchRepo("paperServer") { ++ upstreamRepo = fork.patchedRepo("paperServer") ++ patchesDir = rootDirectory.dir("camellia-server/paper-patches") ++ outputDir = rootDirectory.dir("paper-server") ++ } ++ ++ upstream.patchDir("foliaServer") { ++ upstreamPath = "folia-server" ++ excludes = setOf("src/minecraft", "paper-patches", "minecraft-patches", "build.gradle.kts", "build.gradle.kts.patch") ++ patchesDir = rootDirectory.dir("camellia-server/folia-patches") ++ outputDir = rootDirectory.dir("folia-server") ++ } ++ } ++ ++ activeFork = camellia + + + updatingMinecraft { +@@ -44,6 +_,7 @@ + libraryRepositories.addAll( + "https://repo.maven.apache.org/maven2/", + paperMavenPublicUrl, ++ bacteriawaMavenPublicUrl + ) + } + +@@ -103,10 +_,14 @@ + main { + java { srcDir("../paper-server/src/main/java"); srcDir("../paper-server/src/generated/java") } + resources { srcDir("../paper-server/src/main/resources") } ++ java { srcDir("../folia-server/src/main/java") } ++ resources { srcDir("../folia-server/src/main/resources") } + } + test { + java { srcDir("../paper-server/src/test/java") } + resources { srcDir("../paper-server/src/test/resources") } ++ java { srcDir("../folia-server/src/test/java") } ++ resources { srcDir("../folia-server/src/test/resources") } + } + } + val log4jPlugins = sourceSets.create("log4jPlugins") { +@@ -134,7 +_,15 @@ + } + + dependencies { +- implementation(project(":folia-api")) ++ implementation(project(":camellia-api")) ++ // Camellia start - dependenices ++ implementation("com.electronwill.night-config:toml:3.9.0") ++ implementation("net.openhft:affinity:3.23.3") ++ implementation("com.github.luben:zstd-jni:1.5.4-1") ++ implementation("net.openhft:zero-allocation-hashing:0.16") ++ implementation("net.objecthunter:exp4j:0.4.8") ++ implementation("io.github.classgraph:classgraph:4.8.158") ++ // Camellia end + implementation("ca.spottedleaf:leafpile:1.0.0") + implementation("org.jline:jline-terminal-ffm:3.27.1") // use ffm on java 22+ + implementation("org.jline:jline-terminal-jni:3.27.1") // fall back to jni on java 21 +@@ -189,21 +_,21 @@ + val git = Git(rootProject.layout.projectDirectory.path) + val mcVersion = rootProject.providers.gradleProperty("mcVersion").get() + val build = System.getenv("BUILD_NUMBER") ?: null +- val buildTime = providers.environmentVariable("BUILD_STARTED_AT").map(Instant::parse).orElse(Instant.EPOCH).get() ++ val buildTime = Instant.now() + val gitHash = git.exec(providers, "rev-parse", "--short=7", "HEAD").get().trim() + val implementationVersion = "$mcVersion-${build ?: "DEV"}-$gitHash" + val date = git.exec(providers, "show", "-s", "--format=%ci", gitHash).get().trim() + val gitBranch = git.exec(providers, "rev-parse", "--abbrev-ref", "HEAD").get().trim() + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", +- "Implementation-Title" to "Folia", ++ "Implementation-Title" to "Camellia", + "Implementation-Version" to implementationVersion, + "Implementation-Vendor" to date, +- "Specification-Title" to "Folia", ++ "Specification-Title" to "Camellia", + "Specification-Version" to project.version, +- "Specification-Vendor" to "Paper Team", +- "Brand-Id" to "papermc:folia", +- "Brand-Name" to "Folia", ++ "Specification-Vendor" to "MonnaiRealms Community", ++ "Brand-Id" to "monnairealms:camellia", ++ "Brand-Name" to "Camellia", + "Build-Number" to (build ?: ""), + "Build-Time" to buildTime.toString(), + "Git-Branch" to gitBranch, +@@ -341,7 +_,7 @@ + } + + fill { +- project("folia") ++ project("camellia") + versionFamily(paperweight.minecraftVersion.map { it.split(".", "-").takeWhile { part -> part.toIntOrNull() != null }.take(2).joinToString(".") }) + version(paperweight.minecraftVersion) + +@@ -355,4 +_,16 @@ + } + } + } ++} ++ ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ // Pufferfish SIMD ++ compilerArgs.add("--add-modules=jdk.incubator.vector") ++ ++ // Disable compiler warns ++ compilerArgs.add("-Xlint:-module") ++ compilerArgs.add("-Xlint:-removal") ++ compilerArgs.add("-Xlint:-dep-ann") ++ + } diff --git a/camellia-server/minecraft-patches/features/0001-Config-system-framework.patch b/camellia-server/minecraft-patches/features/0001-Config-system-framework.patch new file mode 100644 index 0000000..39f36a4 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0001-Config-system-framework.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 20:49:06 +0800 +Subject: [PATCH] Config system framework + + +diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java +index a4d608d64b7d3477c9144d93547fd3b4f39a1b02..8cb1cfefa0deb78c1773ba9913138f31ac1e5c92 100644 +--- a/net/minecraft/server/Main.java ++++ b/net/minecraft/server/Main.java +@@ -107,6 +107,7 @@ public class Main { + JvmProfiler.INSTANCE.start(Environment.SERVER); + } + ++ moe.monnairealms.camellia.config.ConfigManager.initConfigs(); // Camellia - Config system framework - pre load config file + io.papermc.paper.plugin.PluginInitializerManager.load(options); // Paper + Bootstrap.bootStrap(); + Bootstrap.validate(); +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index c6a4d52f76e10fcc57b2c49b07fbc2ef9bb90822..e305fd5850d333eb1a30572076b6475e4428f7a7 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1169,6 +1169,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Wed, 8 Jul 2026 21:50:30 +0800 +Subject: [PATCH] Configurable region format framework + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +index a814512fcfb85312474ae2c2c21443843bf57831..5734af81b1f88fde952f498059c791165a9d677f 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +@@ -8,9 +8,9 @@ public interface ChunkSystemRegionFileStorage { + + public boolean moonrise$doesRegionFileNotExistNoIO(final int chunkX, final int chunkZ); + +- public RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); ++ public moe.monnairealms.camellia.data.RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); // Camellia - Configurable region file format + +- public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; ++ public moe.monnairealms.camellia.data.RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; // Camellia - Configurable region file format + + public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite( + final int chunkX, final int chunkZ, final CompoundTag compound +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +index 3bb9b58ee97464687e348e23b99159226415c267..b6d4d9fac961aa4672ef2f4a19e2ae18986bdd29 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +@@ -1273,7 +1273,7 @@ public final class MoonriseRegionFileIO { + this.regionDataController.finishWrite(this.chunkX, this.chunkZ, writeData); + // Paper start - flush regionfiles on save + if (this.world.paperConfig().chunks.flushRegionsOnSave) { +- final RegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); ++ final moe.monnairealms.camellia.data.RegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); // Camellia - Configurable region file format + if (regionFile != null) { + regionFile.flush(); + } // else: evicted from cache, which should have called flush +@@ -1489,7 +1489,7 @@ public final class MoonriseRegionFileIO { + + public static interface IORunnable { + +- public void run(final RegionFile regionFile) throws IOException; ++ public void run(final moe.monnairealms.camellia.data.RegionFile regionFile) throws IOException; // Camellia - Configurable region file format + + } + } +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java +index 51c126735ace8fdde89ad97b5cab62f244212db0..084b64dbaa2807fa29e4ae787144fce262a3b835 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java +@@ -8,5 +8,5 @@ public interface ChunkSystemChunkBuffer { + + public void moonrise$setWriteOnClose(final boolean value); + +- public void moonrise$write(final RegionFile regionFile) throws IOException; ++ public void moonrise$write(final moe.monnairealms.camellia.data.RegionFile regionFile) throws IOException; // Camellia - Configurable region file format + } +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index e305fd5850d333eb1a30572076b6475e4428f7a7..78e9cf21fb5487e5eeec04e589c2a84c1e6b06af 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -986,10 +986,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop chunksToUpgrade) { ++public record FileToUpgrade(moe.monnairealms.camellia.data.RegionFile file, List chunksToUpgrade) { // Camellia - Configurable region file format + } +diff --git a/net/minecraft/util/worldupdate/RegionStorageUpgrader.java b/net/minecraft/util/worldupdate/RegionStorageUpgrader.java +index b228cd6c54e8c8998923ab332882d99092769c67..13ec3d38ffb6e0e87db621c9607c9dc3c6a7b31b 100644 +--- a/net/minecraft/util/worldupdate/RegionStorageUpgrader.java ++++ b/net/minecraft/util/worldupdate/RegionStorageUpgrader.java +@@ -36,7 +36,7 @@ import org.slf4j.Logger; + public class RegionStorageUpgrader { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final String NEW_DIRECTORY_PREFIX = "new_"; +- private static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mca$"); ++ private static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\." + moe.monnairealms.camellia.config.modules.function.RegionFormatConfig.regionFormat.getArgument() + "$"); // Camellia - Configurable region file format + private final DataFixer dataFixer; + private final UpgradeProgress upgradeProgress; + private final String type; +@@ -176,7 +176,8 @@ public class RegionStorageUpgrader { + int zOffset = Integer.parseInt(regex.group(2)) << 5; + List chunkPositions = Lists.newArrayList(); + +- try (RegionFile regionSource = new RegionFile(info, regionFile.toPath(), regionFolder, true)) { ++ var regionFileInfo = new moe.monnairealms.camellia.utils.RegionCreatorInfo(info, regionFile.toPath(), regionFolder, true); // Camellia - Configurable region file format ++ try (moe.monnairealms.camellia.data.RegionFile regionSource = moe.monnairealms.camellia.config.modules.function.RegionFormatConfig.regionFormat.getCreator().newFile(regionFileInfo)) { // Camellia - Configurable region file format + for (int x = 0; x < 32; x++) { + for (int z = 0; z < 32; z++) { + ChunkPos pos = new ChunkPos(x + xOffset, z + zOffset); +@@ -253,7 +254,7 @@ public class RegionStorageUpgrader { + return storage.upgradeChunkTag(chunkTag, this.defaultVersion, this.dataFixContextTag, targetVersion); + } + +- private void onFileFinished(final RegionFile regionFile) { ++ private void onFileFinished(final moe.monnairealms.camellia.data.RegionFile regionFile) { // Camellia - Configurable region file format + if (this.recreateRegionFiles) { + if (this.previousWriteFuture != null) { + this.previousWriteFuture.join(); +diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java +index 3de7fd2b084c38e72d7a6bc416880a881f514ad3..476aa2af20caeb8da293bd9e6c7467e384a2cf12 100644 +--- a/net/minecraft/world/level/chunk/storage/RegionFile.java ++++ b/net/minecraft/world/level/chunk/storage/RegionFile.java +@@ -22,7 +22,7 @@ import net.minecraft.world.level.ChunkPos; + import org.jspecify.annotations.Nullable; + import org.slf4j.Logger; + +-public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile { // Paper - rewrite chunk system ++public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile , moe.monnairealms.camellia.data.RegionFile{ // Paper - rewrite chunk system // Camellia - Configurable region file format + private static final Logger LOGGER = LogUtils.getLogger(); + public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Paper - don't write garbage data to disk if writing serialization fails + private static final int SECTOR_BYTES = 4096; +@@ -130,7 +130,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + return this.recalculateCount.get(); + } + +- boolean recalculateHeader() throws IOException { ++ public boolean recalculateHeader() throws IOException { // Camellia - Configurable region file format + if (!this.canRecalcHeader) { + return false; + } +@@ -789,7 +789,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + } + } + +- protected synchronized void write(final ChunkPos pos, final ByteBuffer data) throws IOException { ++ public synchronized void write(final ChunkPos pos, final ByteBuffer data) throws IOException { // Camellia - Configurable region file format + int offsetIndex = getOffsetIndex(pos); + int offset = this.offsets.get(offsetIndex); + int sectorNumber = getSectorNumber(offset); +@@ -907,7 +907,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + } + + @Override +- public final void moonrise$write(final RegionFile regionFile) throws IOException { ++ public final void moonrise$write(final moe.monnairealms.camellia.data.RegionFile regionFile) throws IOException { // Camellia - Configurable region file format + regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count)); + } + // Paper end - rewrite chunk system +@@ -973,11 +973,11 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + return (x & 31) + (z & 31) * 32; + } + +- synchronized boolean isOversized(int x, int z) { ++ public synchronized boolean isOversized(int x, int z) { // Camellia - Configurable region file format + return this.oversized[getChunkIndex(x, z)] == 1; + } + +- synchronized void setOversized(int x, int z, boolean oversized) throws IOException { ++ public synchronized void setOversized(int x, int z, boolean oversized) throws IOException { // Camellia - Configurable region file format + final int offset = getChunkIndex(x, z); + boolean previous = this.oversized[offset] == 1; + this.oversized[offset] = (byte) (oversized ? 1 : 0); +@@ -1016,7 +1016,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt"); + } + +- synchronized net.minecraft.nbt.CompoundTag getOversizedData(int x, int z) throws IOException { ++ public synchronized net.minecraft.nbt.CompoundTag getOversizedData(int x, int z) throws IOException { // Camellia - Configurable region file format + Path file = getOversizedFile(x, z); + try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new java.util.zip.InflaterInputStream(Files.newInputStream(file))))) { + return net.minecraft.nbt.NbtIo.read((java.io.DataInput) out); +diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +index 63b40c420030d935bb81a219fb33defe0946e21f..cc9913016c6d985644f8af47986db77006d31e77 100644 +--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -19,7 +19,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper + public static final String ANVIL_EXTENSION = ".mca"; + private static final int MAX_CACHE_SIZE = 256; +- private final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); ++ private final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); // Camellia - Configurable region file format + private final RegionStorageInfo info; + private final Path folder; + private final boolean sync; +@@ -30,7 +30,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + @Nullable + public static ChunkPos getRegionFileCoordinates(Path file) { + String fileName = file.getFileName().toString(); +- if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) { ++ if (!fileName.startsWith("r.") || !fileName.endsWith(getExtensionName())) { // Camellia - Configurable region file format + return null; + } + +@@ -55,8 +55,32 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + private static final int MAX_NON_EXISTING_CACHE = 1024 * 4; + private final it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet nonExistingRegionFiles = new it.unimi.dsi.fastutil.longs.LongLinkedOpenHashSet(); + private static String getRegionFileName(final int chunkX, final int chunkZ) { +- return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + ".mca"; ++ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + getExtensionName(); // Camellia - Configurable region file format + } ++ // Camellia start - Configurable region file format ++ public static moe.monnairealms.camellia.data.RegionFile createNew(RegionStorageInfo info, Path filePath, Path folder, boolean sync) throws IOException{ ++ final moe.monnairealms.camellia.enums.EnumRegionFormat regionFormat = moe.monnairealms.camellia.config.modules.function.RegionFormatConfig.regionFormat; ++ final String fullFileName = filePath.getFileName().toString(); ++ final String[] fullNameSplit = fullFileName.split("\\."); ++ final String extensionName = fullNameSplit[fullNameSplit.length - 1]; ++ ++ if (!regionFormat.getArgument().equalsIgnoreCase(extensionName)) { ++ // raise a delayed crash ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ throw new RuntimeException("Invalid region file format: " + extensionName + " expected " + regionFormat.getArgument()); ++ }); ++ ++ ++ throw new IOException("Invalid region file format: " + extensionName + " expected " + regionFormat.getArgument()); ++ } ++ ++ return regionFormat.getCreator().newFile(new moe.monnairealms.camellia.utils.RegionCreatorInfo(info, filePath, folder, sync)); ++ } ++ ++ public static String getExtensionName() { ++ return "." + moe.monnairealms.camellia.config.modules.function.RegionFormatConfig.regionFormat.getArgument(); ++ } ++ // Camellia end + + private boolean doesRegionFilePossiblyExist(final long position) { + synchronized (this.nonExistingRegionFiles) { +@@ -90,15 +114,15 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + } + + @Override +- public synchronized final RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { ++ public synchronized final moe.monnairealms.camellia.data.RegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { // Camellia - Configurable region file format + return this.regionCache.getAndMoveToFirst(ChunkPos.pack(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT)); + } + + @Override +- public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { ++ public synchronized final moe.monnairealms.camellia.data.RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { // Camellia - Configurable region file format + final long key = ChunkPos.pack(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + +- RegionFile ret = this.regionCache.getAndMoveToFirst(key); ++ moe.monnairealms.camellia.data.RegionFile ret = this.regionCache.getAndMoveToFirst(key); // Camellia - Configurable region file format + if (ret != null) { + return ret; + } +@@ -124,7 +148,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + FileUtil.createDirectoriesSafe(this.folder); + +- ret = new RegionFile(this.info, regionPath, this.folder, this.sync); ++ ret = this.createNew(this.info, regionPath, this.folder, this.sync); // Camellia - Configurable region file format + + this.regionCache.putAndMoveToFirst(key, ret); + +@@ -143,7 +167,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + } + + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); +- final RegionFile regionFile = this.getRegionFile(pos); ++ final moe.monnairealms.camellia.data.RegionFile regionFile = this.getRegionFile(pos); // Camellia - Configurable region file format + + // note: not required to keep regionfile loaded after this call, as the write param takes a regionfile as input + // (and, the regionfile parameter is unused for writing until the write call) +@@ -177,7 +201,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + ) throws IOException { + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + if (writeData.result() == ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.DELETE) { +- final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); ++ final moe.monnairealms.camellia.data.RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Camellia - Configurable region file format + if (regionFile != null) { + regionFile.clear(pos); + } // else: didn't exist +@@ -192,7 +216,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData moonrise$readData( + final int chunkX, final int chunkZ + ) throws IOException { +- final RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); ++ final moe.monnairealms.camellia.data.RegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Camellia - Configurable region file format + + final DataInputStream input = regionFile == null ? null : regionFile.getChunkDataInputStream(new ChunkPos(chunkX, chunkZ)); + +@@ -237,7 +261,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); + final ChunkPos headerChunkPos = SerializableChunkData.getChunkCoordinate(ret); +- final RegionFile regionFile = this.getRegionFile(pos); ++ final moe.monnairealms.camellia.data.RegionFile regionFile = this.getRegionFile(pos); // Camellia - Configurable region file format + + if (regionFile.getRecalculateCount() != readData.recalculateCount()) { + return null; +@@ -261,7 +285,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + } + // Paper end - rewrite chunk system + // Paper start - rewrite chunk system +- public RegionFile getRegionFile(ChunkPos pos) throws IOException { ++ public moe.monnairealms.camellia.data.RegionFile getRegionFile(ChunkPos pos) throws IOException { // Camellia - Configurable region file format + return this.getRegionFile(pos, false); + } + // Paper end - rewrite chunk system +@@ -273,7 +297,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + this.isChunkData = info.dfuType()[0] == net.minecraft.util.datafix.DataFixTypes.CHUNK; // Paper - recalculate region file headers + } + +- @org.jetbrains.annotations.Contract("_, false -> !null") private @Nullable RegionFile getRegionFile(final ChunkPos pos, boolean existingOnly) throws IOException { // CraftBukkit ++ @org.jetbrains.annotations.Contract("_, false -> !null") private moe.monnairealms.camellia.data.RegionFile getRegionFile(final ChunkPos pos, boolean existingOnly) throws IOException { // CraftBukkit // Camellia - Configurable region file format + // Paper start - rewrite chunk system + if (existingOnly) { + return this.moonrise$getRegionFileIfExists(pos.x(), pos.z()); +@@ -281,7 +305,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + synchronized (this) { + final long key = ChunkPos.pack(pos.x() >> REGION_SHIFT, pos.z() >> REGION_SHIFT); + +- RegionFile ret = this.regionCache.getAndMoveToFirst(key); ++ moe.monnairealms.camellia.data.RegionFile ret = this.regionCache.getAndMoveToFirst(key); // Camellia - Configurable region file format + if (ret != null) { + return ret; + } +@@ -298,7 +322,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + FileUtil.createDirectoriesSafe(this.folder); + +- ret = new RegionFile(this.info, regionPath, this.folder, this.sync); ++ ret = this.createNew(this.info, regionPath, this.folder, this.sync); // Camellia - Configurable region file format + + this.regionCache.putAndMoveToFirst(key, ret); + +@@ -312,7 +336,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + LOGGER.error("{} ({} - {},{}) Go clean it up to remove this message. /minecraft:tp {} 128 {} - DO NOT REPORT THIS TO PAPER - You may ask for help on Discord, but do not file an issue. These error messages can not be removed.", msg, file.toString().replaceAll(".+[\\\\/]", ""), x, z, x << 4, z << 4); + } + +- private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { ++ private static CompoundTag readOversizedChunk(moe.monnairealms.camellia.data.RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // Camellia - Configurable region file format + synchronized (regionfile) { + try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) { + CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x(), chunkCoordinate.z()); +@@ -346,7 +370,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + public @Nullable CompoundTag read(final ChunkPos pos) throws IOException { + // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing +- RegionFile region = this.getRegionFile(pos, true); ++ moe.monnairealms.camellia.data.RegionFile region = this.getRegionFile(pos, true); // Camellia - Configurable region file format + if (region == null) { + return null; + } +@@ -383,7 +407,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + public void scanChunk(final ChunkPos pos, final StreamTagVisitor scanner) throws IOException { + // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing +- RegionFile region = this.getRegionFile(pos, true); ++ moe.monnairealms.camellia.data.RegionFile region = this.getRegionFile(pos, true); // Camellia - Configurable region file format + if (region == null) { + return; + } +@@ -398,7 +422,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + public void write(final ChunkPos pos, final @Nullable CompoundTag value) throws IOException { + if (!SharedConstants.DEBUG_DONT_SAVE_WORLD) { +- RegionFile region = this.getRegionFile(pos, value == null); // CraftBukkit // Paper - rewrite chunk system ++ moe.monnairealms.camellia.data.RegionFile region = this.getRegionFile(pos, value == null); // CraftBukkit // Paper - rewrite chunk system // Camellia - Configurable region file format + // Paper start - rewrite chunk system + if (region == null) { + // if the RegionFile doesn't exist, no point in deleting from it +@@ -430,7 +454,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + // Paper start - rewrite chunk system + synchronized (this) { + final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); +- for (final RegionFile regionFile : this.regionCache.values()) { ++ for (final moe.monnairealms.camellia.data.RegionFile regionFile : this.regionCache.values()) { // Camellia - Configurable region file format + try { + regionFile.close(); + } catch (final IOException ex) { +@@ -446,7 +470,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + // Paper start - rewrite chunk system + synchronized (this) { + final ExceptionCollector exceptionCollector = new ExceptionCollector<>(); +- for (final RegionFile regionFile : this.regionCache.values()) { ++ for (final moe.monnairealms.camellia.data.RegionFile regionFile : this.regionCache.values()) { // Camellia - Configurable region file format + try { + regionFile.flush(); + } catch (final IOException ex) { diff --git a/camellia-server/minecraft-patches/features/0003-Correct-player-respawn-place.patch b/camellia-server/minecraft-patches/features/0003-Correct-player-respawn-place.patch new file mode 100644 index 0000000..8078b13 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0003-Correct-player-respawn-place.patch @@ -0,0 +1,54 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Wed, 8 Jul 2026 20:55:35 +0800 +Subject: [PATCH] Correct player respawn place + + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index fbe0ef19bfabfc42d9e0e08e17b08159321b3804..4c7aa11f5640e959271afd6bbae9b0ba3d998abc 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -511,8 +511,10 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + double amountX = selectMaxX - selectMinX; + double amountZ = selectMaxZ - selectMinZ; + +- int selectX = amountX < 1.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor((amountX + 1.0) * random.nextDouble() + selectMinX); +- int selectZ = amountZ < 1.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor((amountZ + 1.0) * random.nextDouble() + selectMinZ); ++ // Camellia start - Correct player respawn place ++ int selectX = amountX < 0.0 ? Mth.floor(worldBorder.getCenterX()) : (int)Mth.floor(amountX * random.nextDouble() + selectMinX); ++ int selectZ = amountZ < 0.0 ? Mth.floor(worldBorder.getCenterZ()) : (int)Mth.floor(amountZ * random.nextDouble() + selectMinZ); ++ // Camellia end - Correct player respawn place + + return new BlockPos(selectX, 0, selectZ); + } +@@ -523,10 +525,20 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + + private static BlockPos findSpawnAround(ServerLevel world, BlockPos selected) { ++ // Camellia start - Correct player respawn place ++ BlockPos inChunk = PlayerSpawnFinder.getLevelRespawnPos(world, selected.getX(), selected.getZ()); ++ if (inChunk != null) { ++ AABB checkVolume = PlayerSpawnFinder.PLAYER_DIMENSIONS.makeBoundingBox((double)inChunk.getX() + 0.5, (double)inChunk.getY(), (double)inChunk.getZ() + 0.5); ++ ++ if (world.noCollision(null, checkVolume, true)) { ++ return inChunk; ++ } ++ } ++ // Camellia end - Correct player respawn place + // try hard to find, so that we don't attempt another chunk load + for (int dz = -SPAWN_RADIUS_SELECTION_SEARCH; dz <= SPAWN_RADIUS_SELECTION_SEARCH; ++dz) { + for (int dx = -SPAWN_RADIUS_SELECTION_SEARCH; dx <= SPAWN_RADIUS_SELECTION_SEARCH; ++dx) { +- BlockPos inChunk = PlayerSpawnFinder.getLevelRespawnPos(world, selected.getX() + dx, selected.getZ() + dz); ++ inChunk = PlayerSpawnFinder.getLevelRespawnPos(world, selected.getX() + dx, selected.getZ() + dz); // Camellia - Correct player respawn place + if (inChunk == null) { + continue; + } +@@ -2018,7 +2030,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + + if (newLevel.dimension() == lastDimension) { +- this.connection.internalTeleport(PositionMoveRotation.of(transition), transition.relatives()); // CraftBukkit ++ this.connection.internalTeleport(PositionMoveRotation.of(transition), transition.relatives()); // CraftBukkit // Camellia - Correct player respawn place + this.connection.resetPosition(); + transition.postTeleportTransition().onTransition(this); + return this; diff --git a/camellia-server/minecraft-patches/features/0004-Correct-CAS-get-in-RegionizedTaskQueue.patch b/camellia-server/minecraft-patches/features/0004-Correct-CAS-get-in-RegionizedTaskQueue.patch new file mode 100644 index 0000000..ee93968 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0004-Correct-CAS-get-in-RegionizedTaskQueue.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 20:57:02 +0800 +Subject: [PATCH] Correct CAS get in RegionizedTaskQueue + + +diff --git a/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java +index d1a045d47dcd12f246d95ecdfc202da357897cbf..8c44de1f7bb80809856fd26fa94d6b94aa9963e0 100644 +--- a/io/papermc/paper/threadedregions/RegionizedTaskQueue.java ++++ b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java +@@ -533,7 +533,7 @@ public final class RegionizedTaskQueue { + } + + private ReferenceCountData getReferenceCounterVolatile() { +- return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.get(this); ++ return (ReferenceCountData)REFERENCE_COUNTER_HANDLE.getVolatile(this); // Camellia - Correct CAS get in RegionizedTaskQueue + } + + private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) { diff --git a/camellia-server/minecraft-patches/features/0005-Correct-thread-unsafe-random-sources.patch b/camellia-server/minecraft-patches/features/0005-Correct-thread-unsafe-random-sources.patch new file mode 100644 index 0000000..0acd395 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0005-Correct-thread-unsafe-random-sources.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 20:59:01 +0800 +Subject: [PATCH] Correct thread unsafe random sources + + +diff --git a/net/minecraft/world/entity/npc/wanderingtrader/WanderingTraderSpawner.java b/net/minecraft/world/entity/npc/wanderingtrader/WanderingTraderSpawner.java +index 01b3cda3da79abd30860eb33491ac00f9e958767..445858e4d39bd417e1e85bcb9bc7c1a4bc9d5fb0 100644 +--- a/net/minecraft/world/entity/npc/wanderingtrader/WanderingTraderSpawner.java ++++ b/net/minecraft/world/entity/npc/wanderingtrader/WanderingTraderSpawner.java +@@ -30,7 +30,7 @@ public class WanderingTraderSpawner implements CustomSpawner { + private static final int SPAWN_CHANCE_INCREASE = 25; + private static final int SPAWN_ONE_IN_X_CHANCE = 10; + private static final int NUMBER_OF_SPAWN_ATTEMPTS = 10; +- private final RandomSource random = RandomSource.create(); ++ private final RandomSource random = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Camellia - Correct thread unsafe random sources + private final SavedDataStorage savedDataStorage; + // Folia - moved to global data + +diff --git a/net/minecraft/world/level/block/entity/EnchantingTableBlockEntity.java b/net/minecraft/world/level/block/entity/EnchantingTableBlockEntity.java +index 22b9f70f76ff8f592359e958fa439e9806a29fcc..45fcc35a3e8c4b80b53c96dc810bd51fb07e3dee 100644 +--- a/net/minecraft/world/level/block/entity/EnchantingTableBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/EnchantingTableBlockEntity.java +@@ -28,7 +28,7 @@ public class EnchantingTableBlockEntity extends BlockEntity implements Nameable + public float rot; + public float oRot; + public float tRot; +- private static final RandomSource RANDOM = RandomSource.create(); ++ private static final RandomSource RANDOM = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Camellia - Correct thread unsafe random sources + private @Nullable Component name; + + public EnchantingTableBlockEntity(final BlockPos worldPosition, final BlockState blockState) { diff --git a/camellia-server/minecraft-patches/features/0006-Correct-portal-logic-of-some-projectile-entities-Sho.patch b/camellia-server/minecraft-patches/features/0006-Correct-portal-logic-of-some-projectile-entities-Sho.patch new file mode 100644 index 0000000..522f038 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0006-Correct-portal-logic-of-some-projectile-entities-Sho.patch @@ -0,0 +1,59 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:01:20 +0800 +Subject: [PATCH] Correct portal logic of some projectile entities (Should not + be ticking anymore after portal logics) + + +diff --git a/net/minecraft/world/entity/projectile/ShulkerBullet.java b/net/minecraft/world/entity/projectile/ShulkerBullet.java +index d5506bfc84912ec50cd5391225b9b5291435d3aa..994c069b0bcb8f0f1d4269d7a74d6fe714246c50 100644 +--- a/net/minecraft/world/entity/projectile/ShulkerBullet.java ++++ b/net/minecraft/world/entity/projectile/ShulkerBullet.java +@@ -229,7 +229,7 @@ public class ShulkerBullet extends Projectile { + this.setPos(this.position().add(movement)); + this.applyEffectsFromBlocks(); + if (this.portalProcess != null && this.portalProcess.isInsidePortalThisTick()) { +- this.handlePortal(); ++ if (this.handlePortal()) return; // Camellia - Correct portal logic of some projectile entities (Should not be ticking anymore after portal logics) + } + + if (hitResult != null && this.isAlive() && hitResult.getType() != HitResult.Type.MISS) { +diff --git a/net/minecraft/world/entity/projectile/arrow/AbstractArrow.java b/net/minecraft/world/entity/projectile/arrow/AbstractArrow.java +index 88c2f0f927bd8376522eb9aed184abd6f09230f6..e54267ecf448a1b7fd6c433afba2d7ee122e07f5 100644 +--- a/net/minecraft/world/entity/projectile/arrow/AbstractArrow.java ++++ b/net/minecraft/world/entity/projectile/arrow/AbstractArrow.java +@@ -276,7 +276,7 @@ public abstract class AbstractArrow extends Projectile { + .clipIncludingBorder( + new ClipContext(originalPosition, originalPosition.add(movement), ClipContext.Block.COLLIDER, ClipContext.Fluid.NONE, this) + ); +- this.stepMoveAndHit(blockHitResult); ++ if (this.stepMoveAndHit(blockHitResult)) return; // Camellia - Correct portal logic of some projectile entities (Should not be ticking anymore after portal logics) + } else { + this.setPos(originalPosition.add(movement)); + this.applyEffectsFromBlocks(); +@@ -299,7 +299,7 @@ public abstract class AbstractArrow extends Projectile { + return 0.99F; + } + +- private void stepMoveAndHit(final BlockHitResult blockHitResult) { ++ private boolean stepMoveAndHit(final BlockHitResult blockHitResult) { // Camellia - Correct portal logic of some projectile entities + while (this.isAlive()) { + Vec3 initialPosition = this.position(); + ArrayList entitiesHit = new ArrayList<>(this.findHitEntities(initialPosition, blockHitResult.getLocation())); +@@ -309,7 +309,7 @@ public abstract class AbstractArrow extends Projectile { + this.setPos(nextLocation); + this.applyEffectsFromBlocks(initialPosition, nextLocation); + if (this.portalProcess != null && this.portalProcess.isInsidePortalThisTick()) { +- this.handlePortal(); ++ if (this.handlePortal()) return true; // Camellia - Correct portal logic of some projectile entities (Should not be ticking anymore after portal logics) + } + + if (entitiesHit.isEmpty()) { +@@ -327,6 +327,7 @@ public abstract class AbstractArrow extends Projectile { + break; + } + } ++ return false; // Camellia - Correct portal logic of some projectile entities + } + + private ProjectileDeflection hitTargetsOrDeflectSelf(final Collection entityHitResults) { diff --git a/camellia-server/minecraft-patches/features/0007-Do-not-enable-any-debug-subscriptions.patch b/camellia-server/minecraft-patches/features/0007-Do-not-enable-any-debug-subscriptions.patch new file mode 100644 index 0000000..d89bf5c --- /dev/null +++ b/camellia-server/minecraft-patches/features/0007-Do-not-enable-any-debug-subscriptions.patch @@ -0,0 +1,20 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:02:54 +0800 +Subject: [PATCH] Do not enable any debug subscriptions + +Really this would really crash the server by accident when the operators used F3 + J or toggled the debug synchronizer + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 4c7aa11f5640e959271afd6bbae9b0ba3d998abc..625c013e99f41a7d64e301100139f1bf4ee3e2e1 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -3504,7 +3504,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + + public Set> debugSubscriptions() { +- return !this.server.debugSubscribers().hasRequiredPermissions(this) ? Set.of() : this.requestedDebugSubscriptions; ++ return Set.of(); // Camellia - Do not enable any debug subscriptions + } + + public record RespawnConfig(LevelData.RespawnData respawnData, boolean forced) { diff --git a/camellia-server/minecraft-patches/features/0008-Do-not-load-too-far-poi-chunk-in-poi-compete-scan.patch b/camellia-server/minecraft-patches/features/0008-Do-not-load-too-far-poi-chunk-in-poi-compete-scan.patch new file mode 100644 index 0000000..8a13588 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0008-Do-not-load-too-far-poi-chunk-in-poi-compete-scan.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 17 Apr 2026 23:25:45 +0800 +Subject: [PATCH] Do not load too far poi chunk in poi compete scan + +Beeeeeeeeeeeeeeee + +diff --git a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java +index 53131c1b8d5562f9a1b45dfab4fdbdd37b655139..f4c171c06ac2a3d45c91365781d0789e4ee74fe8 100644 +--- a/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java ++++ b/net/minecraft/world/entity/ai/behavior/PoiCompetitorScan.java +@@ -24,8 +24,18 @@ public class PoiCompetitorScan { + return true; + } + // Folia end - region threading +- level.getPoiManager() +- .getType(pos.pos()) ++ // Camellia start - Do not load too far poi chunk in poi compete scan ++ var blockPosOfJobSite = pos.pos(); ++ var sectionPosOfJobSite = net.minecraft.core.SectionPos.asLong(blockPosOfJobSite); ++ var poiManager = level.getPoiManager(); ++ // we don't care if we should clear the memory of JOB_SITE ++ // as it will be automatically removed in SetWalkTargetFromBlockMemory ++ // so simply break down if it's not loaded ++ var poiChunk = moe.monnairealms.camellia.config.modules.fixes.POIRangeFixes.doNotCompetePOIIfUnloaded ? poiManager.get(sectionPosOfJobSite) : poiManager.getOrLoad(sectionPosOfJobSite); ++ poiChunk.flatMap(poiSection -> poiSection.getType(blockPosOfJobSite)) ++ // Camellia end - Do not load too far poi chunk in poi compete scan ++ /*level.getPoiManager() // Camellia - Do not load too far poi chunk in poi compete scan ++ .getType(pos.pos())*/ // Camellia - Do not load too far poi chunk in poi compete scan + .ifPresent( + // Paper start - Improve performance of PoiCompetitorScan by unrolling stream + // The previous logic used Stream#reduce to simulate a form of single-iteration bubble sort diff --git a/camellia-server/minecraft-patches/features/0009-Entity-portal-teleport-speed-fix.patch b/camellia-server/minecraft-patches/features/0009-Entity-portal-teleport-speed-fix.patch new file mode 100644 index 0000000..3a71df6 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0009-Entity-portal-teleport-speed-fix.patch @@ -0,0 +1,67 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:32:08 +0800 +Subject: [PATCH] Entity portal-teleport speed fix + + +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 3db75ac3f7f111b94178cc29684f24dd55ac5847..39e511061d11039a1027320de624be07e34a2978 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -1491,7 +1491,35 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + foliaProfiler.startTimer(timerId); + try { + // Folia end - profiler ++ // Camellia start - Entity portal-teleport speed fix + if (isActive) { // Paper - EAR 2 ++ if (!(entity instanceof Player) && entity.teleportTickType == 2) { // Camellia - after portal compensate tick ++ entity.tick(); ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { ++ // removed from region while ticking ++ return; ++ } ++ if (entity.handlePortal()) { ++ // portalled ++ return; ++ } ++ entity.tick(); ++ entity.teleportTickType = 0; ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { ++ return; ++ } ++ if (entity.handlePortal()) { ++ return; ++ } ++ } else if (!(entity instanceof Player) && entity.teleportTickType == 1) { // Camellia - portal teleport only ++ entity.teleportTickType++; ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { ++ return; ++ } ++ if (entity.handlePortal()) { ++ return; ++ } ++ } else { + entity.tick(); + // Folia start - region threading + if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { +@@ -1502,6 +1530,8 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + // portalled + return; + } ++ } ++ // Camellia end - Entity portal-teleport speed fix + // Folia end - region threading + } else {entity.inactiveTick();} // Paper - EAR 2 + profiler.pop(); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 2a8a7aeddffdfed31cd6e1162c0756986049fbab..a3f473cab6a37e65de233c7f77d9de7c967fcdd0 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -387,6 +387,7 @@ public abstract class Entity + public long activatedTick = Integer.MIN_VALUE; + public boolean isTemporarilyActive; + public long activatedImmunityTick = Integer.MIN_VALUE; ++ public int teleportTickType = 0; // Camellia - Entity portal-teleport speed fix + + public void inactiveTick() { + } diff --git a/camellia-server/minecraft-patches/features/0010-Fix-region-threading-with-entity-ai-data-access.patch b/camellia-server/minecraft-patches/features/0010-Fix-region-threading-with-entity-ai-data-access.patch new file mode 100644 index 0000000..24115ad --- /dev/null +++ b/camellia-server/minecraft-patches/features/0010-Fix-region-threading-with-entity-ai-data-access.patch @@ -0,0 +1,289 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:12:31 +0800 +Subject: [PATCH] Fix region threading with entity ai data access + + +diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java +index 3b1c3e84ab24b6e3a0e4d129b3617b393b6d6651..c6ce88fb63e0d40ed011958a1afd36c645d965c8 100644 +--- a/net/minecraft/world/entity/Mob.java ++++ b/net/minecraft/world/entity/Mob.java +@@ -299,6 +299,11 @@ public abstract class Mob extends LivingEntity implements Targeting, EquipmentUs + if (Objects.equals(currentTarget, target)) { + return false; + } ++ // Camellia - Fix region threading with entity ai data access ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(target)) { ++ return false; ++ } ++ // Camellia end + LivingEntity originalTarget = target; + target = asValidTarget(target); + if (reason != null) { +diff --git a/net/minecraft/world/entity/ai/Brain.java b/net/minecraft/world/entity/ai/Brain.java +index e097bc1268b4050e3948413c52dea69950858458..977a31f995799cf44fa59cc1db73571edbcf36ff 100644 +--- a/net/minecraft/world/entity/ai/Brain.java ++++ b/net/minecraft/world/entity/ai/Brain.java +@@ -383,7 +383,7 @@ public class Brain { + } + + public void tick(final ServerLevel level, final E body) { +- this.forgetOutdatedMemories(); ++ this.forgetOutdatedMemories(body); // Camellia - Fix region threading with entity ai data access + this.tickSensors(level, body); + this.startEachNonRunningBehavior(level, body); + this.tickEachRunningBehavior(level, body); +@@ -395,8 +395,8 @@ public class Brain { + } + } + +- private void forgetOutdatedMemories() { +- this.memories.values().forEach(MemorySlot::tick); ++ private void forgetOutdatedMemories(E body) { // Camellia - Fix region threading with entity ai data access ++ this.memories.values().forEach(slot -> slot.tick(body)); // Camellia - Fix region threading with entity ai data access + } + + public void stopAll(final ServerLevel level, final E body) { +diff --git a/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java b/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java +index f744ecd20dc70786311a7162b52aa4ceca0717db..c8373151e280764e3175a263d201c078cca6fac8 100644 +--- a/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java ++++ b/net/minecraft/world/entity/ai/behavior/BehaviorUtils.java +@@ -81,6 +81,11 @@ public class BehaviorUtils { + public static void setWalkAndLookTargetMemories( + final LivingEntity walker, final PositionTracker target, final float speedModifier, final int closeEnoughDistance + ) { ++ // Camellia - Fix region threading with entity ai data access ++ if (!target.checkThread(walker.level())) { ++ return; ++ } ++ // Camellia end + WalkTarget walkTarget = new WalkTarget(target, speedModifier, closeEnoughDistance); + walker.getBrain().setMemory(MemoryModuleType.LOOK_TARGET, target); + walker.getBrain().setMemory(MemoryModuleType.WALK_TARGET, walkTarget); +diff --git a/net/minecraft/world/entity/ai/behavior/BlockPosTracker.java b/net/minecraft/world/entity/ai/behavior/BlockPosTracker.java +index 68251875edfa47ac34c64f99510998ad4bbb14b4..cb386eac7855d53c8c1df36305b6b0156cccd62d 100644 +--- a/net/minecraft/world/entity/ai/behavior/BlockPosTracker.java ++++ b/net/minecraft/world/entity/ai/behavior/BlockPosTracker.java +@@ -37,4 +37,11 @@ public class BlockPosTracker implements PositionTracker { + public String toString() { + return "BlockPosTracker{blockPos=" + this.blockPos + ", centerPosition=" + this.centerPosition + "}"; + } ++ ++ // Camellia start - Fix region threading with entity ai data access ++ @Override ++ public boolean checkThread(net.minecraft.world.level.Level currOwnedByLevel) { ++ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(currOwnedByLevel, this.blockPos); ++ } ++ // Camellia end + } +diff --git a/net/minecraft/world/entity/ai/behavior/EntityTracker.java b/net/minecraft/world/entity/ai/behavior/EntityTracker.java +index 3ac52b025ac3e1a3f9135b8e593a385a847afe0f..61cdd99e21cf894cf73c1b90c1d4c45a94d222ff 100644 +--- a/net/minecraft/world/entity/ai/behavior/EntityTracker.java ++++ b/net/minecraft/world/entity/ai/behavior/EntityTracker.java +@@ -55,4 +55,11 @@ public class EntityTracker implements PositionTracker { + public String toString() { + return "EntityTracker for " + this.entity; + } ++ ++ // Camellia start - Fix region threading with entity ai data access ++ @Override ++ public boolean checkThread(net.minecraft.world.level.Level currOwnedByLevel) { ++ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.entity); ++ } ++ // Camellia end + } +diff --git a/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java b/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java +index b40da004b5281a041ee896ae176bfb9c4660f353..1e2a8c2beaec24f04f70baee9eb6a678ce325efa 100644 +--- a/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java ++++ b/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java +@@ -117,7 +117,9 @@ public class MoveToTargetSink extends Behavior { + + private boolean tryComputePath(final Mob body, final WalkTarget walkTarget, final long timestamp) { + BlockPos targetPos = walkTarget.getTarget().currentBlockPosition(); ++ if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(body.level(), targetPos)) // Camellia - Fix region threading with entity ai data access + this.path = body.getNavigation().createPath(targetPos, 0); ++ else this.path = null; // Camellia - Fix region threading with entity ai data access + this.speedModifier = walkTarget.getSpeedModifier(); + Brain brain = body.getBrain(); + if (this.reachedTarget(body, walkTarget)) { +diff --git a/net/minecraft/world/entity/ai/behavior/PositionTracker.java b/net/minecraft/world/entity/ai/behavior/PositionTracker.java +index ce6cf5ecfb190428e3ef9b7dd39c98e3d27a7b9d..2194a77e1746c061e294840ba9a3b8d8604ff0b9 100644 +--- a/net/minecraft/world/entity/ai/behavior/PositionTracker.java ++++ b/net/minecraft/world/entity/ai/behavior/PositionTracker.java +@@ -10,4 +10,6 @@ public interface PositionTracker { + BlockPos currentBlockPosition(); + + boolean isVisibleBy(final LivingEntity body); ++ ++ boolean checkThread(net.minecraft.world.level.Level currOwnedByLevel); // Camellia - Fix region threading with entity ai data access + } +diff --git a/net/minecraft/world/entity/ai/behavior/SleepInBed.java b/net/minecraft/world/entity/ai/behavior/SleepInBed.java +index 16017d819c28b077f732e2ef571eac179d24e323..7d0f7a42c26fe1e236b0b9696b2b962281dd6c70 100644 +--- a/net/minecraft/world/entity/ai/behavior/SleepInBed.java ++++ b/net/minecraft/world/entity/ai/behavior/SleepInBed.java +@@ -49,6 +49,11 @@ public class SleepInBed extends Behavior { + return false; + } + ++ // Camellia - Fix region threading with entity ai data access ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, target.pos())) { ++ return false; ++ } ++ // Camellia end + Optional lastWokenMemory = brain.getMemory(MemoryModuleType.LAST_WOKEN); + if (lastWokenMemory.isPresent()) { + long timeSinceLastWoken = level.getGameTime() - lastWokenMemory.get(); +@@ -56,7 +61,6 @@ public class SleepInBed extends Behavior { + return false; + } + } +- + BlockState blockState = level.getBlockStateIfLoaded(target.pos()); // Paper - Prevent sync chunk loads when villagers try to find beds + if (blockState == null) return false; // Paper - Prevent sync chunk loads when villagers try to find beds + return target.pos().closerToCenterThan(body.position(), 2.0) && blockState.is(BlockTags.BEDS) && !blockState.getValue(BedBlock.OCCUPIED); +diff --git a/net/minecraft/world/entity/ai/memory/MemorySlot.java b/net/minecraft/world/entity/ai/memory/MemorySlot.java +index 88a89b4c72cc99dc89d3f3cc928b2dfda4125759..af210abbe1d4b75e1c6828cb25181c4caa0eca35 100644 +--- a/net/minecraft/world/entity/ai/memory/MemorySlot.java ++++ b/net/minecraft/world/entity/ai/memory/MemorySlot.java +@@ -13,7 +13,7 @@ public class MemorySlot { + this.timeToLive = timeToLive; + } + +- public void tick() { ++ public void tick(net.minecraft.world.entity.Entity owner) { // Camellia - Fix region threading with entity ai data access + if (this.hasValue() && this.canExpire()) { + if (this.hasExpired()) { + this.clear(); +@@ -21,6 +21,41 @@ public class MemorySlot { + this.timeToLive--; + } + } ++ // Camellia start - Fix region threading with entity ai data access ++ final net.minecraft.world.level.Level ownerLevel = owner.level(); ++ ++ // type: entity ++ if (moe.monnairealms.camellia.config.modules.fixes.ForceCleanupEntityBrainMemoryConfig.enabledForEntity && this.value instanceof net.minecraft.world.entity.Entity entity) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(entity)) { ++ this.clear(); ++ } ++ } ++ ++ // type: block_pos ++ if (moe.monnairealms.camellia.config.modules.fixes.ForceCleanupEntityBrainMemoryConfig.enabledForBlockPos && this.value instanceof net.minecraft.core.BlockPos blockPos) { ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(ownerLevel, blockPos)) { ++ this.clear(); ++ } ++ } ++ ++ ++ //type: position_tracker and walk_target ++ if (moe.monnairealms.camellia.config.modules.fixes.ForceCleanupEntityBrainMemoryConfig.enabledForPositionTracker) { ++ net.minecraft.world.entity.ai.behavior.PositionTracker tracker = null; ++ ++ if (value instanceof net.minecraft.world.entity.ai.behavior.PositionTracker positionTracker) { ++ tracker = positionTracker; ++ } ++ ++ if (value instanceof net.minecraft.world.entity.ai.memory.WalkTarget walkTarget) { ++ tracker = walkTarget.getTarget(); ++ } ++ ++ if (tracker != null && !tracker.checkThread(owner.level())) { ++ this.clear(); ++ } ++ } ++ // Camellia end + } + + public static MemorySlot create() { +diff --git a/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java b/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java +index e44814cfb6afb594456b8215bd13a92e93de2c85..e0ca242da89d06005da692172a65a37377de68aa 100644 +--- a/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java ++++ b/net/minecraft/world/entity/ai/navigation/FlyingPathNavigation.java +@@ -60,6 +60,17 @@ public class FlyingPathNavigation extends PathNavigation { + + if (!this.isDone()) { + Vec3 target = this.path.getNextEntityPos(this.mob); ++ // Camellia - Fix region threading with entity ai data access ++ if (moe.monnairealms.camellia.config.modules.fixes.PathfindingFixesConfig.breakDownPathfindingWhenOutOfRegion) { ++ // we assume that: ++ // 1. The code above doesn't touch the 'main thread context' with the position from 'this.path' ++ // 2. The pathfinder could correctly recompute or discard the incorrect target position and this situation is happening rarely ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.mob.level(), target)) { ++ this.hasDelayedRecomputation = true; ++ return; ++ } ++ } ++ // Camellia end + this.mob.getMoveControl().setWantedPosition(target.x, target.y, target.z, this.speedModifier); + } + } +diff --git a/net/minecraft/world/entity/ai/navigation/PathNavigation.java b/net/minecraft/world/entity/ai/navigation/PathNavigation.java +index 2ea1ec39a37899ae1510d0036aa670e758077537..18069d0b38ee08d798edc434875f6b5ca8bb15b5 100644 +--- a/net/minecraft/world/entity/ai/navigation/PathNavigation.java ++++ b/net/minecraft/world/entity/ai/navigation/PathNavigation.java +@@ -188,6 +188,18 @@ public abstract class PathNavigation { + } + } + // Paper end - EntityPathfindEvent ++ // Camellia start - Fix region threading with entity ai data access ++ if (moe.monnairealms.camellia.config.modules.fixes.PathfindingFixesConfig.doNotPathfindToNotOwnedTargets) { ++ // filter the targets not owned by current region ++ targets = new java.util.HashSet<>(targets); // well no idea about how to determine if this should be copied to a modifiable one ++ targets.removeIf(pos -> !ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.mob.level(), pos)); ++ ++ // return if no available (observe the logic in the first if block) ++ if (targets.isEmpty()) { ++ return null; ++ } ++ } ++ // Camellia end + ProfilerFiller profiler = Profiler.get(); + profiler.push("pathfind"); + BlockPos fromPos = above ? this.mob.blockPosition().above() : this.mob.blockPosition(); +diff --git a/net/minecraft/world/entity/animal/allay/AllayAi.java b/net/minecraft/world/entity/animal/allay/AllayAi.java +index c3667e7997551e0f5ce63bc6ee890082d1431400..95c93df76eeec7eb3f19380efb1e2876b0625b2e 100644 +--- a/net/minecraft/world/entity/animal/allay/AllayAi.java ++++ b/net/minecraft/world/entity/animal/allay/AllayAi.java +@@ -112,6 +112,16 @@ public class AllayAi { + Optional likedNoteblockPos = brain.getMemory(MemoryModuleType.LIKED_NOTEBLOCK_POSITION); + if (likedNoteblockPos.isPresent()) { + GlobalPos position = likedNoteblockPos.get(); ++ // Camellia - Fix region threading with entity ai data access ++ final Level targetLevel = allay.level().getServer().getLevel(position.dimension()); ++ final BlockPos targetPos = position.pos(); ++ ++ // thread checks ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(targetLevel, targetPos)) { ++ brain.eraseMemory(MemoryModuleType.LIKED_NOTEBLOCK_POSITION); // The memory value is not being belong to current tick region anymore ++ return Optional.empty(); ++ } ++ // Camellia + if (shouldDepositItemsAtLikedNoteblock(allay, brain, position)) { + return Optional.of(new BlockPosTracker(position.pos().above())); + } +diff --git a/net/minecraft/world/entity/animal/sniffer/Sniffer.java b/net/minecraft/world/entity/animal/sniffer/Sniffer.java +index d5394ae7ce56555ad21aeafb2d78c291c62e2988..c49b0be376f29d21e81680e48659483f0d84b6f1 100644 +--- a/net/minecraft/world/entity/animal/sniffer/Sniffer.java ++++ b/net/minecraft/world/entity/animal/sniffer/Sniffer.java +@@ -279,8 +279,18 @@ public class Sniffer extends Animal { + + private boolean canDig(final BlockPos position) { + return this.level().getBlockState(position).is(BlockTags.SNIFFER_DIGGABLE_BLOCK) +- && this.getExploredPositions().noneMatch(explored -> GlobalPos.of(this.level().dimension(), position).equals(explored)) +- && Optional.ofNullable(this.getNavigation().createPath(position, 1)).map(Path::canReach).orElse(false); ++ && this.getExploredPositions().noneMatch(explored -> { // Camellia - Fix region threading with entity ai data access ++ // thread checks ++ final Level targetLevel = net.minecraft.server.MinecraftServer.getServer().getLevel(explored.dimension()); ++ final BlockPos targetPos = explored.pos(); ++ ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(targetLevel, targetPos)) { ++ return false; ++ } ++ ++ return GlobalPos.of(this.level().dimension(), position).equals(explored); // Original logic ++ }) // Camellia end ++ && Optional.ofNullable(this.getNavigation().createPath(position, 1)).map(Path::canReach).orElse(false); + } + + private void dropSeed() { diff --git a/camellia-server/minecraft-patches/features/0011-Fix-off-region-leashing.patch b/camellia-server/minecraft-patches/features/0011-Fix-off-region-leashing.patch new file mode 100644 index 0000000..61b5904 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0011-Fix-off-region-leashing.patch @@ -0,0 +1,35 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:13:39 +0800 +Subject: [PATCH] Fix off region leashing + + +diff --git a/net/minecraft/world/entity/Leashable.java b/net/minecraft/world/entity/Leashable.java +index 98034a4c96bf2972316078d3b3a49140921aab50..ba4ee88ada40997759f4650b8d4aa9ecce0fab1d 100644 +--- a/net/minecraft/world/entity/Leashable.java ++++ b/net/minecraft/world/entity/Leashable.java +@@ -95,10 +95,24 @@ public interface Leashable { + if (leashUuid.isPresent()) { + Entity leasher = serverLevel.getEntity(leashUuid.get()); + if (leasher != null) { ++ // Camellia start - Fix off region leashing ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(leasher)) { ++ entity.spawnAtLocation(serverLevel, Items.LEAD); ++ entity.setLeashData(null); ++ return; ++ } ++ // Camellia end + setLeashedTo(entity, leasher, true); + return; + } + } else if (pos.isPresent()) { ++ // Camellia start - Fix off region leashing ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(serverLevel, pos.get())) { ++ entity.spawnAtLocation(serverLevel, Items.LEAD); ++ entity.setLeashData(null); ++ return; ++ } ++ // Camellia end + setLeashedTo(entity, LeashFenceKnotEntity.getOrCreateKnot(serverLevel, pos.get()), true); + return; + } diff --git a/camellia-server/minecraft-patches/features/0012-Fix-unpatched-task-returning-of-ServerConfigurationP.patch b/camellia-server/minecraft-patches/features/0012-Fix-unpatched-task-returning-of-ServerConfigurationP.patch new file mode 100644 index 0000000..9b1930c --- /dev/null +++ b/camellia-server/minecraft-patches/features/0012-Fix-unpatched-task-returning-of-ServerConfigurationP.patch @@ -0,0 +1,30 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:15:26 +0800 +Subject: [PATCH] Fix unpatched task returning of + ServerConfigurationPacketListenerImpl#disconnectAsync + +Inspired by https://github.com/CraftCanvasMC/Canvas/blob/ver/1.21.8/canvas-server/minecraft-patches/sources/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java.patch +but Canvas loses its main thread checks so fixed that btw + +diff --git a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +index 40b4eeb561a91264b9ab6ddc49cb8a93daae84a4..efcf294b523ca6b039eb2544546bf863ba717749 100644 +--- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +@@ -290,13 +290,14 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + // Paper start + @Override + public void disconnectAsync(final net.minecraft.network.DisconnectionDetails disconnectionInfo) { +- if (this.cserver.isPrimaryThread()) { ++ if (io.papermc.paper.threadedregions.RegionizedServer.isGlobalTickThread()) { // Camellia - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync + this.disconnect(disconnectionInfo); + return; + } + + this.connection.setReadOnly(); +- this.server.scheduleOnMain(() -> { ++ // this.server.scheduleOnMain(() -> { // Camellia - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Camellia - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync + this.disconnect(disconnectionInfo); // Currently you cannot cancel disconnect during the config stage + }); + } diff --git a/camellia-server/minecraft-patches/features/0013-Fix-riding-statistics-desync.patch b/camellia-server/minecraft-patches/features/0013-Fix-riding-statistics-desync.patch new file mode 100644 index 0000000..f854e1f --- /dev/null +++ b/camellia-server/minecraft-patches/features/0013-Fix-riding-statistics-desync.patch @@ -0,0 +1,64 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:20:44 +0800 +Subject: [PATCH] Fix riding statistics desync + +Referred to: https://github.com/CraftCanvasMC/Canvas/commit/057175b0c10d5a4d1d9059fd9d077750c32633b2 + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 625c013e99f41a7d64e301100139f1bf4ee3e2e1..7ae5cd4a30e56c8877ea505287eb66e8a579d3f8 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -2537,7 +2537,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + } + +- private void checkRidingStatistics(final double dx, final double dy, final double dz) { ++ public void checkRidingStatistics(final double dx, final double dy, final double dz) { // Camellia - Fix riding statics desync - make public + if (this.isPassenger() && !didNotMove(dx, dy, dz)) { + int distance = Math.round((float)Math.sqrt(dx * dx + dy * dy + dz * dz) * 100.0F); + Entity vehicle = this.getVehicle(); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index a3f473cab6a37e65de233c7f77d9de7c967fcdd0..1bf85a60b3223c9a44bb3e3d6bffe8eb093a50e8 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4199,7 +4199,13 @@ public abstract class Entity + } + } + ++ // Camellia start - Fix riding statics desync + public void adjustRiders(boolean teleport) { ++ this.adjustRiders(teleport, false); ++ } ++ ++ public void adjustRiders(boolean teleport, boolean syncStatics) { ++ // Camellia end - Fix riding statics desync + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(this); + +@@ -4212,14 +4218,24 @@ public abstract class Entity + + for (EntityTreeNode passenger : passengers) { + queue.add(passenger); ++ // Camellia start - Fix riding statics desync ++ final double oldX = passenger.root.getX(); ++ final double oldY = passenger.root.getY(); ++ final double oldZ = passenger.root.getZ(); ++ // Camellia end - Fix riding statics desync + curr.root.positionRider(passenger.root, teleport ? Entity::snapTo : Entity::setPos); ++ // Camellia start - Fix riding statics desync ++ if (syncStatics && passenger.root instanceof net.minecraft.server.level.ServerPlayer serverPlayer) { ++ serverPlayer.checkRidingStatistics(serverPlayer.getX() - oldX, serverPlayer.getY() - oldY, serverPlayer.getZ() - oldZ); ++ } ++ // Camellia end - Fix riding statics desync + } + } + } + } + + public void repositionAllPassengers(boolean teleport) { +- this.makePassengerTree().adjustRiders(teleport); ++ this.makePassengerTree().adjustRiders(teleport, true); // Camellia - Fix riding statics desync + } + + protected EntityTreeNode makePassengerTree() { diff --git a/camellia-server/minecraft-patches/features/0014-Fix-player-auto-saving-ignores-interval.patch b/camellia-server/minecraft-patches/features/0014-Fix-player-auto-saving-ignores-interval.patch new file mode 100644 index 0000000..0fafdb8 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0014-Fix-player-auto-saving-ignores-interval.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:22:04 +0800 +Subject: [PATCH] Fix player auto saving ignores interval + + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 7ae5cd4a30e56c8877ea505287eb66e8a579d3f8..f22b5fdec6da8bcff9aef3d9fed5dbe61d39250a 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -208,7 +208,7 @@ import org.slf4j.Logger; + + public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patches.chunk_system.player.ChunkSystemServerPlayer { // Paper - rewrite chunk system + private static final Logger LOGGER = LogUtils.getLogger(); +- public static final long LAST_SAVE_ABSENT = Long.MIN_VALUE; public long lastSave = LAST_SAVE_ABSENT; // Paper // Folia - threaded regions - changed to nanoTime ++ public static final long LAST_SAVE_ABSENT = Long.MIN_VALUE; public long lastSave = System.nanoTime(); // Paper // Folia - threaded regions - changed to nanoTime // Camellia - Fix player auto saving ignores interval + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_XZ = 32; + private static final int NEUTRAL_MOB_DEATH_NOTIFICATION_RADII_Y = 10; + private static final int FLY_STAT_RECORDING_SPEED = 25; diff --git a/camellia-server/minecraft-patches/features/0015-Fix-dragon-part-desync.patch b/camellia-server/minecraft-patches/features/0015-Fix-dragon-part-desync.patch new file mode 100644 index 0000000..4db2968 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0015-Fix-dragon-part-desync.patch @@ -0,0 +1,47 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:23:24 +0800 +Subject: [PATCH] Fix dragon part desync + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +index b31b55f00e2ce1bd6c1011fe852bd1dbb37de524..4e52c3a8887781a2299852c0c0d6f0fd26040d26 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/server/ServerEntityLookup.java +@@ -97,6 +97,7 @@ public final class ServerEntityLookup extends EntityLookup { + if (entity instanceof ThrownEnderpearl enderpearl) { + this.addEnderPearl(CoordinateUtils.getChunkKey(enderpearl.chunkPosition()), enderpearl.getId()); // Folia - region threading + } ++ if (entity instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon dragon) dragon.syncDragonPartsAfterTeleportTransform(); // Camellia - Fix dragon part desync + entity.registerScheduler(); // Paper - optimise Folia entity scheduler + } + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 1bf85a60b3223c9a44bb3e3d6bffe8eb093a50e8..c5a9d380f318f176a2bf5d30a10a6d528d23bb92 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4459,6 +4459,7 @@ public abstract class Entity + Entity copy = this.getType().create(destination, EntitySpawnReason.DIMENSION_TRAVEL); + copy.restoreFrom(this); + copy.transform(pos, yaw, pitch, velocity); ++ if (copy instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon dragon) dragon.syncDragonPartsAfterTeleportTransform(); // Camellia - Fix dragon part desync + // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here + // for example, clearing of inventory after switching dimensions + this.postRemoveAfterChangingDimensions(); +diff --git a/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java b/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +index ab921ab4b4cc860abd77744f2bd201013e136e51..cf227919723aeb1d33565787996821a1968f8106 100644 +--- a/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java ++++ b/net/minecraft/world/entity/boss/enderdragon/EnderDragon.java +@@ -1011,4 +1011,12 @@ public class EnderDragon extends Mob implements Enemy { + return 500; + } + // Paper end - init expToDrop for already dying spawned dragon ++ ++ // Camellia start - Fix dragon part desync ++ public void syncDragonPartsAfterTeleportTransform() { ++ for (EnderDragonPart part : this.subEntities) { ++ this.tickPart(part, 0.0, 0.0, 0.0); // offset -> 0.0 ++ } ++ } ++ // Camellia end + } diff --git a/camellia-server/minecraft-patches/features/0016-Fix-misbehaved-ender-pearls-when-player-switched-dim.patch b/camellia-server/minecraft-patches/features/0016-Fix-misbehaved-ender-pearls-when-player-switched-dim.patch new file mode 100644 index 0000000..74b48d4 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0016-Fix-misbehaved-ender-pearls-when-player-switched-dim.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:25:15 +0800 +Subject: [PATCH] Fix misbehaved ender pearls when player switched dimension + + +diff --git a/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java b/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java +index 501a0e89cc7bcfce6793f059b080b38666e0fb64..a5870da96da005ed682bc25eac3af6c771dee851 100644 +--- a/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java ++++ b/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java +@@ -264,7 +264,7 @@ public class ThrownEnderpearl extends ThrowableItemProjectile { + int previousChunkZ = SectionPos.blockToSectionCoord(this.position().z()); + Entity owner = this.owner != null ? findOwnerIncludingDeadPlayer(serverLevel, this.owner.getUUID()) : null; + if (owner instanceof ServerPlayer serverPlayer +- && !owner.isAlive() ++ && (owner.getBukkitEntity().taskScheduler.isRetired() || serverPlayer.getHealth() <= 0.0D) // Camellia - Fix misbehaved ender pearls when player switched dimension + && !serverPlayer.wonGame + && serverPlayer.level().getGameRules().get(GameRules.ENDER_PEARLS_VANISH_ON_DEATH)) { + this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause diff --git a/camellia-server/minecraft-patches/features/0017-Fix-creative-item-picking.patch b/camellia-server/minecraft-patches/features/0017-Fix-creative-item-picking.patch new file mode 100644 index 0000000..33b97f8 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0017-Fix-creative-item-picking.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Wed, 8 Jul 2026 22:00:35 +0800 +Subject: [PATCH] Fix creative item picking + + +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index 223793bea54a483fefc66caa7e07b29ab351c339..9a75c6b38f1cabd4415067d781f6ad49f4d8ebc8 100644 +--- a/net/minecraft/world/entity/item/ItemEntity.java ++++ b/net/minecraft/world/entity/item/ItemEntity.java +@@ -420,7 +420,7 @@ public class ItemEntity extends Entity implements TraceableEntity { + Item item = itemStack.getItem(); + int orgCount = itemStack.getCount(); + // CraftBukkit start - fire PlayerPickupItemEvent +- int canHold = player.getInventory().canHold(itemStack); ++ int canHold = player.hasInfiniteMaterials() ? orgCount : player.getInventory().canHold(itemStack); // Camellia - Fix creative item picking + int remaining = orgCount - canHold; + boolean flyAtPlayer = false; // Paper + diff --git a/camellia-server/minecraft-patches/features/0018-Fix-high-velocity-moving-issue.patch b/camellia-server/minecraft-patches/features/0018-Fix-high-velocity-moving-issue.patch new file mode 100644 index 0000000..cd2bf17 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0018-Fix-high-velocity-moving-issue.patch @@ -0,0 +1,63 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:34:46 +0800 +Subject: [PATCH] Fix high velocity moving issue + +On folia, entity usually cannot move out of the tickregion, but sometimes it actually does(like some end pearl gun that can shoot an end pearl to the block faraway than 10000 blocks even more). To fix this, we added a temporary fix which teleport these entities to the destination instead running its move logics so that we could ensure anything is under control.But one thing need to consider is that teleportAsync is actually calling halfway of the entity tick and there is still something running when teleportAsync called, which is actually modified the entity in another thread, so there is still need an improvement + +Reference from : https://github.com/KaiijuMC/Kaiiju/blob/ver/1.20.1/patches/server/0040-Teleport-async-if-we-cannot-move-entity-off-main.patch + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index c5a9d380f318f176a2bf5d30a10a6d528d23bb92..5f7dad51fdca3b85f8ae164b1f58241e83ea44d5 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -1163,6 +1163,19 @@ public abstract class Entity + this.moveStartZ = this.getZ(); + this.moveVector = delta; + } ++ // Camellia start - Fix high velocity moving issue ++ // Filter the threads as it may be called by the chunk system worker thread ++ if (moe.monnairealms.camellia.config.modules.fixes.FoliaEntityMovingFixConfig.enabled && ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()){ ++ var finalPosition = delta.add(this.position); ++ // not NaN (Prevent incorrect checks under NaN minecarts) ++ if (!Double.isNaN(finalPosition.x) && !Double.isNaN(finalPosition.y) && !Double.isNaN(finalPosition.z)) { ++ // kill tick passively if it's moving out of region and we'll catch this exception ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.level,finalPosition)) { ++ throw new moe.monnairealms.camellia.utils.entity.EntityMoveOutOfRegionException(this, delta, moverType); ++ } ++ } ++ } ++ // Camellia end + try { + // Paper end - detailed watchdog information + if (this.noPhysics) { +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index a0eb1f608a99301e0197ab1d1a6fbbb0a0be4ce0..52bcb87e96f7b5997559bd532481f548be03e6da 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1571,6 +1571,25 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public void guardEntityTick(final Consumer tick, final T entity) { + try { + tick.accept(entity); ++ } catch (moe.monnairealms.camellia.utils.entity.EntityMoveOutOfRegionException moveOutOfRegionException) { // Camellia - Fix high velocity moving issue ++ // Camellia start - Fix high velocity moving issue ++ final Entity ent = moveOutOfRegionException.getEntity(); ++ var currPosition = ent.position(); ++ var toPosition = moveOutOfRegionException.getMovement().add(currPosition); ++ ++ if (moe.monnairealms.camellia.config.modules.fixes.FoliaEntityMovingFixConfig.warnOnDetected) { ++ MinecraftServer.LOGGER.warn("Entity {} with entityId {} has tried moving to another region!",ent, ent.getId()); ++ } ++ ++ ent.getBukkitEntity().taskScheduler.scheduleOrExecute(entityFresh -> entityFresh.teleportAsync( ++ (ServerLevel) entityFresh.level(), ++ toPosition, ++ entityFresh.getYRot(), entityFresh.getXRot(), ++ entityFresh.getDeltaMovement(), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.UNKNOWN, ++ Entity.TELEPORT_FLAG_LOAD_CHUNK | Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS, ++ null ++ )); ++ // Camellia end + } catch (Throwable t) { + // Paper start - Prevent block entity and entity crashes + final String msg = String.format("Entity threw exception at %s:%s,%s,%s", io.papermc.paper.util.MCUtil.getLevelName(entity.level()), entity.getX(), entity.getY(), entity.getZ()); diff --git a/camellia-server/minecraft-patches/features/0019-Fix-teleport-yam-and-pitch-were-ignored-during-telep.patch b/camellia-server/minecraft-patches/features/0019-Fix-teleport-yam-and-pitch-were-ignored-during-telep.patch new file mode 100644 index 0000000..fdee9e2 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0019-Fix-teleport-yam-and-pitch-were-ignored-during-telep.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:37:19 +0800 +Subject: [PATCH] Fix teleport yam and pitch were ignored during teleport + command + + +diff --git a/net/minecraft/server/commands/TeleportCommand.java b/net/minecraft/server/commands/TeleportCommand.java +index 13d7965fd4f99f0848b080349df41f8b1c31a19b..54a8747351fd50ddd794a777aa405431a0dc6311 100644 +--- a/net/minecraft/server/commands/TeleportCommand.java ++++ b/net/minecraft/server/commands/TeleportCommand.java +@@ -248,8 +248,8 @@ public class TeleportCommand { + // Folia start - region threading + if (true) { + Vec3 posFinal = new Vec3(x, y, z); +- Float yawFinal = Float.valueOf(newYRot); +- Float pitchFinal = Float.valueOf(newXRot); ++ Float yawFinal = Float.valueOf(newYRot + victim.getYRot()); // Camellia - Fix teleport yaw issue ++ Float pitchFinal = Float.valueOf(newXRot + victim.getXRot()); // Camellia - Fix teleport pitch issue + victim.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + nmsEntity.stopRiding(); + nmsEntity.teleportAsync( diff --git a/camellia-server/minecraft-patches/features/0020-Force-disable-builtin-spark-plugin.patch b/camellia-server/minecraft-patches/features/0020-Force-disable-builtin-spark-plugin.patch new file mode 100644 index 0000000..bc9209e --- /dev/null +++ b/camellia-server/minecraft-patches/features/0020-Force-disable-builtin-spark-plugin.patch @@ -0,0 +1,98 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 21:27:00 +0800 +Subject: [PATCH] Force disable builtin spark plugin + +The spark passed down from paper has some memory leaking issue, so we fully removed it from the code to prevent that memory leaking issue. + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 78e9cf21fb5487e5eeec04e589c2a84c1e6b06af..b01e00de94d8fa45359d3d14a8481287276adb68 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -672,8 +672,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop= emptyTickThreshold) { +- this.server.spark.tickStart(); // Paper - spark ++ if (false) this.server.spark.tickStart(); // Paper - spark // Camellia - Force disable builtin spark + if (this.emptyTicks == emptyTickThreshold) { + LOGGER.info("Server empty for {} seconds, pausing", this.pauseWhenEmptySeconds()); + this.autoSave(); +@@ -1683,7 +1683,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Wed, 8 Jul 2026 21:28:42 +0800 +Subject: [PATCH] Prevent tamable animals check can teleport in an unloaded + chunk + +Based on the implementation of Canvas(https://github.com/CraftCanvasMC/Canvas/commit/af2aacb12cbccfd328dda4961167786f5d9dad53) + +We need to check canTeleport for TamableAnimal but this logic could do the check in another tick region itself, so we need to prevent that. But it needs to push off this check to schedule to the correct tickregion, which needs costly rewriting of this logic, so we could simply allow the read when the owner's position is loaded + +diff --git a/net/minecraft/world/entity/TamableAnimal.java b/net/minecraft/world/entity/TamableAnimal.java +index 95ba821a895c7ea8fffcbf1df2e7b3c174463d9e..1e076d368f2e34b83b94e939ca9856bae4e9f86e 100644 +--- a/net/minecraft/world/entity/TamableAnimal.java ++++ b/net/minecraft/world/entity/TamableAnimal.java +@@ -318,7 +318,8 @@ public abstract class TamableAnimal extends Animal implements OwnableEntity { + return false; + } + +- BlockState blockStateBelow = this.level().getBlockState(pos.below()); ++ BlockState blockStateBelow = this.level().getBlockStateIfLoaded(pos.below()); // Camellia - Prevent tamable animals check can teleport in an unloaded chunk ++ if (blockStateBelow == null) return false; // Camellia - Prevent tamable animals check can teleport in an unloaded chunk + if (!this.canFlyToOwner() && blockStateBelow.getBlock() instanceof LeavesBlock) { + return false; + } diff --git a/camellia-server/minecraft-patches/features/0022-Kaiiju-Entity-tick-and-removal-limiter.patch b/camellia-server/minecraft-patches/features/0022-Kaiiju-Entity-tick-and-removal-limiter.patch new file mode 100644 index 0000000..86e3af4 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0022-Kaiiju-Entity-tick-and-removal-limiter.patch @@ -0,0 +1,55 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Tue, 21 Apr 2026 01:41:14 +0800 +Subject: [PATCH] Kaiiju: Entity tick and removal limiter + +Co-authored by: Xymb +As part of: Kaiiju (https://github.com/KaiijuMC/Kaiiju/blob/c2b7aec8f7b418a39a2ec408e6411e6f752379da/patches/server/0021-Entity-ticking-throttling-removal-to-prevent-lag.patch) +Licensed under: GPL-3.0 (https://github.com/KaiijuMC/Kaiiju/blob/c2b7aec8f7b418a39a2ec408e6411e6f752379da/LICENSE) + +diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java +index cb15cfbc684ae9e9d9634886f707c2df10063139..f32d118e1e925b2155cef3fcdcb1e194c541e242 100644 +--- a/io/papermc/paper/threadedregions/RegionizedWorldData.java ++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -354,6 +354,7 @@ public final class RegionizedWorldData { + private final IteratorSafeOrderedReferenceSet navigatingMobs = new IteratorSafeOrderedReferenceSet<>(); + public final ReferenceList trackerEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker + public final ReferenceList trackerUnloadedEntities = new ReferenceList<>(EMPTY_ENTITY_ARRAY); // Moonrise - entity tracker ++ public final dev.kaiijumc.kaiiju.KaiijuEntityThrottler entityThrottler = new dev.kaiijumc.kaiiju.KaiijuEntityThrottler(); // Kaiiju + + // block ticking + private final ObjectLinkedOpenHashSet blockEvents = new ObjectLinkedOpenHashSet<>(); +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 39e511061d11039a1027320de624be07e34a2978..0097c55adf5578f425b476b3498db1c701189e4b 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -910,6 +910,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + } + + foliaProfiler.startTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.ACTIVATE_ENTITIES); try { // Folia - profiler ++ if (dev.kaiijumc.kaiiju.KaiijuEntityLimits.enabled) regionizedWorldData.entityThrottler.tickLimiterStart(); // Kaiiju + io.papermc.paper.entity.activation.ActivationRange.activateEntities(this); // Paper - EAR + } finally { foliaProfiler.stopTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.ACTIVATE_ENTITIES); } // Folia - profiler + foliaProfiler.startTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.ENTITY_TICK); try { // Folia - profiler +@@ -931,6 +932,13 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + + entity.stopRiding(); + } ++ // Kaiiju start ++ if (dev.kaiijumc.kaiiju.KaiijuEntityLimits.enabled) { ++ dev.kaiijumc.kaiiju.KaiijuEntityThrottler.EntityThrottlerReturn throttle = regionizedWorldData.entityThrottler.tickLimiterShouldSkip(entity); ++ if (throttle.remove && !entity.hasCustomName()) entity.remove(Entity.RemovalReason.DISCARDED); ++ if (throttle.skip) return; ++ } ++ // Kaiiju end + + profiler.push("tick"); + this.guardEntityTick(this::tickNonPassenger, entity); +@@ -940,6 +948,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + } + } + ); ++ if (dev.kaiijumc.kaiiju.KaiijuEntityLimits.enabled) regionizedWorldData.entityThrottler.tickLimiterFinish(regionizedWorldData); // Kaiiju + } finally { foliaProfiler.stopTimer(ca.spottedleaf.leafprofiler.LProfilerRegistry.ENTITY_TICK); } // Folia - profiler + if (this.paperConfig().unsupportedSettings.ticking.blockEntities) { // Paper - option to disable ticking + profiler.popPush("blockEntities"); diff --git a/camellia-server/minecraft-patches/features/0023-Kaiiju-Vanilla-end-portal-teleportation.patch b/camellia-server/minecraft-patches/features/0023-Kaiiju-Vanilla-end-portal-teleportation.patch new file mode 100644 index 0000000..21b4641 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0023-Kaiiju-Vanilla-end-portal-teleportation.patch @@ -0,0 +1,81 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 22:35:59 +0800 +Subject: [PATCH] Kaiiju: Vanilla end portal teleportation + +Co-authored by: Sofiane H. Djerbi <46628754+kugge@users.noreply.github.com> +As part of: Kaiiju (https://github.com/KaiijuMC/Kaiiju/blob/c2b7aec8f7b418a39a2ec408e6411e6f752379da/patches/server/0024-Vanilla-end-portal-teleportation.patch) +Licensed under: GPL-3.0 (https://github.com/KaiijuMC/Kaiiju/blob/c2b7aec8f7b418a39a2ec408e6411e6f752379da/LICENSE) + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 5f7dad51fdca3b85f8ae164b1f58241e83ea44d5..5d2f1a90a8e78a95b47c580f6dc4b893dc98f067 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4640,14 +4640,18 @@ public abstract class Entity + targetPos, 16, // load 16 blocks to be safe from block physics + ca.spottedleaf.concurrentutil.util.Priority.HIGH, + (chunks) -> { +- net.minecraft.world.level.levelgen.feature.EndPlatformFeature.createEndPlatform(destination, targetPos.below(), true, null); +- ++ //net.minecraft.world.level.levelgen.feature.EndPlatformFeature.createEndPlatform(destination, targetPos.below(), true, null); // Kaiiju - Vanilla end teleportation - moved down ++ // Kaiiju start - Vanilla end teleportation ++ Vec3 finalPos; ++ if (this instanceof Player) finalPos = Vec3.atBottomCenterOf(targetPos.below()); ++ else finalPos = Vec3.atBottomCenterOf(targetPos); ++ // Kaiiju end + // the portal obsidian is placed at targetPos.y - 2, so if we want to place the entity + // on the obsidian, we need to spawn at targetPos.y - 1 + portalInfoCompletable.complete( + new net.minecraft.world.level.portal.TeleportTransition( +- destination, Vec3.atBottomCenterOf(targetPos.below()), Vec3.ZERO, Direction.WEST.toYRot(), 0.0f, +- Relative.union(Relative.DELTA, Set.of(Relative.X_ROT)), ++ destination, finalPos, this.getDeltaMovement(), Direction.WEST.toYRot(), 0.0f, // Kaiiju - Vanilla end teleportation ++ /*Relative.union(Relative.DELTA, Set.of(Relative.X_ROT))*/Set.of(), // Kaiiju - Vanilla end teleportation + TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL + ) +@@ -4662,11 +4666,15 @@ public abstract class Entity + ca.spottedleaf.concurrentutil.util.Priority.HIGH, + (chunks) -> { + BlockPos adjustedSpawn = destination.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING_NO_LEAVES, spawnPos); +- ++ // Kaiiju start - Vanilla end teleportation ++ Vec3 finalPos; ++ if (this instanceof Player) finalPos = Vec3.atBottomCenterOf(adjustedSpawn.below()); ++ else finalPos = Vec3.atBottomCenterOf(adjustedSpawn); ++ // Kaiiju end + // done + portalInfoCompletable.complete( + new net.minecraft.world.level.portal.TeleportTransition( +- destination, Vec3.atBottomCenterOf(adjustedSpawn), Vec3.ZERO, 0.0f, 0.0f, ++ destination, finalPos, this.getDeltaMovement(), 0.0f, 0.0f, // Kaiiju - Vanilla end teleportation + Relative.union(Relative.DELTA, Relative.ROTATION), + TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL +@@ -4845,6 +4853,10 @@ public abstract class Entity + return false; + } + ++ // Kaiiju start - sync end platform spawning & entity teleportation ++ final java.util.function.Consumer tpComplete = type == PortalType.END && destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END ? ++ e -> {net.minecraft.world.level.levelgen.feature.EndPlatformFeature.createEndPlatform(destination, ServerLevel.END_SPAWN_POINT.below(), true, null); if (teleportComplete != null) {teleportComplete.accept(e);}} : teleportComplete; ++ // Kaiiju end + Vec3 initialPosition = this.position(); + ChunkPos initialPositionChunk = new ChunkPos( + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(initialPosition), +@@ -4908,9 +4920,14 @@ public abstract class Entity + info.postTeleportTransition().onTransition(teleported); + } + +- if (teleportComplete != null) { ++ // Kaiiju start - vanilla end teleportation ++ /*if (teleportComplete != null) { + teleportComplete.accept(teleported); ++ }*/ ++ if (tpComplete != null){ ++ tpComplete.accept(teleported); + } ++ // Kaiiju end + } + ); + }); diff --git a/camellia-server/minecraft-patches/features/0024-Async-protocol-switching-optimization.patch b/camellia-server/minecraft-patches/features/0024-Async-protocol-switching-optimization.patch new file mode 100644 index 0000000..8b37841 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0024-Async-protocol-switching-optimization.patch @@ -0,0 +1,288 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 22:23:29 +0800 +Subject: [PATCH] Async protocol switching optimization + + +diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java +index 40f54536422fa38bd84017fc634808016b4de58b..d0dff5850717bd083b7a29104a61754ce1b2f822 100644 +--- a/net/minecraft/network/Connection.java ++++ b/net/minecraft/network/Connection.java +@@ -1067,4 +1067,127 @@ public class Connection extends SimpleChannelInboundHandler> { + } + } + // Paper end - Optimize network ++ // Camellia start - async protocol switcher ++ public void setupInboundProtocolAsync( ++ ProtocolInfo protocol, ++ T packetListener, ++ @Nullable Runnable callback, ++ boolean resumeAutoReading ++ ) { ++ this.validateListener(protocol, packetListener); ++ if (protocol.flow() != this.getReceiving()) { ++ throw new IllegalStateException("Invalid inbound protocol: " + protocol.id()); ++ } else { ++ this.packetListener = packetListener; ++ this.disconnectListener = null; ++ ++ UnconfiguredPipelineHandler.InboundConfigurationTask configMessage = UnconfiguredPipelineHandler.setupInboundProtocol(protocol); ++ BundlerInfo bundlerInfo = protocol.bundlerInfo(); ++ if (bundlerInfo != null) { ++ PacketBundlePacker newBundler = new PacketBundlePacker(bundlerInfo); ++ configMessage = configMessage.andThen(context -> context.pipeline().addAfter("decoder", "bundler", newBundler)); ++ } ++ ++ // Here we could execute it in event loop async to prevent waiting io task on main ++ // stop reading new packets to prevent some packets came into pipeline too early ++ this.channel.config().setAutoRead(false); ++ ++ // do our configuration task ++ final UnconfiguredPipelineHandler.InboundConfigurationTask finalInboundConfigurationTask = configMessage; ++ Runnable toExecute = () -> this.channel.writeAndFlush(finalInboundConfigurationTask).addListener(future -> { ++ try { ++ if (future.isSuccess()) { ++ if (callback != null) callback.run(); // retire callback if there have one ++ return; ++ } ++ ++ final Throwable ex = future.cause(); ++ ++ // here we process our exceptions like that blocking one ++ if (ex instanceof ClosedChannelException) { ++ LOGGER.info("Connection closed during protocol change"); ++ } else { ++ this.channel.pipeline().fireExceptionCaught(ex); ++ } ++ }finally { ++ // reset auto back and resume reading if needed ++ if (resumeAutoReading) { ++ this.channel.config().setAutoRead(true); ++ this.channel.read(); ++ } ++ } ++ }); ++ ++ // we need to do this inside the event loop ++ if (!this.channel.eventLoop().inEventLoop()) { ++ this.channel.eventLoop().execute(toExecute); ++ return; ++ } ++ ++ toExecute.run(); ++ } ++ } ++ ++ public void setupOutboundProtocolAsync( ++ ProtocolInfo protocol, ++ @Nullable Runnable callback, ++ boolean resumeAutoReading ++ ) { ++ if (protocol.flow() != this.getSending()) { ++ throw new IllegalStateException("Invalid outbound protocol: " + protocol.id()); ++ } else { ++ UnconfiguredPipelineHandler.OutboundConfigurationTask configMessage = UnconfiguredPipelineHandler.setupOutboundProtocol(protocol); ++ BundlerInfo bundlerInfo = protocol.bundlerInfo(); ++ if (bundlerInfo != null) { ++ PacketBundleUnpacker newUnbundler = new PacketBundleUnpacker(bundlerInfo); ++ configMessage = configMessage.andThen( ++ context -> context.pipeline().addAfter("encoder", "unbundler", newUnbundler) ++ ); ++ } ++ ++ boolean isLoginProtocol = protocol.id() == ConnectionProtocol.LOGIN; ++ ++ // Here we could execute it in event loop async to prevent waiting io task on main ++ // stop reading new packets to prevent some packets came into pipeline too early ++ this.channel.config().setAutoRead(false); ++ ++ // do our configuration task ++ final UnconfiguredPipelineHandler.OutboundConfigurationTask finalOutboundConfigurationTask = configMessage; ++ final Runnable writeTask = () -> this.channel.writeAndFlush( ++ finalOutboundConfigurationTask.andThen(context -> this.sendLoginDisconnect = isLoginProtocol) ++ ).addListener(future -> { ++ try { ++ if (future.isSuccess()) { ++ if (callback != null) callback.run(); // retire callback if there have one ++ return; ++ } ++ ++ final Throwable ex = future.cause(); ++ ++ // here we process our exceptions like that blocking one ++ if (ex instanceof ClosedChannelException) { ++ LOGGER.info("Connection closed during protocol change"); ++ } else { ++ this.channel.pipeline().fireExceptionCaught(ex); ++ } ++ }finally { ++ // reset auto back and resume reading if needed ++ if (resumeAutoReading) { ++ this.channel.config().setAutoRead(true); ++ this.channel.read(); // read once ++ } ++ } ++ }); ++ ++ // we need to do this inside the event loop ++ if (!this.channel.eventLoop().inEventLoop()) { ++ this.channel.eventLoop().execute(writeTask); ++ return; ++ } ++ ++ writeTask.run(); ++ } ++ } ++ // Camellia end ++ + } +diff --git a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +index efcf294b523ca6b039eb2544546bf863ba717749..343c98fb35eb4cb736d26f67a11c00efb8622cf8 100644 +--- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +@@ -188,8 +188,9 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + public void handleConfigurationFinished(final ServerboundFinishConfigurationPacket packet) { + PacketUtils.ensureRunningOnSameThread(packet, this, this.server.packetProcessor()); + this.finishCurrentTask(JoinWorldTask.TYPE); +- this.connection.setupOutboundProtocol(GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess()))); ++ // this.connection.setupOutboundProtocol(GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess()))); // Camellia - Async protocol switch - move down + ++ Runnable afterSwitch = () -> { // Camellia - Async protocol switch + try { + PlayerList playerList = this.server.getPlayerList(); + if (playerList.getPlayer(this.gameProfile.id()) != null) { +@@ -235,6 +236,18 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + LOGGER.error("Couldn't place player in world", e); + this.disconnect(DISCONNECT_REASON_INVALID_DATA); + } ++ // Camellia start - Async protocol switch ++ }; ++ if (!moe.monnairealms.camellia.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { ++ this.connection.setupOutboundProtocol(GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess()))); ++ afterSwitch.run(); // directly run callback as we won't process any packet this time ++ } else { ++ this.connection.setupOutboundProtocolAsync(GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess())), () -> { ++ // push back ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(afterSwitch); ++ }, false); // we will start auto read once we set up inbound handler at placeNewPlayer in PlayerList ++ } ++ // Camellia end + } + + @Override +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index eda7e765c7c2ffee305edc81e0c7a6b1e5bfa18d..4c9ea818be83ed3c4dacc762f4d0e8c0f177cd19 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -2884,7 +2884,13 @@ public class ServerGamePacketListenerImpl + } // Folia end - rewrite login process - move connection ownership to global region + this.waitingForSwitchToConfig = true; // Folia - rewrite login process - fix bad ordering of this field write - moved down + this.send(ClientboundStartConfigurationPacket.INSTANCE); ++ if (!moe.monnairealms.camellia.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { // Camellia - Async protocol switch + this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); ++ // Camellia start - Async protcol switch ++ } else { ++ this.connection.setupOutboundProtocolAsync(ConfigurationProtocols.CLIENTBOUND, null, true); ++ } ++ // Camellia end + } + + @Override +@@ -3769,12 +3775,26 @@ public class ServerGamePacketListenerImpl + } + + final ServerConfigurationPacketListenerImpl listener = new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation())); // Paper ++ if (!moe.monnairealms.camellia.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { // Camellia - Async protocol switch + this.connection + .setupInboundProtocol( + ConfigurationProtocols.SERVERBOUND, + listener // Paper + ); + new io.papermc.paper.event.connection.configuration.PlayerConnectionReconfigureEvent(listener.paperConnection).callEvent(); // Paper ++ } // Camellia - Async protocol switch - add "{" ++ // Camellia start - Async protocol switch - move up ++ else ++ this.connection.setupInboundProtocolAsync( ++ ConfigurationProtocols.SERVERBOUND, ++ listener, ++ () -> { ++ new io.papermc.paper.event.connection.configuration.PlayerConnectionReconfigureEvent(listener.paperConnection).callEvent(); // Paper ++ }, ++ true ++ ); ++ // Camellia end ++ // new io.papermc.paper.event.connection.configuration.PlayerConnectionReconfigureEvent(listener.paperConnection).callEvent(); // Paper // Camellia - Async protocol switch - move up + } + + @Override +diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index f2329d1a9d9d4bf4c9d771e25e54f2f9ef65a76c..7de3bbecea2f11e4e1cb65408729f346760dc9b8 100644 +--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -435,12 +435,30 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + public void handleLoginAcknowledgement(final ServerboundLoginAcknowledgedPacket packet) { + net.minecraft.network.protocol.PacketUtils.ensureRunningOnSameThread(packet, this, this.server.packetProcessor()); // CraftBukkit + Validate.validState(this.state == ServerLoginPacketListenerImpl.State.PROTOCOL_SWITCHING, "Unexpected login acknowledgement packet"); +- this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); ++ /*this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); // Camellia - Async protocol switch - Rewrite + CommonListenerCookie cookie = CommonListenerCookie.createInitial(Objects.requireNonNull(this.authenticatedProfile), this.transferred); + ServerConfigurationPacketListenerImpl configPacketListener = new ServerConfigurationPacketListenerImpl(this.server, this.connection, cookie); + this.connection.setupInboundProtocol(ConfigurationProtocols.SERVERBOUND, configPacketListener); + configPacketListener.startConfiguration(); +- this.state = ServerLoginPacketListenerImpl.State.ACCEPTED; ++ this.state = ServerLoginPacketListenerImpl.State.ACCEPTED;*/ // Camellia - Async protocol switch - Rewrite ++ ++ // Camellia start - Async protocol switch ++ CommonListenerCookie cookie = CommonListenerCookie.createInitial(Objects.requireNonNull(this.authenticatedProfile), this.transferred); ++ ServerConfigurationPacketListenerImpl configPacketListener = new ServerConfigurationPacketListenerImpl(this.server, this.connection, cookie); ++ ++ Runnable afterSwitch = () -> io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(configPacketListener::startConfiguration); // push back to main thread ++ ++ if (!moe.monnairealms.camellia.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { ++ this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); ++ this.connection.setupInboundProtocol(ConfigurationProtocols.SERVERBOUND, configPacketListener); ++ afterSwitch.run(); ++ return; ++ } ++ ++ this.connection.setupInboundProtocolAsync(ConfigurationProtocols.SERVERBOUND, configPacketListener, () -> { ++ this.connection.setupOutboundProtocolAsync(ConfigurationProtocols.CLIENTBOUND, afterSwitch, true); // start auto read when everything is ready ++ }, false); // we will resume auto reading once the outbound protocol is also setup ++ // Camellia end + } + + @Override +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index e7579a9873362317dd82b63306cf650af9472574..9e90197b96fcac958c5073ca8f560e77bb811460 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -237,9 +237,11 @@ public abstract class PlayerList { + // only after setting the connection listener to game type, add the connection to this regions list + level.getCurrentWorldData().connections.add(connection); + // Folia end - rewrite login process ++ if (!moe.monnairealms.camellia.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { // Camellia - Async protocol switch // we will run async switch once these main thread logics became done + connection.setupInboundProtocol( + GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess()), playerConnection), playerConnection + ); ++ } // Camellia - Async protocol switch + playerConnection.suspendFlushing(); + GameRules gameRules = level.getGameRules(); + boolean immediateRespawn = gameRules.get(GameRules.IMMEDIATE_RESPAWN); +@@ -403,6 +405,17 @@ public abstract class PlayerList { + ); + } + // Paper end - Send empty chunk ++ // Camellia start - Async protocol switch ++ if (moe.monnairealms.camellia.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { ++ // auto read will be enabled once the async switch is done ++ connection.setupInboundProtocolAsync( ++ GameProtocols.SERVERBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(this.server.registryAccess()), playerConnection), ++ playerConnection, ++ null, // we don't need to do anything more ++ true // start auto read which we have disabled in configuration handler ++ ); ++ } ++ // Camellia end + } + + public void updateEntireScoreboard(final ServerScoreboard scoreboard, final ServerPlayer player) { diff --git a/camellia-server/minecraft-patches/features/0025-Add-config-to-support-for-long-command-inputs.patch b/camellia-server/minecraft-patches/features/0025-Add-config-to-support-for-long-command-inputs.patch new file mode 100644 index 0000000..503dd42 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0025-Add-config-to-support-for-long-command-inputs.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Wed, 8 Jul 2026 21:53:18 +0800 +Subject: [PATCH] Add config to support for long command inputs + +Some long commands can be run through the dialog command, but paper has prohibited it. +Revert to vanilla to fix it. + +diff --git a/net/minecraft/network/protocol/game/ServerboundChatCommandPacket.java b/net/minecraft/network/protocol/game/ServerboundChatCommandPacket.java +index 491af2413a4e793a121fec368259ff8211ed031e..f9076b71f22b016a117a1eeb9cc4b8801db000e7 100644 +--- a/net/minecraft/network/protocol/game/ServerboundChatCommandPacket.java ++++ b/net/minecraft/network/protocol/game/ServerboundChatCommandPacket.java +@@ -12,7 +12,7 @@ public record ServerboundChatCommandPacket(String command) implements Packet +Date: Sat, 18 Apr 2026 09:08:52 +0800 +Subject: [PATCH] Add config for server mod name + + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index b01e00de94d8fa45359d3d14a8481287276adb68..cdb1647146f526dc80b15a09d3a936b5dbc54ba3 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -2060,7 +2060,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Fri, 17 Apr 2026 22:47:34 +0800 +Subject: [PATCH] Add config for cpu affinity + + +diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java +index 5a72fa0d72bfdb927293abe921c57f6ae964a6ec..913161732b0affa70fb114dd4087a6102b611cba 100644 +--- a/io/papermc/paper/threadedregions/TickRegionScheduler.java ++++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java +@@ -66,7 +66,7 @@ public final class TickRegionScheduler { + + @Override + public Thread newThread(final Runnable run) { +- final Thread ret = new TickThreadRunner(this.threadGroup, run, "Folia Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); ++ final Thread ret = new TickThreadRunner(this.threadGroup, moe.monnairealms.camellia.config.modules.optimizations.CpuAffinityConfig.wrapForTickRegion(run), "Folia Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); // Camellia - Add config for cpu affinity + ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException); + return ret; + } diff --git a/camellia-server/minecraft-patches/features/0028-Add-config-for-unsafe-teleportation.patch b/camellia-server/minecraft-patches/features/0028-Add-config-for-unsafe-teleportation.patch new file mode 100644 index 0000000..b785730 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0028-Add-config-for-unsafe-teleportation.patch @@ -0,0 +1,45 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Tue, 21 Apr 2026 01:47:51 +0800 +Subject: [PATCH] Add config for unsafe teleportation + + +diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java +index 480b6b2405706fd4d7158d0e160adafd84099887..ca8207f7bf6a71ae283c6c9739563e9dfe1f0f02 100644 +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -70,7 +70,7 @@ public class FallingBlockEntity extends Entity { + public int fallDamageMax = 40; + public float fallDamagePerDistance = 0.0F; + public @Nullable CompoundTag blockData; +- public boolean forceTickAfterTeleportToDuplicate; ++ public boolean forceTickAfterTeleportToDuplicate = moe.monnairealms.camellia.config.modules.fixes.UnsafeTeleportationConfig.enabled; // Camellia - Add config for unsafe teleportation + protected static final EntityDataAccessor DATA_START_POS = SynchedEntityData.defineId(FallingBlockEntity.class, EntityDataSerializers.BLOCK_POS); + public boolean autoExpire = true; // Paper - Expand FallingBlock API + +@@ -384,7 +384,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; // Paper ++ this.forceTickAfterTeleportToDuplicate = newEntity != null && fromOrToEnd && (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation || moe.monnairealms.camellia.config.modules.fixes.UnsafeTeleportationConfig.enabled); // Paper // Camellia - Add config for unsafe teleportation + return newEntity; + } + } +diff --git a/net/minecraft/world/level/block/EndPortalBlock.java b/net/minecraft/world/level/block/EndPortalBlock.java +index d42bb2a1721e4ab8c9956e18c3ca418b8d648c59..1bb9e32fd0f112deeec936976da0899dae301e56 100644 +--- a/net/minecraft/world/level/block/EndPortalBlock.java ++++ b/net/minecraft/world/level/block/EndPortalBlock.java +@@ -76,6 +76,12 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { + if (level.paperConfig().misc.disableEndCredits) {player.seenCredits = true; return;} // Paper - Option to disable end credits + player.showEndCredits(); + } else { ++ // Camellia start - Add config for unsafe teleportation ++ if (moe.monnairealms.camellia.config.modules.fixes.UnsafeTeleportationConfig.enabled && !(entity instanceof net.minecraft.world.entity.player.Player)) { ++ entity.endPortalLogicAsync(pos); ++ return; ++ } ++ // Camellia end + entity.setAsInsidePortal(this, pos); + } + } diff --git a/camellia-server/minecraft-patches/features/0029-Add-config-for-watchdog-timeout.patch b/camellia-server/minecraft-patches/features/0029-Add-config-for-watchdog-timeout.patch new file mode 100644 index 0000000..757997a --- /dev/null +++ b/camellia-server/minecraft-patches/features/0029-Add-config-for-watchdog-timeout.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:12:26 +0800 +Subject: [PATCH] Add config for watchdog timeout + + +diff --git a/io/papermc/paper/threadedregions/FoliaWatchdogThread.java b/io/papermc/paper/threadedregions/FoliaWatchdogThread.java +index e9ca1a15049b0211d10401cb78e953b93afaf6c7..56c0cac6e6e4c86a1db3c27a2c850d65b5b58c55 100644 +--- a/io/papermc/paper/threadedregions/FoliaWatchdogThread.java ++++ b/io/papermc/paper/threadedregions/FoliaWatchdogThread.java +@@ -65,7 +65,7 @@ public final class FoliaWatchdogThread extends Thread { + + for (final RunningTick tick : ticks) { + final long elapsed = now - tick.lastPrint; +- if (elapsed <= TimeUnit.SECONDS.toNanos(5L)) { ++ if (elapsed <= TimeUnit.MILLISECONDS.toNanos(moe.monnairealms.camellia.config.modules.misc.FoliaWatchogConfig.tickRegionTimeOutMs)) { // Camellia - Add config for watchdog timeout + continue; + } + tick.lastPrint = now; diff --git a/camellia-server/minecraft-patches/features/0030-Add-config-to-disable-entity-tick-catchers.patch b/camellia-server/minecraft-patches/features/0030-Add-config-to-disable-entity-tick-catchers.patch new file mode 100644 index 0000000..036eaee --- /dev/null +++ b/camellia-server/minecraft-patches/features/0030-Add-config-to-disable-entity-tick-catchers.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 20:10:51 +0800 +Subject: [PATCH] Add config to disable entity tick catchers + + +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 52bcb87e96f7b5997559bd532481f548be03e6da..41b84a84a2ae24555a065d0779957b7b71611523 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1591,6 +1591,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + )); + // Camellia end + } catch (Throwable t) { ++ if (moe.monnairealms.camellia.config.modules.experiment.DisableEntityCatchConfig.enabled) throw t; // Camellia - Add config to disable entity tick catchers + // Paper start - Prevent block entity and entity crashes + final String msg = String.format("Entity threw exception at %s:%s,%s,%s", io.papermc.paper.util.MCUtil.getLevelName(entity.level()), entity.getX(), entity.getY(), entity.getZ()); + MinecraftServer.LOGGER.error(msg, t); diff --git a/camellia-server/minecraft-patches/features/0031-Add-config-for-heightmap-warning.patch b/camellia-server/minecraft-patches/features/0031-Add-config-for-heightmap-warning.patch new file mode 100644 index 0000000..e935449 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0031-Add-config-for-heightmap-warning.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 23:34:17 +0800 +Subject: [PATCH] Add config for heightmap warning + + +diff --git a/net/minecraft/world/level/levelgen/Heightmap.java b/net/minecraft/world/level/levelgen/Heightmap.java +index dffa0d4859ecd7e21a9345f946083c56205145a3..8f2e4fa566cefc2089fb6f306a37939f647ebeb2 100644 +--- a/net/minecraft/world/level/levelgen/Heightmap.java ++++ b/net/minecraft/world/level/levelgen/Heightmap.java +@@ -128,6 +128,7 @@ public class Heightmap { + if (rawData.length == data.length) { + System.arraycopy(data, 0, rawData, 0, data.length); + } else { ++ if (!moe.monnairealms.camellia.config.modules.misc.DisableWarningConfig.disableHeightmapWarning) // Camellia - Add config for heightmap warning + LOGGER.warn("Ignoring heightmap data for chunk {}, size does not match; expected: {}, got: {}", chunk.getPos(), rawData.length, data.length); + primeHeightmaps(chunk, EnumSet.of(type)); + } diff --git a/camellia-server/minecraft-patches/features/0032-Add-config-gui-support.patch b/camellia-server/minecraft-patches/features/0032-Add-config-gui-support.patch new file mode 100644 index 0000000..ba3593b --- /dev/null +++ b/camellia-server/minecraft-patches/features/0032-Add-config-gui-support.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 23:35:08 +0800 +Subject: [PATCH] Add config gui support + + +diff --git a/net/minecraft/server/dialog/action/ParsedTemplate.java b/net/minecraft/server/dialog/action/ParsedTemplate.java +index 8a720ef5cd10bf04e97fb34c1ca0f0b265e98468..cbe7ebb8f3b3de6cdb0893c24b52c7cf036402a3 100644 +--- a/net/minecraft/server/dialog/action/ParsedTemplate.java ++++ b/net/minecraft/server/dialog/action/ParsedTemplate.java +@@ -13,7 +13,7 @@ public class ParsedTemplate { + private final String raw; + private final StringTemplate parsed; + +- private ParsedTemplate(final String raw, final StringTemplate parsed) { ++ public ParsedTemplate(final String raw, final StringTemplate parsed) { // Camellia - Add config gui support - make public + this.raw = raw; + this.parsed = parsed; + } diff --git a/camellia-server/minecraft-patches/features/0033-Add-force-the-data-command-to-be-enabled-config.patch b/camellia-server/minecraft-patches/features/0033-Add-force-the-data-command-to-be-enabled-config.patch new file mode 100644 index 0000000..59fd392 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0033-Add-force-the-data-command-to-be-enabled-config.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 23:37:00 +0800 +Subject: [PATCH] Add force the data command to be enabled config + + +diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java +index bc9f6cc286536b87513de81855ad0b61cf787c84..6b91eab354d8e7c93bf148bfc1f5a611f8ffcf86 100644 +--- a/net/minecraft/commands/Commands.java ++++ b/net/minecraft/commands/Commands.java +@@ -202,7 +202,9 @@ public class Commands { + ClearInventoryCommands.register(this.dispatcher, context); + //CloneCommands.register(this.dispatcher, context); // Folia - region threading - TODO + DamageCommand.register(this.dispatcher, context); +- //DataCommands.register(this.dispatcher); // Folia - region threading - TODO ++ if(moe.monnairealms.camellia.config.modules.experiment.CommandConfig.data) { // Camellia - Add force the data command to be enabled config ++ DataCommands.register(this.dispatcher); // Folia - region threading - TODO // Camellia - Add force the data command to be enabled config ++ } // Camellia - Add force the data command to be enabled config + //DataPackCommand.register(this.dispatcher, context); // Folia - region threading - TODO + //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO + DefaultGameModeCommands.register(this.dispatcher); diff --git a/camellia-server/minecraft-patches/features/0034-Add-experimental-config-for-command-block-command-ex.patch b/camellia-server/minecraft-patches/features/0034-Add-experimental-config-for-command-block-command-ex.patch new file mode 100644 index 0000000..8581590 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0034-Add-experimental-config-for-command-block-command-ex.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 23:39:10 +0800 +Subject: [PATCH] Add experimental config for command block command execution + + +diff --git a/net/minecraft/world/level/BaseCommandBlock.java b/net/minecraft/world/level/BaseCommandBlock.java +index 31f1583f29eebe5e13b5a7ac5116fdafc3592e5c..bc567e04791128cc0b78a234c496cf99d2dd5682 100644 +--- a/net/minecraft/world/level/BaseCommandBlock.java ++++ b/net/minecraft/world/level/BaseCommandBlock.java +@@ -91,7 +91,7 @@ public abstract class BaseCommandBlock { + } + + public boolean performCommand(final ServerLevel level) { +- if (true) return false; // Folia - region threading ++ if (!moe.monnairealms.camellia.config.modules.experiment.CommandConfig.commandBlock) return false; // Folia - region threading // Camellia - Add experimental config for command block command execution + if (level.getGameTime() == this.lastExecution) { + return false; + } diff --git a/camellia-server/minecraft-patches/features/0035-Add-config-to-modify-tripwire-behavior.patch b/camellia-server/minecraft-patches/features/0035-Add-config-to-modify-tripwire-behavior.patch new file mode 100644 index 0000000..81ccbcd --- /dev/null +++ b/camellia-server/minecraft-patches/features/0035-Add-config-to-modify-tripwire-behavior.patch @@ -0,0 +1,116 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Thu, 9 Jul 2026 10:05:46 +0800 +Subject: [PATCH] Add config to modify tripwire behavior + + +diff --git a/net/minecraft/world/level/block/TripWireHookBlock.java b/net/minecraft/world/level/block/TripWireHookBlock.java +index 8bcc8b6f83ab4d5a472741185511b5d84f4974be..7e78e67c32f5b577518fe735eaba4ce2fba8023f 100644 +--- a/net/minecraft/world/level/block/TripWireHookBlock.java ++++ b/net/minecraft/world/level/block/TripWireHookBlock.java +@@ -201,10 +201,17 @@ public class TripWireHookBlock extends Block { + BlockPos testPos = pos.relative(direction, i); + BlockState wireData = wireStates[i]; + if (wireData != null) { +- BlockState testPosState = level.getBlockState(testPos); +- if (testPosState.is(Blocks.TRIPWIRE) || testPosState.is(Blocks.TRIPWIRE_HOOK)) { +- if (!io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates || !testPosState.is(Blocks.TRIPWIRE)) level.setBlock(testPos, wireData.trySetValue(ATTACHED, attached), Block.UPDATE_ALL); // Paper - prevent tripwire from updating ++ // Camellia start - tripwire and tripwireHook dupe ++ if (moe.monnairealms.camellia.config.modules.function.TripwireBehaviorConfig.enabled) { ++ level.setBlock(testPos, wireData.trySetValue(ATTACHED, attached), 3); ++ level.getBlockState(testPos); ++ } else { ++ BlockState testPosState = level.getBlockState(testPos); ++ if (testPosState.is(Blocks.TRIPWIRE) || testPosState.is(Blocks.TRIPWIRE_HOOK)) { ++ if (!io.papermc.paper.configuration.GlobalConfiguration.get().blockUpdates.disableTripwireUpdates || !testPosState.is(Blocks.TRIPWIRE)) level.setBlock(testPos, wireData.trySetValue(ATTACHED, attached), Block.UPDATE_ALL); // Paper - prevent tripwire from updating ++ } + } ++ // Camellia end - tripwire and tripwireHook dupe + } + } + } +diff --git a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +index b67f5f7b76037ab77548c37a83e4d0918915223a..4407333785e8e76d08bb8b18510e6db95c954e36 100644 +--- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +@@ -28,15 +28,41 @@ public class EndPlatformFeature extends Feature { + // CraftBukkit end + BlockPos.MutableBlockPos pos = origin.mutable(); + +- for (int dz = -2; dz <= 2; dz++) { ++ // Camellia start - tripwire behavior modifier ++ java.util.List blockList1 = new java.util.ArrayList<>(); ++ java.util.List blockList2 = new java.util.ArrayList<>(); ++ boolean flag21 = moe.monnairealms.camellia.config.modules.function.TripwireBehaviorConfig.behaviorMode == moe.monnairealms.camellia.enums.EnumTripwireBehavior.VANILLA21; for (int dz = -2; dz <= 2; dz++) { + for (int dx = -2; dx <= 2; dx++) { + for (int dy = -1; dy < 3; dy++) { + BlockPos blockPos = pos.set(origin).move(dx, dy, dz); + Block block = dy == -1 ? Blocks.OBSIDIAN : Blocks.AIR; + if (!blockList.getBlockState(blockPos).is(block)) { // CraftBukkit + if (dropResources) { +- blockList.destroyBlock(blockPos, true, null); // CraftBukkit ++ boolean flag = false; ++ if (moe.monnairealms.camellia.config.modules.function.TripwireBehaviorConfig.enabled) { ++ switch (moe.monnairealms.camellia.config.modules.function.TripwireBehaviorConfig.behaviorMode) { ++ case moe.monnairealms.camellia.enums.EnumTripwireBehavior.VANILLA20: { ++ flag = true; ++ } ++ case moe.monnairealms.camellia.enums.EnumTripwireBehavior.MIXED: { ++ net.minecraft.world.level.block.state.BlockState state = newLevel.getBlockState(blockPos); ++ if (state.is(Blocks.TRIPWIRE)) { ++ if (state.getValue(net.minecraft.world.level.block.TripWireBlock.DISARMED)) { ++ flag = true; ++ blockList2.add(blockPos.immutable()); ++ } ++ if (!flag) { ++ flag = checkString(blockList2, blockPos); ++ } ++ } ++ } ++ default: {} // 1.21 & default Logic - default empty ++ } ++ } ++ if (flag) blockList1.add(blockPos.immutable()); ++ else blockList.destroyBlock(blockPos, true, null); // CraftBukkit + } ++ // Camellia end - prevent tripwire dupe in end platform generate + + blockList.setBlock(blockPos, block.defaultBlockState(), Block.UPDATE_ALL); // CraftBukkit + } +@@ -53,11 +79,34 @@ public class EndPlatformFeature extends Feature { + if (portalEvent.isCancelled()) return; + } + +- if (dropResources) { +- blockList.placeBlocks(state -> newLevel.destroyBlock(state.getPosition(), true, null)); ++ // Camellia start - prevent tripwire dupe in end platform generate ++ if (flag21 || !moe.monnairealms.camellia.config.modules.function.TripwireBehaviorConfig.enabled) { ++ if (dropResources) { ++ blockList.placeBlocks(state -> newLevel.destroyBlock(state.getPosition(), true, null)); ++ } else { ++ blockList.placeBlocks(); ++ } ++ // Camellia end - prevent tripwire dupe in end platform generate + } else { ++ // Camellia start - prevent tripwire dupe in end platform generate ++ if (dropResources) { ++ blockList.getSnapshotBlocks().forEach((state) -> { ++ newLevel.destroyBlock(state.getPosition(), !blockList1.contains(state.getPosition()), null); ++ }); ++ } ++ // Camellia end - prevent tripwire dupe in end platform generate + blockList.placeBlocks(); + } + // CraftBukkit end + } ++ ++ // Camellia start - tripwire behavior modifier ++ private static boolean checkString(java.util.List blockList, BlockPos blockPos) { ++ for (BlockPos pos : blockList) { ++ if (pos.getY() != blockPos.getY()) continue; ++ if (pos.getX() == blockPos.getX() || pos.getZ() == blockPos.getZ()) return true; ++ } ++ return false; ++ } ++ // Camellia end - tripwire behavior modifier + } diff --git a/camellia-server/minecraft-patches/features/0036-Add-config-for-item-multitask.patch b/camellia-server/minecraft-patches/features/0036-Add-config-for-item-multitask.patch new file mode 100644 index 0000000..2fd594a --- /dev/null +++ b/camellia-server/minecraft-patches/features/0036-Add-config-for-item-multitask.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Libalpm64 +Date: Thu, 9 Jul 2026 10:08:30 +0800 +Subject: [PATCH] Add config for item multitask + +Allows players to use items while moving or switching hotbar slots. This +is for Anarchy servers or Crystal PVP servers this allows them to pvp +without stopping the item mid animation. + +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 4c9ea818be83ed3c4dacc762f4d0e8c0f177cd19..9fe5ee1c5578be6f6c5ad9a58e10669a4b7ac75a 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -2169,7 +2169,9 @@ public class ServerGamePacketListenerImpl + this.player.sendSpawnProtectionMessage(pos); + } else if (this.awaitingPositionFromClient == null && (level.mayInteract(this.player, pos) || passthroughSignInteraction)) { + // Paper end - Allow using signs inside spawn protection ++ if (!moe.monnairealms.camellia.config.modules.fixes.ItemMultitaskConfig.enabled) { // Camellia - Add config for item multitask + this.player.stopUsingItem(); // CraftBukkit - SPIGOT-4706 ++ } // Camellia - Add config for item multitask + InteractionResult interactionResult = this.player.gameMode.useItemOn(this.player, level, itemStack, hand, blockHit); + if (interactionResult.consumesAction()) { + CriteriaTriggers.ANY_BLOCK_USE.trigger(this.player, blockHit.getBlockPos(), itemStack); +@@ -2368,9 +2370,13 @@ public class ServerGamePacketListenerImpl + return; + } + // CraftBukkit end +- if (this.player.getInventory().getSelectedSlot() != packet.getSlot() && this.player.getUsedItemHand() == InteractionHand.MAIN_HAND) { +- this.player.stopUsingItem(); ++ // Camellia start - Add item multitask config ++ if (!moe.monnairealms.camellia.config.modules.fixes.ItemMultitaskConfig.enabled) { ++ if (this.player.getInventory().getSelectedSlot() != packet.getSlot() && this.player.getUsedItemHand() == InteractionHand.MAIN_HAND) { ++ this.player.stopUsingItem(); ++ } + } ++ // Camellia end + + this.player.getInventory().setSelectedSlot(packet.getSlot()); + this.player.resetLastActionTime(); diff --git a/camellia-server/minecraft-patches/features/0037-Add-config-to-enable-tick-command.patch b/camellia-server/minecraft-patches/features/0037-Add-config-to-enable-tick-command.patch new file mode 100644 index 0000000..03080cb --- /dev/null +++ b/camellia-server/minecraft-patches/features/0037-Add-config-to-enable-tick-command.patch @@ -0,0 +1,104 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Thu, 9 Jul 2026 10:11:39 +0800 +Subject: [PATCH] Add config to enable tick command + +only freeze/unfreeze/step/query can run when enabled + +diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java +index aa575f3b76ef70ffb9f0410e7e5cfe7af384bfbd..a00b086962b68d6f86cfb55eee6f61afdb162d5f 100644 +--- a/io/papermc/paper/threadedregions/RegionizedServer.java ++++ b/io/papermc/paper/threadedregions/RegionizedServer.java +@@ -233,6 +233,11 @@ public final class RegionizedServer { + + private void globalTick(final long tickCount) { + ++this.tickCount; ++ // Camellia start - Add a config to enable tick command ++ if (moe.monnairealms.camellia.config.modules.experiment.CommandConfig.tick) { ++ MinecraftServer.getServer().tickRateManager().tick(); ++ } ++ // Camellia end - Add a config to enable tick command + // expire invalid click command callbacks + io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.ADVENTURE_CLICK_MANAGER.handleQueue((int)this.tickCount); // Paper // Folia - region threading - moved to global tick + io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.DIALOG_CLICK_MANAGER.handleQueue((int)this.tickCount); // Paper // Folia - region threading - moved to global tick +@@ -409,7 +414,7 @@ public final class RegionizedServer { + } + + private void tickTime(final ServerLevel world, final long tickCount) { +- if (world.tickTime) { ++ if ((!moe.monnairealms.camellia.config.modules.experiment.CommandConfig.tick || world.tickRateManager().runsNormally()) && world.tickTime) { // Camellia - Add a config to enable tick command + world.serverLevelData.setGameTime(world.serverLevelData.getGameTime() + tickCount); + } + } +diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java +index 913161732b0affa70fb114dd4087a6102b611cba..ccd86db7b384d99d290869a155792b49385dc9a6 100644 +--- a/io/papermc/paper/threadedregions/TickRegionScheduler.java ++++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java +@@ -479,6 +479,11 @@ public final class TickRegionScheduler { + try { + // next start isn't updated until the end of this tick + this.tickRegion(tickCount, tickStart, scheduledEnd); ++ // Camellia start - Add a config to enable tick command ++ if (moe.monnairealms.camellia.config.modules.experiment.CommandConfig.tick) { ++ MinecraftServer.getServer().tickRateManager().endTickWork(); ++ } ++ // Camellia end - Add a config to enable tick command + } catch (final Throwable thr) { + this.scheduler.regionFailed(this, false, thr); + // regionFailed will schedule a shutdown, so we should avoid letting this region tick further +diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java +index 6b91eab354d8e7c93bf148bfc1f5a611f8ffcf86..d971cc9669aca1005c026133e7e7e7f9b170ed0d 100644 +--- a/net/minecraft/commands/Commands.java ++++ b/net/minecraft/commands/Commands.java +@@ -259,7 +259,11 @@ public class Commands { + TeleportCommand.register(this.dispatcher); + TellRawCommand.register(this.dispatcher, context); + //TestCommand.register(this.dispatcher, context); // Folia - region threading +- //TickCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ // Camellia start - Add a config to enable tick command ++ if (moe.monnairealms.camellia.config.modules.experiment.CommandConfig.tick) { ++ TickCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ } ++ // Camellia end - Add a config to enable tick command + TimeCommand.register(this.dispatcher, context); + TitleCommand.register(this.dispatcher, context); + //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later +diff --git a/net/minecraft/server/commands/TickCommand.java b/net/minecraft/server/commands/TickCommand.java +index e8d6a67143f3f0b4813e51bb273498bc404899b9..12ff74e46d39d3bf6f025cc59a27ba8925724d80 100644 +--- a/net/minecraft/server/commands/TickCommand.java ++++ b/net/minecraft/server/commands/TickCommand.java +@@ -23,14 +23,14 @@ public class TickCommand { + Commands.literal("tick") + .requires(Commands.hasPermission(Commands.LEVEL_ADMINS)) + .then(Commands.literal("query").executes(c -> tickQuery(c.getSource()))) +- .then( ++/* .then( // Camellia - Add config to enable tick command - limit unsupported functions + Commands.literal("rate") + .then( + Commands.argument("rate", FloatArgumentType.floatArg(1.0F, 10000.0F)) + .suggests((c, b) -> SharedSuggestionProvider.suggest(new String[]{DEFAULT_TICKRATE}, b)) + .executes(c -> setTickingRate(c.getSource(), FloatArgumentType.getFloat(c, "rate"))) + ) +- ) ++ )*/ // Camellia - Add config to enable tick command - limit unsupported functions + .then( + Commands.literal("step") + .executes(c -> step(c.getSource(), 1)) +@@ -41,7 +41,7 @@ public class TickCommand { + .executes(c -> step(c.getSource(), IntegerArgumentType.getInteger(c, "time"))) + ) + ) +- .then( ++/* .then( // Camellia - Add config to enable tick command - limit unsupported functions + Commands.literal("sprint") + .then(Commands.literal("stop").executes(c -> stopSprinting(c.getSource()))) + .then( +@@ -49,7 +49,7 @@ public class TickCommand { + .suggests((c, b) -> SharedSuggestionProvider.suggest(new String[]{"60s", "1d", "3d"}, b)) + .executes(c -> sprint(c.getSource(), IntegerArgumentType.getInteger(c, "time"))) + ) +- ) ++ )*/ // Camellia - Add config to enable tick command - limit unsupported functions + .then(Commands.literal("unfreeze").executes(c -> setFreeze(c.getSource(), false))) + .then(Commands.literal("freeze").executes(c -> setFreeze(c.getSource(), true))) + ); diff --git a/camellia-server/minecraft-patches/features/0038-Add-config-for-whether-to-save-portal-tickets.patch b/camellia-server/minecraft-patches/features/0038-Add-config-for-whether-to-save-portal-tickets.patch new file mode 100644 index 0000000..37b80e0 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0038-Add-config-for-whether-to-save-portal-tickets.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 14:34:05 +0800 +Subject: [PATCH] Add config for whether to save portal tickets + + +diff --git a/net/minecraft/server/level/TicketType.java b/net/minecraft/server/level/TicketType.java +index 7e612e03087d63f9b500f7e9f3442b1931bae7ee..9d46c1bc0629faca4ca9e0a2f830912b36148c11 100644 +--- a/net/minecraft/server/level/TicketType.java ++++ b/net/minecraft/server/level/TicketType.java +@@ -60,7 +60,7 @@ public final class TicketType implements ca.spottedleaf.moonrise.patches.chun + public static final TicketType PLAYER_LOADING = register("player_loading", NO_TIMEOUT, FLAG_LOADING); + public static final TicketType PLAYER_SIMULATION = register("player_simulation", NO_TIMEOUT, FLAG_SIMULATION | FLAG_KEEP_DIMENSION_ACTIVE); + public static final TicketType FORCED = register("forced", NO_TIMEOUT, FLAG_PERSIST | FLAG_LOADING | FLAG_SIMULATION | FLAG_KEEP_DIMENSION_ACTIVE); +- public static final TicketType PORTAL = register("portal", 300L, FLAG_PERSIST | FLAG_LOADING | FLAG_SIMULATION | FLAG_KEEP_DIMENSION_ACTIVE); ++ public static final TicketType PORTAL = register("portal", 300L, moe.monnairealms.camellia.config.modules.misc.SavePortalTicketsConfig.doSave ? FLAG_PERSIST | FLAG_LOADING | FLAG_SIMULATION | FLAG_KEEP_DIMENSION_ACTIVE : FLAG_LOADING | FLAG_SIMULATION | FLAG_KEEP_DIMENSION_ACTIVE); // Camellia - Add config for whether to save portal tickets + public static final TicketType ENDER_PEARL = register("ender_pearl", 40L, FLAG_LOADING | FLAG_SIMULATION | FLAG_KEEP_DIMENSION_ACTIVE); + public static final TicketType UNKNOWN = register("unknown", 1L, FLAG_CAN_EXPIRE_IF_UNLOADED | FLAG_LOADING); + public static final TicketType PLUGIN = register("plugin", NO_TIMEOUT, FLAG_LOADING | FLAG_SIMULATION); // CraftBukkit diff --git a/camellia-server/minecraft-patches/features/0039-Add-portal-rate-limiter.patch b/camellia-server/minecraft-patches/features/0039-Add-portal-rate-limiter.patch new file mode 100644 index 0000000..fe53b57 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0039-Add-portal-rate-limiter.patch @@ -0,0 +1,130 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 10:17:22 +0800 +Subject: [PATCH] Add portal rate limiter + + +diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java +index f32d118e1e925b2155cef3fcdcb1e194c541e242..fe17157c4c6fc9ec745c5c11f6837e715d7949ab 100644 +--- a/io/papermc/paper/threadedregions/RegionizedWorldData.java ++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -157,6 +157,10 @@ public final class RegionizedWorldData { + for (final ChunkHolder chunkHolder : from.chunkHoldersToBroadcast) { + into.chunkHoldersToBroadcast.add(chunkHolder); + } ++ // Camellia start - Add portal rate limiter ++ into.portalRateThrottler.mergeWith(from.portalRateThrottler); ++ from.portalRateThrottler.destroy(); ++ // Camellia end + } + + @Override +@@ -322,6 +326,12 @@ public final class RegionizedWorldData { + into.chunkHoldersToBroadcast.add(chunkHolder); + } + } ++ // Camellia start - Add portal rate limiter ++ for (var worldData : dataSet) { ++ from.portalRateThrottler.splitInto(worldData.portalRateThrottler); ++ } ++ from.portalRateThrottler.destroy(); ++ // Camellia end + } + }; + +@@ -343,6 +353,35 @@ public final class RegionizedWorldData { + return this.isHandlingTick; + } + ++ // Camellia start - Add portal rate limiter ++ public final moe.monnairealms.camellia.utils.RateThrottler portalRateThrottler = new moe.monnairealms.camellia.utils.RateThrottler(); ++ public final net.objecthunter.exp4j.Expression portalRateCapExpression = moe.monnairealms.camellia.config.modules.function.PortalRateLimiterConfig.getExpressionIfConfigured(); ++ ++ private int computePortalRateCap() { ++ // expression mode is disabled ++ if (this.portalRateCapExpression == null) { ++ return moe.monnairealms.camellia.config.modules.function.PortalRateLimiterConfig.maxPortalTeleportsPerTick; ++ } ++ ++ final int tickingEntityCount = this.entityTickList.size(); ++ final int tickingChunkCount = this.tickingChunks.size(); ++ final int playerCount = this.localPlayers.size(); ++ ++ return moe.monnairealms.camellia.config.modules.function.PortalRateLimiterConfig.computeExpression( ++ this.portalRateCapExpression, ++ tickingEntityCount, ++ tickingChunkCount, ++ playerCount ++ ); ++ } ++ public boolean isPortalTeleportationOutOfRate() { ++ if (!moe.monnairealms.camellia.config.modules.function.PortalRateLimiterConfig.enabled) { ++ return false; ++ } ++ ++ return this.portalRateThrottler.isOutOfRate(this.computePortalRateCap()); ++ } ++ // Camellia end + // entities + // this is copy on write to allow packet processing to iterate safely + private final CopyOnWriteArrayList localPlayers = new CopyOnWriteArrayList<>(); +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index cdb1647146f526dc80b15a09d3a936b5dbc54ba3..4ce5bdd16b04f76c2e98a5465343a82ed5f28ab7 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1922,6 +1922,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop level + " " + level.dimension().identifier()); + profiler.push("tick"); ++ // Camellia start - Add portal rate limiter ++ regionizedWorldData.portalRateThrottler.begin(); ++ // Camellia end + + try { + foliaProfiler.startTimer(level.tickTimerId); try { // Folia - profiler +@@ -1936,6 +1939,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Thu, 9 Jul 2026 12:10:04 +0800 +Subject: [PATCH] Add status bar + + +diff --git a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java +index 52dcbcf64c3a510d4a4524b7d0ee226d9a73511d..051d601e7e809518e8d82ae4ae3259054d704f51 100644 +--- a/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java ++++ b/ca/spottedleaf/moonrise/paper/util/BaseChunkSystemHooks.java +@@ -122,6 +122,7 @@ public abstract class BaseChunkSystemHooks implements ca.spottedleaf.moonrise.co + + @Override + public void onChunkNotTicking(final LevelChunk chunk, final ChunkHolder holder) { ++ chunk.getChunkHot().clear(); // KioCG + chunk.getLevel().getCurrentWorldData().removeTickingChunk(chunk); // Folia - region threading + } + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 4ce5bdd16b04f76c2e98a5465343a82ed5f28ab7..ab8d69fa54203175760d394a5425601073fcd0d9 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1731,7 +1731,46 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop chunks = new ca.spottedleaf.moonrise.common.list.IteratorSafeOrderedReferenceSet<>(); ++ if (region != null){ ++ for (net.minecraft.world.level.chunk.LevelChunk chunk : region.world.getCurrentWorldData().getTickingChunks()) { ++ /* wait for rewrite - temporarily crash fix ++ for (net.minecraft.server.level.ServerChunkCache.ChunkAndHolder chunkAndHolder : region.world.getCurrentWorldData().getTickingChunks()){ ++ final net.minecraft.world.level.chunk.LevelChunk chunk = chunkAndHolder.chunk(); ++ */ ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(region.world, chunk.locX, chunk.locZ)){ ++ continue; ++ } ++ ++ chunks.add(chunk); ++ } ++ } ++ if (region != null && io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() % 20 == 0){ ++ final java.util.Iterator chunkIterator = chunks.unsafeIterator(); ++ while (chunkIterator.hasNext()){ ++ final net.minecraft.world.level.chunk.LevelChunk targetChunk = chunkIterator.next(); ++ ++ targetChunk.getChunkHot().nextTick(); ++ targetChunk.getChunkHot().start(); ++ } ++ } ++ //KioCG end + this.tickChildren(haveTime, region); // Folia - region threading ++ // KioCG start - ChunkHot ++ if (region != null && io.papermc.paper.threadedregions.RegionizedServer.getCurrentTick() % 20 == 0){ ++ final java.util.Iterator chunkIterator = chunks.unsafeIterator(); ++ while (chunkIterator.hasNext()){ ++ final net.minecraft.world.level.chunk.LevelChunk targetChunk = chunkIterator.next(); ++ ++ if (!targetChunk.getChunkHot().isStarted()){ ++ continue; ++ } ++ ++ targetChunk.getChunkHot().stop(); ++ } ++ } ++ //KioCG end + if (false && nano - this.lastServerStatus >= STATUS_EXPIRE_TIME_NANOS) { // Folia - region threading + this.lastServerStatus = nano; + this.status = this.buildServerStatus(); +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 1a0e5fbc823c328d16f48f2df4507bf240915c79..bd1368def6c2656902bae28dc07b98f8709107a9 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -1484,6 +1484,8 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + public void tickNonPassenger(final Entity entity) { + // Paper start - log detailed entity tick information + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot tick an entity off-main"); ++ LevelChunk levelChunk = entity.shouldTickHot() ? this.getChunkIfLoaded(entity.moonrise$getSectionX(),entity.moonrise$getSectionZ()) : null; // KioCG ++ if (levelChunk != null) levelChunk.getChunkHot().startTicking(); try { // KioCG + try { + // Folia - region threading + // Paper end - log detailed entity tick information +@@ -1560,6 +1562,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + } finally { + // Folia - region threading + } ++ } finally { if (levelChunk != null) levelChunk.getChunkHot().stopTickingAndCount(); } // KioCG + // Paper end - log detailed entity tick information + } + +@@ -1571,6 +1574,8 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + final int timerId = isActive ? entity.getType().tickTimerId : entity.getType().inactiveTickTimerId; + final ca.spottedleaf.leafprofiler.RegionizedProfiler.Handle foliaProfiler = io.papermc.paper.threadedregions.TickRegionScheduler.getProfiler(); + foliaProfiler.startTimer(timerId); ++ LevelChunk levelChunk = !(entity instanceof Player) ? this.getChunkIfLoaded(entity.blockPosition()) : null; // KioCG ++ if (levelChunk != null) levelChunk.getChunkHot().startTicking(); try { // KioCG + try { + // Folia end - profiler + entity.setOldPosAndRot(); +@@ -1612,6 +1617,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + this.tickPassenger(entity, passenger, isActive); // Paper - EAR 2 + } + } finally { foliaProfiler.stopTimer(timerId); } // Folia - profiler ++ } finally { if (levelChunk != null) levelChunk.getChunkHot().stopTickingAndCount(); } // KioCG + } + } + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index f22b5fdec6da8bcff9aef3d9fed5dbe61d39250a..336a1ae13810207696a699a936d4031eb904d0dd 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -889,6 +889,9 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + + @Override + public void tick() { ++ // Camellia start - Add status bars ++ this.statusBarList.tick(); ++ // Camellia end - Add status bars + // CraftBukkit start + if (this.joining) { + this.joining = false; +@@ -949,7 +952,34 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + this.trackEnteredOrExitedLavaOnVehicle(); + this.updatePlayerAttributes(); + this.advancements.flushDirty(this, true); ++ ++ // KioCG start - ChunkHot ++ if (this.tickCount % 20 == 0){ ++ this.nearbyChunkHot = this.refreshNearbyChunkHot(); ++ } ++ // KioCG end ++ } ++ ++ // KioCG start - ChunkHot ++ private volatile long nearbyChunkHot = 0; ++ ++ public long getNearbyChunkHot() { return this.nearbyChunkHot; } ++ ++ private long refreshNearbyChunkHot() { ++ long total = 0L; ++ int searchRadius = ((ServerLevel) this.level()).moonrise$getViewDistanceHolder().getViewDistances().tickViewDistance(); ++ for (int i = this.moonrise$getSectionX() - searchRadius; i <= this.moonrise$getSectionX() + searchRadius; ++i) { ++ for (int j = this.moonrise$getSectionZ() - searchRadius; j <= this.moonrise$getSectionZ() + searchRadius; ++j) { ++ net.minecraft.world.level.chunk.LevelChunk targetChunk = this.level().getChunkIfLoaded(i, j); ++ if (targetChunk != null) { ++ total += targetChunk.getChunkHot().getAverage(); ++ } ++ } ++ } ++ return total; + } ++ // KioCG end ++ + + private void updatePlayerAttributes() { + AttributeInstance blockInteractionRange = this.getAttribute(Attributes.BLOCK_INTERACTION_RANGE); +diff --git a/net/minecraft/world/entity/AreaEffectCloud.java b/net/minecraft/world/entity/AreaEffectCloud.java +index f03fa06c0ba56f7f5e1e45bc1568a490751efde3..eee1c14249addbf4714df733870da9747bda2245 100644 +--- a/net/minecraft/world/entity/AreaEffectCloud.java ++++ b/net/minecraft/world/entity/AreaEffectCloud.java +@@ -415,4 +415,11 @@ public class AreaEffectCloud extends Entity implements TraceableEntity { + return super.applyImplicitComponent(type, value); + } + } ++ ++ // KioCG start ++ @Override ++ public boolean shouldTickHot() { ++ return false; ++ } ++ // KioCG end + } +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 5d2f1a90a8e78a95b47c580f6dc4b893dc98f067..18e0572fe28461e57edb13e6ed6f5a0b6b3604c9 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -6347,4 +6347,6 @@ public abstract class Entity + return ((ServerLevel) this.level()).isPositionEntityTicking(this.blockPosition()); + } + // Paper end ++ ++ public boolean shouldTickHot() { return this.tickCount > 20 * 10 && this.isAlive(); } // KioCG + } +diff --git a/net/minecraft/world/entity/LightningBolt.java b/net/minecraft/world/entity/LightningBolt.java +index 5b19fee8a4861e5dbfb545bbe4dabda7787c430a..0afca6f5f5b02609fd55e9ea4d72b8862cc03420 100644 +--- a/net/minecraft/world/entity/LightningBolt.java ++++ b/net/minecraft/world/entity/LightningBolt.java +@@ -283,4 +283,11 @@ public class LightningBolt extends Entity { + public final boolean hurtServer(final ServerLevel level, final DamageSource source, final float damage) { + return false; + } ++ ++ // KioCG start ++ @Override ++ public boolean shouldTickHot() { ++ return false; ++ } ++ // KioCG end + } +diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java +index c6ce88fb63e0d40ed011958a1afd36c645d965c8..3c59325cb82a66a74d6605b51db83ff63e59e089 100644 +--- a/net/minecraft/world/entity/Mob.java ++++ b/net/minecraft/world/entity/Mob.java +@@ -1735,4 +1735,11 @@ public abstract class Mob extends LivingEntity implements Targeting, EquipmentUs + public float chargeSpeedModifier() { + return 1.0F; + } ++ ++ // KioCG start ++ @Override ++ public boolean shouldTickHot() { ++ return super.shouldTickHot() && (!this.removeWhenFarAway(0.0) || this.isPersistenceRequired() || this.requiresCustomPersistence()); ++ } ++ // KioCG end + } +diff --git a/net/minecraft/world/entity/animal/equine/TraderLlama.java b/net/minecraft/world/entity/animal/equine/TraderLlama.java +index 7c92c9335997cee76ff360c51f704ee41a180796..fb4cab83fd23c5561326a58683558e9a4219b0e9 100644 +--- a/net/minecraft/world/entity/animal/equine/TraderLlama.java ++++ b/net/minecraft/world/entity/animal/equine/TraderLlama.java +@@ -164,4 +164,11 @@ public class TraderLlama extends Llama { + super.start(); + } + } ++ ++ // KioCG start ++ @Override ++ public boolean shouldTickHot() { ++ return super.shouldTickHot() && !this.canDespawn(); ++ } ++ // KioCG end + } +diff --git a/net/minecraft/world/entity/npc/wanderingtrader/WanderingTrader.java b/net/minecraft/world/entity/npc/wanderingtrader/WanderingTrader.java +index f79353aa58b03d00839e4e95d1fd2480a635a592..0a43947bcc9f505ad9bc4d54c564709c2c3e411a 100644 +--- a/net/minecraft/world/entity/npc/wanderingtrader/WanderingTrader.java ++++ b/net/minecraft/world/entity/npc/wanderingtrader/WanderingTrader.java +@@ -271,4 +271,11 @@ public class WanderingTrader extends AbstractVillager implements Consumable.Over + return !pos.closerToCenterThan(this.trader.position(), distance); + } + } ++ ++ // KioCG start ++ @Override ++ public boolean shouldTickHot() { ++ return false; ++ } ++ // KioCG end + } +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index 2eb5fc819e2e17c69de87f8aa1a99e02bc5a62d5..ab0f42c6ed9ba4f920eefba4d1449ded6dda0fe9 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -170,6 +170,7 @@ public abstract class Player extends Avatar implements ContainerUser { + private ItemStack lastItemInMainHand = ItemStack.EMPTY; + private final ItemCooldowns cooldowns = this.createItemCooldowns(); + private Optional lastDeathLocation = Optional.empty(); ++ public moe.monnairealms.camellia.functions.bars.TickableStatusBarList statusBarList = new moe.monnairealms.camellia.functions.bars.TickableStatusBarList(this); // Camellia - Add status bars + public @Nullable FishingHook fishing; + public float hurtDir; + public boolean affectsSpawning = true; // Paper - Affects Spawning API +@@ -679,6 +680,7 @@ public abstract class Player extends Avatar implements ContainerUser { + this.getAttribute(Attributes.MOVEMENT_SPEED).setBaseValue(this.abilities.getWalkingSpeed()); + this.enderChestInventory.fromSlots(input.listOrEmpty("EnderItems", ItemStackWithSlot.CODEC)); + this.setLastDeathLocation(input.read("LastDeathLocation", GlobalPos.CODEC)); ++ this.statusBarList.load(input); // Camellia - Add status bars + } + + @Override +@@ -697,6 +699,7 @@ public abstract class Player extends Avatar implements ContainerUser { + output.store("abilities", Abilities.Packed.CODEC, this.abilities.pack()); + this.enderChestInventory.storeAsSlots(output.list("EnderItems", ItemStackWithSlot.CODEC)); + this.lastDeathLocation.ifPresent(pos -> output.store("LastDeathLocation", GlobalPos.CODEC, pos)); ++ this.statusBarList.save(output); // Camellia - Add status bars + } + + @Override +@@ -2255,4 +2258,12 @@ public abstract class Player extends Avatar implements ContainerUser { + public static final Player.BedSleepingProblem NOT_SAFE = new Player.BedSleepingProblem(Component.translatable("block.minecraft.bed.not_safe")); + public static final Player.BedSleepingProblem EXPLOSION = new Player.BedSleepingProblem(null); // Paper - Added to properly handle explosions in bed events + } ++ ++ // KioCG start ++ @Override ++ public boolean shouldTickHot() { ++ return false; ++ } ++ // KioCG end ++ + } +diff --git a/net/minecraft/world/entity/projectile/Projectile.java b/net/minecraft/world/entity/projectile/Projectile.java +index 67d01bb84cd40a3f67de441e9dbdc2ac7638cada..721e3b083846fd363a31311196bc4c681969215c 100644 +--- a/net/minecraft/world/entity/projectile/Projectile.java ++++ b/net/minecraft/world/entity/projectile/Projectile.java +@@ -512,4 +512,11 @@ public abstract class Projectile extends Entity implements TraceableEntity { + public interface ProjectileFactory { + T create(final ServerLevel level, LivingEntity entity, ItemStack itemStack); + } ++ ++ // KioCG start ++ @Override ++ public boolean shouldTickHot() { ++ return false; ++ } ++ // KioCG end + } +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 64e0c4a60a74cd33f2b99f34fa2a419abf3a0c02..0e84b695a677e71f443a29c78fc5025eec7ae7f4 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -112,6 +112,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + // Paper start - rewrite chunk system + private boolean postProcessingDone; + private ca.spottedleaf.moonrise.patches.chunk_system.scheduling.NewChunkHolder chunkAndHolder; ++ private final com.kiocg.ChunkHot chunkHot = new com.kiocg.ChunkHot(); public com.kiocg.ChunkHot getChunkHot() { return this.chunkHot; } // KioCG + + @Override + public final boolean moonrise$isPostProcessingDone() { +@@ -960,6 +961,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + ProfilerFiller profiler = Profiler.get(); + profiler.push(this::getType); + foliaProfiler.startTimer(timerId); try { // Folia - profiler ++ LevelChunk.this.chunkHot.startTicking(); // KioCG + BlockState blockState = LevelChunk.this.getBlockState(pos); + if (this.blockEntity.getType().isValid(blockState)) { + this.ticker.tick(LevelChunk.this.level, this.blockEntity.getBlockPos(), blockState, this.blockEntity); +@@ -979,7 +981,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + ); + } // Paper - Remove the Block Entity if it's invalid + } +- } finally { foliaProfiler.stopTimer(timerId); } // Folia - profiler ++ } finally { foliaProfiler.stopTimer(timerId); LevelChunk.this.chunkHot.stopTickingAndCount(); } // Folia - profiler // KioCG + + profiler.pop(); + } catch (Throwable t) { +diff --git a/net/minecraft/world/level/redstone/NeighborUpdater.java b/net/minecraft/world/level/redstone/NeighborUpdater.java +index 041ed95948ff6bb4cf3610ce4522a6ba105b6812..44dbe6f83c8593c213132feabc7fe27615cb0049 100644 +--- a/net/minecraft/world/level/redstone/NeighborUpdater.java ++++ b/net/minecraft/world/level/redstone/NeighborUpdater.java +@@ -81,7 +81,10 @@ public interface NeighborUpdater { + return; + } + // CraftBukkit end ++ net.minecraft.world.level.chunk.LevelChunk levelChunk = level.getChunkIfLoaded(pos); // KioCG ++ if (levelChunk != null) levelChunk.getChunkHot().startTicking(); try { // KioCG + state.handleNeighborChanged(level, pos, changedBlock, orientation, movedByPiston); ++ } finally { if (levelChunk != null) levelChunk.getChunkHot().stopTickingAndCount(); } // KioCG + // Spigot start + } catch (StackOverflowError ex) { + level.lastPhysicsProblem = pos.immutable(); diff --git a/camellia-server/minecraft-patches/features/0041-Leaves-Vanilla-Hopper.patch b/camellia-server/minecraft-patches/features/0041-Leaves-Vanilla-Hopper.patch new file mode 100644 index 0000000..8afc782 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0041-Leaves-Vanilla-Hopper.patch @@ -0,0 +1,94 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 11:54:24 +0800 +Subject: [PATCH] Leaves Vanilla Hopper + +A part from leaves + +Origin patch link: https://github.com/LeavesMC/Leaves/blob/master/leaves-server/minecraft-patches/features/0092-Vanilla-hopper.patch +Origin license: https://github.com/LeavesMC/Leaves/blob/master/LICENSE.md + +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index a858cb658af70f07614fa1f4d9e8a3435d5c161f..4c9d0bcd8820b250b51c06fff4531021ab57d408 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -281,36 +281,55 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + ItemStack movedItem = origItemStack; + final int originalItemCount = origItemStack.getCount(); + final int movedItemCount = Math.min(level.spigotConfig.hopperAmount, originalItemCount); +- container.setChanged(); // original logic always marks source inv as changed even if no move happens. +- movedItem.setCount(movedItemCount); +- +- if (!worldData.skipPullModeEventFire) { // Folia - region threading +- movedItem = callPullMoveEvent(hopper, container, movedItem); +- if (movedItem == null) { // cancelled +- origItemStack.setCount(originalItemCount); +- // Drastically improve performance by returning true. +- // No plugin could have relied on the behavior of false as the other call +- // site for IMIE did not exhibit the same behavior ++ // Leaves start - fix vanilla hopper ++ if (movedItem.getCount() <= movedItemCount) { ++ if (!worldData.skipPullModeEventFire) { ++ movedItem = callPullMoveEvent(hopper, container, movedItem); ++ if (movedItem == null) { // cancelled ++ origItemStack.setCount(originalItemCount); ++ return true; ++ } ++ } ++ movedItem = origItemStack.copy(); ++ final ItemStack remainingItem = addItem(container, hopper, container.removeItem(i, movedItemCount), null); ++ if (remainingItem.isEmpty()) { ++ container.setChanged(); + return true; + } +- } ++ container.setItem(i, movedItem); ++ } else { ++ container.setChanged(); // original logic always marks source inv as changed even if no move happens. ++ movedItem.setCount(movedItemCount); + +- final ItemStack remainingItem = addItem(container, hopper, movedItem, null); +- final int remainingItemCount = remainingItem.getCount(); +- if (remainingItemCount != movedItemCount) { +- origItemStack = origItemStack.copy(true); +- origItemStack.setCount(originalItemCount); +- if (!origItemStack.isEmpty()) { +- origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); ++ if (!worldData.skipPullModeEventFire) { ++ movedItem = callPullMoveEvent(hopper, container, movedItem); ++ if (movedItem == null) { // cancelled ++ origItemStack.setCount(originalItemCount); ++ // Drastically improve performance by returning true. ++ // No plugin could have relied on the behavior of false as the other call ++ // site for IMIE did not exhibit the same behavior ++ return true; ++ } + } + +- IGNORE_TILE_UPDATES.set(true); // Folia - region threading +- container.setItem(i, origItemStack); +- IGNORE_TILE_UPDATES.set(false); // Folia - region threading +- container.setChanged(); +- return true; ++ final ItemStack remainingItem = addItem(container, hopper, movedItem, null); ++ final int remainingItemCount = remainingItem.getCount(); ++ if (remainingItemCount != movedItemCount) { ++ origItemStack = origItemStack.copy(true); ++ origItemStack.setCount(originalItemCount); ++ if (!origItemStack.isEmpty()) { ++ origItemStack.setCount(originalItemCount - movedItemCount + remainingItemCount); ++ } ++ ++ IGNORE_TILE_UPDATES.set(true); ++ container.setItem(i, origItemStack); ++ IGNORE_TILE_UPDATES.set(false); ++ container.setChanged(); ++ return true; ++ } ++ origItemStack.setCount(originalItemCount); + } +- origItemStack.setCount(originalItemCount); ++ // Leaves end - fix vanilla hopper + + if (level.paperConfig().hopper.cooldownWhenFull) { + applyCooldown(hopper); diff --git a/camellia-server/minecraft-patches/features/0042-Leaves-Lithium-Sleeping-Block-Entity.patch b/camellia-server/minecraft-patches/features/0042-Leaves-Lithium-Sleeping-Block-Entity.patch new file mode 100644 index 0000000..c96d518 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0042-Leaves-Lithium-Sleeping-Block-Entity.patch @@ -0,0 +1,2739 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 12:00:19 +0800 +Subject: [PATCH] Leaves Lithium Sleeping Block Entity + +As a part of leaves + +Origin patch link: https://github.com/LeavesMC/Leaves/blob/master/leaves-server/minecraft-patches/features/0136-Lithium-Sleeping-Block-Entity.patch +Origin license: https://github.com/LeavesMC/Leaves/blob/master/LICENSE.md + +diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java +index fe17157c4c6fc9ec745c5c11f6837e715d7949ab..be3bdce1b42a32f57d9ec71d8f115ea1ceac9d52 100644 +--- a/io/papermc/paper/threadedregions/RegionizedWorldData.java ++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -65,6 +65,10 @@ import java.util.Set; + import java.util.concurrent.CopyOnWriteArrayList; + import java.util.function.Consumer; + import java.util.function.Predicate; ++// Camellia start - imports for lithium sleeping block entity ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionInventoryEntityTracker; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionItemEntityMovementTracker; ++// Camellia end + + public final class RegionizedWorldData { + +@@ -75,6 +79,32 @@ public final class RegionizedWorldData { + public static final RegionizedData.RegioniserCallback REGION_CALLBACK = new RegionizedData.RegioniserCallback<>() { + @Override + public void merge(final RegionizedWorldData from, final RegionizedWorldData into, final long fromTickOffset) { ++ // Camellia start - Region threading for lithium sleeping block entity ++ // lithium listeners ++ final long fromRedstoneTimeOffsetForLithiumTracker = into.redstoneTime - from.redstoneTime; ++ for (var entry : from.containerEntityMovementTrackerMap.entrySet()) { ++ var key = entry.getKey(); ++ var tracker = entry.getValue(); ++ ++ if (!tracker.hasUser()) { ++ continue; ++ } ++ ++ tracker.updateTicks(fromTickOffset, fromRedstoneTimeOffsetForLithiumTracker); ++ into.containerEntityMovementTrackerMap.put(key, tracker); ++ } ++ for (var entry : from.itemEntityMovementTrackerMap.entrySet()) { ++ var key = entry.getKey(); ++ var tracker = entry.getValue(); ++ ++ if (!tracker.hasUser()) { ++ continue; ++ } ++ ++ tracker.updateTicks(fromTickOffset, fromRedstoneTimeOffsetForLithiumTracker); ++ into.itemEntityMovementTrackerMap.put(key, tracker); ++ } ++ // Camellia end + // connections + for (final Connection conn : from.connections) { + into.connections.add(conn); +@@ -114,16 +144,22 @@ public final class RegionizedWorldData { + from.fluidLevelTicks.merge(into.fluidLevelTicks, fromRedstoneTimeOffset); + + // tile entity ticking +- for (final TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) { ++ for (TickingBlockEntity tileEntityWrapped : from.pendingBlockEntityTickers) { // Camellia - Region threading for lithium sleeping block entity + into.pendingBlockEntityTickers.add(tileEntityWrapped); +- final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); ++ BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); // Camellia - Region threading for lithium sleeping block entity ++ // Camellia start - Region threading for lithium sleeping block entity ++ tileEntityWrapped.updateTicksForLithium(fromRedstoneTimeOffset); ++ // Camellia end - Region threading for lithium sleeping block entity + if (tileEntity != null) { + tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + } + } +- for (final TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) { ++ for (TickingBlockEntity tileEntityWrapped : from.blockEntityTickers) { // Camellia - Region threading for lithium sleeping block entity + into.blockEntityTickers.add(tileEntityWrapped); +- final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); ++ BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); // Camellia - Region threading for lithium sleeping block entity ++ // Camellia start - Region threading for lithium sleeping block entity ++ tileEntityWrapped.updateTicksForLithium(fromRedstoneTimeOffset); ++ // Camellia end - Region threading for lithium sleeping block entity + if (tileEntity != null) { + tileEntity.updateTicks(fromTickOffset, fromRedstoneTimeOffset); + } +@@ -167,6 +203,37 @@ public final class RegionizedWorldData { + public void split(final RegionizedWorldData from, final int chunkToRegionShift, + final Long2ReferenceOpenHashMap regionToData, + final ReferenceOpenHashSet dataSet) { ++ // Camellia start - Region threading for lithium sleeping block entity ++ // lithium entity movement listeners ++ for (var entry : from.containerEntityMovementTrackerMap.entrySet()) { ++ final long key = entry.getKey(); ++ var tracker = entry.getValue(); ++ ++ var sectionPos = net.minecraft.core.SectionPos.of(key); ++ var pos = sectionPos.chunk(); ++ ++ // skip no user listeners ++ if (!tracker.hasUser()) { ++ continue; ++ } ++ ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x() >> chunkToRegionShift, pos.z() >> chunkToRegionShift)).containerEntityMovementTrackerMap.put(key, tracker); ++ } ++ for (var entry : from.itemEntityMovementTrackerMap.entrySet()) { ++ final long key = entry.getKey(); ++ var tracker = entry.getValue(); ++ ++ var sectionPos = net.minecraft.core.SectionPos.of(key); ++ var pos = sectionPos.chunk(); ++ ++ // skip no user listeners ++ if (!tracker.hasUser()) { ++ continue; ++ } ++ ++ regionToData.get(CoordinateUtils.getChunkKey(pos.x() >> chunkToRegionShift, pos.z() >> chunkToRegionShift)).itemEntityMovementTrackerMap.put(key, tracker); ++ } ++ // Camellia end + // connections + for (final Connection conn : from.connections) { + final ServerPlayer player = conn.getPlayer(); +@@ -243,8 +310,9 @@ public final class RegionizedWorldData { + from.fluidLevelTicks.split(chunkToRegionShift, levelTicksFluidRegionData); + + // tile entity ticking +- for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) { +- final BlockPos pos = tileEntity.getPos(); ++ for (final TickingBlockEntity tileEntity : from.pendingBlockEntityTickers) { // Camellia - Region threading for lithium sleeping block entity ++ BlockPos pos = tileEntity.getPos(); // Camellia - Region threading for lithium sleeping block entity ++ if (pos == null) pos = tileEntity.getTileEntity().getBlockPos(); // Camellia - Region threading for lithium sleeping block entity + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + +@@ -254,8 +322,9 @@ public final class RegionizedWorldData { + } // else: when a chunk unloads, it does not actually _remove_ the tile entity from the list, it just gets + // marked as removed. So if there is no section, it's probably removed! + } +- for (final TickingBlockEntity tileEntity : from.blockEntityTickers) { +- final BlockPos pos = tileEntity.getPos(); ++ for (final TickingBlockEntity tileEntity : from.blockEntityTickers) { // Camellia - Region threading for lithium sleeping block entity ++ BlockPos pos = tileEntity.getPos(); // Camellia - Region threading for lithium sleeping block entity ++ if (pos == null) pos = tileEntity.getTileEntity().getBlockPos(); // Camellia - Region threading for lithium sleeping block entity + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + +@@ -483,6 +552,11 @@ public final class RegionizedWorldData { + public final io.papermc.paper.redstone.RedstoneWireTurbo turbo; + // Environment attribute system + public Reference2ObjectOpenHashMap, EnvironmentAttributeSystem.ValueSampler> attributeSamplers; ++ // Camellia start - Region threading for lithium sleeping block entity ++ // Lithium ++ public final Map containerEntityMovementTrackerMap = new it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap<>(); ++ public final Map itemEntityMovementTrackerMap = new it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap<>(); ++ // Camellia end + + public final TickRegions.TickRegionData regionData; + +diff --git a/net/minecraft/core/NonNullList.java b/net/minecraft/core/NonNullList.java +index 8fd27a342f7302284c48b3db7d085bb692e08cdc..8992e13df499333235e1a51b725c0ad3ccd0284d 100644 +--- a/net/minecraft/core/NonNullList.java ++++ b/net/minecraft/core/NonNullList.java +@@ -8,7 +8,7 @@ import java.util.Objects; + import org.jspecify.annotations.Nullable; + + public class NonNullList extends AbstractList { +- private final List list; ++ public final List list; // Leaves - private -> public + private final @Nullable E defaultValue; + + public static NonNullList create() { +diff --git a/net/minecraft/core/component/PatchedDataComponentMap.java b/net/minecraft/core/component/PatchedDataComponentMap.java +index 08ea8107fc8cbec3742278ab5cf03ef9048720f9..e59250af5d769076578017fb880e9168bc720947 100644 +--- a/net/minecraft/core/component/PatchedDataComponentMap.java ++++ b/net/minecraft/core/component/PatchedDataComponentMap.java +@@ -14,7 +14,7 @@ import java.util.Map.Entry; + import java.util.stream.Collectors; + import org.jspecify.annotations.Nullable; + +-public final class PatchedDataComponentMap implements DataComponentMap { ++public final class PatchedDataComponentMap implements DataComponentMap, org.leavesmc.leaves.lithium.common.util.change_tracking.ChangePublisher { // Leaves - Lithium Sleeping Block Entity + private final DataComponentMap prototype; + private Reference2ObjectMap, Optional> patch; + private boolean copyOnWrite; +@@ -137,6 +137,7 @@ public final class PatchedDataComponentMap implements DataComponentMap { + } + + private void ensureMapOwnership() { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.copyOnWrite && this.subscriber != null) this.subscriber.lithium$notify((PatchedDataComponentMap) (Object) this, 0); // Leaves - Lithium Sleeping Block Entity (only notify when actually copying, matches Lithium mixin semantics) + if (this.copyOnWrite) { + this.patch = new Reference2ObjectArrayMap<>(this.patch); + this.copyOnWrite = false; +@@ -233,4 +234,22 @@ public final class PatchedDataComponentMap implements DataComponentMap { + public String toString() { + return "{" + this.stream().map(TypedDataComponent::toString).collect(Collectors.joining(", ")) + "}"; + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber subscriber; ++ ++ @Override ++ public void lithium$subscribe(org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber subscriber, int subscriberData) { ++ if (subscriberData != 0) { ++ throw new UnsupportedOperationException("ComponentMapImpl does not support subscriber data"); ++ } ++ this.subscriber = org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber.combine(this.subscriber, 0, subscriber, 0); ++ } ++ ++ @Override ++ public int lithium$unsubscribe(org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber subscriber) { ++ this.subscriber = org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber.without(this.subscriber, subscriber); ++ return 0; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/server/commands/data/EntityDataAccessor.java b/net/minecraft/server/commands/data/EntityDataAccessor.java +index f2c5a6ce769d8a5fdb4b836eae6a61a4da91cb3f..968cdfa8a153b892963428967fd35c617b089d72 100644 +--- a/net/minecraft/server/commands/data/EntityDataAccessor.java ++++ b/net/minecraft/server/commands/data/EntityDataAccessor.java +@@ -57,6 +57,7 @@ public class EntityDataAccessor implements DataAccessor { + try (ProblemReporter.ScopedCollector reporter = new ProblemReporter.ScopedCollector(this.entity.problemPath(), LOGGER)) { + this.entity.load(TagValueInput.create(reporter, this.entity.registryAccess(), tag)); + this.entity.setUUID(uuid); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.entity instanceof net.minecraft.world.entity.item.ItemEntity itemEntity) itemEntity.levelCallback.onMove(); // Leaves - Lithium Sleeping Block Entity + } + } + +diff --git a/net/minecraft/world/Container.java b/net/minecraft/world/Container.java +index 6e08d50794c4af4405f3e164a3fd46f376b3f78f..dd2d0940eece3e85078e574f66f71a4c497d75fd 100644 +--- a/net/minecraft/world/Container.java ++++ b/net/minecraft/world/Container.java +@@ -16,7 +16,7 @@ import net.minecraft.world.level.Level; + import net.minecraft.world.level.block.entity.BlockEntity; + import org.jspecify.annotations.Nullable; + +-public interface Container extends Clearable, Iterable, SlotProvider { ++public interface Container extends Clearable, Iterable, SlotProvider, org.leavesmc.leaves.lithium.api.inventory.LithiumCooldownReceivingInventory, org.leavesmc.leaves.lithium.api.inventory.LithiumTransferConditionInventory { // Leaves - Lithium Sleeping Block Entity + float DEFAULT_DISTANCE_BUFFER = 4.0F; + + int getContainerSize(); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 18e0572fe28461e57edb13e6ed6f5a0b6b3604c9..83914a091cc8fd6ef726f2732238b164bb590ea2 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -324,7 +324,7 @@ public abstract class Entity + private static final EntityDataAccessor DATA_NO_GRAVITY = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.BOOLEAN); + protected static final EntityDataAccessor DATA_POSE = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.POSE); + public static final EntityDataAccessor DATA_TICKS_FROZEN = SynchedEntityData.defineId(Entity.class, EntityDataSerializers.INT); +- private EntityInLevelCallback levelCallback = EntityInLevelCallback.NULL; ++ public EntityInLevelCallback levelCallback = EntityInLevelCallback.NULL; // Leaves - private -> public + private final VecDeltaCodec packetPositionCodec = new VecDeltaCodec(); + public boolean needsSync; + public boolean syncPosition; +@@ -4467,11 +4467,13 @@ public abstract class Entity + } + + protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ final boolean toSameWorld = this.level != destination; // Camellia - Region threading for lithium sleeping block entity + this.removeAfterChangingDimensions(); // remove before so that any CBEntity#getHandle call affects this entity before copying + + Entity copy = this.getType().create(destination, EntitySpawnReason.DIMENSION_TRAVEL); + copy.restoreFrom(this); + copy.transform(pos, yaw, pitch, velocity); ++ if (toSameWorld) copy.notifyLithiumTrackerIfNeeded(); // Camellia - Region threading for lithium sleeping block entity + if (copy instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon dragon) dragon.syncDragonPartsAfterTeleportTransform(); // Camellia - Fix dragon part desync + // vanilla code used to call remove _after_ copying, and some stuff is required to be after copy - so add hook here + // for example, clearing of inventory after switching dimensions +@@ -5953,8 +5955,29 @@ public abstract class Entity + this.setBoundingBox(this.makeBoundingBox()); + } + // Paper end - Block invalid positions and bounding box ++ this.notifyLithiumTrackerIfNeeded(); // Camellia - Leaves lithium sleeping block entity + } + ++ // Camellia start - Region threading for lithium sleeping block entity ++ private void notifyLithiumTrackerIfNeeded() { ++ // Leaves start - Lithium Sleeping Block Entity ++ if (!moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) return; ++ var currentWorldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); ++ if (currentWorldData == null || currentWorldData.world != this.level) return; ++ if (this instanceof ItemEntity) { ++ long sectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(this); ++ org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionItemEntityMovementTracker tracker = currentWorldData.itemEntityMovementTrackerMap.get(sectionKey); ++ if (tracker != null) tracker.notifyAllListeners(currentWorldData.getRedstoneGameTime()); // Camellia - Region threading for lithium sleeping block entity ++ } ++ else if (this instanceof net.minecraft.world.entity.vehicle.ContainerEntity) { ++ long sectionKey = ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkSectionKey(this); ++ org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionInventoryEntityTracker tracker = currentWorldData.containerEntityMovementTrackerMap.get(sectionKey); ++ if (tracker != null) tracker.notifyAllListeners(currentWorldData.getRedstoneGameTime()); // Camellia - Region threading for lithium sleeping block entity ++ } ++ // Leaves end - Lithium Sleeping Block Entity ++ } ++ // Camellia end ++ + public void checkDespawn() { + } + +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index 9a75c6b38f1cabd4415067d781f6ad49f4d8ebc8..1606bb148203d4bf7bdacb58acf1144c74fcbffb 100644 +--- a/net/minecraft/world/entity/item/ItemEntity.java ++++ b/net/minecraft/world/entity/item/ItemEntity.java +@@ -35,8 +35,12 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.phys.Vec3; + import org.jspecify.annotations.Nullable; ++// Leaves start - Lithium Sleeping Block Entity ++import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangePublisher; ++import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber; ++// Leaves end - Lithium Sleeping Block Entity + +-public class ItemEntity extends Entity implements TraceableEntity { ++public class ItemEntity extends Entity implements TraceableEntity, ChangePublisher, ChangeSubscriber.CountChangeSubscriber { // Leaves - Lithium Sleeping Block Entity + private static final EntityDataAccessor DATA_ITEM = SynchedEntityData.defineId(ItemEntity.class, EntityDataSerializers.ITEM_STACK); + private static final float FLOAT_HEIGHT = 0.1F; + public static final float EYE_HEIGHT = 0.2125F; +@@ -532,6 +536,25 @@ public class ItemEntity extends Entity implements TraceableEntity { + } + + public void setItem(final ItemStack itemStack) { ++ // Leaves start - Lithium Sleeping Block Entity ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.subscriber != null) { ++ ItemStack oldStack = this.getItem(); ++ if (oldStack != itemStack) { ++ if (!oldStack.isEmpty()) { ++ oldStack.lithium$unsubscribe(this); ++ } ++ ++ if (!itemStack.isEmpty()) { ++ itemStack.lithium$subscribe(this, this.subscriberData); ++ this.subscriber.lithium$notify((ItemEntity) (Object) this, this.subscriberData); ++ } else { ++ this.subscriber.lithium$forceUnsubscribe((ItemEntity) (Object) this, this.subscriberData); ++ this.subscriber = null; ++ this.subscriberData = 0; ++ } ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + this.getEntityData().set(DATA_ITEM, itemStack); + this.despawnRate = this.level().paperConfig().entities.spawning.altItemDespawnRate.enabled ? this.level().paperConfig().entities.spawning.altItemDespawnRate.items.getOrDefault(itemStack.getItem(), this.level().spigotConfig.itemDespawnRate) : this.level().spigotConfig.itemDespawnRate; // Paper - Alternative item-despawn-rate + } +@@ -599,4 +622,76 @@ public class ItemEntity extends Entity implements TraceableEntity { + public @Nullable SlotAccess getSlot(final int slot) { + return slot == 0 ? SlotAccess.of(this::getItem, this::setItem) : super.getSlot(slot); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private ChangeSubscriber subscriber; ++ //Stores the data of the subscriber, unless the subscriber is a Multi which stores the data in a list, in which case this variable stores 0 ++ private int subscriberData; ++ ++ private void startTrackingChanges() { ++ ItemStack stack = this.getItem(); ++ if (!stack.isEmpty()) { ++ stack.lithium$subscribe(this, 0); ++ } ++ } ++ ++ @Override ++ public void lithium$subscribe(ChangeSubscriber subscriber, int subscriberData) { ++ if (this.subscriber == null) { ++ this.startTrackingChanges(); ++ } ++ this.subscriber = ChangeSubscriber.combine(this.subscriber, this.subscriberData, subscriber, subscriberData); ++ if (this.subscriber instanceof ChangeSubscriber.Multi) { ++ this.subscriberData = 0; ++ } else { ++ this.subscriberData = subscriberData; ++ } ++ } ++ ++ @Override ++ public int lithium$unsubscribe(ChangeSubscriber subscriber) { ++ int retval = ChangeSubscriber.dataOf(this.subscriber, subscriber, this.subscriberData); ++ this.subscriberData = ChangeSubscriber.dataWithout(this.subscriber, subscriber, this.subscriberData); ++ this.subscriber = ChangeSubscriber.without(this.subscriber, subscriber); ++ ++ if (this.subscriber == null) { ++ ItemStack stack = this.getItem(); ++ if (!stack.isEmpty()) { ++ stack.lithium$unsubscribe(this); ++ } ++ } ++ return retval; ++ } ++ ++ @Override ++ public void lithium$notify(ItemStack publisher, int subscriberData) { ++ if (publisher != this.getItem()) { ++ throw new IllegalStateException("Received notification from an unexpected publisher"); ++ } ++ ++ if (this.subscriber != null) { ++ this.subscriber.lithium$notify(this, this.subscriberData); ++ } ++ } ++ ++ @Override ++ public void lithium$forceUnsubscribe(ItemStack publisher, int subscriberData) { ++ if (this.subscriber != null) { ++ this.subscriber.lithium$forceUnsubscribe(this, this.subscriberData); ++ this.subscriber = null; ++ this.subscriberData = 0; ++ } ++ } ++ ++ @Override ++ public void lithium$notifyCount(ItemStack publisher, int subscriberData, int newCount) { ++ if (publisher != this.getItem()) { ++ throw new IllegalStateException("Received notification from an unexpected publisher"); ++ } ++ ++ if (this.subscriber instanceof ChangeSubscriber.CountChangeSubscriber countChangeSubscriber) { ++ countChangeSubscriber.lithium$notifyCount(this, this.subscriberData, newCount); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/entity/vehicle/minecart/AbstractMinecartContainer.java b/net/minecraft/world/entity/vehicle/minecart/AbstractMinecartContainer.java +index d3651110e15e8e1ad530df4bb4c05dbc73a79a63..d87dcb72fd5d2b22eafc1a444d4fc3807595b6a5 100644 +--- a/net/minecraft/world/entity/vehicle/minecart/AbstractMinecartContainer.java ++++ b/net/minecraft/world/entity/vehicle/minecart/AbstractMinecartContainer.java +@@ -23,7 +23,7 @@ import net.minecraft.world.level.storage.loot.LootTable; + import net.minecraft.world.phys.Vec3; + import org.jspecify.annotations.Nullable; + +-public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity { ++public abstract class AbstractMinecartContainer extends AbstractMinecart implements ContainerEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + private NonNullList itemStacks = NonNullList.withSize(this.getContainerSize(), ItemStack.EMPTY); // CraftBukkit - SPIGOT-3513 + private @Nullable ResourceKey lootTable; + private long lootTableSeed; +@@ -217,4 +217,15 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme + return this.getBukkitEntity().getLocation(); + } + // CraftBukkit end ++ // Leaves start - Lithium Sleeping Block Entity ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return itemStacks; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ itemStacks = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java +index 136e018d83c5be90894358a97b23f5fcfa6c0d38..93c94a059873993b2e3e03a47c61f90d28f82362 100644 +--- a/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -893,6 +893,7 @@ public abstract class AbstractContainerMenu { + + float totalPercent = 0.0F; + ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && container instanceof org.leavesmc.leaves.lithium.api.inventory.LithiumInventory optimizedInventory) return org.leavesmc.leaves.lithium.common.hopper.InventoryHelper.getLithiumStackList(optimizedInventory).getSignalStrength(container); // Leaves - Lithium Sleeping Block Entity + for (int i = 0; i < container.getContainerSize(); i++) { + ItemStack itemStack = container.getItem(i); + if (!itemStack.isEmpty()) { +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index dc1e9acce122f3ebd7bf2b03150b4045db9493df..0bd2f2da669aa1a59d0af65aea3b305dd9be4476 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -98,8 +98,12 @@ import org.apache.commons.lang3.function.TriConsumer; + import org.apache.commons.lang3.mutable.MutableBoolean; + import org.jspecify.annotations.Nullable; + import org.slf4j.Logger; ++// Leaves start - Lithium Sleeping Block Entity ++import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangePublisher; ++import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber; ++// Leaves end - Lithium Sleeping Block Entity + +-public final class ItemStack implements DataComponentHolder, ItemInstance { ++public final class ItemStack implements DataComponentHolder, ItemInstance, ChangePublisher, ChangeSubscriber { // Leaves - Lithium Sleeping Block Entity + private static final List OP_NBT_WARNING = List.of( + Component.translatable("item.op_warning.line1").withStyle(ChatFormatting.RED, ChatFormatting.BOLD), + Component.translatable("item.op_warning.line2").withStyle(ChatFormatting.RED), +@@ -965,6 +969,7 @@ public final class ItemStack implements DataComponentHolder, ItemInstance { + // CraftBukkit end + + public @Nullable T set(final DataComponentType type, final @Nullable T value) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && type == DataComponents.ENCHANTMENTS && this.subscriber instanceof ChangeSubscriber.EnchantmentSubscriber enchantmentSubscriber) enchantmentSubscriber.lithium$notifyAfterEnchantmentChange(this, this.subscriberData); // Leaves - Lithium Sleeping Block Entity + return this.components.set(type, value); + } + +@@ -1304,6 +1309,23 @@ public final class ItemStack implements DataComponentHolder, ItemInstance { + } + + public void setCount(final int count) { ++ // Leaves start - Lithium Sleeping Block Entity ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && count != this.count) { ++ if (this.subscriber instanceof ChangeSubscriber.CountChangeSubscriber countChangeSubscriber) { ++ countChangeSubscriber.lithium$notifyCount(this, this.subscriberData, count); ++ } ++ ++ if (count == 0) { ++ this.components.lithium$unsubscribe(this); ++ ++ if (this.subscriber != null) { ++ this.subscriber.lithium$forceUnsubscribe(this, this.subscriberData); ++ this.subscriber = null; ++ this.subscriberData = 0; ++ } ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + this.count = count; + } + +@@ -1371,4 +1393,90 @@ public final class ItemStack implements DataComponentHolder, ItemInstance { + .or(() -> Optional.ofNullable(this.getItem().getItemDamageSource(attacker))) + .orElseGet(attacker::createDamageSource); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private ChangeSubscriber subscriber; ++ private int subscriberData; ++ ++ @Override ++ public void lithium$subscribe(ChangeSubscriber subscriber, int subscriberData) { ++ if (this.isEmpty()) { ++ throw new IllegalStateException("Cannot subscribe to an empty ItemStack!"); ++ } ++ ++ if (this.subscriber == null) { ++ this.startTrackingChanges(); ++ } ++ this.subscriber = ChangeSubscriber.combine(this.subscriber, this.subscriberData, subscriber, subscriberData); ++ if (this.subscriber instanceof ChangeSubscriber.Multi) { ++ this.subscriberData = 0; ++ } else { ++ this.subscriberData = subscriberData; ++ } ++ } ++ ++ @Override ++ public int lithium$unsubscribe(ChangeSubscriber subscriber) { ++ if (this.isEmpty()) { ++ throw new IllegalStateException("Cannot unsubscribe from an empty ItemStack!"); ++ } ++ ++ int retval = ChangeSubscriber.dataOf(this.subscriber, subscriber, this.subscriberData); ++ this.subscriberData = ChangeSubscriber.dataWithout(this.subscriber, subscriber, this.subscriberData); ++ this.subscriber = ChangeSubscriber.without(this.subscriber, subscriber); ++ ++ if (this.subscriber == null) { ++ this.components.lithium$unsubscribe(this); ++ } ++ return retval; ++ } ++ ++ @Override ++ public void lithium$unsubscribeWithData(ChangeSubscriber subscriber, int subscriberData) { ++ if (this.isEmpty()) { ++ throw new IllegalStateException("Cannot unsubscribe from an empty ItemStack!"); ++ } ++ ++ this.subscriberData = ChangeSubscriber.dataWithout(this.subscriber, subscriber, this.subscriberData, subscriberData, true); ++ this.subscriber = ChangeSubscriber.without(this.subscriber, subscriber, subscriberData, true); ++ ++ if (this.subscriber == null) { ++ this.components.lithium$unsubscribe(this); ++ } ++ } ++ ++ @Override ++ public boolean lithium$isSubscribedWithData(ChangeSubscriber subscriber, int subscriberData) { ++ if (this.isEmpty()) { ++ throw new IllegalStateException("Cannot be subscribed to an empty ItemStack!"); ++ } ++ ++ return ChangeSubscriber.containsSubscriber(this.subscriber, this.subscriberData, subscriber, subscriberData); ++ } ++ ++ @Override ++ public void lithium$forceUnsubscribe(PatchedDataComponentMap publisher, int subscriberData) { ++ if (publisher != this.components) { ++ throw new IllegalStateException("Invalid publisher, expected " + this.components + " but got " + publisher); ++ } ++ this.subscriber.lithium$forceUnsubscribe(this, this.subscriberData); ++ this.subscriber = null; ++ this.subscriberData = 0; ++ } ++ ++ private void startTrackingChanges() { ++ this.components.lithium$subscribe(this, 0); ++ } ++ ++ @Override ++ public void lithium$notify(PatchedDataComponentMap publisher, int subscriberData) { ++ if (publisher != this.components) { ++ throw new IllegalStateException("Invalid publisher, expected " + this.components + " but got " + publisher); ++ } ++ ++ if (this.subscriber != null) { ++ this.subscriber.lithium$notify(this, this.subscriberData); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 41b84a84a2ae24555a065d0779957b7b71611523..a6d1f8ccf2e33394508d8d14bec2141942e31ccd 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1553,7 +1553,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + // Paper end - Fix MC-117075 use removeAll + if (ticker.isRemoved()) { + toRemove.add(ticker); // Paper - Fix MC-117075 use removeAll +- } else if (tickBlockEntities && this.shouldTickBlocksAt(ticker.getPos())) { ++ } else if (tickBlockEntities && this.shouldTickBlockPosFilterNull(ticker.getPos())) { // Leaves - Lithium Sleeping Block Entity + ticker.tick(); + // Paper start - rewrite chunk system + if ((++tickedEntities & 7) == 0) { +@@ -1631,6 +1631,27 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + return this.shouldTickBlocksAt(ChunkPos.pack(pos)); + } + ++ // Leaves start - Lithium Sleeping Block Entity ++ public @org.jspecify.annotations.Nullable BlockEntity lithium$getLoadedExistingBlockEntity(BlockPos pos) { ++ if (!this.isOutsideBuildHeight(pos)) { ++ if (this.isClientSide || ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this, pos)) { ++ ChunkAccess chunk = this.getChunk(SectionPos.blockToSectionCoord(pos.getX()), SectionPos.blockToSectionCoord(pos.getZ()), ChunkStatus.FULL, false); ++ if (chunk != null) { ++ return chunk.getBlockEntity(pos); ++ } ++ } ++ } ++ return null; ++ } ++ ++ private boolean shouldTickBlockPosFilterNull(BlockPos pos) { ++ if (pos == null) { ++ return false; ++ } ++ return shouldTickBlocksAt(pos); ++ } ++ // Leaves end - Lithium Sleeping Block Entity ++ + public void explode( + final @Nullable Entity source, final double x, final double y, final double z, final float r, final Level.ExplosionInteraction blockInteraction + ) { +diff --git a/net/minecraft/world/level/block/ComposterBlock.java b/net/minecraft/world/level/block/ComposterBlock.java +index 69e3969242ad52168e947af209e6fa72f36178ae..82f041a6c72eb09d365b5f83dd4731467ad49563 100644 +--- a/net/minecraft/world/level/block/ComposterBlock.java ++++ b/net/minecraft/world/level/block/ComposterBlock.java +@@ -426,7 +426,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + } + +- public static class EmptyContainer extends SimpleContainer implements WorldlyContainer { ++ public static class EmptyContainer extends SimpleContainer implements WorldlyContainer, org.leavesmc.leaves.lithium.common.hopper.BlockStateOnlyInventory { // Leaves - Lithium Sleeping Block Entity + public EmptyContainer(LevelAccessor levelAccessor, BlockPos blockPos) { // CraftBukkit + super(0); + this.bukkitOwner = new org.bukkit.craftbukkit.inventory.CraftBlockInventoryHolder(levelAccessor, blockPos, this); // CraftBukkit +@@ -448,7 +448,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + } + +- public static class InputContainer extends SimpleContainer implements WorldlyContainer { ++ public static class InputContainer extends SimpleContainer implements WorldlyContainer, org.leavesmc.leaves.lithium.common.hopper.BlockStateOnlyInventory { // Leaves - Lithium Sleeping Block Entity + private final BlockState state; + private final LevelAccessor level; + private final BlockPos pos; +@@ -494,12 +494,13 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + } + // Paper end - Add CompostItemEvent and EntityCompostItemEvent + this.level.levelEvent(LevelEvent.COMPOSTER_FILL, this.pos, newState != this.state ? 1 : 0); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.changed = false; // Leaves - Lithium Sleeping Block Entity + this.removeItemNoUpdate(0); + } + } + } + +- public static class OutputContainer extends SimpleContainer implements WorldlyContainer { ++ public static class OutputContainer extends SimpleContainer implements WorldlyContainer, org.leavesmc.leaves.lithium.common.hopper.BlockStateOnlyInventory { // Leaves - Lithium Sleeping Block Entity + private final BlockState state; + private final LevelAccessor level; + private final BlockPos pos; +diff --git a/net/minecraft/world/level/block/DiodeBlock.java b/net/minecraft/world/level/block/DiodeBlock.java +index 2339099e05af80b234e64778822b012cc6318157..d828f2535a4860d96f3ca6c95a54e00b035ffdc3 100644 +--- a/net/minecraft/world/level/block/DiodeBlock.java ++++ b/net/minecraft/world/level/block/DiodeBlock.java +@@ -179,6 +179,7 @@ public abstract class DiodeBlock extends HorizontalDirectionalBlock { + @Override + protected void onPlace(final BlockState state, final Level level, final BlockPos pos, final BlockState oldState, final boolean movedByPiston) { + this.updateNeighborsInFront(level, pos, state); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this instanceof ComparatorBlock && !oldState.is(Blocks.COMPARATOR)) org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracking.notifyNearbyBlockEntitiesAboutNewComparator(level, pos); // Leaves - Lithium Sleeping Block Entity + } + + @Override +diff --git a/net/minecraft/world/level/block/HopperBlock.java b/net/minecraft/world/level/block/HopperBlock.java +index 3932da81757ea0ab7a41617165f5b64dc44ea3eb..2ccabd98f9fa7f2a228c7ee320d0f8c98b4926ac 100644 +--- a/net/minecraft/world/level/block/HopperBlock.java ++++ b/net/minecraft/world/level/block/HopperBlock.java +@@ -38,7 +38,7 @@ import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + import org.jspecify.annotations.Nullable; + +-public class HopperBlock extends BaseEntityBlock { ++public class HopperBlock extends BaseEntityBlock implements org.leavesmc.leaves.lithium.common.block.entity.ShapeUpdateHandlingBlockBehaviour { // Leaves - Lithium Sleeping Block Entity + public static final MapCodec CODEC = simpleCodec(HopperBlock::new); + public static final EnumProperty FACING = BlockStateProperties.FACING_HOPPER; + public static final BooleanProperty ENABLED = BlockStateProperties.ENABLED; +@@ -100,6 +100,17 @@ public class HopperBlock extends BaseEntityBlock { + protected void onPlace(final BlockState state, final Level level, final BlockPos pos, final BlockState oldState, final boolean movedByPiston) { + if (!oldState.is(state.getBlock())) { + this.checkPoweredState(level, pos, state); ++ // Leaves start - Lithium Sleeping Block Entity ++ //invalidate caches of nearby hoppers when placing an update suppressed hopper ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && level.getBlockState(pos) != state) { ++ for (Direction direction : UPDATE_SHAPE_ORDER) { ++ BlockEntity hopper = level.lithium$getLoadedExistingBlockEntity(pos.relative(direction)); ++ if (hopper instanceof org.leavesmc.leaves.lithium.common.hopper.UpdateReceiver updateReceiver) { ++ updateReceiver.lithium$invalidateCacheOnNeighborUpdate(direction == Direction.DOWN); ++ } ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } + } + +@@ -118,6 +129,7 @@ public class HopperBlock extends BaseEntityBlock { + protected void neighborChanged( + final BlockState state, final Level level, final BlockPos pos, final Block block, final @Nullable Orientation orientation, final boolean movedByPiston + ) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && level.lithium$getLoadedExistingBlockEntity(pos) instanceof org.leavesmc.leaves.lithium.common.hopper.UpdateReceiver updateReceiver) updateReceiver.lithium$invalidateCacheOnUndirectedNeighborUpdate(); // Leaves - Lithium Sleeping Block Entity + this.checkPoweredState(level, pos, state); + } + +@@ -177,4 +189,25 @@ public class HopperBlock extends BaseEntityBlock { + protected boolean isPathfindable(final BlockState state, final PathComputationType type) { + return false; + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ @Override ++ public void lithium$handleShapeUpdate(net.minecraft.world.level.LevelReader levelReader, BlockState myBlockState, BlockPos myPos, BlockPos posFrom, BlockState newState) { ++ //invalidate cache when composters change state ++ if (newState.getBlock() instanceof net.minecraft.world.WorldlyContainerHolder) { ++ this.updateHopper(levelReader, myBlockState, myPos, posFrom); ++ } ++ } ++ ++ private void updateHopper(net.minecraft.world.level.LevelReader world, BlockState myBlockState, BlockPos myPos, BlockPos posFrom) { ++ Direction facing = myBlockState.getValue(HopperBlock.FACING); ++ boolean above = posFrom.getY() == myPos.getY() + 1; ++ if (above || posFrom.getX() == myPos.getX() + facing.getStepX() && posFrom.getY() == myPos.getY() + facing.getStepY() && posFrom.getZ() == myPos.getZ() + facing.getStepZ()) { ++ BlockEntity hopper = ((net.minecraft.world.level.Level) world).lithium$getLoadedExistingBlockEntity(myPos); ++ if (hopper instanceof org.leavesmc.leaves.lithium.common.hopper.UpdateReceiver updateReceiver) { ++ updateReceiver.lithium$invalidateCacheOnNeighborUpdate(above); ++ } ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index 78dbdf43ac9095eb3cf0b0653bab2603d5527ff4..601471d7578b93f30d8033bfab4d4050a8ef78f8 100644 +--- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -39,7 +39,7 @@ import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.phys.Vec3; + import org.jspecify.annotations.Nullable; + +-public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, StackedContentsCompatible, RecipeCraftingHolder { ++public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, StackedContentsCompatible, RecipeCraftingHolder, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SetChangedHandlingBlockEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + protected static final int SLOT_INPUT = 0; + protected static final int SLOT_FUEL = 1; + protected static final int SLOT_RESULT = 2; +@@ -162,6 +162,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + this.recipesUsed.clear(); + this.recipesUsed.putAll(input.read("RecipesUsed", RECIPES_USED_CODEC).orElse(Map.of())); + this.cookSpeedMultiplier = input.getDoubleOr("Paper.CookSpeedMultiplier", 1); // Paper - cook speed multiplier API ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.isSleeping() && this.level != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -269,6 +270,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + if (changed) { + setChanged(level, pos, state); + } ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.checkSleep(state); // Leaves - Lithium Sleeping Block Entity + } + + private static void consumeFuel(final NonNullList items, final ItemStack fuel) { +@@ -510,4 +512,53 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + this.getRecipesToAwardAndPopExperience(serverLevel, Vec3.atCenterOf(pos)); + } + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ private void checkSleep(BlockState state) { ++ if (this.litTimeRemaining <= 0 && this.cookingTimer == 0 && (state.is(net.minecraft.world.level.block.Blocks.FURNACE) || state.is(net.minecraft.world.level.block.Blocks.BLAST_FURNACE) || state.is(net.minecraft.world.level.block.Blocks.SMOKER)) && this.level != null) { // Leaves - Paper 26.1 fix ++ this.lithium$startSleeping(); ++ } ++ } ++ ++ @Override ++ public void lithium$handleSetChanged() { ++ if (this.isSleeping() && this.level != null && !this.level.isClientSide()) { ++ this.wakeUpNow(); ++ } ++ } ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +index 118af4e35eec652e7eb8a484a44652cfabc1bab5..fd2c62a4c9a9dbd81c3c4d1410694e5c6eea5d29 100644 +--- a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +@@ -23,7 +23,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class BarrelBlockEntity extends RandomizableContainerBlockEntity { ++public class BarrelBlockEntity extends RandomizableContainerBlockEntity implements org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + // CraftBukkit start - add fields and methods + public java.util.List transaction = new java.util.ArrayList<>(); + private int maxStack = MAX_STACK; +@@ -129,6 +129,7 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + @Override + protected void setItems(final NonNullList items) { + this.items = items; ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -180,4 +181,18 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + double z = this.worldPosition.getZ() + 0.5 + direction.getZ() / 2.0; + this.level.playSound(null, x, y, z, event, SoundSource.BLOCKS, 0.5F, this.level.getRandom().nextFloat() * 0.1F + 0.9F); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +index 0e594121b60aa5a01b5bbe94b80350f2fc698b1a..f100d51275522748664592453c9c51cb627ef5bd 100644 +--- a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +@@ -25,8 +25,17 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.phys.Vec3; + import org.jspecify.annotations.Nullable; ++// Leaves start - Lithium Sleeping Block Entity ++import it.unimi.dsi.fastutil.objects.ReferenceArraySet; ++import org.leavesmc.leaves.lithium.api.inventory.LithiumInventory; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeEmitter; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeListener; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; ++import org.leavesmc.leaves.lithium.common.hopper.InventoryHelper; ++import org.leavesmc.leaves.lithium.common.hopper.LithiumStackList; ++// Leaves end - Lithium Sleeping Block Entity + +-public abstract class BaseContainerBlockEntity extends BlockEntity implements Container, MenuProvider, Nameable { ++public abstract class BaseContainerBlockEntity extends BlockEntity implements Container, MenuProvider, Nameable, InventoryChangeEmitter { // Leaves - Lithium Sleeping Block Entity + public LockCode lockKey = LockCode.NO_LOCK; + public @Nullable Component name; + +@@ -39,6 +48,7 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co + super.loadAdditional(input); + this.lockKey = LockCode.fromTag(input); + this.name = parseCustomNameSafe(input, "CustomName"); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this instanceof InventoryChangeTracker inventoryChangeTracker) inventoryChangeTracker.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -183,4 +193,98 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co + return org.bukkit.craftbukkit.util.CraftLocation.toBukkit(this.worldPosition, this.level); + } + // CraftBukkit end ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ ReferenceArraySet inventoryChangeListeners = null; ++ ReferenceArraySet inventoryHandlingTypeListeners = null; ++ ++ @Override ++ public void lithium$emitContentModified() { ++ ReferenceArraySet inventoryChangeListeners = this.inventoryChangeListeners; ++ if (inventoryChangeListeners != null) { ++ for (InventoryChangeListener inventoryChangeListener : inventoryChangeListeners) { ++ inventoryChangeListener.lithium$handleInventoryContentModified(this); ++ } ++ inventoryChangeListeners.clear(); ++ } ++ } ++ ++ @Override ++ public void lithium$emitStackListReplaced() { ++ ReferenceArraySet listeners = this.inventoryHandlingTypeListeners; ++ if (listeners != null && !listeners.isEmpty()) { ++ for (InventoryChangeListener inventoryChangeListener : listeners) { ++ inventoryChangeListener.handleStackListReplaced(this); ++ } ++ listeners.clear(); ++ } ++ ++ if (this instanceof InventoryChangeListener listener) { ++ listener.handleStackListReplaced(this); ++ } ++ ++ this.invalidateChangeListening(); ++ } ++ ++ @Override ++ public void lithium$emitRemoved() { ++ ReferenceArraySet listeners = this.inventoryHandlingTypeListeners; ++ if (listeners != null && !listeners.isEmpty()) { ++ for (InventoryChangeListener listener : listeners) { ++ listener.lithium$handleInventoryRemoved(this); ++ } ++ listeners.clear(); ++ } ++ ++ if (this instanceof InventoryChangeListener listener) { ++ listener.lithium$handleInventoryRemoved(this); ++ } ++ ++ this.invalidateChangeListening(); ++ } ++ ++ private void invalidateChangeListening() { ++ if (this.inventoryChangeListeners != null) { ++ this.inventoryChangeListeners.clear(); ++ } ++ ++ LithiumStackList lithiumStackList = this instanceof LithiumInventory ? InventoryHelper.getLithiumStackListOrNull((LithiumInventory) this) : null; ++ if (lithiumStackList != null && this instanceof InventoryChangeTracker inventoryChangeTracker) { ++ lithiumStackList.removeInventoryModificationCallback(inventoryChangeTracker); ++ } ++ } ++ ++ @Override ++ public void lithium$emitFirstComparatorAdded() { ++ ReferenceArraySet inventoryChangeListeners = this.inventoryChangeListeners; ++ if (inventoryChangeListeners != null && !inventoryChangeListeners.isEmpty()) { ++ inventoryChangeListeners.removeIf(inventoryChangeListener -> inventoryChangeListener.lithium$handleComparatorAdded(this)); ++ } ++ } ++ ++ @Override ++ public void lithium$forwardContentChangeOnce(InventoryChangeListener inventoryChangeListener, LithiumStackList stackList, InventoryChangeTracker thisTracker) { ++ if (this.inventoryChangeListeners == null) { ++ this.inventoryChangeListeners = new ReferenceArraySet<>(1); ++ } ++ stackList.setInventoryModificationCallback(thisTracker); ++ this.inventoryChangeListeners.add(inventoryChangeListener); ++ ++ } ++ ++ @Override ++ public void lithium$forwardMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { ++ if (this.inventoryHandlingTypeListeners == null) { ++ this.inventoryHandlingTypeListeners = new ReferenceArraySet<>(1); ++ } ++ this.inventoryHandlingTypeListeners.add(inventoryChangeListener); ++ } ++ ++ @Override ++ public void lithium$stopForwardingMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { ++ if (this.inventoryHandlingTypeListeners != null) { ++ this.inventoryHandlingTypeListeners.remove(inventoryChangeListener); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java +index 2887cbaa954771d0fe357d8ec8df75073e9afa91..69227ca2534a3abe3be3d5c704335bc5397e2d20 100644 +--- a/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -37,8 +37,16 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import org.jspecify.annotations.Nullable; + import org.slf4j.Logger; +- +-public abstract class BlockEntity implements DebugValueSource, TypedInstance> { ++// Leaves start - Lithium Sleeping Block Entity ++import net.minecraft.core.Direction; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracker; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracking; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; ++import org.leavesmc.leaves.lithium.common.block.entity.SetBlockStateHandlingBlockEntity; ++import org.leavesmc.leaves.lithium.common.block.entity.SetChangedHandlingBlockEntity; ++// Leaves end - Lithium Sleeping Block Entity ++ ++public abstract class BlockEntity implements DebugValueSource, TypedInstance>, ComparatorTracker, SetBlockStateHandlingBlockEntity, SetChangedHandlingBlockEntity { // Leaves - Lithium Sleeping Block Entity + static final ThreadLocal IGNORE_TILE_UPDATES = ThreadLocal.withInitial(() -> Boolean.FALSE); // Paper - Perf: Optimize Hoppers // Folia - region threading + // CraftBukkit start - data containers + private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); +@@ -65,6 +73,7 @@ public abstract class BlockEntity implements DebugValueSource, TypedInstance= 1 && offset <= 2) { ++ this.hasComparators = COMPARATOR_PRESENT; ++ ++ if (this instanceof InventoryChangeTracker inventoryChangeTracker) { ++ inventoryChangeTracker.lithium$emitFirstComparatorAdded(); ++ } ++ } ++ } ++ ++ @Override ++ public boolean lithium$hasAnyComparatorNearby() { ++ if (this.hasComparators == UNKNOWN) { ++ this.hasComparators = ComparatorTracking.findNearbyComparators(this.level, this.worldPosition) ? COMPARATOR_PRESENT : COMPARATOR_ABSENT; ++ } ++ return this.hasComparators == COMPARATOR_PRESENT; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +index 5d26efb1feb1fd7d0f3bcc0fd7678b303518312a..bd320bcaa7964bf5532c6cb9634a5227e6859d1a 100644 +--- a/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +@@ -27,7 +27,7 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import org.jspecify.annotations.Nullable; + +-public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer { ++public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SetChangedHandlingBlockEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + private static final int INGREDIENT_SLOT = 3; + private static final int FUEL_SLOT = 4; + private static final int[] SLOTS_FOR_UP = new int[]{3}; +@@ -139,6 +139,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + } + + public static void serverTick(final Level level, final BlockPos pos, final BlockState selfState, final BrewingStandBlockEntity entity) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.checkSleep(selfState); // Leaves - Lithium Sleeping Block Entity + ItemStack fuel = entity.items.get(4); + if (entity.fuel <= 0 && fuel.is(ItemTags.BREWING_FUEL)) { + // CraftBukkit start +@@ -156,6 +157,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + fuel.shrink(1); + } + // CraftBukkit end ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + setChanged(level, pos, selfState); + } + +@@ -170,7 +172,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + } else if (!brewable || !ingredient.is(entity.ingredient)) { + entity.brewTime = 0; + } +- ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + setChanged(level, pos, selfState); + } else if (brewable && entity.fuel > 0) { + entity.fuel--; +@@ -183,6 +185,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + entity.brewTime = event.getBrewingTime(); // 400 -> event.getTotalBrewTime() // Paper - use brewing time from event + // CraftBukkit end + entity.ingredient = ingredient.getItem(); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + setChanged(level, pos, selfState); + } + +@@ -291,6 +294,7 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + } + + this.fuel = input.getByteOr("Fuel", (byte)0); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.isSleeping() && this.level != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -337,4 +341,53 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + protected AbstractContainerMenu createMenu(final int containerId, final Inventory inventory) { + return new BrewingStandMenu(containerId, inventory, this, this.dataAccess); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ private void checkSleep(BlockState state) { ++ if (this.brewTime == 0 && state.is(net.minecraft.world.level.block.Blocks.BREWING_STAND) && this.level != null) { ++ this.lithium$startSleeping(); ++ } ++ } ++ ++ @Override ++ public void lithium$handleSetChanged() { ++ if (this.isSleeping() && this.level != null) { ++ this.wakeUpNow(); ++ } ++ } ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/CampfireBlockEntity.java b/net/minecraft/world/level/block/entity/CampfireBlockEntity.java +index 64d0d60d9025de2b5eb16b31c6437372d4c20c5e..54070ae70b1d4a6ed4fd51ba758dead40275864e 100644 +--- a/net/minecraft/world/level/block/entity/CampfireBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/CampfireBlockEntity.java +@@ -39,7 +39,7 @@ import net.minecraft.world.level.storage.ValueOutput; + import org.jspecify.annotations.Nullable; + import org.slf4j.Logger; + +-public class CampfireBlockEntity extends BlockEntity implements Clearable { ++public class CampfireBlockEntity extends BlockEntity implements Clearable, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity { // Leaves - Lithium Sleeping Block Entity + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int BURN_COOL_SPEED = 2; + private static final int NUM_SLOTS = 4; +@@ -111,7 +111,7 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + + if (changed) { + setChanged(level, pos, state); +- } ++ } else if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.lithium$startSleeping(); // Leaves - Lithium Sleeping Block Entity + } + + public static void cooldownTick(final Level level, final BlockPos pos, final BlockState state, final CampfireBlockEntity entity) { +@@ -126,7 +126,7 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + + if (changed) { + setChanged(level, pos, state); +- } ++ } else if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.lithium$startSleeping(); // Leaves - Lithium Sleeping Block Entity + } + + public static void particleTick(final Level level, final BlockPos pos, final BlockState state, final CampfireBlockEntity entity) { +@@ -183,6 +183,7 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + System.arraycopy(cookingState, 0, this.stopCooking, 0, Math.min(this.stopCooking.length, bytes.capacity())); + }); + // Paper end - Add more Campfire API ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -234,6 +235,7 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + this.cookingTime[slot] = event.getTotalCookTime(); // recipe.get().value().cookingTime() -> event.getTotalCookTime() + // CraftBukkit end + this.cookingProgress[slot] = 0; ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + this.items.set(slot, placeItem.consumeAndReturn(1, sourceEntity)); + serverLevel.gameEvent(GameEvent.BLOCK_CHANGE, this.getBlockPos(), GameEvent.Context.of(sourceEntity, this.getBlockState())); + this.markUpdated(); +@@ -277,4 +279,30 @@ public class CampfireBlockEntity extends BlockEntity implements Clearable { + public void removeComponentsFromTag(final ValueOutput output) { + output.discard("Items"); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/ChestBlockEntity.java b/net/minecraft/world/level/block/entity/ChestBlockEntity.java +index a9273aebb5c8efe78e88158f11e1ee2af0dff3f1..c0d636ebe2fd9f2c63c1b9cb62ba257bd545de36 100644 +--- a/net/minecraft/world/level/block/entity/ChestBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ChestBlockEntity.java +@@ -25,7 +25,7 @@ import net.minecraft.world.level.block.state.properties.ChestType; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class ChestBlockEntity extends RandomizableContainerBlockEntity implements LidBlockEntity { ++public class ChestBlockEntity extends RandomizableContainerBlockEntity implements LidBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeEmitter, org.leavesmc.leaves.lithium.common.block.entity.SetBlockStateHandlingBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + private static final int EVENT_SET_OPEN_COUNT = 1; + public static final Component DEFAULT_NAME = Component.translatable("container.chest"); + private NonNullList items = NonNullList.withSize(27, ItemStack.EMPTY); +@@ -140,6 +140,7 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + + public static void lidAnimateTick(final Level level, final BlockPos pos, final BlockState state, final ChestBlockEntity entity) { + entity.chestLidController.tickLid(); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.checkSleep(); // Leaves - Lithium Sleeping Block Entity + } + + public static void playSound(final Level level, final BlockPos worldPosition, final BlockState blockState, final SoundEvent event) { +@@ -161,6 +162,7 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + @Override + public boolean triggerEvent(final int b0, final int b1) { + if (b0 == EVENT_SET_OPEN_COUNT) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.sleepingTicker != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + this.chestLidController.shouldBeOpen(b1 > 0); + return true; + } else { +@@ -198,6 +200,7 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + @Override + protected void setItems(final NonNullList items) { + this.items = items; ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -233,4 +236,52 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + Block block = blockState.getBlock(); + level.blockEvent(pos, block, EVENT_SET_OPEN_COUNT, current); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ private void checkSleep() { ++ //If the animation is finished, it will stay unchanged until the next triggerEvent, which may change shouldBeOpen ++ if (this.getOpenNess(0.0F) == this.getOpenNess(1.0F)) { ++ this.lithium$startSleeping(); ++ } ++ } ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return this.tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return this.sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$handleSetBlockState() { ++ //Handle switching double / single chest state ++ this.lithium$emitRemoved(); ++ } ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java b/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java +index 8697f4d589766163a56b0de5be4fad999d09f809..19db4302ca6f1b3f2f86aef34f0d80229fb1ace8 100644 +--- a/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ChiseledBookShelfBlockEntity.java +@@ -22,7 +22,7 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import org.slf4j.Logger; + +-public class ChiseledBookShelfBlockEntity extends BlockEntity implements ListBackedContainer { ++public class ChiseledBookShelfBlockEntity extends BlockEntity implements ListBackedContainer, org.leavesmc.leaves.lithium.api.inventory.LithiumTransferConditionInventory { // Leaves - Lithium Sleeping Block Entity + public static final int MAX_BOOKS_IN_STORAGE = 6; + private static final Logger LOGGER = LogUtils.getLogger(); + private static final int DEFAULT_LAST_INTERACTED_SLOT = -1; +@@ -171,4 +171,6 @@ public class ChiseledBookShelfBlockEntity extends BlockEntity implements ListBac + public void removeComponentsFromTag(final ValueOutput output) { + output.discard("Items"); + } ++ ++ @Override public boolean lithium$itemInsertionTestRequiresStackSize1() {return true;} // Leaves - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/CrafterBlockEntity.java b/net/minecraft/world/level/block/entity/CrafterBlockEntity.java +index 02d86d0675de22546a225dd6eaac5875eba0ce54..d3aff67c70b7c69daf6c6674325b26a41b780920 100644 +--- a/net/minecraft/world/level/block/entity/CrafterBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/CrafterBlockEntity.java +@@ -23,7 +23,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class CrafterBlockEntity extends RandomizableContainerBlockEntity implements CraftingContainer { ++public class CrafterBlockEntity extends RandomizableContainerBlockEntity implements CraftingContainer, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SetChangedHandlingBlockEntity { // Leaves - Lithium Sleeping Block Entity + public static final int CONTAINER_WIDTH = 3; + public static final int CONTAINER_HEIGHT = 3; + public static final int CONTAINER_SIZE = 9; +@@ -171,6 +171,7 @@ public class CrafterBlockEntity extends RandomizableContainerBlockEntity impleme + } + }); + this.containerData.set(9, input.getIntOr("triggered", 0)); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.isSleeping() && this.level != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -280,10 +281,12 @@ public class CrafterBlockEntity extends RandomizableContainerBlockEntity impleme + level.setBlock(blockPos, blockState.setValue(CrafterBlock.CRAFTING, false), Block.UPDATE_ALL); + } + } ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && craftingTicksRemaining < 0) entity.checkSleep(); // Leaves - Lithium Sleeping Block Entity + } + + public void setCraftingTicksRemaining(final int maxCraftingTicks) { + this.craftingTicksRemaining = maxCraftingTicks; ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.isSleeping() && this.level != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + } + + public int getRedstoneSignal() { +@@ -302,4 +305,43 @@ public class CrafterBlockEntity extends RandomizableContainerBlockEntity impleme + private boolean slotCanBeDisabled(final int slotId) { + return slotId > -1 && slotId < 9 && this.items.get(slotId).isEmpty(); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return this.tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return this.sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ private void checkSleep() { ++ if (this.craftingTicksRemaining == 0) { ++ this.lithium$startSleeping(); ++ } ++ } ++ ++ @Override ++ public void lithium$handleSetChanged() { ++ if (this.isSleeping() && this.level != null && !this.level.isClientSide()) { ++ this.wakeUpNow(); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/DispenserBlockEntity.java b/net/minecraft/world/level/block/entity/DispenserBlockEntity.java +index 736f5fd76f4a86ab16740904b5c0823413d65cd2..8995d91aed44b8c7933eaf0111332d83e9789817 100644 +--- a/net/minecraft/world/level/block/entity/DispenserBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/DispenserBlockEntity.java +@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + +-public class DispenserBlockEntity extends RandomizableContainerBlockEntity { ++public class DispenserBlockEntity extends RandomizableContainerBlockEntity implements org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + public static final int CONTAINER_SIZE = 9; + private static final Component DEFAULT_NAME = Component.translatable("container.dispenser"); + private NonNullList items = NonNullList.withSize(9, ItemStack.EMPTY); +@@ -135,10 +135,23 @@ public class DispenserBlockEntity extends RandomizableContainerBlockEntity { + @Override + protected void setItems(final NonNullList items) { + this.items = items; ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + @Override + protected AbstractContainerMenu createMenu(final int containerId, final Inventory inventory) { + return new DispenserMenu(containerId, inventory, this); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ items = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java b/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java +index 6109d8da984b398bb1eb6cd6ab9a55f0511a615c..4b92beb60cb56dc35107dd2c2e7cc6a29f6c3473 100644 +--- a/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/EnderChestBlockEntity.java +@@ -11,7 +11,7 @@ import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.ChestBlock; + import net.minecraft.world.level.block.state.BlockState; + +-public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity { ++public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity { // Leaves - Lithium Sleeping Block Entity + private final ChestLidController chestLidController = new ChestLidController(); + public final ContainerOpenersCounter openersCounter = new ContainerOpenersCounter() { + // Paper start - delay open/close callbacks +@@ -66,11 +66,13 @@ public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity + + public static void lidAnimateTick(final Level level, final BlockPos pos, final BlockState state, final EnderChestBlockEntity entity) { + entity.chestLidController.tickLid(); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.checkSleep(); // Leaves - Lithium Sleeping Block Entity + } + + @Override + public boolean triggerEvent(final int b0, final int b1) { + if (b0 == ChestBlock.EVENT_SET_OPEN_COUNT) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.sleepingTicker != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + this.chestLidController.shouldBeOpen(b1 > 0); + return true; + } else { +@@ -107,4 +109,36 @@ public class EnderChestBlockEntity extends BlockEntity implements LidBlockEntity + public float getOpenNess(final float a) { + return this.chestLidController.getOpenness(a); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return this.tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return this.sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ private void checkSleep() { ++ //If the animation is finished, it will stay unchanged until the next triggerEvent, which may change shouldBeOpen ++ if (this.getOpenNess(0.0F) == this.getOpenNess(1.0F)) { ++ this.lithium$startSleeping(); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 4c9d0bcd8820b250b51c06fff4531021ab57d408..fa34778b1ce7a34385c192182e521b00cc7f4d8e 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -27,8 +27,29 @@ import net.minecraft.world.level.storage.ValueInput; + import net.minecraft.world.level.storage.ValueOutput; + import net.minecraft.world.phys.AABB; + import org.jspecify.annotations.Nullable; +- +-public class HopperBlockEntity extends RandomizableContainerBlockEntity implements Hopper { ++// Leaves start - Lithium Sleeping Block Entity ++import java.util.Objects; ++import net.minecraft.world.level.chunk.LevelChunk; ++import net.minecraft.world.CompoundContainer; ++import net.minecraft.server.level.ServerLevel; ++import org.leavesmc.leaves.lithium.api.inventory.LithiumInventory; ++import org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeListener; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; ++import org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracker; ++import org.leavesmc.leaves.lithium.common.hopper.BlockStateOnlyInventory; ++import org.leavesmc.leaves.lithium.common.hopper.HopperCachingState; ++import org.leavesmc.leaves.lithium.common.hopper.HopperHelper; ++import org.leavesmc.leaves.lithium.common.hopper.InventoryHelper; ++import org.leavesmc.leaves.lithium.common.hopper.LithiumStackList; ++import org.leavesmc.leaves.lithium.common.hopper.UpdateReceiver; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionEntityMovementListener; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionEntityMovementTracker; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionInventoryEntityTracker; ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionItemEntityMovementTracker; ++// Leaves end - Lithium Sleeping Block Entity ++ ++public class HopperBlockEntity extends RandomizableContainerBlockEntity implements Hopper, SleepingBlockEntity, ChunkSectionEntityMovementListener, LithiumInventory, InventoryChangeListener, UpdateReceiver, InventoryChangeTracker { // Leaves - Lithium Sleeping Block Entity + public static final int MOVE_ITEM_SPEED = 8; + public static final int HOPPER_CONTAINER_SIZE = 5; + private static final int[][] CACHED_SLOTS = new int[54][]; +@@ -77,9 +98,19 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + @Override + public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { + super.updateTicks(fromTickOffset, fromRedstoneTimeOffset); +- if (this.tickedGameTime != Long.MIN_VALUE) { ++ if (this.tickedGameTime != Long.MIN_VALUE && this.tickedGameTime != Long.MAX_VALUE) { // Camellia - Region threading for lithium sleeping block entity + this.tickedGameTime += fromRedstoneTimeOffset; + } ++ // Camellia start - Region threading for lithium sleeping block entity ++ if (this.insertInventoryEntityFailedSearchTime != Long.MIN_VALUE) ++ this.insertInventoryEntityFailedSearchTime += fromRedstoneTimeOffset; ++ ++ if (this.extractInventoryEntityFailedSearchTime != Long.MIN_VALUE) ++ this.extractInventoryEntityFailedSearchTime += fromRedstoneTimeOffset; ++ ++ if (this.collectItemEntityAttemptTime != Long.MIN_VALUE) ++ this.collectItemEntityAttemptTime += fromRedstoneTimeOffset; ++ // Camellia end + } + // Folia end - region threading + +@@ -129,6 +160,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Override + public void setBlockState(final BlockState blockState) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.level != null && !this.level.isClientSide() && blockState.getValue(HopperBlock.FACING) != this.getBlockState().getValue(HopperBlock.FACING)) this.invalidateCachedData(); // Leaves - Lithium Sleeping Block Entity + super.setBlockState(blockState); + this.facing = blockState.getValue(HopperBlock.FACING); + } +@@ -145,6 +177,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + entity.setCooldown(0); + // Spigot start + boolean result = tryMoveItems(level, pos, state, entity, () -> suckInItems(level, entity)); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) entity.checkSleepingConditions(); // Leaves - Lithium Sleeping Block Entity + if (!result && entity.level.spigotConfig.hopperCheck > 1) { + entity.setCooldown(entity.level.spigotConfig.hopperCheck); + } +@@ -210,6 +243,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + if (changed) { + entity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot + setChanged(level, pos, state); ++ // Leaves start - Lithium Sleeping Block Entity ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled ++ && !entity.isOnCooldown() ++ && !entity.isSleeping() ++ && !state.getValue(HopperBlock.ENABLED)) { ++ entity.lithium$startSleeping(); ++ } ++ // Leaves end - Lithium Sleeping Block Entity + return true; + } + } +@@ -406,6 +447,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + private static void applyCooldown(final Hopper hopper) { + if (hopper instanceof HopperBlockEntity blockEntity && blockEntity.getLevel() != null) { + blockEntity.setCooldown(blockEntity.getLevel().spigotConfig.hopperTransfer); ++ blockEntity.skipNextSleepCheckAfterCooldown = true; // Leaves - Lithium Sleeping Block Entity + } + } + +@@ -449,12 +491,20 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + // Paper end - Perf: Optimize Hoppers + + private static boolean ejectItems(final Level level, final BlockPos blockPos, final HopperBlockEntity self) { +- Container container = getAttachedContainer(level, blockPos, self); ++ Container container = moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled ? self.getInsertInventory(level) : getAttachedContainer(level, blockPos, self); // Leaves - Lithium Sleeping Block Entity + if (container == null) { + return false; + } + + Direction direction = self.facing.getOpposite(); ++ // Leaves start - Lithium Sleeping Block Entity ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) { ++ Boolean res = lithiumInsert(level, blockPos, self, container); ++ if (res != null) { ++ return res; ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + if (isFullContainer(container, direction)) { + return false; + } +@@ -510,13 +560,21 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + io.papermc.paper.threadedregions.RegionizedWorldData worldData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); // Folia - region threading + BlockPos blockPos = BlockPos.containing(hopper.getLevelX(), hopper.getLevelY() + 1.0, hopper.getLevelZ()); + BlockState blockState = level.getBlockState(blockPos); +- Container container = getSourceContainer(level, hopper, blockPos, blockState); +- if (container != null) { ++ Container sourceContainer = moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled ? getExtractInventory(level, hopper, blockPos, blockState) : getSourceContainer(level, hopper, blockPos, blockState); // Leaves - Lithium Sleeping Block Entity ++ if (sourceContainer != null) { + Direction direction = Direction.DOWN; + worldData.skipPullModeEventFire = worldData.skipHopperEvents; // Paper - Perf: Optimize Hoppers // Folia - region threading ++ // Leaves start - Lithium Sleeping Block Entity ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) { ++ Boolean res = lithiumExtract(level, hopper, sourceContainer); ++ if (res != null) { ++ return res; ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + +- for (int slot : getSlots(container, direction)) { +- if (tryTakeInItemFromSlot(hopper, container, slot, direction, level)) { // Spigot ++ for (int slot : getSlots(sourceContainer, direction)) { ++ if (tryTakeInItemFromSlot(hopper, sourceContainer, slot, direction, level)) { // Spigot + return true; + } + } +@@ -527,7 +585,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + && blockState.isCollisionShapeFullBlock(level, blockPos) + && !blockState.is(BlockTags.DOES_NOT_BLOCK_HOPPERS); + if (!isBlocked) { +- for (ItemEntity entity : getItemsAtAndAbove(level, hopper)) { ++ for (ItemEntity entity : moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled ? lithiumGetInputItemEntities(level, hopper) : getItemsAtAndAbove(level, hopper)) { // Leaves - Lithium Sleeping Block Entity + if (addItem(hopper, entity)) { + return true; + } +@@ -649,7 +707,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + // CraftBukkit start + private static @Nullable Container runHopperInventorySearchEvent( +- Container container, ++ @Nullable Container container, // Leaves - Lithium Sleeping Block Entity + org.bukkit.craftbukkit.block.CraftBlock hopper, + org.bukkit.craftbukkit.block.CraftBlock searchLocation, + org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType containerType +@@ -775,6 +833,19 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + + public void setCooldown(final int time) { ++ // Leaves start - Lithium Sleeping Block Entity ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) { ++ if (time == 7) { ++ if (this.tickedGameTime == Long.MAX_VALUE) { ++ this.sleepOnlyCurrentTick(); ++ } else { ++ this.wakeUpNow(); ++ } ++ } else if (time > 0 && this.sleepingTicker != null) { ++ this.wakeUpNow(); ++ } ++ } ++ // Leaves end - Lithium Sleeping Block Entity + this.cooldownTime = time; + } + +@@ -794,6 +865,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + @Override + protected void setItems(final NonNullList items) { + this.items = items; ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + public static void entityInside(final Level level, final BlockPos pos, final BlockState blockState, final Entity entity, final HopperBlockEntity hopper) { +@@ -808,4 +880,758 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + protected AbstractContainerMenu createMenu(final int containerId, final Inventory inventory) { + return new HopperMenu(containerId, inventory, this); + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private LevelChunk.@org.jspecify.annotations.Nullable RebindableTickingBlockEntityWrapper tickWrapper = null; // Leaves - Paper 26.1 fix ++ @Nullable private TickingBlockEntity sleepingTicker = null; ++ private long myModCountAtLastInsert, myModCountAtLastExtract, myModCountAtLastItemCollect; ++ private boolean skipNextSleepCheckAfterCooldown = false; ++ ++ private HopperCachingState.BlockInventory insertionMode = HopperCachingState.BlockInventory.UNKNOWN; ++ private HopperCachingState.BlockInventory extractionMode = HopperCachingState.BlockInventory.UNKNOWN; ++ ++ //The currently used block inventories ++ @Nullable ++ private Container insertBlockInventory, extractBlockInventory; ++ ++ //The currently used inventories (optimized type, if not present, skip optimizations) ++ @Nullable ++ private LithiumInventory insertInventory, extractInventory; ++ @Nullable //Null iff corresp. LithiumInventory field is null ++ private LithiumStackList insertStackList, extractStackList; ++ //Mod count used to avoid transfer attempts that are known to fail (no change since last attempt) ++ private long insertStackListModCount, extractStackListModCount; ++ ++ @Nullable ++ private List collectItemEntityTracker; ++ private boolean collectItemEntityTrackerWasEmpty; ++ @Nullable ++ private AABB collectItemEntityBox; ++ private long collectItemEntityAttemptTime; ++ ++ @Nullable ++ private List extractInventoryEntityTracker; ++ @Nullable ++ private AABB extractInventoryEntityBox; ++ private long extractInventoryEntityFailedSearchTime; ++ ++ @Nullable ++ private List insertInventoryEntityTracker; ++ @Nullable ++ private AABB insertInventoryEntityBox; ++ private long insertInventoryEntityFailedSearchTime; ++ ++ private boolean shouldCheckSleep; ++ ++ private void checkSleepingConditions() { ++ if (this.cooldownTime > 0 || this.getLevel() == null || skipNextSleepCheckAfterCooldown) { ++ return; ++ } ++ if (isSleeping()) { ++ return; ++ } ++ if (!this.shouldCheckSleep) { ++ this.shouldCheckSleep = true; ++ return; ++ } ++ boolean listenToExtractTracker = false; ++ boolean listenToInsertTracker = false; ++ boolean listenToExtractEntities = false; ++ boolean listenToItemEntities = false; ++ boolean listenToInsertEntities = false; ++ ++ LithiumStackList thisStackList = InventoryHelper.getLithiumStackList(this); ++ ++ if (this.extractionMode != HopperCachingState.BlockInventory.BLOCK_STATE && thisStackList.getFullSlots() != thisStackList.size()) { ++ if (this.extractionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ Container blockInventory = this.extractBlockInventory; ++ if (this.extractStackList != null && ++ blockInventory instanceof InventoryChangeTracker) { ++ if (!this.extractStackList.maybeSendsComparatorUpdatesOnFailedExtract() || (blockInventory instanceof ComparatorTracker comparatorTracker && !comparatorTracker.lithium$hasAnyComparatorNearby())) { ++ listenToExtractTracker = true; ++ } else { ++ return; ++ } ++ } else { ++ return; ++ } ++ } else if (this.extractionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY) { ++ BlockState hopperState = this.getBlockState(); ++ listenToExtractEntities = true; ++ ++ BlockPos blockPos = this.getBlockPos().above(); ++ BlockState blockState = this.getLevel().getBlockState(blockPos); ++ if (!blockState.isCollisionShapeFullBlock(this.getLevel(), blockPos) || blockState.is(BlockTags.DOES_NOT_BLOCK_HOPPERS)) { ++ listenToItemEntities = true; ++ } ++ } else { ++ return; ++ } ++ } ++ if (this.insertionMode != HopperCachingState.BlockInventory.BLOCK_STATE && 0 < thisStackList.getOccupiedSlots()) { ++ if (this.insertionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ Container blockInventory = this.insertBlockInventory; ++ if (this.insertStackList != null && blockInventory instanceof InventoryChangeTracker) { ++ listenToInsertTracker = true; ++ } else { ++ return; ++ } ++ } else if (this.insertionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY) { ++ BlockState hopperState = this.getBlockState(); ++ listenToInsertEntities = true; ++ } else { ++ return; ++ } ++ } ++ ++ if (listenToExtractTracker) { ++ ((InventoryChangeTracker) this.extractBlockInventory).listenForContentChangesOnce(this.extractStackList, this); ++ } ++ if (listenToInsertTracker) { ++ ((InventoryChangeTracker) this.insertBlockInventory).listenForContentChangesOnce(this.insertStackList, this); ++ } ++ if (listenToInsertEntities) { ++ if (this.insertInventoryEntityTracker == null || this.insertInventoryEntityTracker.isEmpty()) { ++ return; ++ } ++ ChunkSectionEntityMovementTracker.listenToEntityMovementOnce(this, insertInventoryEntityTracker); ++ } ++ if (listenToExtractEntities) { ++ if (this.extractInventoryEntityTracker == null || this.extractInventoryEntityTracker.isEmpty()) { ++ return; ++ } ++ ChunkSectionEntityMovementTracker.listenToEntityMovementOnce(this, extractInventoryEntityTracker); ++ } ++ if (listenToItemEntities) { ++ if (this.collectItemEntityTracker == null || this.collectItemEntityTracker.isEmpty()) { ++ return; ++ } ++ ChunkSectionEntityMovementTracker.listenToEntityMovementOnce(this, collectItemEntityTracker); ++ } ++ ++ this.listenForContentChangesOnce(thisStackList, this); ++ lithium$startSleeping(); ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(@Nullable TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ @Override ++ public @Nullable TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ this.lithium$setSleepingTicker(null); ++ } ++ ++ @Override ++ public LevelChunk.@org.jspecify.annotations.Nullable RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { // Leaves - Paper 26.1 fix ++ return tickWrapper; ++ } ++ ++ @Override ++ public boolean lithium$startSleeping() { ++ if (this.isSleeping()) { ++ return false; ++ } ++ ++ LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = this.lithium$getTickWrapper(); ++ if (tickWrapper != null) { ++ this.lithium$setSleepingTicker(tickWrapper.ticker); ++ tickWrapper.rebind(SleepingBlockEntity.SLEEPING_BLOCK_ENTITY_TICKER); ++ ++ // Set the last tick time to max value, so other hoppers transferring into this hopper will set it to 7gt ++ // cooldown. Then when waking up, we make sure to not tick this hopper in the same gametick. ++ // This makes the observable hopper cooldown not be different from vanilla. ++ this.tickedGameTime = Long.MAX_VALUE; ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public void handleEntityMovement() { ++ this.wakeUpNow(); ++ } ++ ++ @Override ++ public NonNullList getInventoryLithium() { ++ return items; ++ } ++ ++ @Override ++ public void setInventoryLithium(NonNullList inventory) { ++ this.items = inventory; ++ } ++ ++ @Override ++ public void lithium$handleInventoryContentModified(Container inventory) { ++ wakeUpNow(); ++ } ++ ++ @Override ++ public void lithium$handleInventoryRemoved(Container inventory) { ++ wakeUpNow(); ++ if (inventory == this.insertBlockInventory) { ++ this.invalidateBlockInsertionData(); ++ } ++ if (inventory == this.extractBlockInventory) { ++ this.invalidateBlockExtractionData(); ++ } ++ if (inventory == this) { ++ this.invalidateCachedData(); ++ } ++ } ++ ++ @Override ++ public boolean lithium$handleComparatorAdded(Container inventory) { ++ if (inventory == this.extractBlockInventory) { ++ wakeUpNow(); ++ return true; ++ } ++ return false; ++ } ++ ++ @Override ++ public void lithium$invalidateCacheOnNeighborUpdate(boolean fromAbove) { ++ //Clear the block inventory cache (composter inventories and no inventory present) on block update / observer update ++ if (fromAbove) { ++ if (this.extractionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY || this.extractionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ this.invalidateBlockExtractionData(); ++ } ++ } else { ++ if (this.insertionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY || this.insertionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ this.invalidateBlockInsertionData(); ++ } ++ } ++ } ++ ++ @Override ++ public void lithium$invalidateCacheOnUndirectedNeighborUpdate() { ++ if (this.extractionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY || this.extractionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ this.invalidateBlockExtractionData(); ++ } ++ if (this.insertionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY || this.insertionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ this.invalidateBlockInsertionData(); ++ } ++ } ++ ++ @Override ++ public void lithium$invalidateCacheOnNeighborUpdate(Direction fromDirection) { ++ boolean fromAbove = fromDirection == Direction.UP; ++ if (fromAbove || this.getBlockState().getValue(HopperBlock.FACING) == fromDirection) { ++ this.lithium$invalidateCacheOnNeighborUpdate(fromAbove); ++ } ++ } ++ ++ private void invalidateBlockInsertionData() { ++ this.insertionMode = HopperCachingState.BlockInventory.UNKNOWN; ++ this.insertBlockInventory = null; ++ this.insertInventory = null; ++ this.insertStackList = null; ++ this.insertStackListModCount = 0; ++ ++ wakeUpNow(); ++ } ++ ++ private void invalidateCachedData() { ++ this.shouldCheckSleep = false; ++ this.invalidateInsertionData(); ++ this.invalidateExtractionData(); ++ } ++ ++ private void invalidateInsertionData() { ++ if (this.level instanceof ServerLevel) { ++ if (this.insertInventoryEntityTracker != null) { ++ ChunkSectionEntityMovementTracker.unregister(this.insertInventoryEntityTracker); ++ this.insertInventoryEntityTracker = null; ++ this.insertInventoryEntityBox = null; ++ this.insertInventoryEntityFailedSearchTime = 0L; ++ } ++ } ++ ++ if (this.insertionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ assert this.insertBlockInventory != null; ++ ((InventoryChangeTracker) this.insertBlockInventory).stopListenForMajorInventoryChanges(this); ++ } ++ this.invalidateBlockInsertionData(); ++ } ++ ++ private void invalidateExtractionData() { ++ if (this.level instanceof ServerLevel) { ++ if (this.extractInventoryEntityTracker != null) { ++ ChunkSectionEntityMovementTracker.unregister(this.extractInventoryEntityTracker); ++ this.extractInventoryEntityTracker = null; ++ this.extractInventoryEntityBox = null; ++ this.extractInventoryEntityFailedSearchTime = 0L; ++ } ++ if (this.collectItemEntityTracker != null) { ++ ChunkSectionEntityMovementTracker.unregister(this.collectItemEntityTracker); ++ this.collectItemEntityTracker = null; ++ this.collectItemEntityBox = null; ++ this.collectItemEntityTrackerWasEmpty = false; ++ } ++ } ++ if (this.extractionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ assert this.extractBlockInventory != null; ++ ((InventoryChangeTracker) this.extractBlockInventory).stopListenForMajorInventoryChanges(this); ++ } ++ this.invalidateBlockExtractionData(); ++ } ++ ++ private void invalidateBlockExtractionData() { ++ this.extractionMode = HopperCachingState.BlockInventory.UNKNOWN; ++ this.extractBlockInventory = null; ++ this.extractInventory = null; ++ this.extractStackList = null; ++ this.extractStackListModCount = 0; ++ ++ this.wakeUpNow(); ++ } ++ ++ private static @Nullable Container getExtractInventory(Level world, Hopper hopper, BlockPos extractBlockPos, BlockState extractBlockState) { ++ if (!(hopper instanceof HopperBlockEntity hopperBlockEntity)) { ++ return getSourceContainer(world, hopper, extractBlockPos, extractBlockState); //Hopper Minecarts do not cache Inventories ++ } ++ ++ Container blockInventory = hopperBlockEntity.lithium$getExtractBlockInventory(world, extractBlockPos, extractBlockState); ++ if (blockInventory == null) { ++ blockInventory = hopperBlockEntity.lithium$getExtractEntityInventory(world); ++ } ++ return org.bukkit.event.inventory.HopperInventorySearchEvent.getHandlerList().getRegisteredListeners().length == 0 ? blockInventory : runHopperInventorySearchEvent( ++ blockInventory, ++ org.bukkit.craftbukkit.block.CraftBlock.at(world, hopperBlockEntity.getBlockPos()), ++ org.bukkit.craftbukkit.block.CraftBlock.at(world, extractBlockPos), ++ org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType.SOURCE ++ ); ++ } ++ ++ public @Nullable Container lithium$getExtractBlockInventory(Level world, BlockPos extractBlockPos, BlockState extractBlockState) { ++ Container blockInventory = this.extractBlockInventory; ++ if (this.extractionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY) { ++ return null; ++ } else if (this.extractionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ return blockInventory; ++ } else if (this.extractionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ return blockInventory; ++ } else if (this.extractionMode == HopperCachingState.BlockInventory.BLOCK_ENTITY) { ++ BlockEntity blockEntity = (BlockEntity) Objects.requireNonNull(blockInventory); ++ //Movable Block Entity compatibility - position comparison ++ BlockPos pos = blockEntity.getBlockPos(); ++ if (!(blockEntity).isRemoved() && pos.equals(extractBlockPos)) { ++ LithiumInventory optimizedInventory; ++ if ((optimizedInventory = this.extractInventory) != null) { ++ LithiumStackList insertInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ //This check is necessary as sometimes the stacklist is silently replaced (e.g. command making furnace read inventory from nbt) ++ if (insertInventoryStackList == this.extractStackList) { ++ return optimizedInventory; ++ } else { ++ this.invalidateBlockExtractionData(); ++ } ++ } else { ++ return blockInventory; ++ } ++ } ++ } ++ ++ //No Cached Inventory: Get like vanilla and cache ++ blockInventory = getBlockContainer(world, extractBlockPos, extractBlockState); ++ blockInventory = HopperHelper.replaceDoubleInventory(blockInventory); ++ this.cacheExtractBlockInventory(blockInventory); ++ return blockInventory; ++ } ++ ++ public @Nullable Container lithium$getInsertBlockInventory(Level world) { ++ Container blockInventory = this.insertBlockInventory; ++ if (this.insertionMode == HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY) { ++ return null; ++ } else if (this.insertionMode == HopperCachingState.BlockInventory.BLOCK_STATE) { ++ return blockInventory; ++ } else if (this.insertionMode == HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY) { ++ return blockInventory; ++ } else if (this.insertionMode == HopperCachingState.BlockInventory.BLOCK_ENTITY) { ++ BlockEntity blockEntity = (BlockEntity) Objects.requireNonNull(blockInventory); ++ //Movable Block Entity compatibility - position comparison ++ BlockPos pos = blockEntity.getBlockPos(); ++ Direction direction = this.facing; ++ BlockPos transferPos = this.getBlockPos().relative(direction); ++ if (!(blockEntity).isRemoved() && ++ pos.equals(transferPos)) { ++ LithiumInventory optimizedInventory; ++ if ((optimizedInventory = this.insertInventory) != null) { ++ LithiumStackList insertInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ //This check is necessary as sometimes the stacklist is silently replaced (e.g. command making furnace read inventory from nbt) ++ if (insertInventoryStackList == this.insertStackList) { ++ return optimizedInventory; ++ } else { ++ this.invalidateBlockInsertionData(); ++ } ++ } else { ++ return blockInventory; ++ } ++ } ++ } ++ ++ //No Cached Inventory: Get like vanilla and cache ++ Direction direction = this.facing; ++ BlockPos insertBlockPos = this.getBlockPos().relative(direction); ++ BlockState blockState = world.getBlockState(insertBlockPos); ++ blockInventory = getBlockContainer(world, insertBlockPos, blockState); ++ blockInventory = HopperHelper.replaceDoubleInventory(blockInventory); ++ this.cacheInsertBlockInventory(blockInventory); ++ return blockInventory; ++ } ++ ++ public @Nullable Container getInsertInventory(Level world) { ++ Container blockInventory = getInsertInventory0(world); ++ return org.bukkit.event.inventory.HopperInventorySearchEvent.getHandlerList().getRegisteredListeners().length == 0 ? blockInventory : runHopperInventorySearchEvent( ++ blockInventory, ++ org.bukkit.craftbukkit.block.CraftBlock.at(world, this.getBlockPos()), ++ org.bukkit.craftbukkit.block.CraftBlock.at(world, this.getBlockPos().relative(this.facing)), ++ org.bukkit.event.inventory.HopperInventorySearchEvent.ContainerType.DESTINATION ++ ); ++ } ++ ++ public @Nullable Container getInsertInventory0(Level world) { ++ Container blockInventory = this.lithium$getInsertBlockInventory(world); ++ if (blockInventory != null) { ++ return blockInventory; ++ } ++ ++ if (this.insertInventoryEntityTracker == null) { ++ this.initInsertInventoryTracker(world); ++ } ++ if (ChunkSectionEntityMovementTracker.isUnchangedSince(this.insertInventoryEntityFailedSearchTime, this.insertInventoryEntityTracker)) { ++ this.insertInventoryEntityFailedSearchTime = this.tickedGameTime; ++ return null; ++ } ++ this.insertInventoryEntityFailedSearchTime = Long.MIN_VALUE; ++ this.shouldCheckSleep = false; ++ ++ List inventoryEntities = ChunkSectionInventoryEntityTracker.getEntities(world, this.insertInventoryEntityBox); ++ if (inventoryEntities.isEmpty()) { ++ this.insertInventoryEntityFailedSearchTime = this.tickedGameTime; ++ //Remember failed entity search timestamp. This allows shortcutting if no entity movement happens. ++ return null; ++ } ++ Container inventory = inventoryEntities.get(world.getRandom().nextInt(inventoryEntities.size())); // Leaves - Paper 26.1 fix ++ if (inventory instanceof LithiumInventory optimizedInventory) { ++ LithiumStackList insertInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ if (inventory != this.insertInventory || this.insertStackList != insertInventoryStackList) { ++ this.cacheInsertLithiumInventory(optimizedInventory); ++ } ++ } ++ ++ return inventory; ++ } ++ ++ private void initCollectItemEntityTracker() { ++ assert this.level instanceof ServerLevel; ++ AABB inputBox = this.getSuckAabb().move(this.worldPosition.getX(), this.worldPosition.getY(), this.worldPosition.getZ()); ++ this.collectItemEntityBox = inputBox; ++ this.collectItemEntityTracker = ++ ChunkSectionItemEntityMovementTracker.registerAt( ++ (ServerLevel) this.level, ++ inputBox ++ ); ++ this.collectItemEntityAttemptTime = Long.MIN_VALUE; ++ } ++ ++ private void initExtractInventoryTracker(Level world) { ++ assert world instanceof ServerLevel; ++ BlockPos pos = this.worldPosition.relative(Direction.UP); ++ this.extractInventoryEntityBox = new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1); ++ this.extractInventoryEntityTracker = ++ ChunkSectionInventoryEntityTracker.registerAt( ++ (ServerLevel) this.level, ++ this.extractInventoryEntityBox ++ ); ++ this.extractInventoryEntityFailedSearchTime = Long.MIN_VALUE; ++ } ++ ++ private void initInsertInventoryTracker(Level world) { ++ assert world instanceof ServerLevel; ++ Direction direction = this.facing; ++ BlockPos pos = this.worldPosition.relative(direction); ++ this.insertInventoryEntityBox = new AABB(pos.getX(), pos.getY(), pos.getZ(), pos.getX() + 1, pos.getY() + 1, pos.getZ() + 1); ++ this.insertInventoryEntityTracker = ++ ChunkSectionInventoryEntityTracker.registerAt( ++ (ServerLevel) this.level, ++ this.insertInventoryEntityBox ++ ); ++ this.insertInventoryEntityFailedSearchTime = Long.MIN_VALUE; ++ } ++ ++ private @Nullable Container lithium$getExtractEntityInventory(Level world) { ++ if (this.extractInventoryEntityTracker == null) { ++ this.initExtractInventoryTracker(world); ++ } ++ if (ChunkSectionEntityMovementTracker.isUnchangedSince(this.extractInventoryEntityFailedSearchTime, this.extractInventoryEntityTracker)) { ++ this.extractInventoryEntityFailedSearchTime = this.tickedGameTime; ++ return null; ++ } ++ this.extractInventoryEntityFailedSearchTime = Long.MIN_VALUE; ++ this.shouldCheckSleep = false; ++ ++ List inventoryEntities = ChunkSectionInventoryEntityTracker.getEntities(world, this.extractInventoryEntityBox); ++ if (inventoryEntities.isEmpty()) { ++ this.extractInventoryEntityFailedSearchTime = this.tickedGameTime; ++ //only set unchanged when no entity present. this allows shortcutting this case ++ //shortcutting the entity present case requires checking its change counter ++ return null; ++ } ++ Container inventory = inventoryEntities.get(world.getRandom().nextInt(inventoryEntities.size())); // Leaves - Paper 26.1 fix ++ if (inventory instanceof LithiumInventory optimizedInventory) { ++ LithiumStackList extractInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ if (inventory != this.extractInventory || this.extractStackList != extractInventoryStackList) { ++ //not caching the inventory (NO_BLOCK_INVENTORY prevents it) ++ //make change counting on the entity inventory possible, without caching it as block inventory ++ this.cacheExtractLithiumInventory(optimizedInventory); ++ } ++ } ++ return inventory; ++ } ++ ++ /** ++ * Makes this hopper remember the given inventory. ++ * ++ * @param insertInventory Block inventory / Blockentity inventory to be remembered ++ */ ++ private void cacheInsertBlockInventory(@Nullable Container insertInventory) { ++ assert !(insertInventory instanceof Entity); ++ if (insertInventory instanceof LithiumInventory optimizedInventory) { ++ this.cacheInsertLithiumInventory(optimizedInventory); ++ } else { ++ this.insertInventory = null; ++ this.insertStackList = null; ++ this.insertStackListModCount = 0; ++ } ++ ++ if (insertInventory instanceof BlockEntity || insertInventory instanceof CompoundContainer) { ++ this.insertBlockInventory = insertInventory; ++ if (insertInventory instanceof InventoryChangeTracker) { ++ this.insertionMode = HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY; ++ ((InventoryChangeTracker) insertInventory).listenForMajorInventoryChanges(this); ++ } else { ++ this.insertionMode = HopperCachingState.BlockInventory.BLOCK_ENTITY; ++ } ++ } else { ++ if (insertInventory == null) { ++ this.insertBlockInventory = null; ++ this.insertionMode = HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY; ++ } else { ++ this.insertBlockInventory = insertInventory; ++ this.insertionMode = insertInventory instanceof BlockStateOnlyInventory ? HopperCachingState.BlockInventory.BLOCK_STATE : HopperCachingState.BlockInventory.UNKNOWN; ++ } ++ } ++ } ++ ++ private void cacheInsertLithiumInventory(LithiumInventory optimizedInventory) { ++ LithiumStackList insertInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ this.insertInventory = optimizedInventory; ++ this.insertStackList = insertInventoryStackList; ++ this.insertStackListModCount = insertInventoryStackList.getModCount() - 1; ++ } ++ ++ private void cacheExtractLithiumInventory(LithiumInventory optimizedInventory) { ++ LithiumStackList extractInventoryStackList = InventoryHelper.getLithiumStackList(optimizedInventory); ++ this.extractInventory = optimizedInventory; ++ this.extractStackList = extractInventoryStackList; ++ this.extractStackListModCount = extractInventoryStackList.getModCount() - 1; ++ } ++ ++ /** ++ * Makes this hopper remember the given inventory. ++ * ++ * @param extractInventory Block inventory / Blockentity inventory to be remembered ++ */ ++ private void cacheExtractBlockInventory(@Nullable Container extractInventory) { ++ assert !(extractInventory instanceof Entity); ++ if (extractInventory instanceof LithiumInventory optimizedInventory) { ++ this.cacheExtractLithiumInventory(optimizedInventory); ++ } else { ++ this.extractInventory = null; ++ this.extractStackList = null; ++ this.extractStackListModCount = 0; ++ } ++ ++ if (extractInventory instanceof BlockEntity || extractInventory instanceof CompoundContainer) { ++ this.extractBlockInventory = extractInventory; ++ if (extractInventory instanceof InventoryChangeTracker) { ++ this.extractionMode = HopperCachingState.BlockInventory.REMOVAL_TRACKING_BLOCK_ENTITY; ++ ((InventoryChangeTracker) extractInventory).listenForMajorInventoryChanges(this); ++ } else { ++ this.extractionMode = HopperCachingState.BlockInventory.BLOCK_ENTITY; ++ } ++ } else { ++ if (extractInventory == null) { ++ this.extractBlockInventory = null; ++ this.extractionMode = HopperCachingState.BlockInventory.NO_BLOCK_INVENTORY; ++ } else { ++ this.extractBlockInventory = extractInventory; ++ this.extractionMode = extractInventory instanceof BlockStateOnlyInventory ? HopperCachingState.BlockInventory.BLOCK_STATE : HopperCachingState.BlockInventory.UNKNOWN; ++ } ++ } ++ } ++ ++ private static List lithiumGetInputItemEntities(Level world, Hopper hopper) { ++ if (!(hopper instanceof HopperBlockEntity hopperBlockEntity)) { ++ return getItemsAtAndAbove(world, hopper); //optimizations not implemented for hopper minecarts ++ } ++ ++ if (hopperBlockEntity.collectItemEntityTracker == null) { ++ hopperBlockEntity.initCollectItemEntityTracker(); ++ } ++ ++ long modCount = InventoryHelper.getLithiumStackList(hopperBlockEntity).getModCount(); ++ ++ if ((hopperBlockEntity.collectItemEntityTrackerWasEmpty || hopperBlockEntity.myModCountAtLastItemCollect == modCount) && ++ ChunkSectionEntityMovementTracker.isUnchangedSince(hopperBlockEntity.collectItemEntityAttemptTime, hopperBlockEntity.collectItemEntityTracker)) { ++ hopperBlockEntity.collectItemEntityAttemptTime = hopperBlockEntity.tickedGameTime; ++ return java.util.Collections.emptyList(); ++ } ++ ++ hopperBlockEntity.myModCountAtLastItemCollect = modCount; ++ hopperBlockEntity.shouldCheckSleep = false; ++ ++ List itemEntities = ChunkSectionItemEntityMovementTracker.getEntities(world, hopperBlockEntity.collectItemEntityBox); ++ hopperBlockEntity.collectItemEntityAttemptTime = hopperBlockEntity.tickedGameTime; ++ hopperBlockEntity.collectItemEntityTrackerWasEmpty = itemEntities.isEmpty(); ++ //set unchanged so that if this extract fails and there is no other change to hoppers or items, extracting ++ // items can be skipped. ++ return itemEntities; ++ } ++ ++ private static @Nullable Boolean lithiumInsert(Level world, BlockPos pos, HopperBlockEntity hopperBlockEntity, @Nullable Container insertInventory) { ++ if (insertInventory == null || hopperBlockEntity instanceof net.minecraft.world.WorldlyContainer) { ++ //call the vanilla code to allow other mods inject features ++ //e.g. carpet mod allows hoppers to insert items into wool blocks ++ return null; ++ } ++ ++ LithiumStackList hopperStackList = InventoryHelper.getLithiumStackList(hopperBlockEntity); ++ if (hopperBlockEntity.insertInventory == insertInventory && hopperStackList.getModCount() == hopperBlockEntity.myModCountAtLastInsert) { ++ if (hopperBlockEntity.insertStackList != null && hopperBlockEntity.insertStackList.getModCount() == hopperBlockEntity.insertStackListModCount) { ++// ComparatorUpdatePattern.NO_UPDATE.apply(hopperBlockEntity, hopperStackList); //commented because it's a noop, Hoppers do not send useless comparator updates ++ return false; ++ } ++ } ++ ++ boolean insertInventoryWasEmptyHopperNotDisabled = insertInventory instanceof HopperBlockEntity hopperInv && ++ !hopperInv.isOnCustomCooldown() && hopperBlockEntity.insertStackList != null && ++ hopperBlockEntity.insertStackList.getOccupiedSlots() == 0; ++ ++ boolean insertInventoryHandlesModdedCooldown = ++ insertInventory.canReceiveTransferCooldown() && ++ hopperBlockEntity.insertStackList != null ? ++ hopperBlockEntity.insertStackList.getOccupiedSlots() == 0 : ++ insertInventory.isEmpty(); ++ ++ var worldData = world.getCurrentWorldData(); ++ worldData.skipPushModeEventFire = worldData.skipHopperEvents; ++ //noinspection ConstantConditions ++ if (!(hopperBlockEntity.insertInventory == insertInventory && hopperBlockEntity.insertStackList.getFullSlots() == hopperBlockEntity.insertStackList.size())) { ++ Direction fromDirection = hopperBlockEntity.facing.getOpposite(); ++ int size = hopperStackList.size(); ++ for (int i = 0; i < size; ++i) { ++ ItemStack transferStack = hopperStackList.get(i); ++ if (!transferStack.isEmpty()) { ++ if (!worldData.skipPushModeEventFire && canTakeItemFromContainer(insertInventory, hopperBlockEntity, transferStack, i, Direction.DOWN)) { ++ transferStack = callPushMoveEvent(insertInventory, transferStack, hopperBlockEntity); ++ if (transferStack == null) { // cancelled ++ break; ++ } ++ } ++ boolean transferSuccess = HopperHelper.tryMoveSingleItem(insertInventory, transferStack, fromDirection); ++ if (transferSuccess) { ++ if (insertInventoryWasEmptyHopperNotDisabled) { ++ HopperBlockEntity receivingHopper = (HopperBlockEntity) insertInventory; ++ int k = 8; ++ if (receivingHopper.tickedGameTime >= hopperBlockEntity.tickedGameTime) { ++ k = 7; ++ } ++ receivingHopper.setCooldown(k); ++ } ++ if (insertInventoryHandlesModdedCooldown) { ++ insertInventory.setTransferCooldown(hopperBlockEntity.tickedGameTime); ++ } ++ insertInventory.setChanged(); ++ return true; ++ } ++ } ++ } ++ } ++ hopperBlockEntity.myModCountAtLastInsert = hopperStackList.getModCount(); ++ if (hopperBlockEntity.insertStackList != null) { ++ hopperBlockEntity.insertStackListModCount = hopperBlockEntity.insertStackList.getModCount(); ++ } ++ return false; ++ } ++ ++ private static @Nullable Boolean lithiumExtract(Level world, Hopper to, Container from) { ++ if (!(to instanceof HopperBlockEntity hopperBlockEntity)) { ++ return null; //optimizations not implemented for hopper minecarts ++ } ++ ++ if (from != hopperBlockEntity.extractInventory || hopperBlockEntity.extractStackList == null) { ++ return null; //from inventory is not an optimized inventory, vanilla fallback ++ } ++ ++ LithiumStackList hopperStackList = InventoryHelper.getLithiumStackList(hopperBlockEntity); ++ LithiumStackList fromStackList = hopperBlockEntity.extractStackList; ++ ++ if (hopperStackList.getModCount() == hopperBlockEntity.myModCountAtLastExtract) { ++ if (fromStackList.getModCount() == hopperBlockEntity.extractStackListModCount) { ++ if (!(from instanceof ComparatorTracker comparatorTracker) || comparatorTracker.lithium$hasAnyComparatorNearby()) { ++ //noinspection CollectionAddedToSelf ++ fromStackList.runComparatorUpdatePatternOnFailedExtract(fromStackList, from); ++ } ++ return false; ++ } ++ } ++ ++ int[] availableSlots = from instanceof WorldlyContainer ? ((WorldlyContainer) from).getSlotsForFace(Direction.DOWN) : null; ++ int fromSize = availableSlots != null ? availableSlots.length : from.getContainerSize(); ++ for (int i = 0; i < fromSize; i++) { ++ int fromSlot = availableSlots != null ? availableSlots[i] : i; ++ ItemStack itemStack = fromStackList.get(fromSlot); ++ if (!itemStack.isEmpty() && canTakeItemFromContainer(to, from, itemStack, fromSlot, Direction.DOWN)) { ++ if (!world.getCurrentWorldData().skipPullModeEventFire) { ++ itemStack = callPullMoveEvent(to, from, itemStack); ++ if (itemStack == null) { // cancelled ++ return true; ++ } ++ } ++ //calling removeStack is necessary due to its side effects (markDirty in LootableContainerBlockEntity) ++ ItemStack takenItem = from.removeItem(fromSlot, 1); ++ assert !takenItem.isEmpty(); ++ boolean transferSuccess = HopperHelper.tryMoveSingleItem(to, takenItem, null); ++ if (transferSuccess) { ++ to.setChanged(); ++ from.setChanged(); ++ return true; ++ } ++ //put the item back similar to vanilla ++ ItemStack restoredStack = fromStackList.get(fromSlot); ++ if (restoredStack.isEmpty()) { ++ restoredStack = takenItem; ++ } else { ++ restoredStack.grow(1); ++ } ++ //calling setStack is necessary due to its side effects (markDirty in LootableContainerBlockEntity) ++ from.setItem(fromSlot, restoredStack); ++ } ++ } ++ hopperBlockEntity.myModCountAtLastExtract = hopperStackList.getModCount(); ++ if (fromStackList != null) { ++ hopperBlockEntity.extractStackListModCount = fromStackList.getModCount(); ++ } ++ return false; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +index 174e39fb109bef3dd27ebc14aa13f6faabb10547..e898171acd7c4ef83d630e25c71222f4ee83445a 100644 +--- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +@@ -32,7 +32,7 @@ import net.minecraft.world.phys.AABB; + import net.minecraft.world.phys.Vec3; + import org.jspecify.annotations.Nullable; + +-public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity implements WorldlyContainer { ++public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity implements WorldlyContainer, org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker, org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity, org.leavesmc.leaves.lithium.api.inventory.LithiumInventory { // Leaves - Lithium Sleeping Block Entity + public static final int COLUMNS = 9; + public static final int ROWS = 3; + public static final int CONTAINER_SIZE = 27; +@@ -134,6 +134,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + doNeighborUpdates(level, pos, blockState); + } + } ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.animationStatus == ShulkerBoxBlockEntity.AnimationStatus.CLOSED && this.progressOld == 0.0f && this.progress == 0.0f) this.lithium$startSleeping(); // Leaves - Lithium Sleeping Block Entity + } + + public ShulkerBoxBlockEntity.AnimationStatus getAnimationStatus() { +@@ -174,6 +175,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + + @Override + public boolean triggerEvent(final int b0, final int b1) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && this.sleepingTicker != null) this.wakeUpNow(); // Leaves - Lithium Sleeping Block Entity + if (b0 == EVENT_SET_OPEN_COUNT) { + this.openCount = b1; + if (b1 == 0) { +@@ -269,6 +271,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + @Override + protected void setItems(final NonNullList items) { + this.itemStacks = items; ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.lithium$emitStackListReplaced(); // Leaves - Lithium Sleeping Block Entity + } + + @Override +@@ -309,4 +312,39 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + OPENED, + CLOSING; + } ++ ++ // Leaves start - Lithium Sleeping Block Entity ++ private net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = null; ++ private TickingBlockEntity sleepingTicker = null; ++ ++ @Override ++ public net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper() { ++ return tickWrapper; ++ } ++ ++ @Override ++ public void lithium$setTickWrapper(net.minecraft.world.level.chunk.LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper) { ++ this.tickWrapper = tickWrapper; ++ } ++ ++ @Override ++ public TickingBlockEntity lithium$getSleepingTicker() { ++ return sleepingTicker; ++ } ++ ++ @Override ++ public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { ++ this.sleepingTicker = sleepingTicker; ++ } ++ ++ @Override ++ public net.minecraft.core.NonNullList getInventoryLithium() { ++ return itemStacks; ++ } ++ ++ @Override ++ public void setInventoryLithium(net.minecraft.core.NonNullList inventory) { ++ itemStacks = inventory; ++ } ++ // Leaves end - Lithium Sleeping Block Entity + } +diff --git a/net/minecraft/world/level/block/entity/TickingBlockEntity.java b/net/minecraft/world/level/block/entity/TickingBlockEntity.java +index c8facee29ee08e0975528083f89b64f0b593957f..2499535f0aa699decad88917ab46fccc9b374088 100644 +--- a/net/minecraft/world/level/block/entity/TickingBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/TickingBlockEntity.java +@@ -12,4 +12,8 @@ public interface TickingBlockEntity { + String getType(); + + BlockEntity getTileEntity(); // Folia - region threading ++ ++ // Camellia start - Region threading for lithium sleeping block entity ++ default void updateTicksForLithium(long redstoneGameTimeOffset) {} ++ // Camellia end + } +diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java +index c78d77089e34c4bb559f173b449bfb7d91b05d5c..f4cce42ddd319cfd11cc43d68044f51b4514af38 100644 +--- a/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -83,7 +83,7 @@ import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + import org.jspecify.annotations.Nullable; + +-public abstract class BlockBehaviour implements FeatureElement { ++public abstract class BlockBehaviour implements FeatureElement, org.leavesmc.leaves.lithium.common.block.entity.ShapeUpdateHandlingBlockBehaviour { // Leaves - Lithium Sleeping Block Entity + protected static final Direction[] UPDATE_SHAPE_ORDER = new Direction[]{ + Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH, Direction.DOWN, Direction.UP + }; +@@ -155,6 +155,7 @@ public abstract class BlockBehaviour implements FeatureElement { + final BlockState neighbourState, + final RandomSource random + ) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) this.lithium$handleShapeUpdate(level, state, pos, neighbourPos, neighbourState); // Leaves - Lithium Sleeping Block Entity /* Triggers when a shape update (= update that observers can detect) is sent */ + return state; + } + +diff --git a/net/minecraft/world/level/block/state/pattern/BlockPattern.java b/net/minecraft/world/level/block/state/pattern/BlockPattern.java +index ed5a5fb38bfe8fb9216c7eab00b833485c4bbade..fc45703525901d700c98b5ad0c6eb10b65e775d8 100644 +--- a/net/minecraft/world/level/block/state/pattern/BlockPattern.java ++++ b/net/minecraft/world/level/block/state/pattern/BlockPattern.java +@@ -57,7 +57,7 @@ public class BlockPattern { + return this.matches(origin, forwards, up, cache); + } + +- private BlockPattern.@Nullable BlockPatternMatch matches( ++ public BlockPattern.@Nullable BlockPatternMatch matches( // Leaves - private -> public + final BlockPos origin, final Direction forwards, final Direction up, final LoadingCache cache + ) { + for (int x = 0; x < this.width; x++) { +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 0e84b695a677e71f443a29c78fc5025eec7ae7f4..3cb6c24a21538569d73b95803dde7e4a2fdb67f5 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -622,6 +622,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + LevelChunk.RebindableTickingBlockEntityWrapper ticker = this.tickersInLevel.remove(pos); + if (ticker != null) { + ticker.rebind(NULL_TICKER); ++ ticker.createdBy = null; // Camellia - Region threading for sleeping block entity + } + } + +@@ -883,6 +884,7 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + this.blockEntities.values().forEach(BlockEntity::setRemoved); + this.blockEntities.clear(); + this.tickersInLevel.values().forEach(ticker -> ticker.rebind(NULL_TICKER)); ++ this.tickersInLevel.values().forEach(ticker -> ticker.createdBy = null); // Camellia - Region threading for sleeping block entity + this.tickersInLevel.clear(); + } + +@@ -916,10 +918,14 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + this.tickersInLevel.compute(blockEntity.getBlockPos(), (blockPos, existingTicker) -> { + TickingBlockEntity actualTicker = this.createTicker(blockEntity, ticker); + if (existingTicker != null) { ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && blockEntity instanceof org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity sleepingBlockEntity) sleepingBlockEntity.lithium$setTickWrapper(existingTicker); // Leaves - Lithium Sleeping Block Entity ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) existingTicker.createdBy = blockEntity; // Camellia - Region threading for lithium sleeping block entity + existingTicker.rebind(actualTicker); + return (LevelChunk.RebindableTickingBlockEntityWrapper)existingTicker; + } else if (this.isInLevel()) { + LevelChunk.RebindableTickingBlockEntityWrapper result = new LevelChunk.RebindableTickingBlockEntityWrapper(actualTicker); ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && blockEntity instanceof org.leavesmc.leaves.lithium.common.block.entity.SleepingBlockEntity sleepingBlockEntity) sleepingBlockEntity.lithium$setTickWrapper(result); // Leaves - Lithium Sleeping Block Entity ++ if (moe.monnairealms.camellia.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) result.createdBy = blockEntity; // Camellia - Region threading for lithium sleeping block entity + this.level.addBlockEntityTicker(result); + return result; + } else { +@@ -1028,20 +1034,36 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + void run(LevelChunk levelChunk); + } + +- private static class RebindableTickingBlockEntityWrapper implements TickingBlockEntity { +- private TickingBlockEntity ticker; ++ public static class RebindableTickingBlockEntityWrapper implements TickingBlockEntity { // Leaves - default -> public ++ public TickingBlockEntity ticker; // Leaves - private -> public ++ @org.jetbrains.annotations.Nullable private BlockEntity createdBy; // Camellia - Region threading for lithium sleeping block entity + + private RebindableTickingBlockEntityWrapper(final TickingBlockEntity ticker) { + this.ticker = ticker; + } + +- private void rebind(final TickingBlockEntity ticker) { ++ public void rebind(final TickingBlockEntity ticker) { // Leaves - default -> public + this.ticker = ticker; + } + + // Folia start - region threading + @Override + public BlockEntity getTileEntity() { ++ // Camellia start - Region threading for lithium sleeping block entity ++ if (true) { ++ if (this.ticker != null) { ++ var ret = this.ticker.getTileEntity(); ++ ++ if (ret == null) { ++ return this.createdBy; ++ } ++ ++ return ret; ++ } ++ ++ return null; ++ } ++ // Camellia end + return this.ticker == null ? null : this.ticker.getTileEntity(); + } + // Folia end - region threading +@@ -1070,6 +1092,16 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + public String toString() { + return this.ticker + " "; + } ++ ++ // Camellia start - Region threading for lithium sleeping block entity ++ @Override ++ public void updateTicksForLithium(long redstoneGameTimeOffset) { ++ if (this.ticker != null) { ++ this.ticker.updateTicksForLithium(redstoneGameTimeOffset); ++ } ++ } ++ ++ // Camellia end + } + + @FunctionalInterface diff --git a/camellia-server/minecraft-patches/features/0043-Petal-Reduce-sensor-work.patch b/camellia-server/minecraft-patches/features/0043-Petal-Reduce-sensor-work.patch new file mode 100644 index 0000000..383527d --- /dev/null +++ b/camellia-server/minecraft-patches/features/0043-Petal-Reduce-sensor-work.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 12:30:47 +0800 +Subject: [PATCH] Petal: Reduce sensor work + +Co-authored by: peaches94 +As part of: Petal (https://github.com/Bloom-host/Petal/blob/cc691540fb48240f38b376f3d94c8b0db2b60d99/patches/server/0005-feat-reduce-sensor-work.patch) +Licensed under: GPL-3.0 (https://github.com/Bloom-host/Petal/blob/cc691540fb48240f38b376f3d94c8b0db2b60d99/LICENSE) + +diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java +index 3c59325cb82a66a74d6605b51db83ff63e59e089..1d6f66ed1f5b2f7be7a361685452473756dd10a1 100644 +--- a/net/minecraft/world/entity/Mob.java ++++ b/net/minecraft/world/entity/Mob.java +@@ -840,11 +840,12 @@ public abstract class Mob extends LivingEntity implements Targeting, EquipmentUs + return; + } + // Paper end - Allow nerfed mobs to jump and float ++ int idBasedTickCount = this.tickCount + this.getId(); // Camellia - Petal - Move up + ProfilerFiller profiler = Profiler.get(); + profiler.push("sensing"); +- this.sensing.tick(); ++ if (idBasedTickCount % moe.monnairealms.camellia.config.modules.optimizations.PetalReduceSensorWorkConfig.delayTicks == 0 || !me.earthme.luminol.config.modules.optimizations.PetalReduceSensorWorkConfig.enabled) this.sensing.tick(); // Camellia - Petal - Reduce sensor work + profiler.pop(); +- int idBasedTickCount = this.tickCount + this.getId(); ++ //int idBasedTickCount = this.tickCount + this.getId(); // Camellia - Petal - Move up + if (idBasedTickCount % 2 != 0 && this.tickCount > 1) { + profiler.push("targetSelector"); + this.targetSelector.tickRunningGoals(false); diff --git a/camellia-server/minecraft-patches/features/0044-Purpur-Lobotomize-stuck-villagers.patch b/camellia-server/minecraft-patches/features/0044-Purpur-Lobotomize-stuck-villagers.patch new file mode 100644 index 0000000..12208ac --- /dev/null +++ b/camellia-server/minecraft-patches/features/0044-Purpur-Lobotomize-stuck-villagers.patch @@ -0,0 +1,89 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 12:13:58 +0800 +Subject: [PATCH] Purpur: Lobotomize stuck villagers + +Co-authored by: William Blake Galbreath +As part of: Purpur (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/purpur-server/minecraft-patches/features/0001-Ridables.patch) +Licensed under: MIT (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/LICENSE) + +diff --git a/net/minecraft/world/entity/npc/villager/Villager.java b/net/minecraft/world/entity/npc/villager/Villager.java +index f649c46cc9a34e646f8da3244866c6e011d30aa3..b0b017f61f1c7832068d2ba7b10efc72b9815b39 100644 +--- a/net/minecraft/world/entity/npc/villager/Villager.java ++++ b/net/minecraft/world/entity/npc/villager/Villager.java +@@ -190,6 +190,53 @@ public class Villager extends AbstractVillager implements VillagerDataHolder, Re + this.setCanPickUpLoot(true); + } + ++ // Purpur start ++ private boolean isLobotomized = false; public boolean isLobotomized() { return this.isLobotomized; } // Purpur ++ private int notLobotomizedCount = 0; // Purpur ++ ++ private boolean checkLobotomized() { ++ int interval = moe.monnairealms.camellia.config.modules.optimizations.LobotomizeVillageConfig.villagerLobotomizeCheckInterval; ++ boolean shouldCheckForTradeLocked = moe.monnairealms.camellia.config.modules.optimizations.LobotomizeVillageConfig.villagerLobotomizeWaitUntilTradeLocked; ++ if (this.notLobotomizedCount > 3) { ++ // check half as often if not lobotomized for the last 3+ consecutive checks ++ interval *= 2; ++ } ++ if (this.level().getGameTime() % interval == 0) { ++ // offset Y for short blocks like dirt_path/farmland ++ this.isLobotomized = !(shouldCheckForTradeLocked && this.getVillagerXp() == 0) && !canTravelFrom(net.minecraft.core.BlockPos.containing(this.position().x, this.getBoundingBox().minY + 0.0625D, this.position().z)); ++ ++ if (this.isLobotomized) { ++ this.notLobotomizedCount = 0; ++ } else { ++ this.notLobotomizedCount++; ++ } ++ } ++ return this.isLobotomized; ++ } ++ // Purpur end ++ ++ private boolean canTravelFrom(net.minecraft.core.BlockPos pos) { ++ return canTravelTo(pos.east()) || canTravelTo(pos.west()) || canTravelTo(pos.north()) || canTravelTo(pos.south()); ++ } ++ ++ private boolean canTravelTo(net.minecraft.core.BlockPos pos) { ++ net.minecraft.world.level.block.state.BlockState state = this.level().getBlockStateIfLoaded(pos); ++ if (state == null) { ++ // chunk not loaded ++ return false; ++ } ++ net.minecraft.world.level.block.Block bottom = state.getBlock(); ++ if (bottom instanceof net.minecraft.world.level.block.FenceBlock || ++ bottom instanceof net.minecraft.world.level.block.FenceGateBlock || ++ bottom instanceof net.minecraft.world.level.block.WallBlock) { ++ // bottom block is too tall to get over ++ return false; ++ } ++ net.minecraft.world.level.block.Block top = level().getBlockState(pos.above()).getBlock(); ++ // only if both blocks have no collision ++ return !bottom.hasCollision && !top.hasCollision; ++ } ++ + @Override + public Brain getBrain() { + return (Brain)super.getBrain(); +@@ -259,11 +306,20 @@ public class Villager extends AbstractVillager implements VillagerDataHolder, Re + // Paper start - EAR 2 + this.customServerAiStep(level, false); + } +- protected void customServerAiStep(ServerLevel level, final boolean inactive) { ++ protected void customServerAiStep(ServerLevel level, boolean inactive) { // Purpur - not final + // Paper end - EAR 2 + ProfilerFiller profiler = Profiler.get(); + profiler.push("villagerBrain"); ++ // Purpur start ++ if (moe.monnairealms.camellia.config.modules.optimizations.LobotomizeVillageConfig.villagerLobotomizeEnabled) { ++ // treat as inactive if lobotomized ++ inactive = inactive || checkLobotomized(); ++ } else { ++ this.isLobotomized = false; ++ } ++ // Purpur end + if (!inactive) this.getBrain().tick(level, this); // Paper - EAR 2 ++ else if (this.isLobotomized && shouldRestock(level)) restock(); // Purpur - Lobotomize stuck villagers + profiler.pop(); + if (this.assignProfessionWhenSpawned) { + this.assignProfessionWhenSpawned = false; diff --git a/camellia-server/minecraft-patches/features/0045-Pufferfish-Reduce-projectile-chunk-loading.patch b/camellia-server/minecraft-patches/features/0045-Pufferfish-Reduce-projectile-chunk-loading.patch new file mode 100644 index 0000000..1020fb9 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0045-Pufferfish-Reduce-projectile-chunk-loading.patch @@ -0,0 +1,68 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 1 May 2026 21:47:48 +0800 +Subject: [PATCH] Pufferfish: Reduce projectile chunk loading + +A part of Pufferfish(https://github.com/Pufferfish-gg/Pufferfish) +Co-authored-by: Paul Sauve +Original patch: https://github.com/pufferfish-gg/Pufferfish/blob/ver/1.21/pufferfish-server/minecraft-patches/features/0006-Reduce-projectile-chunk-loading.patch +Original license(GPL-3.0): https://github.com/pufferfish-gg/Pufferfish/blob/ver/1.21/PATCH-LICENSE + +diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java +index be3bdce1b42a32f57d9ec71d8f115ea1ceac9d52..fc5161a77a4a846888c564123265a669188646b5 100644 +--- a/io/papermc/paper/threadedregions/RegionizedWorldData.java ++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -408,6 +408,10 @@ public final class RegionizedWorldData { + + private RegionizedServer.WorldLevelData tickData; + ++ // Camellia start - Pufferfish - Reduce projectile chunk loading ++ public long pufferfish$loadedThisTick = 0L; ++ public long pufferfish$loadedTick = 0L; ++ // Camellia end + // connections + public final List connections = new ArrayList<>(); + +diff --git a/net/minecraft/world/entity/projectile/Projectile.java b/net/minecraft/world/entity/projectile/Projectile.java +index 721e3b083846fd363a31311196bc4c681969215c..9f49f0b801e23a82492e6b2865ba60493bf2f944 100644 +--- a/net/minecraft/world/entity/projectile/Projectile.java ++++ b/net/minecraft/world/entity/projectile/Projectile.java +@@ -58,6 +58,38 @@ public abstract class Projectile extends Entity implements TraceableEntity { + this.setOwner(EntityReference.of(owner)); + } + ++ // Pufferfish start ++ private int loadedLifetime = 0; ++ @Override ++ public void setPos(double x, double y, double z) { ++ var currRegionData = io.papermc.paper.threadedregions.TickRegionScheduler.getCurrentRegionizedWorldData(); ++ // we might run this on a chunk system worker(chunk gen), so skip this check if no world data was fetched ++ if (currRegionData == null || currRegionData.world != this.level()) { ++ return; ++ } ++ long currentTick = currRegionData.getRedstoneGameTime(); ++ if (currRegionData.pufferfish$loadedTick != currentTick) { ++ currRegionData.pufferfish$loadedTick = currentTick; ++ currRegionData.pufferfish$loadedThisTick = 0L; ++ } ++ int previousX = Mth.floor(this.getX()) >> 4, previousZ = Mth.floor(this.getZ()) >> 4; ++ int newX = Mth.floor(x) >> 4, newZ = Mth.floor(z) >> 4; ++ if (previousX != newX || previousZ != newZ) { ++ boolean isLoaded = ((net.minecraft.server.level.ServerChunkCache) this.level().getChunkSource()).getChunkAtIfLoadedImmediately(newX, newZ) != null; ++ if (!isLoaded) { ++ if (currRegionData.pufferfish$loadedThisTick > moe.monnairealms.camellia.config.modules.optimizations.ProjectileChunkReduceConfig.maxProjectileLoadsPerTick) { ++ if (++this.loadedLifetime > moe.monnairealms.camellia.config.modules.optimizations.ProjectileChunkReduceConfig.maxProjectileLoadsPerProjectile) { ++ this.discard(); ++ } ++ return; ++ } ++ currRegionData.pufferfish$loadedThisTick++; ++ } ++ } ++ super.setPos(x, y, z); ++ } ++ // Pufferfish end ++ + // Folia start - region threading + // In general, this is an entire mess. At the time of writing, there are fifty usages of getOwner. + // Usage of this function is to avoid concurrency issues, even if it sacrifices behavior. diff --git a/camellia-server/minecraft-patches/features/0046-Pufferfish-Throttle-goal-selector-during-inactive-ti.patch b/camellia-server/minecraft-patches/features/0046-Pufferfish-Throttle-goal-selector-during-inactive-ti.patch new file mode 100644 index 0000000..2461b8f --- /dev/null +++ b/camellia-server/minecraft-patches/features/0046-Pufferfish-Throttle-goal-selector-during-inactive-ti.patch @@ -0,0 +1,30 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 1 May 2026 21:48:40 +0800 +Subject: [PATCH] Pufferfish: Throttle goal selector during inactive ticking + +A part of Pufferfish(https://github.com/Pufferfish-gg/Pufferfish) +Co-authored-by: Kevin Raneri +Original patch: https://github.com/pufferfish-gg/Pufferfish/blob/ver/1.21/pufferfish-server/minecraft-patches/features/0015-Throttle-goal-selector-during-inactive-ticking.patch +Original license(GPL-3.0): https://github.com/pufferfish-gg/Pufferfish/blob/ver/1.21/PATCH-LICENSE + +diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java +index 1d6f66ed1f5b2f7be7a361685452473756dd10a1..85f903a5c61fe957594f1278500f2dc589081561 100644 +--- a/net/minecraft/world/entity/Mob.java ++++ b/net/minecraft/world/entity/Mob.java +@@ -215,12 +215,14 @@ public abstract class Mob extends LivingEntity implements Targeting, EquipmentUs + return this.lookControl; + } + ++ int _pufferfish_inactiveTickDisableCounter = 0; // Pufferfish - throttle inactive goal selector ticking + // Paper start + @Override + public void inactiveTick() { + super.inactiveTick(); + if (!this.aware) return; // Paper - Do not tick AI for inactive unaware mobs +- if (this.goalSelector.inactiveTick()) { ++ boolean isThrottled = moe.monnairealms.camellia.config.modules.optimizations.EntityGoalSelectorInactiveTickConfig.enabled && _pufferfish_inactiveTickDisableCounter++ % 20 != 0; // Pufferfish - throttle inactive goal selector ticking ++ if (this.goalSelector.inactiveTick() && !isThrottled) { // Pufferfish + this.goalSelector.tick(); + } + if (this.targetSelector.inactiveTick()) { diff --git a/camellia-server/minecraft-patches/features/0047-Leaf-Secure-seed-and-matter-seed-command.patch b/camellia-server/minecraft-patches/features/0047-Leaf-Secure-seed-and-matter-seed-command.patch new file mode 100644 index 0000000..1eec0a0 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0047-Leaf-Secure-seed-and-matter-seed-command.patch @@ -0,0 +1,423 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 20:39:22 +0800 +Subject: [PATCH] Leaf: Secure seed and matter seed command + +Co-authored by: Apehum +As part of: Leaf (https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/leaf-server/paper-patches/features/0019-Matter-Secure-Seed.patch and https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/leaf-server/minecraft-patches/features/0050-Matter-Secure-Seed-command.patch) +Licensed under: GPL-3.0 (https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/licenses/GPL-3.0.txt) + +diff --git a/net/minecraft/server/commands/SeedCommand.java b/net/minecraft/server/commands/SeedCommand.java +index 86ee3b3ae028597576b9549bc4954dfcbaa6732c..65121d2b17432f0ec5e8ec106cd205396ff96b73 100644 +--- a/net/minecraft/server/commands/SeedCommand.java ++++ b/net/minecraft/server/commands/SeedCommand.java +@@ -13,6 +13,15 @@ public class SeedCommand { + long seed = c.getSource().getLevel().getSeed(); + Component seedText = ComponentUtils.copyOnClickText(String.valueOf(seed)); + c.getSource().sendSuccess(() -> Component.translatable("commands.seed.success", seedText), false); ++ // Leaf start - Matter - SecureSeed Command ++ if (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled) { ++ su.plo.matter.Globals.setupGlobals(c.getSource().getLevel()); ++ String seedStr = su.plo.matter.Globals.seedToString(su.plo.matter.Globals.worldSeed); ++ Component featureSeedComponent = ComponentUtils.copyOnClickText(seedStr); ++ ++ c.getSource().sendSuccess(() -> Component.translatable(("Feature seed: %s"), featureSeedComponent), false); ++ } ++ // Leaf end - Matter - SecureSeed Command + return (int)seed; + }) + ); +diff --git a/net/minecraft/server/dedicated/DedicatedServerProperties.java b/net/minecraft/server/dedicated/DedicatedServerProperties.java +index 639a5400499266f83cf8b7c1251483cd8fb699a4..24e355b71c196cb0ea7958ef7d33fb49a3de9bb0 100644 +--- a/net/minecraft/server/dedicated/DedicatedServerProperties.java ++++ b/net/minecraft/server/dedicated/DedicatedServerProperties.java +@@ -139,7 +139,17 @@ public class DedicatedServerProperties extends Settings GsonHelper.parse(!s.isEmpty() ? s : "{}"), new JsonObject()), + this.get("level-type", v -> v.toLowerCase(Locale.ROOT), WorldPresets.NORMAL.identifier().toString()) +diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java +index b71c16640b1a2495e61480e9eb752fafbada7c44..0011653c857ee82f86355cc49b170120d9f96cfd 100644 +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/net/minecraft/server/level/ServerChunkCache.java +@@ -671,6 +671,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + + public ChunkGenerator getGenerator() { ++ su.plo.matter.Globals.setupGlobals(level); // Leaf - Matter - Secure Seed + return this.chunkMap.generator(); + } + +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index bd1368def6c2656902bae28dc07b98f8709107a9..fca5017a13148a928ae320ee99cddc45f617ffcd 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -633,6 +633,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + generator = new org.bukkit.craftbukkit.generator.CustomChunkGenerator(this, generator, gen); + } + // CraftBukkit end ++ su.plo.matter.Globals.setupGlobals(this); // Leaf - Matter - Secure Seed + boolean syncWrites = server.forceSynchronousWrites(); + DataFixer fixerUpper = server.getFixerUpper(); + // Paper - rewrite chunk system +diff --git a/net/minecraft/world/entity/monster/cubemob/Slime.java b/net/minecraft/world/entity/monster/cubemob/Slime.java +index 984ec15bdebcb541cd6ee9014a654ea58988f6f1..582fcd553e85ff6e2437c117dc0a655cb0017bae 100644 +--- a/net/minecraft/world/entity/monster/cubemob/Slime.java ++++ b/net/minecraft/world/entity/monster/cubemob/Slime.java +@@ -94,7 +94,12 @@ public class Slime extends AbstractCubeMob implements Enemy { + } + + ChunkPos chunkPos = ChunkPos.containing(pos); +- boolean slimeChunk = level.getMinecraftWorld().paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(chunkPos.x(), chunkPos.z(), worldGenLevel.getSeed(), level.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Paper ++ // Leaf start - Matter - Secure Seed ++ boolean isSlimeChunk = moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled ++ ? level.getChunk(chunkPos.x(), chunkPos.z()).isSlimeChunk() ++ : WorldgenRandom.seedSlimeChunk(chunkPos.x(), chunkPos.z(), worldGenLevel.getSeed(), level.getMinecraftWorld().spigotConfig.slimeSeed).nextInt(10) == 0; // Paper ++ boolean slimeChunk = level.getMinecraftWorld().paperConfig().entities.spawning.allChunksAreSlimeChunks || isSlimeChunk; ++ // Leaf end - Matter - Secure Seed + // Paper start - Replace rules for Height in Slime Chunks + final double maxHeightSlimeChunk = level.getMinecraftWorld().paperConfig().entities.spawning.slimeSpawnHeight.slimeChunk.maximum; + if (random.nextInt(10) == 0 && slimeChunk && pos.getY() < maxHeightSlimeChunk) { +diff --git a/net/minecraft/world/level/chunk/ChunkAccess.java b/net/minecraft/world/level/chunk/ChunkAccess.java +index 28f703204afd834cd50335346ef065670d6b37ad..f6cecf0ec1ebf9dd7028877dcadf191d1a2d954c 100644 +--- a/net/minecraft/world/level/chunk/ChunkAccess.java ++++ b/net/minecraft/world/level/chunk/ChunkAccess.java +@@ -83,6 +83,10 @@ public abstract class ChunkAccess implements LightChunk, StructureAccess, BiomeM + private static final org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry DATA_TYPE_REGISTRY = new org.bukkit.craftbukkit.persistence.CraftPersistentDataTypeRegistry(); + public org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer persistentDataContainer = new org.bukkit.craftbukkit.persistence.DirtyCraftPersistentDataContainer(ChunkAccess.DATA_TYPE_REGISTRY); + // CraftBukkit end ++ // Leaf start - Matter - Secure Seed ++ private boolean slimeChunk; ++ private boolean hasComputedSlimeChunk; ++ // Leaf end - Matter - Secure Seed + + // Paper start - rewrite chunk system + private volatile ca.spottedleaf.moonrise.patches.starlight.light.SWMRNibbleArray[] blockNibbles; +@@ -186,6 +190,17 @@ public abstract class ChunkAccess implements LightChunk, StructureAccess, BiomeM + return GameEventListenerRegistry.NOOP; + } + ++ // Leaf start - Matter - Secure Seed ++ public boolean isSlimeChunk() { ++ if (!hasComputedSlimeChunk) { ++ hasComputedSlimeChunk = true; ++ slimeChunk = su.plo.matter.WorldgenCryptoRandom.seedSlimeChunk(chunkPos.x(), chunkPos.z()).nextInt(10) == 0; ++ } ++ ++ return slimeChunk; ++ } ++ // Leaf end - Matter - Secure Seed ++ + public abstract BlockState getBlockState(final int x, final int y, final int z); // Paper + + public @Nullable BlockState setBlockState(final BlockPos pos, final BlockState state) { +diff --git a/net/minecraft/world/level/chunk/ChunkGenerator.java b/net/minecraft/world/level/chunk/ChunkGenerator.java +index b7325170369299a74be35ecc66a6446c301605b8..e3a57d7fc2ad9244594870ffac7c6625c5781eff 100644 +--- a/net/minecraft/world/level/chunk/ChunkGenerator.java ++++ b/net/minecraft/world/level/chunk/ChunkGenerator.java +@@ -347,7 +347,11 @@ public abstract class ChunkGenerator { + Map> structuresByStep = structuresRegistry.stream() + .collect(Collectors.groupingBy(structure -> structure.step().ordinal())); + List featureList = this.featuresPerStep.get(); +- WorldgenRandom random = new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); ++ // Leaf start - Matter - Secure Seed ++ WorldgenRandom random = moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled ++ ? new su.plo.matter.WorldgenCryptoRandom(origin.getX(), origin.getZ(), su.plo.matter.Globals.Salt.UNDEFINED, 0) ++ : new WorldgenRandom(new XoroshiroRandomSource(RandomSupport.generateUniqueSeed())); ++ // Leaf end - Matter - Secure Seed + long decorationSeed = random.setDecorationSeed(level.getSeed(), origin.getX(), origin.getZ()); + Set> possibleBiomes = new ObjectArraySet<>(); + ChunkPos.rangeClosed(sectionPos.chunk(), 1).forEach(chunkPos -> { +@@ -571,8 +575,15 @@ public abstract class ChunkGenerator { + } else { + ArrayList options = new ArrayList<>(structures.size()); + options.addAll(structures); +- WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L)); +- random.setLargeFeatureSeed(state.getLevelSeed(), sourceChunkPos.x(), sourceChunkPos.z()); ++ // Leaf start - Matter - Secure Seed ++ WorldgenRandom random; ++ if (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled) { ++ random = new su.plo.matter.WorldgenCryptoRandom(sourceChunkPos.x(), sourceChunkPos.z(), su.plo.matter.Globals.Salt.GENERATE_FEATURE, 0); ++ } else { ++ random = new WorldgenRandom(new LegacyRandomSource(0L)); ++ random.setLargeFeatureSeed(state.getLevelSeed(), sourceChunkPos.x(), sourceChunkPos.z()); ++ } ++ // Leaf end - Matter - Secure Seed + int total = 0; + + for (StructureSet.StructureSelectionEntry option : options) { +diff --git a/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java b/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java +index 8b4ea2b16da45bcefe743990866e43c94e121825..144bf28dac810c7009ec3ce06b4f08e958ebc1f6 100644 +--- a/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java ++++ b/net/minecraft/world/level/chunk/ChunkGeneratorStructureState.java +@@ -203,14 +203,20 @@ public class ChunkGeneratorStructureState { + List> tasks = new ArrayList<>(count); + int spread = placement.spread(); + HolderSet preferredBiomes = placement.preferredBiomes(); +- RandomSource random = RandomSource.create(); +- // Paper start - Add missing structure set seed configs ++ // Leaf start - Matter - Secure Seed ++ RandomSource random = moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled ++ ? new su.plo.matter.WorldgenCryptoRandom(0, 0, su.plo.matter.Globals.Salt.STRONGHOLDS, 0) ++ :RandomSource.create(); ++ // Leaf end - Matter - Secure Seed ++ if (!moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled) { ++ //Paper start - Add missing structure set seed configs + if (this.conf.strongholdSeed != null && structureSet.is(net.minecraft.world.level.levelgen.structure.BuiltinStructureSets.STRONGHOLDS)) { + random.setSeed(this.conf.strongholdSeed); + } else { + // Paper end - Add missing structure set seed configs + random.setSeed(this.concentricRingsSeed); + } // Paper - Add missing structure set seed configs ++ } // Leaf - Matter - Secure Seed + double angle = random.nextDouble() * Math.PI * 2.0; + int positionInCircle = 0; + int circle = 0; +diff --git a/net/minecraft/world/level/chunk/status/ChunkStep.java b/net/minecraft/world/level/chunk/status/ChunkStep.java +index 9208924f54d5024bc50ad4501fbff9eb2a289181..01d87d9c474a5f3e14713579e6a1d1e17d2455ae 100644 +--- a/net/minecraft/world/level/chunk/status/ChunkStep.java ++++ b/net/minecraft/world/level/chunk/status/ChunkStep.java +@@ -60,6 +60,7 @@ public final class ChunkStep implements ca.spottedleaf.moonrise.patches.chunk_sy + } + + public CompletableFuture apply(final WorldGenContext context, final StaticCache2D cache, final ChunkAccess chunk) { ++ su.plo.matter.Globals.setupGlobals(context.level()); // Leaf - Matter - Secure Seed + if (chunk.getPersistedStatus().isBefore(this.targetStatus)) { + ProfiledDuration profiledDuration = JvmProfiler.INSTANCE.onChunkGenerate(chunk.getPos(), context.level().dimension(), this.targetStatus.getName()); + return this.task.doWork(context, this, cache, chunk).thenApply(newCenterChunk -> this.completeChunkGeneration(newCenterChunk, profiledDuration)); +diff --git a/net/minecraft/world/level/levelgen/WorldOptions.java b/net/minecraft/world/level/levelgen/WorldOptions.java +index 4ad94af024d81b7dfe910dcee2a6ec7a9b200d56..075bffa587e7ca394421a3a4d03b7eb125955247 100644 +--- a/net/minecraft/world/level/levelgen/WorldOptions.java ++++ b/net/minecraft/world/level/levelgen/WorldOptions.java +@@ -10,8 +10,22 @@ import net.minecraft.util.RandomSource; + import org.apache.commons.lang3.StringUtils; + + public class WorldOptions { ++ // Leaf start - Matter - Secure Seed ++ private static final com.google.gson.Gson gson = new com.google.gson.Gson(); ++ private static final boolean isSecureSeedEnabled = moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled; + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( +- i -> i.group( ++ // Leaf start - Matter - Secure Seed ++ i -> isSecureSeedEnabled ++ ? i.group( ++ Codec.LONG.fieldOf("seed").stable().forGetter(WorldOptions::seed), ++ Codec.STRING.fieldOf("feature_seed").orElse(gson.toJson(su.plo.matter.Globals.createRandomWorldSeed())).stable().forGetter(WorldOptions::featureSeedSerialize), ++ Codec.BOOL.fieldOf("generate_features").orElse(true).stable().forGetter(WorldOptions::generateStructures), ++ Codec.BOOL.fieldOf("bonus_chest").orElse(false).stable().forGetter(WorldOptions::generateBonusChest), ++ Codec.STRING.lenientOptionalFieldOf("legacy_custom_options").stable().forGetter(worldOptions -> worldOptions.legacyCustomOptions) ++ ) ++ .apply(i, i.stable(WorldOptions::new)) ++ : i.group( ++ // Leaf end + Codec.LONG.fieldOf("seed").stable().forGetter(WorldOptions::seed), + ExtraCodecs.optionalAlwaysPresentFieldOf(Codec.BOOL, "generate_structures", true).stable().forGetter(WorldOptions::generateStructures), + ExtraCodecs.optionalAlwaysPresentFieldOf(Codec.BOOL, "bonus_chest", false).stable().forGetter(WorldOptions::generateBonusChest), +@@ -19,8 +33,14 @@ public class WorldOptions { + ) + .apply(i, i.stable(WorldOptions::new)) + ); +- public static final WorldOptions DEMO_OPTIONS = new WorldOptions("North Carolina".hashCode(), true, true); ++ // Leaf end - Matter - Secure Seed ++ // Leaf start - Matter - Secure Seed ++ public static final WorldOptions DEMO_OPTIONS = isSecureSeedEnabled ++ ? new WorldOptions((long) "North Carolina".hashCode(), su.plo.matter.Globals.createRandomWorldSeed(), true, true) ++ : new WorldOptions("North Carolina".hashCode(), true, true); ++ // Leaf end - Matter - Secure Seed + private final long seed; ++ private long[] featureSeed = su.plo.matter.Globals.createRandomWorldSeed(); // Leaf - Matter - Secure Seed + private final boolean generateStructures; + private final boolean generateBonusChest; + private final Optional legacyCustomOptions; +@@ -29,14 +49,35 @@ public class WorldOptions { + this(seed, generateStructures, generateBonusChest, Optional.empty()); + } + ++ // Leaf start - Matter - Secure Seed ++ public WorldOptions(long seed, long[] featureSeed, boolean generateStructures, boolean bonusChest) { ++ this(seed, featureSeed, generateStructures, bonusChest, Optional.empty()); ++ } ++ ++ private WorldOptions(long seed, String featureSeedJson, boolean generateStructures, boolean bonusChest, Optional legacyCustomOptions) { ++ this(seed, gson.fromJson(featureSeedJson, long[].class), generateStructures, bonusChest, legacyCustomOptions); ++ } ++ // Leaf end - Matter - Secure Seed ++ + public static WorldOptions defaultWithRandomSeed() { +- return new WorldOptions(randomSeed(), true, false); ++ // Leaf start - Matter - Secure Seed ++ return isSecureSeedEnabled ++ ? new WorldOptions(randomSeed(), su.plo.matter.Globals.createRandomWorldSeed(), true, false) ++ : new WorldOptions(randomSeed(), true, false); ++ // Leaf end - Matter - Secure Seed + } + + public static WorldOptions testWorldWithRandomSeed() { + return new WorldOptions(randomSeed(), false, false); + } + ++ // Leaf start - Matter - Secure Seed ++ private WorldOptions(long seed, long[] featureSeed, boolean generateStructures, boolean bonusChest, Optional legacyCustomOptions) { ++ this(seed, generateStructures, bonusChest, legacyCustomOptions); ++ this.featureSeed = featureSeed; ++ } ++ // Leaf end - Matter - Secure Seed ++ + private WorldOptions(final long seed, final boolean generateStructures, final boolean generateBonusChest, final Optional legacyCustomOptions) { + this.seed = seed; + this.generateStructures = generateStructures; +@@ -48,6 +89,16 @@ public class WorldOptions { + return this.seed; + } + ++ // Leaf start - Matter - Secure Seed ++ public long[] featureSeed() { ++ return this.featureSeed; ++ } ++ ++ private String featureSeedSerialize() { ++ return gson.toJson(this.featureSeed); ++ } ++ // Leaf end - Matter - Secure Seed ++ + public boolean generateStructures() { + return this.generateStructures; + } +@@ -60,17 +111,25 @@ public class WorldOptions { + return this.legacyCustomOptions.isPresent(); + } + ++ // Leaf start - Matter - Secure Seed + public WorldOptions withBonusChest(final boolean generateBonusChest) { +- return new WorldOptions(this.seed, this.generateStructures, generateBonusChest, this.legacyCustomOptions); ++ return isSecureSeedEnabled ++ ? new WorldOptions(this.seed, this.featureSeed, this.generateStructures, generateBonusChest, this.legacyCustomOptions) ++ : new WorldOptions(this.seed, this.generateStructures, generateBonusChest, this.legacyCustomOptions); + } + + public WorldOptions withStructures(final boolean generateStructures) { +- return new WorldOptions(this.seed, generateStructures, this.generateBonusChest, this.legacyCustomOptions); ++ return isSecureSeedEnabled ++ ? new WorldOptions(this.seed, this.featureSeed, generateStructures, this.generateBonusChest, this.legacyCustomOptions) ++ : new WorldOptions(this.seed, generateStructures, this.generateBonusChest, this.legacyCustomOptions); + } + + public WorldOptions withSeed(final OptionalLong seed) { +- return new WorldOptions(seed.orElse(randomSeed()), this.generateStructures, this.generateBonusChest, this.legacyCustomOptions); ++ return isSecureSeedEnabled ++ ? new WorldOptions(seed.orElse(randomSeed()), su.plo.matter.Globals.createRandomWorldSeed(), this.generateStructures, this.generateBonusChest, this.legacyCustomOptions) ++ : new WorldOptions(seed.orElse(randomSeed()), this.generateStructures, this.generateBonusChest, this.legacyCustomOptions); + } ++ // Leaf end - Matter - Secure Seed + + public static OptionalLong parseSeed(String seedString) { + seedString = seedString.trim(); +diff --git a/net/minecraft/world/level/levelgen/feature/GeodeFeature.java b/net/minecraft/world/level/levelgen/feature/GeodeFeature.java +index baed0479a4552676844989868ee4c37ffa344def..bcda97e7439945d2a559e45e33f55ccf7ebe801b 100644 +--- a/net/minecraft/world/level/levelgen/feature/GeodeFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/GeodeFeature.java +@@ -43,7 +43,11 @@ public class GeodeFeature extends Feature { + int maxGenOffset = config.maxGenOffset(); + List> points = Lists.newLinkedList(); + int numPoints = config.distributionPoints().sample(random); +- WorldgenRandom random1 = new WorldgenRandom(new LegacyRandomSource(level.getSeed())); ++ // Leaf start - Matter - Secure Seed ++ WorldgenRandom random1 = moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled ++ ? new su.plo.matter.WorldgenCryptoRandom(0, 0, su.plo.matter.Globals.Salt.GEODE_FEATURE, 0) ++ : new WorldgenRandom(new LegacyRandomSource(level.getSeed())); ++ // Leaf end - Matter - Secure Seed + NormalNoise noise = NormalNoise.create(random1, -4, 1.0); + List crackPoints = Lists.newLinkedList(); + double crackSizeAdjustment = (double)numPoints / config.outerWallDistance().maxInclusive(); +diff --git a/net/minecraft/world/level/levelgen/structure/Structure.java b/net/minecraft/world/level/levelgen/structure/Structure.java +index f1e13ede1f7345089ece8d8ef391f7fd5e55c445..2bfc339e3301deb0d46a92b1c138ff1abbf5b409 100644 +--- a/net/minecraft/world/level/levelgen/structure/Structure.java ++++ b/net/minecraft/world/level/levelgen/structure/Structure.java +@@ -248,6 +248,11 @@ public abstract class Structure { + } + + private static WorldgenRandom makeRandom(final long seed, final ChunkPos chunkPos) { ++ // Leaf start - Matter - Secure Seed ++ if (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled) { ++ return new su.plo.matter.WorldgenCryptoRandom(chunkPos.x(), chunkPos.z(), su.plo.matter.Globals.Salt.GENERATE_FEATURE, seed); ++ } ++ // Leaf end - Matter - Secure Seed + WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L)); + random.setLargeFeatureSeed(seed, chunkPos.x(), chunkPos.z()); + return random; +diff --git a/net/minecraft/world/level/levelgen/structure/placement/RandomSpreadStructurePlacement.java b/net/minecraft/world/level/levelgen/structure/placement/RandomSpreadStructurePlacement.java +index 1310b2ca994b8f6d3d92bdc676fe44de3619cc09..ae55fd5d4b08f00557b384671412ee31b5d1aeef 100644 +--- a/net/minecraft/world/level/levelgen/structure/placement/RandomSpreadStructurePlacement.java ++++ b/net/minecraft/world/level/levelgen/structure/placement/RandomSpreadStructurePlacement.java +@@ -67,8 +67,15 @@ public class RandomSpreadStructurePlacement extends StructurePlacement { + public ChunkPos getPotentialStructureChunk(final long seed, final int sourceX, final int sourceZ) { + int spacedGridX = Math.floorDiv(sourceX, this.spacing); + int spacedGridZ = Math.floorDiv(sourceZ, this.spacing); +- WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L)); +- random.setLargeFeatureWithSalt(seed, spacedGridX, spacedGridZ, this.salt()); ++ // Leaf start - Matter - Secure Seed ++ WorldgenRandom random; ++ if (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled) { ++ random = new su.plo.matter.WorldgenCryptoRandom(spacedGridX, spacedGridZ, su.plo.matter.Globals.Salt.POTENTIONAL_FEATURE, this.salt); ++ } else { ++ random = new WorldgenRandom(new LegacyRandomSource(0L)); ++ random.setLargeFeatureWithSalt(seed, spacedGridX, spacedGridZ, this.salt()); ++ } ++ // Leaf end - Matter - Secure Seed + int limit = this.spacing - this.separation; + int spreadX = this.spreadType.evaluate(random, limit); + int spreadZ = this.spreadType.evaluate(random, limit); +diff --git a/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java b/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java +index 646df0237a5a2863f75e6c5f9dfe9ddf512c540f..3fddb11c1bd0277d878d90d0dae8e5ed8a42556a 100644 +--- a/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java ++++ b/net/minecraft/world/level/levelgen/structure/placement/StructurePlacement.java +@@ -137,8 +137,16 @@ public abstract class StructurePlacement { + } + + private static boolean legacyArbitrarySaltProbabilityReducer(final long seed, final int salt, final int sourceX, final int sourceZ, final float probability, final @org.jspecify.annotations.Nullable Integer saltOverride) { // Paper - Add missing structure set seed configs +- WorldgenRandom random = new WorldgenRandom(new LegacyRandomSource(0L)); +- random.setLargeFeatureWithSalt(seed, sourceX, sourceZ, saltOverride != null ? saltOverride : HIGHLY_ARBITRARY_RANDOM_SALT); // Paper - Add missing structure set seed configs ++ // Leaf start - Matter - Secure Seed ++ WorldgenRandom random; ++ if (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled) { ++ random = new su.plo.matter.WorldgenCryptoRandom(sourceX, sourceZ, su.plo.matter.Globals.Salt.UNDEFINED, saltOverride != null ? saltOverride : HIGHLY_ARBITRARY_RANDOM_SALT); ++ } else { ++ random = new WorldgenRandom(new LegacyRandomSource(0L)); ++ random.setLargeFeatureWithSalt(seed, sourceX, sourceZ, saltOverride != null ? saltOverride : HIGHLY_ARBITRARY_RANDOM_SALT); // Paper - Add missing structure set seed configs ++ } ++ // Leaf end - Matter - Secure Seed ++ + return random.nextFloat() < probability; + } + +diff --git a/net/minecraft/world/level/levelgen/structure/pools/JigsawPlacement.java b/net/minecraft/world/level/levelgen/structure/pools/JigsawPlacement.java +index b92da7acae0789d955a9f71072b95ed030b879e2..2c81e8e7dd0380233b0f17e3d83ff704335dad53 100644 +--- a/net/minecraft/world/level/levelgen/structure/pools/JigsawPlacement.java ++++ b/net/minecraft/world/level/levelgen/structure/pools/JigsawPlacement.java +@@ -64,7 +64,11 @@ public class JigsawPlacement { + ChunkGenerator chunkGenerator = context.chunkGenerator(); + StructureTemplateManager structureTemplateManager = context.structureTemplateManager(); + LevelHeightAccessor heightAccessor = context.heightAccessor(); +- WorldgenRandom random = context.random(); ++ // Leaf start - Matter - Secure Seed ++ WorldgenRandom random = moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled ++ ? new su.plo.matter.WorldgenCryptoRandom(context.chunkPos().x(), context.chunkPos().z(), su.plo.matter.Globals.Salt.JIGSAW_PLACEMENT, 0) ++ : context.random(); ++ // Leaf end - Matter - Secure Seed + Registry pools = registryAccess.lookupOrThrow(Registries.TEMPLATE_POOL); + Rotation centerRotation = Rotation.getRandom(random); + StructureTemplatePool centerPool = startPool.unwrapKey() diff --git a/camellia-server/minecraft-patches/features/0048-Secure-seed-V2-with-Blake3.patch b/camellia-server/minecraft-patches/features/0048-Secure-seed-V2-with-Blake3.patch new file mode 100644 index 0000000..bbf3f57 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0048-Secure-seed-V2-with-Blake3.patch @@ -0,0 +1,70 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Libalpm64 +Date: Mon, 2 Mar 2026 00:00:00 +0000 +Subject: [PATCH] Secure seed V2 with Blake3 + + +diff --git a/net/minecraft/world/level/levelgen/RandomState.java b/net/minecraft/world/level/levelgen/RandomState.java +index 7b74440fbbe79c018ad59d24b99da0c6d4a9d007..20536f4ed6d19ec2cc6a54aa9c5c980d2ffea0ef 100644 +--- a/net/minecraft/world/level/levelgen/RandomState.java ++++ b/net/minecraft/world/level/levelgen/RandomState.java +@@ -33,10 +33,28 @@ public final class RandomState { + } + + private RandomState(final NoiseGeneratorSettings settings, final HolderGetter noises, final long seed) { +- this.random = settings.getRandomSource().newInstance(seed).forkPositional(); ++ // this.random = settings.getRandomSource().newInstance(seed).forkPositional(); // Camellia - Add secure seed V2 with Blake3 ++ // Camellia start - Add secure seed V2 with Blake3 ++ final long[] secureWorldSeed = (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled && moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.expandLevelSeedTo1024Bits(seed) ++ : null; ++ ++ long terrainSeed = (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled && moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.BASE_TERRAIN) ++ : seed; ++ long aquiferSeed = (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled && moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.AQUIFER) ++ : seed; ++ long oreSeed = (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled && moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.ORE) ++ : seed; ++ // Camellia end ++ this.random = settings.getRandomSource().newInstance(terrainSeed).forkPositional(); // Camellia - Add secure seed V2 with Blake3 + this.noises = noises; +- this.aquiferRandom = this.random.fromHashOf(Identifier.withDefaultNamespace("aquifer")).forkPositional(); +- this.oreRandom = this.random.fromHashOf(Identifier.withDefaultNamespace("ore")).forkPositional(); ++ //this.aquiferRandom = this.random.fromHashOf(Identifier.withDefaultNamespace("aquifer")).forkPositional(); // Camellia - Add secure seed V2 with Blake3 ++ //this.oreRandom = this.random.fromHashOf(Identifier.withDefaultNamespace("ore")).forkPositional(); // Camellia - Add secure seed V2 with Blake3 ++ this.aquiferRandom = settings.getRandomSource().newInstance(aquiferSeed).forkPositional(); // Camellia - Add secure seed V2 with Blake3 ++ this.oreRandom = settings.getRandomSource().newInstance(oreSeed).forkPositional(); // Camellia - Add secure seed V2 with Blake3 + this.noiseIntances = new ConcurrentHashMap<>(); + this.positionalRandoms = new ConcurrentHashMap<>(); + this.surfaceSystem = new SurfaceSystem(this, settings.defaultBlock(), settings.seaLevel(), this.random); +@@ -46,6 +64,12 @@ public final class RandomState { + private final Map wrapped = new HashMap<>(); + + private RandomSource newLegacyInstance(final long seedOffset) { ++ // Camellia start - Add secure seed V2 with Blake3 ++ if (moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled && moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.version == 2) { ++ long climateSeed = su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.CLIMATE); ++ return new su.plo.matter.WorldgenCryptoRandom(0, 0, su.plo.matter.Globals.Salt.UNDEFINED, climateSeed + seed); ++ } ++ // Camellia end + return new LegacyRandomSource(seed + seedOffset); + } + +@@ -71,7 +95,13 @@ public final class RandomState { + : RandomState.this.random.fromHashOf(Identifier.withDefaultNamespace("terrain")); + return noise.withNewRandom(terrainRandom); + } else { +- return function instanceof DensityFunctions.EndIslandDensityFunction ? new DensityFunctions.EndIslandDensityFunction(seed) : function; ++ return function instanceof DensityFunctions.EndIslandDensityFunction //? new DensityFunctions.EndIslandDensityFunction(seed)// Camellia - Add secure seed V2 with Blake3 ++ // Camellia start - Add secure seed V2 with Blake3 ++ ? new DensityFunctions.EndIslandDensityFunction((moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled && moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.SURFACE) ++ : seed) ++ // Camellia end ++ : function; + } + } + diff --git a/camellia-server/minecraft-patches/features/0049-Leaf-Replace-brain-maps-with-optimized-collection.patch b/camellia-server/minecraft-patches/features/0049-Leaf-Replace-brain-maps-with-optimized-collection.patch new file mode 100644 index 0000000..edef6d4 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0049-Leaf-Replace-brain-maps-with-optimized-collection.patch @@ -0,0 +1,34 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 12:27:21 +0800 +Subject: [PATCH] Leaf: Replace brain maps with optimized collection + +Co-authored by: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> +As part of: Leaf (https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/leaf-server/minecraft-patches/features/0070-Replace-brain-maps-with-optimized-collection.patch) +Licensed under: MIT (https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/licenses/MIT.txt) + +diff --git a/net/minecraft/world/entity/ai/Brain.java b/net/minecraft/world/entity/ai/Brain.java +index 977a31f995799cf44fa59cc1db73571edbcf36ff..ba28111819aacc30b410763adffc4d4dd2b545d4 100644 +--- a/net/minecraft/world/entity/ai/Brain.java ++++ b/net/minecraft/world/entity/ai/Brain.java +@@ -37,14 +37,14 @@ import org.jspecify.annotations.Nullable; + + public class Brain { + private static final int SCHEDULE_UPDATE_DELAY = 20; +- private final Map, MemorySlot> memories = Maps.newHashMap(); +- private final Map>, Sensor> sensors = Maps.newLinkedHashMap(); ++ private final Map, MemorySlot> memories = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // Leaf - Replace brain maps with optimized collection ++ private final Map>, Sensor> sensors = new it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap<>(); // Leaf - Replace brain maps with optimized collection + private final Map>>> availableBehaviorsByPriority = Maps.newTreeMap(); + private @Nullable EnvironmentAttribute schedule; +- private final Map, MemoryStatus>>> activityRequirements = Maps.newHashMap(); +- private final Map>> activityMemoriesToEraseWhenStopped = Maps.newHashMap(); +- private Set coreActivities = Sets.newHashSet(); +- private final Set activeActivities = Sets.newHashSet(); ++ private final Map, MemoryStatus>>> activityRequirements = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // Leaf - Replace brain maps with optimized collection ++ private final Map>> activityMemoriesToEraseWhenStopped = new it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap<>(); // Leaf - Replace brain maps with optimized collection ++ private Set coreActivities = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(); // Leaf - Replace brain maps with optimized collection ++ private final Set activeActivities = new it.unimi.dsi.fastutil.objects.ObjectOpenHashSet<>(); // Leaf - Replace brain maps with optimized collection + private Activity defaultActivity = Activity.IDLE; + private long lastScheduleUpdate = -9999L; + diff --git a/camellia-server/minecraft-patches/features/0050-Leaf-Remove-useless-creating-stats-json-bases-on-pla.patch b/camellia-server/minecraft-patches/features/0050-Leaf-Remove-useless-creating-stats-json-bases-on-pla.patch new file mode 100644 index 0000000..fa6f802 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0050-Leaf-Remove-useless-creating-stats-json-bases-on-pla.patch @@ -0,0 +1,54 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 20:51:53 +0800 +Subject: [PATCH] Leaf: Remove useless creating stats json bases on player name + logic + +Co-authored by: Dreeam <61569423+Dreeam-qwq@users.noreply.github.com> +As part of: Leaf (https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/leaf-server/minecraft-patches/features/0043-Remove-useless-creating-stats-json-bases-on-player-n.patch) +Licensed under: MIT (https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/licenses/MIT.txt) + +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 9e90197b96fcac958c5073ca8f560e77bb811460..134d74d8ed2c61b28cda0bd5107dea88bc114f51 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -1294,22 +1294,26 @@ public abstract class PlayerList { + Path uuidStatsFile = statFolder.resolve(gameProfile.id() + ".json"); + if (Files.exists(uuidStatsFile)) { + return uuidStatsFile; +- } +- +- String playerNameStatsFile = gameProfile.name() + ".json"; +- if (FileUtil.isValidPathSegment(playerNameStatsFile)) { +- Path playerNameStatsPath = statFolder.resolve(playerNameStatsFile); +- if (Files.isRegularFile(playerNameStatsPath)) { +- try { +- return Files.move(playerNameStatsPath, uuidStatsFile); +- } catch (IOException e) { +- LOGGER.warn("Failed to copy file {} to {}", playerNameStatsFile, uuidStatsFile); +- return playerNameStatsPath; ++ } else { ++ // Leaf start - Remove useless creating stats json bases on player name logic ++ /* ++ String playerNameStatsFile = gameProfile.name() + ".json"; ++ if (FileUtil.isValidPathSegment(playerNameStatsFile)) { ++ Path playerNameStatsPath = statFolder.resolve(playerNameStatsFile); ++ if (Files.isRegularFile(playerNameStatsPath)) { ++ try { ++ return Files.move(playerNameStatsPath, uuidStatsFile); ++ } catch (IOException e) { ++ LOGGER.warn("Failed to copy file {} to {}", playerNameStatsFile, uuidStatsFile); ++ return playerNameStatsPath; ++ } + } + } +- } ++ */ ++ // Leaf end - Remove useless creating stats json bases on player name logic + +- return uuidStatsFile; ++ return uuidStatsFile; ++ } + } + + public PlayerAdvancements getPlayerAdvancements(final ServerPlayer player) { diff --git a/camellia-server/minecraft-patches/features/0051-Leaf-Optimize-PatchedDataComponentMap-equals.patch b/camellia-server/minecraft-patches/features/0051-Leaf-Optimize-PatchedDataComponentMap-equals.patch new file mode 100644 index 0000000..3d91937 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0051-Leaf-Optimize-PatchedDataComponentMap-equals.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 2 May 2026 22:26:57 +0800 +Subject: [PATCH] Leaf Optimize PatchedDataComponentMap equals + +This is a part of Leaf(https://github.com/Winds-Studio/Leaf/blob/db41340915c7768d726342fb29d16bc428081074/leaf-server/minecraft-patches/features/0300-Optimize-PatchedDataComponentMap-equals.patch) + +Original author: HaHaWTH <102713261+HaHaWTH@users.noreply.github.com> + +Original license: https://github.com/Winds-Studio/Leaf/blob/db41340915c7768d726342fb29d16bc428081074/LICENSE.md + +diff --git a/net/minecraft/core/component/PatchedDataComponentMap.java b/net/minecraft/core/component/PatchedDataComponentMap.java +index e59250af5d769076578017fb880e9168bc720947..ae355b2d92090e89ab4671ff13f77d632e7dfcf9 100644 +--- a/net/minecraft/core/component/PatchedDataComponentMap.java ++++ b/net/minecraft/core/component/PatchedDataComponentMap.java +@@ -222,7 +222,19 @@ public final class PatchedDataComponentMap implements DataComponentMap, org.leav + + @Override + public boolean equals(final Object obj) { +- return this == obj || obj instanceof PatchedDataComponentMap otherMap && this.prototype.equals(otherMap.prototype) && this.patch.equals(otherMap.patch); ++ // return this == obj || obj instanceof PatchedDataComponentMap otherMap && this.prototype.equals(otherMap.prototype) && this.patch.equals(otherMap.patch); // Leaf - Optimize PatchedDataComponentMap equals ++ // Leaf start - Optimize PatchedDataComponentMap equals ++ if (this == obj) return true; ++ if (!(obj instanceof PatchedDataComponentMap that)) return false; ++ if (this.patch.size() != that.patch.size()) { ++ return false; ++ } ++ if (!this.prototype.equals(that.prototype)) { ++ return false; ++ } ++ if (this.patch == that.patch) return true; ++ return this.patch.equals(that.patch); ++ // Leaf end - Optimize PatchedDataComponentMap equals + } + + @Override diff --git a/camellia-server/minecraft-patches/features/0052-Leaf-Better-checking-for-useless-move-packets.patch b/camellia-server/minecraft-patches/features/0052-Leaf-Better-checking-for-useless-move-packets.patch new file mode 100644 index 0000000..71d3d00 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0052-Leaf-Better-checking-for-useless-move-packets.patch @@ -0,0 +1,53 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Mon, 4 May 2026 13:44:05 +0800 +Subject: [PATCH] Leaf Better checking for useless move packets + +A part of leaf(https://github.com/Winds-Studio/Leaf/blob/b794fca6080c47ff305b6d065a579171b1fe1e98/leaf-server/minecraft-patches/features/0018-Better-checking-for-useless-move-packets.patch) + +Original license: https://github.com/Winds-Studio/Leaf/blob/b794fca6080c47ff305b6d065a579171b1fe1e98/LICENSE.md + +diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java +index 60af82925d2f7e605bf7340cf01f1a5668f4d725..2e71a1cbabf3405e3e4fd5349a1784d895e7e60c 100644 +--- a/net/minecraft/server/level/ServerEntity.java ++++ b/net/minecraft/server/level/ServerEntity.java +@@ -186,18 +186,35 @@ public class ServerEntity { + packet = ClientboundEntityPositionSyncPacket.of(this.entity); + sentPosition = true; + sentRotation = true; +- } else if ((!pos || !shouldSendRotation) && !(this.entity instanceof AbstractArrow)) { ++ /*} else if ((!pos || !shouldSendRotation) && !(this.entity instanceof AbstractArrow)) { // Gale - Airplane - better checking for useless move packets + if (pos) { + packet = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short)xa, (short)ya, (short)za, this.entity.onGround()); + sentPosition = true; + } else if (shouldSendRotation) { + packet = new ClientboundMoveEntityPacket.Rot(this.entity.getId(), yRotn, xRotn, this.entity.onGround()); + sentRotation = true; +- } ++ }*/ // Gale - Airplane - better checking for useless move packets + } else { +- packet = new ClientboundMoveEntityPacket.PosRot(this.entity.getId(), (short)xa, (short)ya, (short)za, yRotn, xRotn, this.entity.onGround()); ++ /*packet = new ClientboundMoveEntityPacket.PosRot(this.entity.getId(), (short)xa, (short)ya, (short)za, yRotn, xRotn, this.entity.onGround()); // Gale - Airplane - better checking for useless move packets + sentPosition = true; +- sentRotation = true; ++ sentRotation = true;*/ // Gale - Airplane - better checking for useless move packets ++ // Gale start - Airplane - better checking for useless move packets ++ if (pos || shouldSendRotation || this.entity instanceof AbstractArrow) { ++ if ((!pos || !shouldSendRotation) && !(this.entity instanceof AbstractArrow)) { ++ if (pos) { ++ packet = new ClientboundMoveEntityPacket.Pos(this.entity.getId(), (short) xa, (short) ya, (short) za, this.entity.onGround()); ++ sentPosition = true; ++ } else if (shouldSendRotation) { ++ packet = new ClientboundMoveEntityPacket.Rot(this.entity.getId(), yRotn, xRotn, this.entity.onGround()); ++ sentRotation = true; ++ } ++ } else { ++ packet = new ClientboundMoveEntityPacket.PosRot(this.entity.getId(), (short) xa, (short) ya, (short) za, yRotn, xRotn, this.entity.onGround()); ++ sentPosition = true; ++ sentRotation = true; ++ } ++ } ++ // Gale end - Airplane - better checking for useless move packets + } + + if (this.entity.needsSync || this.trackDelta || this.entity instanceof LivingEntity livingEntity && livingEntity.isFallFlying()) { diff --git a/camellia-server/minecraft-patches/features/0053-Leaf-fast-bit-radix-sort.patch b/camellia-server/minecraft-patches/features/0053-Leaf-fast-bit-radix-sort.patch new file mode 100644 index 0000000..26c38a1 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0053-Leaf-fast-bit-radix-sort.patch @@ -0,0 +1,70 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sun, 17 May 2026 10:10:21 +0800 +Subject: [PATCH] Leaf fast bit radix sort + +This is a part of Leaf(https://github.com/Winds-Studio/Leaf/blob/d0ed97cf45dfa94f686ef32f0eea1ec7323f78d5/leaf-server/minecraft-patches/features/0284-fast-bit-radix-sort.patch) + +Original license: https://github.com/Winds-Studio/Leaf/blob/d0ed97cf45dfa94f686ef32f0eea1ec7323f78d5/LICENSE.md + +diff --git a/io/papermc/paper/threadedregions/RegionizedWorldData.java b/io/papermc/paper/threadedregions/RegionizedWorldData.java +index fc5161a77a4a846888c564123265a669188646b5..be8f0b5bc26c8384ea027a90e3ab0cdc8f9411fb 100644 +--- a/io/papermc/paper/threadedregions/RegionizedWorldData.java ++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -530,6 +530,7 @@ public final class RegionizedWorldData { + public final PathTypeCache pathTypesByPosCache = new PathTypeCache(); + public final List temporaryChunkTickList = new java.util.ArrayList<>(); + public final Set chunkHoldersToBroadcast = new ReferenceLinkedOpenHashSet<>(); ++ public final org.dreeam.leaf.util.FastBitRadixSort radixBitSorter = new org.dreeam.leaf.util.FastBitRadixSort(); // Leaf - quick sort + + // not transient + public java.util.ArrayDeque redstoneUpdateInfos; +diff --git a/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java b/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +index 8aeea55380462421cde43afb4b2fb0ce44b65195..73247b46d778dcb9b3be742e4e4c4c037e3ac0e5 100644 +--- a/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java ++++ b/net/minecraft/world/entity/ai/sensing/NearestItemSensor.java +@@ -15,6 +15,7 @@ public class NearestItemSensor extends Sensor { + private static final long XZ_RANGE = 32L; + private static final long Y_RANGE = 16L; + public static final int MAX_DISTANCE_TO_WANTED_ITEM = 32; ++ private static final double MAX_DIST_SQ = (double) MAX_DISTANCE_TO_WANTED_ITEM * MAX_DISTANCE_TO_WANTED_ITEM; // Leaf - quick sort + + @Override + public Set> requires() { +@@ -24,8 +25,16 @@ public class NearestItemSensor extends Sensor { + @Override + protected void doTick(final ServerLevel level, final Mob body) { + Brain brain = body.getBrain(); +- List items = level.getEntitiesOfClass(ItemEntity.class, body.getBoundingBox().inflate(32.0, 16.0, 32.0), item -> item.closerThan(body, MAX_DISTANCE_TO_WANTED_ITEM) && body.wantsToPickUp(level, item.getItem())); // Paper - Perf: Move predicate into getEntities +- items.sort(Comparator.comparingDouble(body::distanceToSqr)); ++ // Leaf start - fast bit radix sort ++ net.minecraft.core.Position pos = body.position(); ++ double x = pos.x(); ++ double y = pos.y(); ++ double z = pos.z(); ++ net.minecraft.world.phys.AABB boundingBox = body.getBoundingBox().inflate(32.0, 16.0, 32.0); ++ it.unimi.dsi.fastutil.objects.ObjectArrayList items = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel) level).moonrise$getEntityLookup().getEntities(ItemEntity.class, null, boundingBox, items, (ItemEntity itemEntity) -> itemEntity.distanceToSqr(x, y, z) < MAX_DIST_SQ && body.wantsToPickUp(level, itemEntity.getItem())); // Paper - Perf: Move predicate into getEntities ++ level.getCurrentWorldData().radixBitSorter.sort(items.elements(), items.size(), pos); ++ // Leaf end - fast bit radix sort + // Paper start - Perf: remove streams from hot code + ItemEntity nearest = null; + for (final ItemEntity item : items) { +diff --git a/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java b/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java +index 3578ece216827fd5d8c1206c689fc608e97b50df..9fbe439663edf57e018369a44ed7576d947d1fbc 100644 +--- a/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java ++++ b/net/minecraft/world/entity/ai/sensing/NearestLivingEntitySensor.java +@@ -17,8 +17,11 @@ public class NearestLivingEntitySensor extends Sensor + protected void doTick(final ServerLevel level, final T body) { + double followRange = body.getAttributeValue(Attributes.FOLLOW_RANGE); + AABB boundingBox = body.getBoundingBox().inflate(followRange, followRange, followRange); +- List livingEntities = level.getEntitiesOfClass(LivingEntity.class, boundingBox, mob -> mob != body && mob.isAlive()); +- livingEntities.sort(Comparator.comparingDouble(body::distanceToSqr)); ++ // Leaf start - fast bit radix sort ++ it.unimi.dsi.fastutil.objects.ObjectArrayList livingEntities = new it.unimi.dsi.fastutil.objects.ObjectArrayList<>(); ++ ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel) level).moonrise$getEntityLookup().getEntities(LivingEntity.class, body, boundingBox, livingEntities, LivingEntity::isAlive); ++ level.getCurrentWorldData().radixBitSorter.sort(livingEntities.elements(), livingEntities.size(), body.position()); ++ // Leaf end - fast bit radix sort + Brain brain = body.getBrain(); + brain.setMemory(MemoryModuleType.NEAREST_LIVING_ENTITIES, livingEntities); + brain.setMemory(MemoryModuleType.NEAREST_VISIBLE_LIVING_ENTITIES, new NearestVisibleLivingEntities(level, body, livingEntities)); diff --git a/camellia-server/minecraft-patches/features/0054-Leaf-Configurable-vanilla-username-check.patch b/camellia-server/minecraft-patches/features/0054-Leaf-Configurable-vanilla-username-check.patch new file mode 100644 index 0000000..7cc20e2 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0054-Leaf-Configurable-vanilla-username-check.patch @@ -0,0 +1,203 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 27 Jun 2026 17:23:22 +0800 +Subject: [PATCH] Leaf Configurable vanilla username check + +A part of Leaf(https://github.com/Winds-Studio/Leaf/blob/edb0504069139beaa6f39efa4702370c2576b3fc/leaf-server/minecraft-patches/features/0086-Configurable-vanilla-username-check.patch) + +License: https://github.com/Winds-Studio/Leaf/blob/edb0504069139beaa6f39efa4702370c2576b3fc/LICENSE.md + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index ab8d69fa54203175760d394a5425601073fcd0d9..6d5beffa1a771182d717f2baab5c626b4970c203 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -217,7 +217,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop tickables = Lists.newArrayList(); + // Paper - per-level GameRules +diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index 7de3bbecea2f11e4e1cb65408729f346760dc9b8..b72b81849c7fedad8efc5c7b112e89eb01dc74dd 100644 +--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -167,11 +167,20 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + public void handleHello(final ServerboundHelloPacket packet) { + Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet"); + // Paper start - Validate usernames +- if (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() ++ // Leaf start - Configurable vanilla username check ++ boolean allPrevChecksPassed; ++ if (moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.enabled ++ && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() + && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.performUsernameValidation + && !this.iKnowThisMayNotBeTheBestIdeaButPleaseDisableUsernameValidation) { +- Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username"); ++ allPrevChecksPassed = true; ++ if (!moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.allowOldPlayersJoin) { ++ Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username"); ++ } ++ } else { ++ allPrevChecksPassed = false; + } ++ // Leaf end - Configurable vanilla username check + this.requestedUuid = packet.profileId(); + // Paper end - Validate usernames + this.requestedUsername = packet.name(); +@@ -197,6 +206,15 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + authenticatorPool.execute(() -> { + try { + GameProfile gameprofile = ServerLoginPacketListenerImpl.this.createOfflineProfile(ServerLoginPacketListenerImpl.this.requestedUsername); // Spigot ++ // Leaf start - Configurable vanilla username check ++ if (moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.allowOldPlayersJoin) { ++ if (server.playerDataStorage.load(new net.minecraft.server.players.NameAndId(gameprofile)).orElse(null) != null) { ++ server.getPlayerList().playedPlayers.add(packet.name()); ++ } else if (allPrevChecksPassed) { ++ Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username"); ++ } ++ } ++ // Leaf end - Configurable vanilla username check + + gameprofile = ServerLoginPacketListenerImpl.this.callPlayerPreLoginEvents(gameprofile); // Paper - Add more fields to AsyncPlayerPreLoginEvent + ServerLoginPacketListenerImpl.LOGGER.info("UUID of player {} is {}", gameprofile.name(), gameprofile.id()); +@@ -341,7 +359,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, + server.getPluginManager().callEvent(asyncEvent); + profile = asyncEvent.getPlayerProfile(); + profile.complete(true); // Paper - setPlayerProfileAPI +- gameprofile = com.destroystokyo.paper.profile.CraftPlayerProfile.asAuthlibCopy(profile); ++ gameprofile = com.destroystokyo.paper.profile.CraftPlayerProfile.asAuthlibCopyCustomValidation(profile); // Leaf - Configurable vanilla username check + playerName = gameprofile.name(); + uniqueId = gameprofile.id(); + // Paper end - Add more fields to AsyncPlayerPreLoginEvent +diff --git a/net/minecraft/server/players/CachedUserNameToIdResolver.java b/net/minecraft/server/players/CachedUserNameToIdResolver.java +index 7443744e3f256983e52a1cefcaf66082e4a46a7b..b7553611723f7120c66e1f3e51d89df28bd2756b 100644 +--- a/net/minecraft/server/players/CachedUserNameToIdResolver.java ++++ b/net/minecraft/server/players/CachedUserNameToIdResolver.java +@@ -67,7 +67,7 @@ public class CachedUserNameToIdResolver implements UserNameToIdResolver { + } + + private Optional lookupGameProfile(final GameProfileRepository profileRepository, final String name) { +- if (!StringUtil.isValidPlayerName(name)) { ++ if (!StringUtil.isValidPlayerName(name, false)) { // Leaf - Configurable vanilla username check - Directly return, skip unnecessary following logic + return this.createUnknownProfile(name); + } + +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 134d74d8ed2c61b28cda0bd5107dea88bc114f51..0392962ed409e3f970904de05926c356610f0e4c 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -134,6 +134,7 @@ public abstract class PlayerList { + private org.bukkit.craftbukkit.CraftServer cserver; + private final Map playersByName = new java.util.HashMap<>(); + public @Nullable String collideRuleTeamName; // Paper - Configurable player collision ++ public final List playedPlayers = new java.util.concurrent.CopyOnWriteArrayList<>(); // Leaf - Configurable vanilla username check + + // Folia start - region threading + private final Object connectionsStateLock = new Object(); +@@ -581,6 +582,7 @@ public abstract class PlayerList { + player.getAdvancements().clearTriggers(); + this.players.remove(player); + this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot ++ if (moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.allowOldPlayersJoin) this.playedPlayers.remove(player.getGameProfile().name()); // Leaf - Configurable vanilla username check + this.server.getCustomBossEvents().onPlayerDisconnect(player); + UUID uuid = player.getUUID(); + ServerPlayer serverPlayer = this.playersByUUID.get(uuid); +diff --git a/net/minecraft/util/StringUtil.java b/net/minecraft/util/StringUtil.java +index 7957e0cfc43909c5268698a11c4933d20b4d9155..1cee7f26a5cd55b146b89da5912952d16269d77e 100644 +--- a/net/minecraft/util/StringUtil.java ++++ b/net/minecraft/util/StringUtil.java +@@ -64,6 +64,15 @@ public class StringUtil { + } + + public static boolean isValidPlayerName(final String name) { ++ // Leaf start - Configurable vanilla username check ++ return isValidPlayerName(name, moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.shouldSkipNonPlayerNameCheck()); ++ } ++ public static boolean isValidPlayerNameVanilla(final String name) { ++ return name.length() <= 16 && name.chars().filter(i -> i <= 32 || i >= 127).findAny().isEmpty(); ++ } ++ public static boolean isValidPlayerName(final String name, final boolean bypassCheck) { ++ if (bypassCheck) return name.length() <= 16; ++ // Leaf end - Configurable vanilla username check + return name.length() <= 16 && name.chars().filter(c -> c <= 32 || c >= 127).findAny().isEmpty(); + } + +@@ -87,6 +96,12 @@ public class StringUtil { + + // Paper start - Username validation + public static boolean isReasonablePlayerName(final String name) { ++ // Leaf start - Configurable vanilla username check ++ if (moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.allowOldPlayersJoin && net.minecraft.server.MinecraftServer.getServer().getPlayerList().playedPlayers.contains(name)) return true; ++ if (moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.useCustomUsernameRegex()) { ++ return moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.usernameRegex.matcher(name).matches() && name.length() <= 16; // Leaf - Configurable username check ++ } ++ // Leaf end - Configurable vanilla username check + if (name.isEmpty() || name.length() > 16) { + return false; + } +diff --git a/net/minecraft/world/item/component/ResolvableProfile.java b/net/minecraft/world/item/component/ResolvableProfile.java +index f1b62a6e0d8176d8ba1875e78cc730ee55b1b0c1..c0329b8304cce7ecfe53b102027f73bf4bbf64de 100644 +--- a/net/minecraft/world/item/component/ResolvableProfile.java ++++ b/net/minecraft/world/item/component/ResolvableProfile.java +@@ -68,6 +68,30 @@ public abstract sealed class ResolvableProfile implements TooltipProvider permit + + public abstract Either unpack(); + ++ // Leaf start - Configurable vanilla username check - Enforce skull validation ++ private static Either sanitizeDynamicPlayerName(final Either nameOrId) { ++ if (nameOrId.left().isEmpty()) { ++ return nameOrId; ++ } ++ return moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.enforceSkullValidation && !net.minecraft.util.StringUtil.isValidPlayerNameVanilla(nameOrId.left().get()) ? Either.left("INVALID_OWNER") : nameOrId; ++ } ++ ++ private static Optional sanitizePartialPlayerName(final Optional name) { ++ if (name.isEmpty()) { ++ return name; ++ } ++ return moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.enforceSkullValidation && !net.minecraft.util.StringUtil.isValidPlayerNameVanilla(name.get()) ? Optional.of("INVALID_OWNER") : name; ++ } ++ ++ private static Either sanitizeStaticPlayerName(final Either contents) { ++ if (contents.left().isEmpty()) { ++ return contents; ++ } ++ GameProfile gameProfile = contents.left().get(); ++ return moe.monnairealms.camellia.config.modules.misc.UsernameCheckConfig.enforceSkullValidation && !net.minecraft.util.StringUtil.isValidPlayerNameVanilla(gameProfile.name()) ? Either.left(new GameProfile(gameProfile.id(), "INVALID_OWNER", gameProfile.properties())) : contents; ++ } ++ // Leaf end - Configurable vanilla username check - Enforce skull validation ++ + protected ResolvableProfile(final GameProfile partialProfile, final PlayerSkin.Patch skinPatch) { + this.partialProfile = partialProfile; + this.skinPatch = skinPatch; +@@ -96,6 +120,7 @@ public abstract sealed class ResolvableProfile implements TooltipProvider permit + private final Either nameOrId; + + public Dynamic(final Either nameOrId, final PlayerSkin.Patch skinPatch) { ++ sanitizeDynamicPlayerName(nameOrId); // Leaf - Configurable vanilla username check + super(ResolvableProfile.createPartialProfile(nameOrId.left(), nameOrId.right(), PropertyMap.EMPTY), skinPatch); + this.nameOrId = nameOrId; + } +@@ -135,6 +160,11 @@ public abstract sealed class ResolvableProfile implements TooltipProvider permit + } + + public record Partial(Optional name, Optional id, PropertyMap properties) { ++ // Leaf start - Configurable vanilla username check ++ public Partial { ++ name = ResolvableProfile.sanitizePartialPlayerName(name); ++ } ++ // Leaf end - Configurable vanilla username check + public static final ResolvableProfile.Partial EMPTY = new ResolvableProfile.Partial(Optional.empty(), Optional.empty(), PropertyMap.EMPTY); + public static final MapCodec MAP_CODEC = RecordCodecBuilder.mapCodec( + i -> i.group( +@@ -165,6 +195,7 @@ public abstract sealed class ResolvableProfile implements TooltipProvider permit + private final Either contents; + + public Static(final Either contents, final PlayerSkin.Patch skinPatch) { ++ sanitizeStaticPlayerName(contents); // Leaf - Configurable vanilla username check + super(contents.map(gameProfile -> (GameProfile)gameProfile, ResolvableProfile.Partial::createProfile), skinPatch); + this.contents = contents; + } diff --git a/camellia-server/minecraft-patches/features/0055-Leaves-Disable-packet-limit-Camellia-Option-to-full-.patch b/camellia-server/minecraft-patches/features/0055-Leaves-Disable-packet-limit-Camellia-Option-to-full-.patch new file mode 100644 index 0000000..75e5f92 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0055-Leaves-Disable-packet-limit-Camellia-Option-to-full-.patch @@ -0,0 +1,61 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 20:57:48 +0800 +Subject: [PATCH] Leaves: Disable packet limit & Camellia: Option to full + disable packet limiter + + +diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java +index d0dff5850717bd083b7a29104a61754ce1b2f822..d0809790e3b3526622af4f7a7eca6c52a6b4044e 100644 +--- a/net/minecraft/network/Connection.java ++++ b/net/minecraft/network/Connection.java +@@ -258,8 +258,8 @@ public class Connection extends SimpleChannelInboundHandler> { + if (this.stopReadingPackets) { + return; + } +- if (this.allPacketCounts != null || +- io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass())) { ++ if (!moe.monnairealms.camellia.config.modules.misc.PaperPacketLimiterConfig.forceDisable && (this.allPacketCounts != null || // Camellia - Add config to force disable the packet limiter of Paper ++ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass()))) { // Camellia - Add config to force disable the packet limiter of Paper + long time = System.nanoTime(); + synchronized (PACKET_LIMIT_LOCK) { + if (this.allPacketCounts != null) { +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 9fe5ee1c5578be6f6c5ad9a58e10669a4b7ac75a..ee4b6d293c32c00e6173cf8efe8aecce67574e6e 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -877,7 +877,7 @@ public class ServerGamePacketListenerImpl + public void handleCustomCommandSuggestions(final ServerboundCommandSuggestionPacket packet) { + // PacketUtils.ensureRunningOnSameThread(packet, this, this.player.level()); // Paper - AsyncTabCompleteEvent; run this async + // CraftBukkit start +- if (!this.tabSpamThrottler.isIncrementAndUnderThreshold() && !this.server.getPlayerList().isOp(this.player.nameAndId()) && !this.server.isSingleplayerOwner(this.player.nameAndId())) { // Paper - configurable tab spam limits ++ if (!moe.monnairealms.camellia.config.modules.misc.PaperPacketLimiterConfig.forceDisable && !this.tabSpamThrottler.isIncrementAndUnderThreshold() && !this.server.getPlayerList().isOp(this.player.nameAndId()) && !this.server.isSingleplayerOwner(this.player.nameAndId())) { // Paper - configurable tab spam limits // Leaves - can disable + this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - Kick event cause // Paper - add proper async disconnect + return; + } +@@ -2111,6 +2111,7 @@ public class ServerGamePacketListenerImpl + private long lastLimitedPacket = -1; + + private boolean checkLimit(long timestamp) { ++ if (moe.monnairealms.camellia.config.modules.misc.PaperPacketLimiterConfig.forceDisable) return true; // Leaves - disable + if (!io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.incomingPacketThreshold.enabled()) { + return true; + } +@@ -2677,6 +2678,8 @@ public class ServerGamePacketListenerImpl + + // Spigot start - spam exclusions + private void detectRateSpam(final TickThrottler throttler, final String message) { ++ if (moe.monnairealms.camellia.config.modules.misc.PaperPacketLimiterConfig.forceDisable) return; // Leaves - disable ++ // CraftBukkit start - replaced with thread safe throttle + if (org.spigotmc.SpigotConfig.enableSpamExclusions) { + for (String exclude : org.spigotmc.SpigotConfig.spamExclusions) { + if (exclude != null && message.startsWith(exclude)) { +@@ -3475,7 +3478,7 @@ public class ServerGamePacketListenerImpl + @Override + public void handlePlaceRecipe(final ServerboundPlaceRecipePacket packet) { + // Paper start - auto recipe limit +- if (!org.bukkit.Bukkit.isPrimaryThread()) { ++ if (!moe.monnairealms.camellia.config.modules.misc.PaperPacketLimiterConfig.forceDisable && !org.bukkit.Bukkit.isPrimaryThread()) { // Leaves - can disable + if (!this.recipeSpamPackets.isIncrementAndUnderThreshold()) { + this.disconnectAsync(net.minecraft.network.chat.Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - kick event cause // Paper - add proper async disconnect + return; diff --git a/camellia-server/minecraft-patches/features/0056-Leaves-Configurable-collision-behavior.patch b/camellia-server/minecraft-patches/features/0056-Leaves-Configurable-collision-behavior.patch new file mode 100644 index 0000000..7e3bf51 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0056-Leaves-Configurable-collision-behavior.patch @@ -0,0 +1,56 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 12:26:25 +0800 +Subject: [PATCH] Leaves: Configurable collision behavior + +Co-authored by: Fortern +As part of: Leaves (https://github.com/LeavesMC/Leaves/blob/c5f18b7864206cea4411211b51787f10affbcb9c/leaves-server/minecraft-patches/features/0111-Configurable-collision-behavior.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +index 8d2518600ad518999b75124f0a87db9efe541f2e..c2c4a83ac4fb787402400d490ecc94be50e49bd8 100644 +--- a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java ++++ b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +@@ -101,6 +101,14 @@ public final class CollisionUtil { + (box1.minZ - box2.maxZ) < -COLLISION_EPSILON && (box1.maxZ - box2.minZ) > COLLISION_EPSILON; + } + ++ // Leaves start - Configurable collision behavior ++ public static boolean voxelShapeIntersectVanilla(final AABB box1, final AABB box2) { ++ return box1.minX < box2.maxX && box1.maxX > box2.minX && ++ box1.minY < box2.maxY && box1.maxY > box2.minY && ++ box1.minZ < box2.maxZ && box1.maxZ > box2.minZ; ++ } ++ // Leaves end - Configurable collision behavior ++ + // assume !isEmpty(target) && abs(source_move) >= COLLISION_EPSILON + public static double collideX(final AABB target, final AABB source, final double source_move) { + if ((source.minY - target.maxY) < -COLLISION_EPSILON && (source.maxY - target.minY) > COLLISION_EPSILON && +@@ -2033,7 +2041,7 @@ public final class CollisionUtil { + continue; + } + } else { +- if (!voxelShapeIntersect(aabb, singleAABB)) { ++ if (shouldSkip(aabb, blockCollision, singleAABB)) { // Leaves - Configurable collision behavior + continue; + } + } +@@ -2087,6 +2095,18 @@ public final class CollisionUtil { + return ret; + } + ++ // Leaves start - Configurable collision behavior ++ private static boolean shouldSkip(AABB aabb, VoxelShape blockCollision, AABB singleAABB) { ++ boolean isBlockShape = blockCollision == Shapes.block(); ++ return switch (moe.monnairealms.camellia.config.modules.fixes.CollisionBehaviorConfig.behaviorMode) { ++ case moe.monnairealms.camellia.enums.EnumCollisionBehaviorMode.VANILLA -> !voxelShapeIntersectVanilla(aabb, singleAABB); ++ case moe.monnairealms.camellia.enums.EnumCollisionBehaviorMode.PAPER -> !voxelShapeIntersect(aabb, singleAABB); ++ default -> isBlockShape && !voxelShapeIntersectVanilla(aabb, singleAABB) || !isBlockShape && !voxelShapeIntersect(aabb, singleAABB); ++ // All other value as BLOCK_SHAPE_VANILLA to process ++ }; ++ } ++ // Leaves end - Configurable collision behavior ++ + public static boolean getEntityHardCollisions(final Level world, final Entity entity, AABB aabb, + final List into, final int collisionFlags, final Predicate predicate) { + final boolean checkOnly = (collisionFlags & COLLISION_FLAG_CHECK_ONLY) != 0; diff --git a/camellia-server/minecraft-patches/features/0057-Leaves-Optimized-dragon-respawn.patch b/camellia-server/minecraft-patches/features/0057-Leaves-Optimized-dragon-respawn.patch new file mode 100644 index 0000000..e2e32b1 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0057-Leaves-Optimized-dragon-respawn.patch @@ -0,0 +1,95 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Tue, 21 Apr 2026 23:52:23 +0800 +Subject: [PATCH] Leaves: Optimized dragon respawn + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As part of: Leaves (https://github.com/LeavesMC/Leaves/blob/4ade1001e4dd19c47d95c27f0b12df3175697f29/leaves-server/minecraft-patches/features/0049-Optimized-dragon-respawn.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/world/level/dimension/end/EnderDragonFight.java b/net/minecraft/world/level/dimension/end/EnderDragonFight.java +index 824ef3f458505569fe5b8efb2055ecea7ba6cc56..64a2fc0e49a6fc50773f6e0c40c2535a7533d41b 100644 +--- a/net/minecraft/world/level/dimension/end/EnderDragonFight.java ++++ b/net/minecraft/world/level/dimension/end/EnderDragonFight.java +@@ -317,7 +317,67 @@ public class EnderDragonFight extends SavedData { + return false; + } + ++ // Leaves start - optimizedDragonRespawn ++ private int cachePortalChunkIteratorX = -8; ++ private int cachePortalChunkIteratorZ = -8; ++ private int cachePortalOriginIteratorY = -1; ++ + public BlockPattern.@Nullable BlockPatternMatch findExitPortal() { ++ if (moe.monnairealms.camellia.config.modules.optimizations.OptimizedDragonRespawnConfig.optimizedRespawn) { ++ int i, j; ++ for (i = cachePortalChunkIteratorX; i <= 8; ++i) { ++ for (j = cachePortalChunkIteratorZ; j <= 8; ++j) { ++ LevelChunk worldChunk = this.level.getChunk(i, j); ++ for (BlockEntity blockEntity : worldChunk.getBlockEntities().values()) { ++ if (blockEntity instanceof net.minecraft.world.level.block.entity.TheEndGatewayBlockEntity) { ++ continue; ++ } ++ if (blockEntity instanceof TheEndPortalBlockEntity) { ++ BlockPattern.BlockPatternMatch blockPatternMatch = this.exitPortalPattern.find(this.level, blockEntity.getBlockPos()); ++ if (blockPatternMatch != null) { ++ BlockPos blockPos = blockPatternMatch.getBlock(3, 3, 3).getPos(); ++ if (this.exitPortalLocation == null) { ++ this.exitPortalLocation = blockPos; ++ } ++ //No need to judge whether optimizing option is open ++ cachePortalChunkIteratorX = i; ++ cachePortalChunkIteratorZ = j; ++ return blockPatternMatch; ++ } ++ } ++ } ++ } ++ } ++ ++ if (this.needsStateScanning || this.exitPortalLocation == null) { ++ if (cachePortalOriginIteratorY != -1) { ++ i = cachePortalOriginIteratorY; ++ } else { ++ i = this.level.getHeightmapPos(Heightmap.Types.MOTION_BLOCKING, EndPodiumFeature.getLocation(BlockPos.ZERO)).getY(); ++ } ++ boolean notFirstSearch = false; ++ for (j = i; j >= 0; --j) { ++ BlockPattern.BlockPatternMatch result2 = null; ++ if (notFirstSearch) { ++ result2 = org.leavesmc.leaves.util.BlockPatternHelper.partialSearchAround(this.exitPortalPattern, this.level, new BlockPos(EndPodiumFeature.getLocation(BlockPos.ZERO).getY(), j, EndPodiumFeature.getLocation(BlockPos.ZERO).getZ())); ++ } else { ++ result2 = this.exitPortalPattern.find(this.level, new BlockPos(EndPodiumFeature.getLocation(BlockPos.ZERO).getX(), j, EndPodiumFeature.getLocation(BlockPos.ZERO).getZ())); ++ } ++ if (result2 != null) { ++ if (this.exitPortalLocation == null) { ++ this.exitPortalLocation = result2.getBlock(3, 3, 3).getPos(); ++ } ++ cachePortalOriginIteratorY = j; ++ return result2; ++ } ++ notFirstSearch = true; ++ } ++ } ++ ++ return null; ++ } ++ // Leaves end - optimizedDragonRespawn ++ + ChunkPos chunkOrigin = ChunkPos.containing(this.origin); + + for (int x = -8 + chunkOrigin.x(); x <= 8 + chunkOrigin.x(); x++) { +@@ -623,8 +683,12 @@ public class EnderDragonFight extends SavedData { + } + return false; // CraftBukkit - return value + } +- + public boolean respawnDragon(final List crystals) { // CraftBukkit - return boolean ++ // Leaves - start optimizedDragonRespawn ++ cachePortalChunkIteratorX = -8; ++ cachePortalChunkIteratorZ = -8; ++ cachePortalOriginIteratorY = -1; ++ // Leaves - end optimizedDragonRespawn + if (this.dragonKilled && this.respawnStage == null) { + for (BlockPattern.BlockPatternMatch portal = this.findExitPortal(); portal != null; portal = this.findExitPortal()) { + for (int x = 0; x < this.exitPortalPattern.getWidth(); x++) { diff --git a/camellia-server/minecraft-patches/features/0058-Gale-Variable-entity-wake-up-duration.patch b/camellia-server/minecraft-patches/features/0058-Gale-Variable-entity-wake-up-duration.patch new file mode 100644 index 0000000..8c62f0d --- /dev/null +++ b/camellia-server/minecraft-patches/features/0058-Gale-Variable-entity-wake-up-duration.patch @@ -0,0 +1,57 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 12:22:30 +0800 +Subject: [PATCH] Gale: Variable entity wake-up duration + +Co-authored by: Martijn Muijsers +As part of: Gale (https://github.com/GaleMC/Gale/blob/276e903b2688f23b19bdc8d493c0bf87656d2400/patches/server/0054-Variable-entity-wake-up-duration.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java +index b4f9f41a56bbc40e90217c0344c070ebdd08c119..33cab0d9d144a37d144f18c6fff31afc82d3f8a9 100644 +--- a/io/papermc/paper/entity/activation/ActivationRange.java ++++ b/io/papermc/paper/entity/activation/ActivationRange.java +@@ -61,27 +61,39 @@ public final class ActivationRange { + if (entity.activationType == ActivationType.VILLAGER) { + if (inactiveFor > config.wakeUpInactiveVillagersEvery && worldData.wakeupInactiveRemainingVillagers > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingVillagers--; // Folia - threaded regions +- return config.wakeUpInactiveVillagersFor; ++ return getWakeUpDurationWithVariance(entity, config.wakeUpInactiveVillagersFor); // Gale - variable entity wake-up duration + } + } else if (entity.activationType == ActivationType.ANIMAL) { + if (inactiveFor > config.wakeUpInactiveAnimalsEvery && worldData.wakeupInactiveRemainingAnimals > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingAnimals--; // Folia - threaded regions +- return config.wakeUpInactiveAnimalsFor; ++ return getWakeUpDurationWithVariance(entity, config.wakeUpInactiveAnimalsFor); // Gale - variable entity wake-up duration + } + } else if (entity.activationType == ActivationType.FLYING_MONSTER) { + if (inactiveFor > config.wakeUpInactiveFlyingEvery && worldData.wakeupInactiveRemainingFlying > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingFlying--; // Folia - threaded regions +- return config.wakeUpInactiveFlyingFor; ++ return getWakeUpDurationWithVariance(entity, config.wakeUpInactiveFlyingFor); // Gale - variable entity wake-up duration + } + } else if (entity.activationType == ActivationType.MONSTER || entity.activationType == ActivationType.RAIDER) { + if (inactiveFor > config.wakeUpInactiveMonstersEvery && worldData.wakeupInactiveRemainingMonsters > 0) { // Folia - threaded regions + worldData.wakeupInactiveRemainingMonsters--; // Folia - threaded regions +- return config.wakeUpInactiveMonstersFor; ++ return getWakeUpDurationWithVariance(entity, config.wakeUpInactiveMonstersFor); // Gale - variable entity wake-up duration + } + } + return -1; + } + ++ // Gale start - variable entity wake-up duration ++ private static final java.util.concurrent.ThreadLocalRandom wakeUpDurationRandom = java.util.concurrent.ThreadLocalRandom.current(); ++ ++ private static int getWakeUpDurationWithVariance(Entity entity, int wakeUpDuration) { ++ double deviation = moe.monnairealms.camellia.config.modules.optimizations.GaleVariableEntityWakeupConfig.entityWakeUpDurationRatioStandardDeviation; ++ if (deviation <= 0) { ++ return wakeUpDuration; ++ } ++ return (int) Math.min(Integer.MAX_VALUE, Math.max(1, Math.round(wakeUpDuration * wakeUpDurationRandom.nextGaussian(1, deviation)))); ++ } ++ // Gale end - variable entity wake-up duration ++ + //static AABB maxBB = new AABB(0, 0, 0, 0, 0, 0); // Folia - threaded regions - replaced by local variable + + /** diff --git a/camellia-server/minecraft-patches/features/0059-Gale-Replace-AI-attributes-with-optimized-collection.patch b/camellia-server/minecraft-patches/features/0059-Gale-Replace-AI-attributes-with-optimized-collection.patch new file mode 100644 index 0000000..63f1987 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0059-Gale-Replace-AI-attributes-with-optimized-collection.patch @@ -0,0 +1,30 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 12:23:37 +0800 +Subject: [PATCH] Gale: Replace AI attributes with optimized collections + +Co-authored by: Martijn Muijsers +2No2Name <2No2Name@web.de> +As part of: Gale (https://github.com/GaleMC/Gale/blob/276e903b2688f23b19bdc8d493c0bf87656d2400/patches/server/0087-Replace-AI-attributes-with-optimized-collections.patch) +Lithium (https://github.com/CaffeineMC/lithium-fabric) +Licensed under: LGPL-3.0 (https://www.gnu.org/licenses/lgpl-3.0.html) + +diff --git a/net/minecraft/world/entity/ai/attributes/AttributeMap.java b/net/minecraft/world/entity/ai/attributes/AttributeMap.java +index 68bad6752bd313ca3ebde7fd4e3b2c46c5dee3ef..c951a04126a75c7caf046237ae00768a1114c03f 100644 +--- a/net/minecraft/world/entity/ai/attributes/AttributeMap.java ++++ b/net/minecraft/world/entity/ai/attributes/AttributeMap.java +@@ -14,9 +14,11 @@ import net.minecraft.resources.Identifier; + import org.jspecify.annotations.Nullable; + + public class AttributeMap { +- private final Map, AttributeInstance> attributes = new Object2ObjectOpenHashMap<>(); +- private final Set attributesToSync = new ObjectOpenHashSet<>(); +- private final Set attributesToUpdate = new ObjectOpenHashSet<>(); ++ // Gale start - Lithium - replace AI attributes with optimized collections ++ private final Map, AttributeInstance> attributes = new it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap<>(0); ++ private final Set attributesToSync = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(0); ++ private final Set attributesToUpdate = new it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet<>(0); ++ // Gale end - Lithium - replace AI attributes with optimized collections + private final AttributeSupplier supplier; + + public AttributeMap(final AttributeSupplier supplier) { diff --git a/camellia-server/minecraft-patches/features/0060-Gale-Skip-entity-move-if-movement-is-zero.patch b/camellia-server/minecraft-patches/features/0060-Gale-Skip-entity-move-if-movement-is-zero.patch new file mode 100644 index 0000000..5799a56 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0060-Gale-Skip-entity-move-if-movement-is-zero.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 12:24:51 +0800 +Subject: [PATCH] Gale: Skip entity move if movement is zero + +Co-authored by: Martijn Muijsers +ishland +A part of: Gale (https://github.com/GaleMC/Gale/blob/276e903b2688f23b19bdc8d493c0bf87656d2400/patches/server/0103-Skip-entity-move-if-movement-is-zero.patch) +VMP (https://github.com/RelativityMC/VMP-fabric) +Licensed under: MIT (https://opensource.org/licenses/MIT) + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 83914a091cc8fd6ef726f2732238b164bb590ea2..d66efc7015b00f35d949b952a920f42a957168fa 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -1152,8 +1152,14 @@ public abstract class Entity + private double moveStartY; + private double moveStartZ; + // Paper end - detailed watchdog information ++ private boolean boundingBoxChanged = false; // Gale - VMP - skip entity move if movement is zero + + public void move(final MoverType moverType, Vec3 delta) { ++ // Gale start - VMP - skip entity move if movement is zero ++ if (!this.boundingBoxChanged && delta.equals(Vec3.ZERO)) { ++ return; ++ } ++ // Gale end - VMP - skip entity move if movement is zero + final Vec3 originalMovement = delta; // Paper - Expose pre-collision velocity + // Paper start - detailed watchdog information + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot move an entity off-main"); +@@ -5447,6 +5453,11 @@ public abstract class Entity + } + + public final void setBoundingBox(final AABB bb) { ++ // Gale start - VMP - skip entity move if movement is zero ++ if (!this.bb.equals(bb)) { ++ this.boundingBoxChanged = true; ++ } ++ // Gale end - VMP - skip entity move if movement is zero + // CraftBukkit start - block invalid bounding boxes + double minX = bb.minX, + minY = bb.minY, diff --git a/camellia-server/minecraft-patches/features/0061-Gale-Store-mob-counts-in-an-array.patch b/camellia-server/minecraft-patches/features/0061-Gale-Store-mob-counts-in-an-array.patch new file mode 100644 index 0000000..272d37c --- /dev/null +++ b/camellia-server/minecraft-patches/features/0061-Gale-Store-mob-counts-in-an-array.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sun, 17 May 2026 08:47:31 +0800 +Subject: [PATCH] Gale Store mob counts in an array + +License: MIT (https://opensource.org/licenses/MIT) +Gale - https://github.com/GaleMC/Gale + +This patch is based on the following mixin: +"com/ishland/vmp/mixins/general/spawn_density_cap/MixinSpawnDensityCapperDensityCap.java" +By: ishland +As part of: VMP (https://github.com/RelativityMC/VMP-fabric) +Licensed under: MIT (https://opensource.org/licenses/MIT) + +diff --git a/net/minecraft/world/level/LocalMobCapCalculator.java b/net/minecraft/world/level/LocalMobCapCalculator.java +index 5b3808e6ff58d350fe3fd65fb56e8f209e1b2c93..150dfb4c8465dea0139bdbf804a84ce28e8d9023 100644 +--- a/net/minecraft/world/level/LocalMobCapCalculator.java ++++ b/net/minecraft/world/level/LocalMobCapCalculator.java +@@ -42,14 +42,14 @@ public class LocalMobCapCalculator { + } + + private static class MobCounts { +- private final Object2IntMap counts = new Object2IntOpenHashMap<>(MobCategory.values().length); ++ private final int[] counts = new int[MobCategory.values().length]; // Gale - VMP - store mob counts in an array + + public void add(final MobCategory category) { +- this.counts.computeInt(category, (k, count) -> count == null ? 1 : count + 1); ++ this.counts[category.ordinal()]++; // Gale - VMP - store mob counts in an array + } + + public boolean canSpawn(final MobCategory category) { +- return this.counts.getOrDefault(category, 0) < category.getMaxInstancesPerChunk(); ++ return this.counts[category.ordinal()] < category.getMaxInstancesPerChunk(); // Gale - VMP - store mob counts in an array + } + } + } diff --git a/camellia-server/minecraft-patches/features/0062-Gale-Optimize-random-calls-in-chunk-ticking.patch b/camellia-server/minecraft-patches/features/0062-Gale-Optimize-random-calls-in-chunk-ticking.patch new file mode 100644 index 0000000..b24fdd5 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0062-Gale-Optimize-random-calls-in-chunk-ticking.patch @@ -0,0 +1,113 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 4 Jun 2026 17:15:00 +0800 +Subject: [PATCH] Gale: Optimize random calls in chunk ticking + +License: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) +Gale - https://github.com/GaleMC/Gale + +This patch is based on the following patch: +"Optimize random calls in chunk ticking" +By: Paul Sauve +As part of: Airplane (https://github.com/TECHNOVE/Airplane) +Licensed under: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) + +The patch also received the following subsequent modification: +By: Kevin Raneri +As part of: Pufferfish (https://github.com/pufferfish-gg/Pufferfish) +Licensed under: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) + +* Description * + +Throttling of ice and snow tick has been moved to another patch as +configurable ice and snow tick chance. + +* Airplane description * + +Especially at over 30,000 chunks these random calls are fairly heavy. We +use a different method here for checking lightning, and for checking +ice. + +Lightning: Each chunk now keeps an int of how many ticks until the +lightning should strike. This int is a random number from 0 to 100000 * 2, +the multiplication is required to keep the probability the same. + +Ice and snow: We just generate a single random number 0-16 and increment +it, while checking if it's 0 for the current chunk. + +Depending on configuration for things that tick in a chunk, this is a +5-10% improvement. + +* Airplane copyright * + +Airplane +Copyright (C) 2020 Technove LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index fca5017a13148a928ae320ee99cddc45f617ffcd..c22f17a517254541b1ca627ec149cbe956962e61 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -1051,7 +1051,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + } + + // Paper start - optimise random ticking +- private final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource.INSTANCE; // Folia - region threading ++ public final io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource simpleRandom = io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource.INSTANCE; // Folia - region threading // Camellia - Make public for : Gale: Optimize random calls in chunk ticking + + private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { + final LevelChunkSection[] sections = chunk.getSections(); +@@ -1133,7 +1133,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + int minZ = chunkPos.getMinBlockZ(); + ProfilerFiller profiler = Profiler.get(); + profiler.push("thunder"); +- if (!this.paperConfig().environment.disableThunder && raining && this.isThundering() && this.spigotConfig.thunderChance > 0 && this.random.nextInt(this.spigotConfig.thunderChance) == 0) { // Spigot // Paper - Option to disable thunder ++ if (!this.paperConfig().environment.disableThunder && raining && this.isThundering() && this.spigotConfig.thunderChance > 0 /*&& this.random.nextInt(this.spigotConfig.thunderChance) == 0*/ && chunk.shouldDoLightning(this.random)) { // Spigot // Paper - Option to disable thunder // Gale - Airplane - optimize random calls in chunk ticking - replace random with shouldDoLightning + BlockPos pos = this.findLightningTargetAround(this.getBlockRandomPos(minX, 0, minZ, 15)); + if (this.isRainingAt(pos)) { + DifficultyInstance difficulty = this.getCurrentDifficultyAt(pos); +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 3cb6c24a21538569d73b95803dde7e4a2fdb67f5..6b41256e8f5112ed8608870342317ca7503c25d0 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -144,6 +144,19 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + } + // Paper end - get block chunk optimisation + ++ // Gale start - Airplane - optimize random calls in chunk ticking - instead of using a random every time the chunk is ticked, define when lightning strikes preemptively ++ private int lightningTick; ++ // shouldDoLightning compiles down to 29 bytes, which with the default of 35 byte inlining should guarantee an inline ++ public final boolean shouldDoLightning(net.minecraft.util.RandomSource random) { ++ if (this.lightningTick-- <= 0) { ++ this.lightningTick = random.nextInt(this.level.spigotConfig.thunderChance) << 1; ++ return true; ++ } ++ return false; ++ } ++ // Gale end - Airplane - optimize random calls in chunk ticking - instead of using a random every time the chunk is ticked, define when lightning strikes preemptively ++ ++ + public LevelChunk(final Level level, final ChunkPos pos) { + this(level, pos, UpgradeData.EMPTY, new LevelChunkTicks<>(), new LevelChunkTicks<>(), 0L, null, null, null); + } +@@ -180,6 +193,8 @@ public class LevelChunk extends ChunkAccess implements DebugValueSource, ca.spot + this.debug = !empty && this.level.isDebug(); + this.defaultBlockState = empty ? VOID_AIR_BLOCKSTATE : AIR_BLOCKSTATE; + // Paper end - get block chunk optimisation ++ ++ this.lightningTick = io.papermc.paper.threadedregions.util.SimpleThreadLocalRandomSource.INSTANCE.nextInt(100000) << 1; // Gale - Airplane - optimize random calls in chunk ticking - initialize lightning tick + } + + public LevelChunk(final ServerLevel level, final ProtoChunk protoChunk, final LevelChunk.@Nullable PostLoadProcessor postLoad) { diff --git a/camellia-server/minecraft-patches/features/0063-Gale-Reduce-enderman-teleport-chunk-lookups.patch b/camellia-server/minecraft-patches/features/0063-Gale-Reduce-enderman-teleport-chunk-lookups.patch new file mode 100644 index 0000000..cc780f6 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0063-Gale-Reduce-enderman-teleport-chunk-lookups.patch @@ -0,0 +1,58 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 4 Jun 2026 17:22:55 +0800 +Subject: [PATCH] Gale: Reduce enderman teleport chunk lookups + +License: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) +Gale - https://github.com/GaleMC/Gale + +This patch is based on the following patch: +"Reduce chunk loading & lookups" +By: Paul Sauve +As part of: Airplane (https://github.com/TECHNOVE/Airplane) +Licensed under: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) + +* Airplane copyright * + +Airplane +Copyright (C) 2020 Technove LLC + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program 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 General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . + +diff --git a/net/minecraft/world/entity/monster/EnderMan.java b/net/minecraft/world/entity/monster/EnderMan.java +index 9b4ae8afda1f2202761cb49bc4f1181ce4eb55d5..d2ac2a0c5bb18c2bca18e5166ed65afd01166e04 100644 +--- a/net/minecraft/world/entity/monster/EnderMan.java ++++ b/net/minecraft/world/entity/monster/EnderMan.java +@@ -298,11 +298,19 @@ public class EnderMan extends Monster implements NeutralMob { + private boolean teleport(final double x, final double y, final double z) { + BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(x, y, z); + +- while (pos.getY() > this.level().getMinY() && !this.level().getBlockState(pos).blocksMotion()) { ++ // Gale start - Airplane - single chunk lookup ++ net.minecraft.world.level.chunk.LevelChunk chunk = this.level().getChunkIfLoaded(pos); ++ ++ if (chunk == null) { ++ return false; ++ } ++ ++ while (pos.getY() > this.level().getMinY() && !chunk.getBlockState(pos).blocksMotion()) { ++ // Gale end - Airplane - single chunk lookup + pos.move(Direction.DOWN); + } + +- BlockState blockState = this.level().getBlockState(pos); ++ BlockState blockState = chunk.getBlockState(pos); // Gale - Airplane - single chunk lookup + boolean couldStandOn = blockState.blocksMotion(); + boolean isWet = blockState.getFluidState().is(FluidTags.WATER); + if (couldStandOn && !isWet) { diff --git a/camellia-server/minecraft-patches/features/0064-Gale-Cache-ShapePairKey-hash.patch b/camellia-server/minecraft-patches/features/0064-Gale-Cache-ShapePairKey-hash.patch new file mode 100644 index 0000000..1169ebc --- /dev/null +++ b/camellia-server/minecraft-patches/features/0064-Gale-Cache-ShapePairKey-hash.patch @@ -0,0 +1,45 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 4 Jun 2026 17:28:24 +0800 +Subject: [PATCH] Gale: Cache ShapePairKey hash + +License: LGPL-3.0-only (https://www.gnu.org/licenses/lgpl-3.0.html) +Gale - https://github.com/GaleMC/Gale + +The JMH benchmark of this patch can be found in SunBox's `RecordHashCode` + +diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java +index 423c45d7be8ac0c27fd3caa0aa8f15f25ad4a3a6..1af396a051cc47dccb973ca864cf1ec2e8b756ed 100644 +--- a/net/minecraft/world/level/block/Block.java ++++ b/net/minecraft/world/level/block/Block.java +@@ -696,7 +696,20 @@ public class Block extends BlockBehaviour implements ItemLike { + } + // CraftBukkit end + +- private record ShapePairKey(VoxelShape first, VoxelShape second) { ++ // Gale start - cache ShapePairKey hash ++ static class ShapePairKey { ++ ++ private final VoxelShape first; ++ private final VoxelShape second; ++ private final int hash; ++ ++ private ShapePairKey(VoxelShape first, VoxelShape second) { ++ this.first = first; ++ this.second = second; ++ this.hash = System.identityHashCode(this.first) * 31 + System.identityHashCode(this.second); ++ } ++ // Gale end - cache ShapePairKey hash ++ + @Override + public boolean equals(final Object o) { + return o instanceof Block.ShapePairKey that && this.first == that.first && this.second == that.second; +@@ -704,7 +717,7 @@ public class Block extends BlockBehaviour implements ItemLike { + + @Override + public int hashCode() { +- return System.identityHashCode(this.first) * 31 + System.identityHashCode(this.second); ++ return this.hash; // Gale - cache ShapePairKey hash + } + } + diff --git a/camellia-server/minecraft-patches/features/0065-Gale-For-collision-check-has-physics-before-same-veh.patch b/camellia-server/minecraft-patches/features/0065-Gale-For-collision-check-has-physics-before-same-veh.patch new file mode 100644 index 0000000..ef7febd --- /dev/null +++ b/camellia-server/minecraft-patches/features/0065-Gale-For-collision-check-has-physics-before-same-veh.patch @@ -0,0 +1,35 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 4 Jun 2026 17:30:18 +0800 +Subject: [PATCH] Gale: For collision check has physics before same vehicle +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +License: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) +Gale - https://github.com/GaleMC/Gale + +This patch is based on the following patch: +"Swaps the predicate order of collision" +By: ㄗㄠˋ ㄑㄧˊ +As part of: Akarin (https://github.com/Akarin-project/Akarin) +Licensed under: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index d66efc7015b00f35d949b952a920f42a957168fa..15c43ee830a8f7b38bb8a65f11d4485a64920450 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -2438,8 +2438,8 @@ public abstract class Entity + } + + public void push(final Entity entity) { ++ if (!entity.noPhysics && !this.noPhysics) { // Gale - Akarin - collision physics check before vehicle check + if (!this.isPassengerOfSameVehicle(entity)) { +- if (!entity.noPhysics && !this.noPhysics) { + if (this.level.paperConfig().collisions.onlyPlayersCollide && !(entity instanceof ServerPlayer || this instanceof ServerPlayer)) return; // Paper - Collision option for requiring a player participant + double xa = entity.getX() - this.getX(); + double za = entity.getZ() - this.getZ(); diff --git a/camellia-server/minecraft-patches/features/0066-Gale-Skip-negligible-planar-movement-multiplication.patch b/camellia-server/minecraft-patches/features/0066-Gale-Skip-negligible-planar-movement-multiplication.patch new file mode 100644 index 0000000..5356d0d --- /dev/null +++ b/camellia-server/minecraft-patches/features/0066-Gale-Skip-negligible-planar-movement-multiplication.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 4 Jun 2026 17:32:39 +0800 +Subject: [PATCH] Gale: Skip negligible planar movement multiplication + +License: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) +Gale - https://github.com/GaleMC/Gale + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 15c43ee830a8f7b38bb8a65f11d4485a64920450..de0858cb11b0dd4d0e35f37cced537a8940f8a4a 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -1299,8 +1299,17 @@ public abstract class Entity + } + } + +- float blockSpeedFactor = this.getBlockSpeedFactor(); +- this.setDeltaMovement(this.getDeltaMovement().multiply(blockSpeedFactor, 1.0, blockSpeedFactor)); ++ // Gale start - skip negligible planar movement multiplication ++ Vec3 oldDeltaMovement = this.getDeltaMovement(); ++ if (oldDeltaMovement.x < -1e-6 || oldDeltaMovement.x > 1e-6 || oldDeltaMovement.z < -1e-6 || oldDeltaMovement.z > 1e-6) { ++ // Gale end - skip negligible planar movement multiplication ++ float blockSpeedFactor = this.getBlockSpeedFactor(); ++ // Gale start - skip negligible planar movement multiplication ++ if (blockSpeedFactor < 1 - 1e-6 || blockSpeedFactor > 1 + 1e-6) { ++ this.setDeltaMovement(oldDeltaMovement.multiply(blockSpeedFactor, 1.0, blockSpeedFactor)); ++ } ++ } ++ // Gale end - skip negligible planar movement multiplication + profiler.pop(); + } + } diff --git a/camellia-server/minecraft-patches/features/0067-Gale-Optimize-matching-item-checks.patch b/camellia-server/minecraft-patches/features/0067-Gale-Optimize-matching-item-checks.patch new file mode 100644 index 0000000..f43a136 --- /dev/null +++ b/camellia-server/minecraft-patches/features/0067-Gale-Optimize-matching-item-checks.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 4 Jun 2026 17:34:43 +0800 +Subject: [PATCH] Gale: Optimize matching item checks + +License: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) +Gale - https://github.com/GaleMC/Gale + +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index 0bd2f2da669aa1a59d0af65aea3b305dd9be4476..b384ddb0eedb9286f27705554952a2eacc05d6db 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -841,11 +841,11 @@ public final class ItemStack implements DataComponentHolder, ItemInstance, Chang + } + + public static boolean isSameItem(final ItemStack a, final ItemStack b) { +- return a.is(b.getItem()); ++ return a == b || a.is(b.getItem()); // Gale - optimize identical item checks + } + + public static boolean isSameItemSameComponents(final ItemStack a, final ItemStack b) { +- return a.is(b.getItem()) && (a.isEmpty() && b.isEmpty() || Objects.equals(a.components, b.components)); ++ return a == b || a.is(b.getItem()) && (a.isEmpty() && b.isEmpty() || Objects.equals(a.components, b.components)); // Gale - optimize identical item checks + } + + public static boolean matchesIgnoringComponents(final ItemStack a, final ItemStack b, final Predicate> ignoredPredicate) { diff --git a/camellia-server/minecraft-patches/features/0068-Gale-Reduce-lambda-and-Optional-allocation-in-Entity.patch b/camellia-server/minecraft-patches/features/0068-Gale-Reduce-lambda-and-Optional-allocation-in-Entity.patch new file mode 100644 index 0000000..19ddafc --- /dev/null +++ b/camellia-server/minecraft-patches/features/0068-Gale-Reduce-lambda-and-Optional-allocation-in-Entity.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 20 Jun 2026 23:16:15 +0800 +Subject: [PATCH] Gale Reduce lambda and Optional allocation in + EntityBasedExplosionDamageCalculator + +License: LGPL-3.0-only (https://www.gnu.org/licenses/lgpl-3.0.html) +Gale - https://github.com/GaleMC/Gale + +This patch is based on the following mixin: +"net/caffeinemc/mods/lithium/mixin/alloc/explosion_behavior/EntityBasedExplosionDamageCalculatorMixin.java" +By: 2No2Name <2No2Name@web.de> +As part of: Lithium (https://github.com/CaffeineMC/lithium) +Licensed under: LGPL-3.0-only (https://www.gnu.org/licenses/lgpl-3.0.html) + +diff --git a/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java b/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java +index 356a3a0dda09af007c3fbddfe360b38f4d7204ce..aff8d67c9b9797402a3bf6c5bce54af19af0ade7 100644 +--- a/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java ++++ b/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java +@@ -17,8 +17,20 @@ public class EntityBasedExplosionDamageCalculator extends ExplosionDamageCalcula + public Optional getBlockExplosionResistance( + final Explosion explosion, final BlockGetter level, final BlockPos pos, final BlockState block, final FluidState fluid + ) { +- return super.getBlockExplosionResistance(explosion, level, pos, block, fluid) +- .map(resistance -> this.source.getBlockExplosionResistance(explosion, level, pos, block, fluid, resistance)); ++ // Gale start - Lithium - reduce lambda and Optional allocation in EntityBasedExplosionDamageCalculator ++ Optional optionalBlastResistance = super.getBlockExplosionResistance(explosion, level, pos, block, fluid); ++ ++ if (optionalBlastResistance.isPresent()) { ++ float resistance = optionalBlastResistance.get(); ++ float effectiveExplosionResistance = this.source.getBlockExplosionResistance(explosion, level, pos, block, fluid, resistance); ++ ++ if (effectiveExplosionResistance != resistance) { ++ return Optional.of(effectiveExplosionResistance); ++ } ++ } ++ ++ return optionalBlastResistance; ++ // Gale end - Lithium - reduce lambda and Optional allocation in EntityBasedExplosionDamageCalculator + } + + @Override diff --git a/camellia-server/minecraft-patches/features/0069-Gale-Reduce-array-allocations.patch b/camellia-server/minecraft-patches/features/0069-Gale-Reduce-array-allocations.patch new file mode 100644 index 0000000..3571e9a --- /dev/null +++ b/camellia-server/minecraft-patches/features/0069-Gale-Reduce-array-allocations.patch @@ -0,0 +1,441 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 22:45:55 +0800 +Subject: [PATCH] Gale Reduce array allocations + +License: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) +Gale - https://github.com/GaleMC/Gale + +Enum's values returns anew array copy of the enums, this behavior is defined in +`src/jdk.compiler/share/classes/com/sun/tools/javac/comp/Lower.java#visitEnumDef` + +This is a defensive programming strategy to prevent enums from being modified. However, +copying is unnecessary if we only have read calls. +So we can cache the values result to avoid useless allocations. + +Cached as the array since it does not create iterator on the enhanced for loop, +But the list does, and may spend more time than iterating using the array. + +One-time calls are excluded from this patch, since no need. + +The JMH benchmark of this patch can be found in SunBox's `CachedEnumValuesForLoop` + +This patch is based on the following patch: +"reduce allocs" +By: Simon Gardling +As part of: JettPack (https://gitlab.com/Titaniumtown/JettPack) +Licensed under: GPL-3.0-only (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +index 8d9ce3d301d5f7e4106587ae00adb8dd5b8b6f88..dd8a4e445928d93667702dedbd35ab3e0f94f3be 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +@@ -400,7 +400,6 @@ public final class ChunkEntitySlices { + + private static final class BasicEntityList { + +- private static final Entity[] EMPTY = new Entity[0]; + private static final int DEFAULT_CAPACITY = 4; + + private E[] storage; +@@ -411,7 +410,7 @@ public final class ChunkEntitySlices { + } + + public BasicEntityList(final int cap) { +- this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); ++ this.storage = (E[])(cap <= 0 ? me.titaniumtown.ArrayConstants.emptyEntityArray : new Entity[cap]);// Gale - JettPack - reduce array allocations + } + + public boolean isEmpty() { +@@ -423,7 +422,7 @@ public final class ChunkEntitySlices { + } + + private void resize() { +- if (this.storage == EMPTY) { ++ if (this.storage == me.titaniumtown.ArrayConstants.emptyEntityArray) { // Gale - JettPack - reduce array allocations + this.storage = (E[])new Entity[DEFAULT_CAPACITY]; + } else { + this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); +diff --git a/net/minecraft/nbt/ByteArrayTag.java b/net/minecraft/nbt/ByteArrayTag.java +index 4fc0cfeabf60d4bd0dcfd9814d13858454bc39da..0e4dbc0c736ec2ce4826a81ef561a4cd7a8b974d 100644 +--- a/net/minecraft/nbt/ByteArrayTag.java ++++ b/net/minecraft/nbt/ByteArrayTag.java +@@ -144,7 +144,7 @@ public final class ByteArrayTag implements CollectionTag { + + @Override + public void clear() { +- this.data = new byte[0]; ++ this.data = me.titaniumtown.ArrayConstants.emptyByteArray; // Gale - JettPack - reduce array allocations + } + + @Override +diff --git a/net/minecraft/nbt/IntArrayTag.java b/net/minecraft/nbt/IntArrayTag.java +index 8ed91b84eb90cfe31993fa32b35263e7adea918e..bf4fe7956ceb8c79427e4d89d458ed30ee1dcb54 100644 +--- a/net/minecraft/nbt/IntArrayTag.java ++++ b/net/minecraft/nbt/IntArrayTag.java +@@ -151,7 +151,7 @@ public final class IntArrayTag implements CollectionTag { + + @Override + public void clear() { +- this.data = new int[0]; ++ this.data = me.titaniumtown.ArrayConstants.emptyIntArray; // Gale - JettPack - reduce array allocations + } + + @Override +diff --git a/net/minecraft/nbt/LongArrayTag.java b/net/minecraft/nbt/LongArrayTag.java +index fe75d82cb43364ff58a531306bac934cbca4f729..5141d9765dc29f23a2a7f0918d2c76ddfe09fa6f 100644 +--- a/net/minecraft/nbt/LongArrayTag.java ++++ b/net/minecraft/nbt/LongArrayTag.java +@@ -150,7 +150,7 @@ public final class LongArrayTag implements CollectionTag { + + @Override + public void clear() { +- this.data = new long[0]; ++ this.data = me.titaniumtown.ArrayConstants.emptyLongArray; // Gale - JettPack - reduce array allocations + } + + @Override +diff --git a/net/minecraft/network/CipherBase.java b/net/minecraft/network/CipherBase.java +index 5a1fcd0a552dfcebff351c408a0403a8017930c1..1cdb3a6292133719b2e6b824ed9f78857f7b595e 100644 +--- a/net/minecraft/network/CipherBase.java ++++ b/net/minecraft/network/CipherBase.java +@@ -7,8 +7,8 @@ import javax.crypto.ShortBufferException; + + public class CipherBase { + private final Cipher cipher; +- private byte[] heapIn = new byte[0]; +- private byte[] heapOut = new byte[0]; ++ private byte[] heapIn = me.titaniumtown.ArrayConstants.emptyByteArray; // Gale - JettPack - reduce array allocations ++ private byte[] heapOut = me.titaniumtown.ArrayConstants.emptyByteArray; // Gale - JettPack - reduce array allocations + + protected CipherBase(final Cipher cipher) { + this.cipher = cipher; +diff --git a/net/minecraft/network/chat/contents/TranslatableContents.java b/net/minecraft/network/chat/contents/TranslatableContents.java +index b6973f9b48092676029ff58485ec8b8dece7387b..785f5c6f683ffdb73a70afabc42d6c09abb1e1d8 100644 +--- a/net/minecraft/network/chat/contents/TranslatableContents.java ++++ b/net/minecraft/network/chat/contents/TranslatableContents.java +@@ -28,7 +28,7 @@ import net.minecraft.util.ExtraCodecs; + import org.jspecify.annotations.Nullable; + + public class TranslatableContents implements ComponentContents { +- public static final Object[] NO_ARGS = new Object[0]; ++ public static final Object[] NO_ARGS = me.titaniumtown.ArrayConstants.emptyObjectArray; // Gale - JettPack - reduce array allocations + public static final Codec PRIMITIVE_ARG_CODEC = ExtraCodecs.JAVA.validate(TranslatableContents::filterAllowedArguments); + private static final Codec ARG_CODEC = Codec.either(PRIMITIVE_ARG_CODEC, ComponentSerialization.CODEC) + .xmap( +diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java +index 2e71a1cbabf3405e3e4fd5349a1784d895e7e60c..42de919064680b1a464d813976ca5e69a0bd3b01 100644 +--- a/net/minecraft/server/level/ServerEntity.java ++++ b/net/minecraft/server/level/ServerEntity.java +@@ -364,7 +364,7 @@ public class ServerEntity { + if (this.entity instanceof LivingEntity livingEntity) { + List> slots = Lists.newArrayList(); + +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + ItemStack itemStack = livingEntity.getItemBySlot(slot); + if (!itemStack.isEmpty()) { + slots.add(Pair.of(slot, itemStack.copy())); +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 336a1ae13810207696a699a936d4031eb904d0dd..82e8b3bfd1c30f6cd4a2af4b67c34e7d16c09cd2 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -1422,7 +1422,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + this.getInventory().getNonEquipmentItems().set(i, net.minecraft.world.item.ItemStack.EMPTY); + } + } +- for (final EquipmentSlot value : EquipmentSlot.VALUES) { ++ for (final EquipmentSlot value : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + if (this.getInventory().equipment.has(value) && !shouldKeepDeathEventItem(event, this.getInventory().equipment.get(value))) { + this.getInventory().equipment.set(value, net.minecraft.world.item.ItemStack.EMPTY); + } +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index ee4b6d293c32c00e6173cf8efe8aecce67574e6e..15bc120dad98d993f1c8a0a73dbd3be2de37ff72 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -3009,7 +3009,7 @@ public class ServerGamePacketListenerImpl + // SPIGOT-7136 - Allays + if (target instanceof net.minecraft.world.entity.animal.allay.Allay || target instanceof net.minecraft.world.entity.animal.equine.AbstractHorse) { // Paper - Fix horse armor desync + ServerGamePacketListenerImpl.this.send(new net.minecraft.network.protocol.game.ClientboundSetEquipmentPacket( +- target.getId(), java.util.Arrays.stream(net.minecraft.world.entity.EquipmentSlot.values()) ++ target.getId(), java.util.Arrays.stream(net.minecraft.world.entity.EquipmentSlot.VALUES_ARRAY) // Gale - JettPack - reduce array allocations + .map((slot) -> com.mojang.datafixers.util.Pair.of(slot, ((LivingEntity) target).getItemBySlot(slot).copy())) + .collect(Collectors.toList()), true)); // Paper - sanitize + player.containerMenu.sendAllDataToRemote(); +diff --git a/net/minecraft/server/players/StoredUserList.java b/net/minecraft/server/players/StoredUserList.java +index cf6c71db3dd39094608755998ceff6ca067238f3..1353fdfb68d8dbedafd64ac69d2869b22c6bfe09 100644 +--- a/net/minecraft/server/players/StoredUserList.java ++++ b/net/minecraft/server/players/StoredUserList.java +@@ -96,7 +96,7 @@ public abstract class StoredUserList> { + } + + public String[] getUserList() { +- return this.map.keySet().toArray(new String[0]); ++ return this.map.keySet().toArray(me.titaniumtown.ArrayConstants.emptyStringArray); // Gale - JettPack - reduce array allocations + } + + public boolean isEmpty() { +diff --git a/net/minecraft/util/NullOps.java b/net/minecraft/util/NullOps.java +index c7510c99c68c66a64d391b67d93f31cfcd3dccf0..5785ecf52bbd68e6df0f25b917722999addb6a20 100644 +--- a/net/minecraft/util/NullOps.java ++++ b/net/minecraft/util/NullOps.java +@@ -171,7 +171,7 @@ public class NullOps implements DynamicOps { + + @Override + public DataResult getByteBuffer(final Unit input) { +- return DataResult.success(ByteBuffer.wrap(new byte[0])); ++ return DataResult.success(ByteBuffer.wrap(me.titaniumtown.ArrayConstants.emptyByteArray)); // Gale - JettPack - reduce array allocations + } + + @Override +diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java +index 3666c3efd188508153b6482db425648e3ca77dc7..6c52fcac63fb2b79472ff10cb9f544f347a3fcdd 100644 +--- a/net/minecraft/util/ZeroBitStorage.java ++++ b/net/minecraft/util/ZeroBitStorage.java +@@ -5,7 +5,7 @@ import java.util.function.IntConsumer; + import org.apache.commons.lang3.Validate; + + public class ZeroBitStorage implements BitStorage { +- public static final long[] RAW = new long[0]; ++ public static final long[] RAW = me.titaniumtown.ArrayConstants.emptyLongArray; // Gale - JettPack - reduce array allocations + private final int size; + + public ZeroBitStorage(final int size) { +diff --git a/net/minecraft/world/entity/ConversionType.java b/net/minecraft/world/entity/ConversionType.java +index e5e268a1b2e8c1d728d39e66a53a5ad17fa6694f..59514750c3024d0f16512d2cd2c5b79f297c7beb 100644 +--- a/net/minecraft/world/entity/ConversionType.java ++++ b/net/minecraft/world/entity/ConversionType.java +@@ -37,7 +37,7 @@ public enum ConversionType { + } + + if (params.keepEquipment()) { +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + ItemStack itemStack = from.getItemBySlot(slot); + if (!itemStack.isEmpty()) { + to.setItemSlot(slot, itemStack.copyAndClear()); +diff --git a/net/minecraft/world/entity/EquipmentSlot.java b/net/minecraft/world/entity/EquipmentSlot.java +index ffb5f07697e9f1fb07f83ea4739c6b9d5a622892..63a9956f4aaf6feb6de2ef59a2211448f816e4fb 100644 +--- a/net/minecraft/world/entity/EquipmentSlot.java ++++ b/net/minecraft/world/entity/EquipmentSlot.java +@@ -20,6 +20,7 @@ public enum EquipmentSlot implements StringRepresentable { + SADDLE(EquipmentSlot.Type.SADDLE, 0, 1, 7, "saddle"); + + public static final int NO_COUNT_LIMIT = 0; ++ public static final EquipmentSlot[] VALUES_ARRAY = values(); // Gale - JettPack - reduce array allocations + public static final List VALUES = List.of(values()); + public static final IntFunction BY_ID = ByIdMap.continuous(s -> s.id, values(), ByIdMap.OutOfBoundsStrategy.ZERO); + public static final StringRepresentable.EnumCodec CODEC = StringRepresentable.fromEnum(EquipmentSlot::values); +diff --git a/net/minecraft/world/entity/EquipmentSlotGroup.java b/net/minecraft/world/entity/EquipmentSlotGroup.java +index e9b1290f24ba30663110abc61310d0a1e784e5a1..0112c19b99c62dde19af1009030c1645297163d9 100644 +--- a/net/minecraft/world/entity/EquipmentSlotGroup.java ++++ b/net/minecraft/world/entity/EquipmentSlotGroup.java +@@ -24,6 +24,7 @@ public enum EquipmentSlotGroup implements StringRepresentable, Iterable BY_ID = ByIdMap.continuous(s -> s.id, values(), ByIdMap.OutOfBoundsStrategy.ZERO); + public static final Codec CODEC = StringRepresentable.fromEnum(EquipmentSlotGroup::values); + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, s -> s.id); +diff --git a/net/minecraft/world/entity/EquipmentTable.java b/net/minecraft/world/entity/EquipmentTable.java +index 4b018c64d3123bb7a0687491d2ac3a535302890c..0e4d6c9a35d29c240a0cf30694211ea3db66895c 100644 +--- a/net/minecraft/world/entity/EquipmentTable.java ++++ b/net/minecraft/world/entity/EquipmentTable.java +@@ -35,7 +35,7 @@ public record EquipmentTable(ResourceKey lootTable, Map createForAllSlots(final float dropChance) { +- return createForAllSlots(List.of(EquipmentSlot.values()), dropChance); ++ return createForAllSlots(List.of(EquipmentSlot.VALUES_ARRAY), dropChance); // Gale - JettPack - reduce array allocations + } + + private static Map createForAllSlots(final List slots, final float dropChance) { +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index 1cb8265c1313ad4893d45c344b03007fc4e2de4b..fcaeb471dada57e9ba4d224693cefbff2fa631be 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -3609,7 +3609,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin + Map equipmentChanges = null; + // Paper end - EntityEquipmentChangedEvent + +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + ItemStack previous = lastEquipmentItems.get(slot); + ItemStack current = this.getItemBySlot(slot); + if (this.equipmentHasChanged(previous, current)) { +@@ -3892,7 +3892,7 @@ public abstract class LivingEntity extends Entity implements Attackable, Waypoin + + protected boolean canGlide() { + if (!this.onGround() && !this.isPassenger() && !this.hasEffect(MobEffects.LEVITATION)) { +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + if (canGlideUsing(this.getItemBySlot(slot), slot)) { + return true; + } +diff --git a/net/minecraft/world/entity/decoration/ArmorStand.java b/net/minecraft/world/entity/decoration/ArmorStand.java +index 9e4293c6df1851a852423b469577388d2254e169..952b09fba4cf7de90110a7aa4b96dc03afab25e1 100644 +--- a/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -484,7 +484,7 @@ public class ArmorStand extends LivingEntity { + if (this.deathDropItems == null) this.deathDropItems = new java.util.ArrayList<>(); // Paper + this.dropAllDeathLoot(level, source); + +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + this.postDeathEventTasks.add(() -> this.equipment.set(slot, ItemStack.EMPTY)); // Paper - move equipment removal past event call + ItemStack itemStack = this.equipment.get(slot); // Paper + if (!itemStack.isEmpty() && !EnchantmentHelper.has(itemStack, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) { +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index ab0f42c6ed9ba4f920eefba4d1449ded6dda0fe9..df412aa8c29307b6186c1c9435cb589c8ed2da22 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -346,7 +346,7 @@ public abstract class Player extends Avatar implements ContainerUser { + } + + private boolean isEquipped(final Item item) { +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + ItemStack itemStack = this.getItemBySlot(slot); + Equippable equippable = itemStack.get(DataComponents.EQUIPPABLE); + if (itemStack.is(item) && equippable != null && equippable.slot() == slot) { +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index b384ddb0eedb9286f27705554952a2eacc05d6db..9445551c6c4b67c9c278b541ca2c9b01ab19096a 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -1182,7 +1182,7 @@ public final class ItemStack implements DataComponentHolder, ItemInstance, Chang + + private void addAttributeTooltips(final Consumer consumer, final TooltipDisplay display, final @Nullable Player player) { + if (display.shows(DataComponents.ATTRIBUTE_MODIFIERS)) { +- for (EquipmentSlotGroup slot : EquipmentSlotGroup.values()) { ++ for (EquipmentSlotGroup slot : EquipmentSlotGroup.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + MutableBoolean first = new MutableBoolean(true); + this.forEachModifier(slot, (attribute, modifier, tooltip) -> { + if (tooltip != ItemAttributeModifiers.Display.hidden()) { +diff --git a/net/minecraft/world/item/crafting/ShapedRecipePattern.java b/net/minecraft/world/item/crafting/ShapedRecipePattern.java +index b2ca30b84a6f498a2b18145eb1efaea7c4c5c943..f60e6188d25c13fc433931b0e8760869ccee8fb0 100644 +--- a/net/minecraft/world/item/crafting/ShapedRecipePattern.java ++++ b/net/minecraft/world/item/crafting/ShapedRecipePattern.java +@@ -121,7 +121,7 @@ public final class ShapedRecipePattern { + } + + if (pattern.size() == bottom) { +- return new String[0]; ++ return me.titaniumtown.ArrayConstants.emptyStringArray; // Gale - JettPack - reduce array allocations + } + + String[] result = new String[pattern.size() - bottom - top]; +diff --git a/net/minecraft/world/item/enchantment/Enchantment.java b/net/minecraft/world/item/enchantment/Enchantment.java +index 0d981b86fa7ee8713e2a419fc2f377defbc2ed9c..26d850a0ee40c5042e99689f8bd67d8859c42d40 100644 +--- a/net/minecraft/world/item/enchantment/Enchantment.java ++++ b/net/minecraft/world/item/enchantment/Enchantment.java +@@ -107,7 +107,7 @@ public record Enchantment(Component description, Enchantment.EnchantmentDefiniti + public Map getSlotItems(final LivingEntity entity) { + Map itemStacks = Maps.newEnumMap(EquipmentSlot.class); + +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + if (this.matchingSlot(slot)) { + ItemStack itemStack = entity.getItemBySlot(slot); + if (!itemStack.isEmpty()) { +diff --git a/net/minecraft/world/item/enchantment/EnchantmentHelper.java b/net/minecraft/world/item/enchantment/EnchantmentHelper.java +index ed84782f4fcaec8f12ab68641a42ad6e09079875..4f2017c4ca12dcedb9587e0d3d3c9a6be4996167 100644 +--- a/net/minecraft/world/item/enchantment/EnchantmentHelper.java ++++ b/net/minecraft/world/item/enchantment/EnchantmentHelper.java +@@ -157,7 +157,7 @@ public class EnchantmentHelper { + } + + private static void runIterationOnEquipment(final LivingEntity owner, final EnchantmentHelper.EnchantmentInSlotVisitor method) { +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + runIterationOnItem(owner.getItemBySlot(slot), slot, owner, method); + } + } +@@ -495,7 +495,7 @@ public class EnchantmentHelper { + ) { + List items = new ArrayList<>(); + +- for (EquipmentSlot slot : EquipmentSlot.VALUES) { ++ for (EquipmentSlot slot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + ItemStack item = source.getItemBySlot(slot); + if (predicate.test(item)) { + ItemEnchantments enchantments = item.getOrDefault(DataComponents.ENCHANTMENTS, ItemEnchantments.EMPTY); +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index a6d1f8ccf2e33394508d8d14bec2141942e31ccd..582796847afc202bca408cd6c3135462ecf432a5 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1966,7 +1966,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + public org.bukkit.entity.Entity[] getChunkEntities(int chunkX, int chunkZ) { + ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices slices = ((ServerLevel)this).moonrise$getEntityLookup().getChunk(chunkX, chunkZ); + if (slices == null) { +- return new org.bukkit.entity.Entity[0]; ++ return me.titaniumtown.ArrayConstants.emptyBukkitEntityArray; // Gale - JettPack - reduce array allocations + } + + List ret = new java.util.ArrayList<>(); +@@ -1977,7 +1977,7 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl + } + } + +- return ret.toArray(new org.bukkit.entity.Entity[0]); ++ return ret.toArray(me.titaniumtown.ArrayConstants.emptyBukkitEntityArray); // Gale - JettPack - reduce array allocations + } + // Paper end - rewrite chunk system + +diff --git a/net/minecraft/world/level/block/ComposterBlock.java b/net/minecraft/world/level/block/ComposterBlock.java +index 82f041a6c72eb09d365b5f83dd4731467ad49563..d7a64b74dd3eed07f641cfe6d758a3eb0fb6ef5b 100644 +--- a/net/minecraft/world/level/block/ComposterBlock.java ++++ b/net/minecraft/world/level/block/ComposterBlock.java +@@ -434,7 +434,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + + @Override + public int[] getSlotsForFace(final Direction direction) { +- return new int[0]; ++ return me.titaniumtown.ArrayConstants.emptyIntArray; // Gale - JettPack - reduce array allocations + } + + @Override +@@ -469,7 +469,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + + @Override + public int[] getSlotsForFace(final Direction direction) { +- return direction == Direction.UP ? new int[]{0} : new int[0]; ++ return direction == Direction.UP ? me.titaniumtown.ArrayConstants.zeroSingletonIntArray : me.titaniumtown.ArrayConstants.emptyIntArray; // Gale - JettPack - reduce array allocations + } + + @Override +@@ -521,7 +521,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + + @Override + public int[] getSlotsForFace(final Direction direction) { +- return direction == Direction.DOWN ? new int[]{0} : new int[0]; ++ return direction == Direction.DOWN ? me.titaniumtown.ArrayConstants.zeroSingletonIntArray : me.titaniumtown.ArrayConstants.emptyIntArray; // Gale - JettPack - reduce array allocations + } + + @Override +diff --git a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index 601471d7578b93f30d8033bfab4d4050a8ef78f8..abf73b5f5f780cb3ba947da4067c5ded1a53699a 100644 +--- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -44,7 +44,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + protected static final int SLOT_FUEL = 1; + protected static final int SLOT_RESULT = 2; + public static final int DATA_LIT_TIME = 0; +- private static final int[] SLOTS_FOR_UP = new int[]{0}; ++ private static final int[] SLOTS_FOR_UP = me.titaniumtown.ArrayConstants.zeroSingletonIntArray; // Gale - JettPack - reduce array allocations + private static final int[] SLOTS_FOR_DOWN = new int[]{2, 1}; + private static final int[] SLOTS_FOR_SIDES = new int[]{1}; + public static final int DATA_LIT_DURATION = 1; +diff --git a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +index e00caf8dc68ce49f4011f881f3558347fe848822..a9201522bdca5a6e6aae48624d3f95fcb0e3d941 100644 +--- a/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java ++++ b/net/minecraft/world/level/saveddata/maps/MapItemSavedData.java +@@ -268,7 +268,7 @@ public class MapItemSavedData extends SavedData { + } + + private static boolean hasMapInvisibilityItemEquipped(final Player player) { +- for (EquipmentSlot equipmentSlot : EquipmentSlot.values()) { ++ for (EquipmentSlot equipmentSlot : EquipmentSlot.VALUES_ARRAY) { // Gale - JettPack - reduce array allocations + if (equipmentSlot != EquipmentSlot.MAINHAND + && equipmentSlot != EquipmentSlot.OFFHAND + && player.getItemBySlot(equipmentSlot).is(ItemTags.MAP_INVISIBILITY_EQUIPMENT)) { diff --git a/camellia-server/minecraft-patches/features/0070-Some-optimizations-from-krypton.patch b/camellia-server/minecraft-patches/features/0070-Some-optimizations-from-krypton.patch new file mode 100644 index 0000000..30795af --- /dev/null +++ b/camellia-server/minecraft-patches/features/0070-Some-optimizations-from-krypton.patch @@ -0,0 +1,80 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 1 May 2026 21:51:11 +0800 +Subject: [PATCH] Some optimizations from krypton + +A part of krypton's mixin: + +https://github.com/astei/krypton/blob/master/src/main/java/me/steinborn/krypton/mixin/shared/network/microopt/StringEncodingMixin.java and https://github.com/astei/krypton/blob/master/src/main/java/me/steinborn/krypton/mixin/shared/network/microopt/VarIntsMixin.java + +Original project license: https://github.com/astei/krypton/blob/master/LICENSE + +diff --git a/net/minecraft/network/Utf8String.java b/net/minecraft/network/Utf8String.java +index 8b778744a5f01943976f180ad340a1cbce7db506..f3e514d801ab14d3baaf1943b7cbca1b2a26492b 100644 +--- a/net/minecraft/network/Utf8String.java ++++ b/net/minecraft/network/Utf8String.java +@@ -33,6 +33,22 @@ public class Utf8String { + } + + public static void write(final ByteBuf output, final CharSequence value, final int maxLength) { ++ // Camellia start - Krypton optimizations ++ if (true) { ++ if (value.length() > maxLength) { ++ throw new EncoderException("String too big (was " + value.length() + " characters, max " + maxLength + ")"); ++ } ++ int utf8Bytes = ByteBufUtil.utf8Bytes(value); ++ int maxBytesPermitted = ByteBufUtil.utf8MaxBytes(maxLength); ++ if (utf8Bytes > maxBytesPermitted) { ++ throw new EncoderException("String too big (was " + utf8Bytes + " bytes encoded, max " + maxBytesPermitted + ")"); ++ } else { ++ VarInt.write(output, utf8Bytes); ++ output.writeCharSequence(value, StandardCharsets.UTF_8); ++ } ++ return; ++ } ++ // Camellia end + if (value.length() > maxLength) { + throw new EncoderException("String too big (was " + value.length() + " characters, max " + maxLength + ")"); + } +diff --git a/net/minecraft/network/VarInt.java b/net/minecraft/network/VarInt.java +index 1428ce80e8316a24cfca7b6aafdea8588a5a790e..740182c0b1c6918e81a45dddba1c4f488b8762b3 100644 +--- a/net/minecraft/network/VarInt.java ++++ b/net/minecraft/network/VarInt.java +@@ -60,7 +60,8 @@ public class VarInt { + int s = (value & 0x7F | 0x80) << 8 | (value >>> 7); + output.writeShort(s); + } else { +- writeSlow(output, value); ++ // writeSlow(output, value); // Camellia - Krypton optimizations ++ writeVarIntFull(output, value); // Camellia - Krypton optimizations + } + return output; + } +@@ -74,4 +75,27 @@ public class VarInt { + output.writeByte(value); + return output; + } ++ // Camellia start - Krypton optimizations ++ private static void writeVarIntFull(ByteBuf buf, int value) { ++ // See https://steinborn.me/posts/performance/how-fast-can-you-write-a-varint/ ++ if ((value & (0xFFFFFFFF << 7)) == 0) { ++ buf.writeByte(value); ++ } else if ((value & (0xFFFFFFFF << 14)) == 0) { ++ int w = (value & 0x7F | 0x80) << 8 | (value >>> 7); ++ buf.writeShort(w); ++ } else if ((value & (0xFFFFFFFF << 21)) == 0) { ++ int w = (value & 0x7F | 0x80) << 16 | ((value >>> 7) & 0x7F | 0x80) << 8 | (value >>> 14); ++ buf.writeMedium(w); ++ } else if ((value & (0xFFFFFFFF << 28)) == 0) { ++ int w = (value & 0x7F | 0x80) << 24 | (((value >>> 7) & 0x7F | 0x80) << 16) ++ | ((value >>> 14) & 0x7F | 0x80) << 8 | (value >>> 21); ++ buf.writeInt(w); ++ } else { ++ int w = (value & 0x7F | 0x80) << 24 | ((value >>> 7) & 0x7F | 0x80) << 16 ++ | ((value >>> 14) & 0x7F | 0x80) << 8 | ((value >>> 21) & 0x7F | 0x80); ++ buf.writeInt(w); ++ buf.writeByte(value >>> 28); ++ } ++ } ++ // Camellia end + } diff --git a/camellia-server/paper-patches/features/0001-Rebrand-to-Camellia.patch b/camellia-server/paper-patches/features/0001-Rebrand-to-Camellia.patch new file mode 100644 index 0000000..ca66349 --- /dev/null +++ b/camellia-server/paper-patches/features/0001-Rebrand-to-Camellia.patch @@ -0,0 +1,115 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 18:15:19 +0800 +Subject: [PATCH] Rebrand to Camellia + + +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 08745a35b1ce8cc56d970472088de3cd17b9e43a..287cff1f4d994584457f00b1e42f283e92585033 100644 +--- a/src/main/java/com/destroystokyo/paper/Metrics.java ++++ b/src/main/java/com/destroystokyo/paper/Metrics.java +@@ -593,7 +593,7 @@ public class Metrics { + boolean logFailedRequests = config.getBoolean("logFailedRequests", false); + // Only start Metrics, if it's enabled in the config + if (config.getBoolean("enabled", true)) { +- Metrics metrics = new Metrics("Folia", serverUUID, logFailedRequests, Bukkit.getLogger()); // Folia - we have our own bstats page ++ Metrics metrics = new Metrics("Camellia", serverUUID, logFailedRequests, Bukkit.getLogger()); // Folia - we have our own bstats page // Camellia - Rebrand to Camellia + + metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { + String minecraftVersion = Bukkit.getVersion(); +@@ -607,11 +607,11 @@ public class Metrics { + final String implVersion = org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion(); + if (implVersion != null) { + final String buildOrHash = implVersion.substring(implVersion.lastIndexOf('-') + 1); +- paperVersion = "git-Folia-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); // Folia - we have our own bstats page ++ paperVersion = "git-Camellia-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); // Folia - we have our own bstats page // Camellia - Rebrand to Camellia + } else { + paperVersion = "unknown"; + } +- metrics.addCustomChart(new Metrics.SimplePie("folia_version", () -> paperVersion)); // Folia - we have our own bstats page ++ metrics.addCustomChart(new Metrics.SimplePie("camellia_version", () -> paperVersion)); // Folia - we have our own bstats page // Camellia - Rebrand to Camellia + + metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { + Map> map = new HashMap<>(); +diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +index 30815c8970b1f1c88a3c7bb5fd36efd915f1687d..05f159bb5a8499f8957367ed11a9f11035de418a 100644 +--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +@@ -40,8 +40,8 @@ public class PaperVersionFetcher implements VersionFetcher { + private static final ComponentLogger COMPONENT_LOGGER = ComponentLogger.logger(LogManager.getRootLogger().getName()); + private static final int DISTANCE_ERROR = -1; + private static final int DISTANCE_UNKNOWN = -2; +- private static final String DOWNLOAD_PAGE = "https://papermc.io/downloads/folia"; // Folia +- private static final String REPOSITORY = "PaperMC/Folia"; // Folia ++ private static final String DOWNLOAD_PAGE = "https://github.com/MonnaiRealms/Camellia"; // Folia // Camellia - Rebrand to Camellia ++ private static final String REPOSITORY = "MonnaiRealms/Camellia"; // Folia // Camellia - Rebrand to Camellia + private static final ServerBuildInfo BUILD_INFO = ServerBuildInfo.buildInfo(); + private static final String USER_AGENT = BUILD_INFO.brandName() + "/" + BUILD_INFO.asString(VERSION_SIMPLE) + " (https://papermc.io)"; + private static final Gson GSON = new Gson(); +@@ -72,7 +72,7 @@ public class PaperVersionFetcher implements VersionFetcher { + COMPONENT_LOGGER.warn(text("*** You are running a development version without access to version information ***")); + } else { + final Optional apiResult = fetchMinecraftVersionList(); +- if (buildNumber.isPresent()) { ++ if (buildNumber.isPresent() && false) { // Camellia - Rebrand to Camellia - we do not have a version manager api currently, disable it + distance = fetchDistanceFromSiteApi(buildNumber.getAsInt()); + } else { + final Optional gitBranch = BUILD_INFO.gitBranch(); +@@ -111,7 +111,7 @@ public class PaperVersionFetcher implements VersionFetcher { + int distance = DISTANCE_ERROR; + + final OptionalInt buildNumber = PaperVersionFetcher.BUILD_INFO.buildNumber(); +- if (buildNumber.isPresent()) { ++ if (buildNumber.isPresent() && false) { // Camellia - Rebrand to Camellia - we do not have a version manager api currently, disable it + distance = fetchDistanceFromSiteApi(buildNumber.getAsInt()); + } else { + final Optional gitBranch = PaperVersionFetcher.BUILD_INFO.gitBranch(); +diff --git a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java +index 10a0be7a4db1a51579d113d279af7a9effe7f438..45cf7c5c9a4de2c7422e4d9c27fa0064f81f11cf 100644 +--- a/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java ++++ b/src/main/java/io/papermc/paper/ServerBuildInfoImpl.java +@@ -31,6 +31,7 @@ public record ServerBuildInfoImpl( + private static final String ATTRIBUTE_GIT_COMMIT = "Git-Commit"; + + private static final String BRAND_PAPER_NAME = "Paper"; ++ private static final String BRAND_CAMELLIA_NAME = "Camellia"; // Camellia - Rebrand to Camellia + + private static final String BUILD_DEV = "DEV"; + +@@ -42,9 +43,9 @@ public record ServerBuildInfoImpl( + this( + getManifestAttribute(manifest, ATTRIBUTE_BRAND_ID) + .map(Key::key) +- .orElse(Key.key("papermc", "folia")), // Folia ++ .orElse(BRAND_CAMELLIA_ID), // Folia // Camellia - Rebrand to Camellia + getManifestAttribute(manifest, ATTRIBUTE_BRAND_NAME) +- .orElse("Folia"), // Folia ++ .orElse(BRAND_CAMELLIA_NAME), // Folia // Camellia - Rebrand to Camellia + SharedConstants.getCurrentVersion().id(), + SharedConstants.getCurrentVersion().name(), + getManifestAttribute(manifest, ATTRIBUTE_BUILD_NUMBER) +@@ -61,7 +62,7 @@ public record ServerBuildInfoImpl( + + @Override + public boolean isBrandCompatible(final @NotNull Key brandId) { +- return brandId.equals(this.brandId); ++ return brandId.equals(this.brandId) || brandId.equals(BRAND_PAPER_ID) || brandId.equals(BRAND_FOLIA_ID); // Camellia - Rebrand to Camellia + } + + @Override +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index 5607f4cc8ca2f620461eb5e97d01d9db0d6b2303..5a86fa68007bc00fcf3ceda2d24923227410c42e 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -80,9 +80,9 @@ public class WatchdogThread extends ca.spottedleaf.moonrise.common.util.TickThre + logger.log(Level.SEVERE, "\t *Especially* if it looks like HTTP or MySQL operations are occurring"); + logger.log(Level.SEVERE, "If you see a world save or edit, then it means you did far more than your server can handle at once"); + logger.log(Level.SEVERE, "\t If this is the case, consider increasing timeout-time in spigot.yml but note that this will replace the crash with LARGE lag spikes"); +- logger.log(Level.SEVERE, "If you are unsure or still think this is a Paper bug, please report this to https://github.com/PaperMC/Paper/issues"); ++ logger.log(Level.SEVERE, "If you are unsure or still think this is a Camellia bug, please report this to https://github.com/MonnaiRealms/Camellia/issues"); // Camellia - Rebrand to Camellia + logger.log(Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports"); +- logger.log(Level.SEVERE, "Paper version: " + Bukkit.getServer().getVersion()); ++ logger.log(Level.SEVERE, "Camellia version: " + Bukkit.getServer().getVersion()); // Camellia - Rebrand to Camellia + + if (net.minecraft.world.level.Level.lastPhysicsProblem != null) { + logger.log(Level.SEVERE, "------------------------------"); diff --git a/camellia-server/paper-patches/features/0002-Correct-thread-unsafe-random-sources.patch b/camellia-server/paper-patches/features/0002-Correct-thread-unsafe-random-sources.patch new file mode 100644 index 0000000..43025bc --- /dev/null +++ b/camellia-server/paper-patches/features/0002-Correct-thread-unsafe-random-sources.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 20:59:03 +0800 +Subject: [PATCH] Correct thread unsafe random sources + + +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java +index 70e21e1fc337c6ea40e64020fa1832d1630cae97..f4af14dfd277fa0261a6e0623f8d11f266e6d7c0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftItemFactory.java +@@ -40,7 +40,7 @@ import org.bukkit.inventory.meta.ItemMeta; + public final class CraftItemFactory implements ItemFactory { + static final Color DEFAULT_LEATHER_COLOR = Color.fromRGB(0xA06540); + private static final CraftItemFactory instance; +- private static final RandomSource randomSource = RandomSource.create(); ++ private static final RandomSource randomSource = io.papermc.paper.threadedregions.util.ThreadLocalRandomSource.INSTANCE; // Camellia - Correct thread unsafe random sources + + static { + instance = new CraftItemFactory(); diff --git a/camellia-server/paper-patches/features/0003-Force-disable-reload-command-and-api.patch b/camellia-server/paper-patches/features/0003-Force-disable-reload-command-and-api.patch new file mode 100644 index 0000000..6ee3c63 --- /dev/null +++ b/camellia-server/paper-patches/features/0003-Force-disable-reload-command-and-api.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 09:53:07 +0800 +Subject: [PATCH] Force disable reload command and api + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 9d0dc8815842a0583b212891680bd0c6461c30b9..9968f5064571b0917c8897c23fe06a148c63a305 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -981,6 +981,7 @@ public final class CraftServer implements Server { + + @Override + public void reload() { ++ if (true) throw new UnsupportedOperationException(); // Camellia - Force disable reload command and api + // Paper start - lifecycle events + if (io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.blocksPluginReloading()) { + throw new IllegalStateException(org.bukkit.command.defaults.ReloadCommand.RELOADING_DISABLED_MESSAGE); diff --git a/camellia-server/paper-patches/features/0004-Force-disable-builtin-spark-plugin.patch b/camellia-server/paper-patches/features/0004-Force-disable-builtin-spark-plugin.patch new file mode 100644 index 0000000..7051a4c --- /dev/null +++ b/camellia-server/paper-patches/features/0004-Force-disable-builtin-spark-plugin.patch @@ -0,0 +1,59 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 23:45:12 +0800 +Subject: [PATCH] Force disable builtin spark plugin + + +diff --git a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java +index 9661f033bbb2c00073f1c891e1411c2a9943a37a..70fccec55edb67c24c7ecbf3687682de41c73eed 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/source/FileProviderSource.java +@@ -76,7 +76,7 @@ public class FileProviderSource implements ProviderSource { + } + + final PluginMeta config = type.getConfig(file); +- if ((config.getName().equals("spark") && config.getMainClass().equals("me.lucko.spark.bukkit.BukkitSparkPlugin")) && !SparksFly.isPluginPreferred()) { ++ if ((config.getName().equals("spark") && config.getMainClass().equals("me.lucko.spark.bukkit.BukkitSparkPlugin")) && !SparksFly.isPluginPreferred() && false) { // Camellia - Force disable builtin spark + LOGGER.info("The spark plugin will not be loaded as this server bundles the spark profiler."); + return; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 9968f5064571b0917c8897c23fe06a148c63a305..3c46e2cc23ac57df52996e3d2c5ae416a9633a67 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -302,7 +302,7 @@ public final class CraftServer implements Server { + public static Exception excessiveVelEx; + private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); + private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; +- public final io.papermc.paper.SparksFly spark; ++ public io.papermc.paper.SparksFly spark; // Paper - spark // Camellia - Force disable builtin spark + private final ServerConfiguration serverConfig = new PaperServerConfiguration(); + + // Paper start - Folia region threading API +@@ -478,7 +478,7 @@ public final class CraftServer implements Server { + } + this.potionBrewer = new io.papermc.paper.potion.PaperPotionBrewer(console); // Paper - custom potion mixes + datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper +- this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark ++ if (false) this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark // Camellia - Force disable builtin spark + } + + public boolean getCommandBlockOverride(String command) { +@@ -1050,7 +1050,7 @@ public final class CraftServer implements Server { + this.reloadData(); + org.spigotmc.SpigotConfig.registerCommands(); // Spigot + io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper +- this.spark.registerCommandBeforePlugins(this); // Paper - spark ++ if (false) this.spark.registerCommandBeforePlugins(this); // Paper - spark // Camellia - Force disable builtin spark + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); + +@@ -1079,7 +1079,7 @@ public final class CraftServer implements Server { + this.loadPlugins(); + this.enablePlugins(PluginLoadOrder.STARTUP); + this.enablePlugins(PluginLoadOrder.POSTWORLD); +- this.spark.registerCommandAfterPlugins(this); // Paper - spark ++ if (false) this.spark.registerCommandAfterPlugins(this); // Paper - spark // Camellia - Force disable builtin spark + // Paper start - brigadier command API + io.papermc.paper.command.brigadier.PaperCommands.INSTANCE.setValid(); // to clear invalid state for event fire below + io.papermc.paper.plugin.lifecycle.event.LifecycleEventRunner.INSTANCE.callReloadableRegistrarEvent(io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents.COMMANDS, io.papermc.paper.command.brigadier.PaperCommands.INSTANCE, org.bukkit.plugin.Plugin.class, io.papermc.paper.plugin.lifecycle.event.registrar.ReloadableRegistrarEvent.Cause.RELOAD); // call commands event for regular plugins diff --git a/camellia-server/paper-patches/features/0005-Add-config-for-server-mod-name.patch b/camellia-server/paper-patches/features/0005-Add-config-for-server-mod-name.patch new file mode 100644 index 0000000..b825870 --- /dev/null +++ b/camellia-server/paper-patches/features/0005-Add-config-for-server-mod-name.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 23:46:10 +0800 +Subject: [PATCH] Add config for server mod name + + +diff --git a/src/main/java/com/destroystokyo/paper/network/PaperServerListPingEventImpl.java b/src/main/java/com/destroystokyo/paper/network/PaperServerListPingEventImpl.java +index 13b03dab807c5d83e086993af705e1d9a2676b4d..83894904de29c01a4e105c1fae625f912232f8f2 100644 +--- a/src/main/java/com/destroystokyo/paper/network/PaperServerListPingEventImpl.java ++++ b/src/main/java/com/destroystokyo/paper/network/PaperServerListPingEventImpl.java +@@ -14,7 +14,7 @@ class PaperServerListPingEventImpl extends PaperServerListPingEvent { + + PaperServerListPingEventImpl(MinecraftServer server, StatusClient client, int protocolVersion, @Nullable CachedServerIcon icon) { + super(client, server.motd(), server.getPlayerCount(), server.getMaxPlayers(), +- server.getServerModName() + ' ' + server.getServerVersion(), protocolVersion, icon); ++ moe.monnairealms.camellia.config.modules.misc.ServerModNameConfig.fakeVanilla ? server.getServerVersion() : server.getServerModName() + ' ' + server.getServerVersion(), protocolVersion, icon); // Camellia - Add config for server mod name + this.server = server; + } + diff --git a/camellia-server/paper-patches/features/0006-Add-config-to-disable-async-catchers.patch b/camellia-server/paper-patches/features/0006-Add-config-to-disable-async-catchers.patch new file mode 100644 index 0000000..3ed89b2 --- /dev/null +++ b/camellia-server/paper-patches/features/0006-Add-config-to-disable-async-catchers.patch @@ -0,0 +1,94 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 23:48:59 +0800 +Subject: [PATCH] Add config to disable async catchers + + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +index 6c74787321748702022787b65ff5465f8bcec4ad..5d7a4f42ed274d78e79f0d4e5dd974ffa8b560dc 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/TickThread.java +@@ -48,14 +48,14 @@ public class TickThread extends Thread { + */ + @Deprecated + public static void ensureTickThread(final String reason) { +- if (!isTickThread()) { ++ if (!isTickThread() && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Camellia - Add config to disable async catchers + LOGGER.error("Thread failed main thread check: " + reason + ", context=" + getThreadContext(), new Throwable()); + throw new IllegalStateException(reason); + } + } + + public static void ensureTickThread(final Level world, final BlockPos pos, final String reason) { +- if (!isTickThreadFor(world, pos)) { ++ if (!isTickThreadFor(world, pos) && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Camellia - Add config to disable async catchers + final String ex = "Thread failed main thread check: " + + reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos; + LOGGER.error(ex, new Throwable()); +@@ -64,7 +64,7 @@ public class TickThread extends Thread { + } + + public static void ensureTickThread(final Level world, final BlockPos pos, final int blockRadius, final String reason) { +- if (!isTickThreadFor(world, pos, blockRadius)) { ++ if (!isTickThreadFor(world, pos, blockRadius) && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Camellia - Add config to disable async catchers + final String ex = "Thread failed main thread check: " + + reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + pos + ", block_radius=" + blockRadius; + LOGGER.error(ex, new Throwable()); +@@ -73,7 +73,7 @@ public class TickThread extends Thread { + } + + public static void ensureTickThread(final Level world, final ChunkPos pos, final String reason) { +- if (!isTickThreadFor(world, pos)) { ++ if (!isTickThreadFor(world, pos) && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Camellia - Add config to disable async catchers + final String ex = "Thread failed main thread check: " + + reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + pos; + LOGGER.error(ex, new Throwable()); +@@ -82,7 +82,7 @@ public class TickThread extends Thread { + } + + public static void ensureTickThread(final Level world, final int chunkX, final int chunkZ, final String reason) { +- if (!isTickThreadFor(world, chunkX, chunkZ)) { ++ if (!isTickThreadFor(world, chunkX, chunkZ) && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Camellia - Add config to disable async catchers + final String ex = "Thread failed main thread check: " + + reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", chunk_pos=" + new ChunkPos(chunkX, chunkZ); + LOGGER.error(ex, new Throwable()); +@@ -91,7 +91,7 @@ public class TickThread extends Thread { + } + + public static void ensureTickThread(final Entity entity, final String reason) { +- if (!isTickThreadFor(entity)) { ++ if (!isTickThreadFor(entity) && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Camellia - Add config to disable async catchers + final String ex = "Thread failed main thread check: " + + reason + ", context=" + getThreadContext() + ", entity=" + EntityUtil.dumpEntity(entity); + LOGGER.error(ex, new Throwable()); +@@ -100,7 +100,7 @@ public class TickThread extends Thread { + } + + public static void ensureTickThread(final Level world, final AABB aabb, final String reason) { +- if (!isTickThreadFor(world, aabb)) { ++ if (!isTickThreadFor(world, aabb) && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Camellia - Add config to disable async catchers + final String ex = "Thread failed main thread check: " + + reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", aabb=" + aabb; + LOGGER.error(ex, new Throwable()); +@@ -109,7 +109,7 @@ public class TickThread extends Thread { + } + + public static void ensureTickThread(final Level world, final double blockX, final double blockZ, final String reason) { +- if (!isTickThreadFor(world, blockX, blockZ)) { ++ if (!isTickThreadFor(world, blockX, blockZ) && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Camellia - Add config to disable async catchers + final String ex = "Thread failed main thread check: " + + reason + ", context=" + getThreadContext() + ", world=" + WorldUtil.getWorldName(world) + ", block_pos=" + new Vec3(blockX, 0.0, blockZ); + LOGGER.error(ex, new Throwable()); +diff --git a/src/main/java/org/spigotmc/AsyncCatcher.java b/src/main/java/org/spigotmc/AsyncCatcher.java +index 7e7a0ed944961af5ab7a49bc659da2862d2e2c82..1dc664a97442744d0a9364805cc6d6c5b8b647a2 100644 +--- a/src/main/java/org/spigotmc/AsyncCatcher.java ++++ b/src/main/java/org/spigotmc/AsyncCatcher.java +@@ -5,7 +5,7 @@ import net.minecraft.server.MinecraftServer; + public class AsyncCatcher { + + public static void catchOp(String reason) { +- if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread()) { // Paper - chunk system ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThread() && !moe.monnairealms.camellia.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Paper - chunk system // Camellia - Add config to disable async catchers + MinecraftServer.LOGGER.error("Thread {} failed main thread check: {}", Thread.currentThread().getName(), reason, new Throwable()); // Paper + throw new IllegalStateException("Asynchronous " + reason + "!"); + } diff --git a/camellia-server/paper-patches/features/0007-Add-config-to-avoid-of-folia-supported-check.patch b/camellia-server/paper-patches/features/0007-Add-config-to-avoid-of-folia-supported-check.patch new file mode 100644 index 0000000..0cc89ca --- /dev/null +++ b/camellia-server/paper-patches/features/0007-Add-config-to-avoid-of-folia-supported-check.patch @@ -0,0 +1,48 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:47:28 +0800 +Subject: [PATCH] Add config to avoid of folia-supported check + +This is in dangerous zone, we won't support it if you find any plugin bug if you enable it + +Co-authored-by: Helvetica Volubi <88063803+suisuroru@users.noreply.github.com> + +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java +index 6369b13e1fcdbdb25dd9d6e4d3bffdedbee4f739..ea27ce9d08b3b8bbb0cd4918dbf51fd8077c3bb4 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginProviderFactory.java +@@ -25,7 +25,7 @@ class PaperPluginProviderFactory implements PluginTypeFactory +Date: Wed, 8 Jul 2026 23:56:00 +0800 +Subject: [PATCH] Add config for cpu affinity + + +diff --git a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java +index b9f788693b0c4be48e8a923a3dec50555d135e7e..1ce8304688ff3751d2de3ada14539842d2cca4e1 100644 +--- a/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java ++++ b/src/main/java/ca/spottedleaf/moonrise/common/util/MoonriseCommon.java +@@ -22,7 +22,7 @@ public final class MoonriseCommon { + + @Override + public Thread newThread(final Runnable run) { +- final Thread thread = new Thread(run, PlatformHooks.get().getBrand() + " Common Worker #" + this.idGenerator.getAndIncrement()); ++ final Thread thread = new Thread(moe.monnairealms.camellia.config.modules.optimizations.CpuAffinityConfig.wrapForChunkSystemWorker(run), PlatformHooks.get().getBrand() + " Common Worker #" + this.idGenerator.getAndIncrement()); // Camellia - Add config for cpu affinity + + thread.setDaemon(true); + thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { +@@ -70,7 +70,7 @@ public final class MoonriseCommon { + + @Override + public Thread newThread(final Runnable run) { +- final Thread thread = new Thread(run, PlatformHooks.get().getBrand() + " I/O Worker #" + this.idGenerator.getAndIncrement()); ++ final Thread thread = new Thread(moe.monnairealms.camellia.config.modules.optimizations.CpuAffinityConfig.wrapForChunkSystemIo(run), PlatformHooks.get().getBrand() + " I/O Worker #" + this.idGenerator.getAndIncrement()); // Camellia - Add config for cpu affinity + + thread.setDaemon(true); + thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { diff --git a/camellia-server/paper-patches/features/0009-Add-status-bar.patch b/camellia-server/paper-patches/features/0009-Add-status-bar.patch new file mode 100644 index 0000000..1a8600d --- /dev/null +++ b/camellia-server/paper-patches/features/0009-Add-status-bar.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 9 Jul 2026 12:12:08 +0800 +Subject: [PATCH] Add status bar + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index f020f310c4bca49a23de91bea72ccd9941c0b03c..10b351d8bd6b0e117ecbe961ae2f101686a38fe4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -406,4 +406,12 @@ public class CraftChunk implements Chunk { + static { + Arrays.fill(FULL_LIGHT, (byte) 0xFF); + } ++ ++ // KioCG start - ChunkHot ++ @Override ++ public long getChunkHotAvg() { ++ final net.minecraft.world.level.chunk.LevelChunk target = this.level.getChunkIfLoaded(this.x,this.z); ++ return target == null ? -1 : target.getChunkHot().getAverage(); ++ } ++ // KioCG end + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 3d7def71d03d3ec3b3c3565ec07b46a3c1263aab..e655c6b817b1ef2b3186c4b16a1039081139e97f 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3396,4 +3396,11 @@ public class CraftPlayer extends CraftHumanEntity implements Player, PluginMessa + super.knockback(strength, directionX, directionZ); + this.entity.hurtMarked = true; + } ++ ++ // KioCG start - ChunkHot ++ @Override ++ public long getNearbyChunkHot() { ++ return this.getHandle().getNearbyChunkHot(); ++ } ++ // KioCG end + } diff --git a/camellia-server/paper-patches/features/0010-Add-back-BlockPos-transform-to-Position-support.patch b/camellia-server/paper-patches/features/0010-Add-back-BlockPos-transform-to-Position-support.patch new file mode 100644 index 0000000..bed8d37 --- /dev/null +++ b/camellia-server/paper-patches/features/0010-Add-back-BlockPos-transform-to-Position-support.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 8 Jul 2026 23:56:26 +0800 +Subject: [PATCH] Add back BlockPos transform to Position support + + +diff --git a/src/main/java/io/papermc/paper/util/MCUtil.java b/src/main/java/io/papermc/paper/util/MCUtil.java +index 532076c7358aa355ce5144075fe6bd79de6ef9e6..fa940f0dfc19aa2853b7a0d19743321d3c4ce292 100644 +--- a/src/main/java/io/papermc/paper/util/MCUtil.java ++++ b/src/main/java/io/papermc/paper/util/MCUtil.java +@@ -183,6 +183,12 @@ public final class MCUtil { + return CraftLocation.toBukkit(pos, world); + } + ++ // Camellia start - add BlockPos support ++ public static Location toLocation(Level world, BlockPos pos) { ++ return CraftLocation.toBukkit(pos, world); ++ } ++ // Camellia end - add BlockPos support ++ + /** + * Converts a NMS World/Vector to Bukkit Location + * diff --git a/camellia-server/paper-patches/features/0011-Purpur-Lobotomize-stuck-villagers.patch b/camellia-server/paper-patches/features/0011-Purpur-Lobotomize-stuck-villagers.patch new file mode 100644 index 0000000..5aab612 --- /dev/null +++ b/camellia-server/paper-patches/features/0011-Purpur-Lobotomize-stuck-villagers.patch @@ -0,0 +1,25 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 10:48:33 +0800 +Subject: [PATCH] Purpur: Lobotomize stuck villagers + +Co-authored by: William Blake Galbreath +As part of: Purpur (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/purpur-server/minecraft-patches/features/0001-Ridables.patch) +Licensed under: MIT (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/LICENSE) + +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +index 92eab10ec20f44e81d9e38bd9d4b6cf64dd6ef37..df26b41bf1516fac3124b0fc4a0ab71277ce2004 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftVillager.java +@@ -244,4 +244,11 @@ public class CraftVillager extends CraftAbstractVillager implements Villager { + public void restock() { + this.getHandle().restock(); + } ++ ++ // Purpur start ++ @Override ++ public boolean isLobotomized() { ++ return getHandle().isLobotomized(); ++ } ++ // Purpur end + } diff --git a/camellia-server/paper-patches/features/0012-Leaf-Skip-event-if-no-listeners.patch b/camellia-server/paper-patches/features/0012-Leaf-Skip-event-if-no-listeners.patch new file mode 100644 index 0000000..07a5d42 --- /dev/null +++ b/camellia-server/paper-patches/features/0012-Leaf-Skip-event-if-no-listeners.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:04:34 +0800 +Subject: [PATCH] Leaf Skip event if no listeners + +As part of: Leaf +Licensed under: GPL-3.0 (https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/licenses/GPL-3.0.txt) + +diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +index 7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b..9180f23529a31b6b0a5b38bb7cda3e32d487f691 100644 +--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +@@ -36,15 +36,17 @@ class PaperEventManager { + + // SimplePluginManager + public void callEvent(@NotNull Event event) { ++ // Leaf start - Skip event if no listeners ++ RegisteredListener[] listeners = event.getHandlers().getRegisteredListeners(); ++ if (listeners.length == 0) return; ++ // Leaf end - Skip event if no listeners ++ + if (event.isAsynchronous() && this.server.isPrimaryThread()) { + throw new IllegalStateException(event.getEventName() + " may only be triggered asynchronously."); + } else if (!event.isAsynchronous() && !this.server.isPrimaryThread() && !this.server.isStopping()) { + throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously."); + } + +- HandlerList handlers = event.getHandlers(); +- RegisteredListener[] listeners = handlers.getRegisteredListeners(); +- + for (RegisteredListener registration : listeners) { + if (!registration.getPlugin().isEnabled()) { + continue; diff --git a/camellia-server/paper-patches/features/0013-Leaf-Secure-seed-and-matter-seed-command.patch b/camellia-server/paper-patches/features/0013-Leaf-Secure-seed-and-matter-seed-command.patch new file mode 100644 index 0000000..dcbea5b --- /dev/null +++ b/camellia-server/paper-patches/features/0013-Leaf-Secure-seed-and-matter-seed-command.patch @@ -0,0 +1,43 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 10:31:41 +0800 +Subject: [PATCH] Leaf Secure seed and matter seed command + +As part of: Leaf +Licensed under: GPL-3.0 (https://github.com/Winds-Studio/Leaf/blob/7f3e240bbe0970683c40279a7a65f0fde47503b6/licenses/GPL-3.0.txt) + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +index 10b351d8bd6b0e117ecbe961ae2f101686a38fe4..7a93251c575870235a0aabf2fad846353025f5f2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftChunk.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftChunk.java +@@ -191,7 +191,12 @@ public class CraftChunk implements Chunk { + @Override + public boolean isSlimeChunk() { + // 987234911L is taken from Slime when seeing if a slime can spawn in a chunk +- return this.level.paperConfig().entities.spawning.allChunksAreSlimeChunks || WorldgenRandom.seedSlimeChunk(this.getX(), this.getZ(), this.getWorld().getSeed(), level.spigotConfig.slimeSeed).nextInt(10) == 0; // Paper ++ // Leaf start - Matter - Secure Seed ++ boolean isSlimeChunk = moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled ++ ? level.getChunk(this.getX(), this.getZ()).isSlimeChunk() ++ : WorldgenRandom.seedSlimeChunk(this.getX(), this.getZ(), this.getWorld().getSeed(), level.spigotConfig.slimeSeed).nextInt(10) == 0; // Paper ++ return this.level.paperConfig().entities.spawning.allChunksAreSlimeChunks || isSlimeChunk; ++ // Leaf end - Matter - Secure Seed + } + + @Override +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 3c46e2cc23ac57df52996e3d2c5ae416a9633a67..e0b1a7c8bc4e69afe5f78638e8fddbe8fe81da40 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1261,7 +1261,11 @@ public final class CraftServer implements Server { + .orElse(null); + RegistryAccess contextRegistryAccess = registryAccess; + if (worldGenSettings == null) { +- WorldOptions worldOptions = new WorldOptions(creator.seed(), creator.generateStructures(), creator.bonusChest()); ++ // Leaf start - Matter - Secure Seed ++ WorldOptions worldOptions = moe.monnairealms.camellia.config.modules.function.SecureSeedConfig.enabled ++ ? new WorldOptions(creator.seed(), su.plo.matter.Globals.createRandomWorldSeed(), creator.generateStructures(), false) ++ : new WorldOptions(creator.seed(), creator.generateStructures(), creator.bonusChest()); ++ // Leaf end - Matter - Secure Seed + + String flatGenSettings = creator.generatorSettings(); + if (flatGenSettings.isEmpty()) { diff --git a/camellia-server/paper-patches/features/0014-Leaf-Configurable-vanilla-username-check.patch b/camellia-server/paper-patches/features/0014-Leaf-Configurable-vanilla-username-check.patch new file mode 100644 index 0000000..bffc2a8 --- /dev/null +++ b/camellia-server/paper-patches/features/0014-Leaf-Configurable-vanilla-username-check.patch @@ -0,0 +1,82 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 27 Jun 2026 17:39:33 +0800 +Subject: [PATCH] Leaf Configurable vanilla username check + +A part of Leaf(https://github.com/Winds-Studio/Leaf/blob/edb0504069139beaa6f39efa4702370c2576b3fc/leaf-server/paper-patches/features/0061-Configurable-vanilla-username-check.patch) + +License: https://github.com/Winds-Studio/Leaf/blob/edb0504069139beaa6f39efa4702370c2576b3fc/LICENSE.md + +diff --git a/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java b/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java +index ceaf63c76272b2a11b92f877b9828e93ec79d47f..185f4448559064219338705927efe0b37795fa2e 100644 +--- a/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java ++++ b/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java +@@ -67,6 +67,14 @@ public class CraftPlayerProfile implements PlayerProfile, SharedPlayerProfile { + copyProfileProperties(resolvableProfile.partialProfile(), this.profile); + } + ++ // Leaf start - Configurable vanilla username check ++ public CraftPlayerProfile(UUID id, String name, boolean useCustomValidation) { ++ this.profile = createAuthLibProfile(id, name, useCustomValidation); ++ this.emptyName = name == null; ++ this.emptyUUID = id == null; ++ } ++ // Leaf end - Configurable vanilla username check ++ + @Override + public boolean hasProperty(String property) { + return profile.properties().containsKey(property); +@@ -205,6 +213,14 @@ public class CraftPlayerProfile implements PlayerProfile, SharedPlayerProfile { + return clone; + } + ++ // Leaf start - Configurable vanilla username check ++ public CraftPlayerProfile cloneCustom() { ++ CraftPlayerProfile clone = new CraftPlayerProfile(this.getId(), this.getName(), me.earthme.luminol.config.modules.misc.UsernameCheckConfig.useCustomUsernameRegex()); ++ clone.setProperties(getProperties()); ++ return clone; ++ } ++ // Leaf end - Configurable vanilla username check ++ + @Override + public boolean isComplete() { + return this.getId() != null && StringUtils.isNotBlank(this.getName()); +@@ -324,9 +340,10 @@ public class CraftPlayerProfile implements PlayerProfile, SharedPlayerProfile { + } + } + +- private static GameProfile createAuthLibProfile(UUID uniqueId, String name) { ++ // Leaf start - Configurable vanilla username check ++ private static GameProfile createAuthLibProfile(UUID uniqueId, String name, boolean useCustomValidation) { + Preconditions.checkArgument(name == null || name.length() <= 16, "Name cannot be longer than 16 characters"); +- Preconditions.checkArgument(name == null || StringUtil.isValidPlayerName(name), "The name of the profile contains invalid characters: %s", name); ++ Preconditions.checkArgument(name == null || (useCustomValidation ? StringUtil.isReasonablePlayerName(name) : StringUtil.isValidPlayerName(name)), "The name of the profile contains invalid characters: %s", name); + return new GameProfile( + uniqueId != null ? uniqueId : Util.NIL_UUID, + name != null ? name : "", +@@ -334,6 +351,11 @@ public class CraftPlayerProfile implements PlayerProfile, SharedPlayerProfile { + ); + } + ++ private static GameProfile createAuthLibProfile(UUID uniqueId, String name) { ++ return createAuthLibProfile(uniqueId, name, false); ++ } ++ // Leaf end - Configurable vanilla username check ++ + private static ProfileProperty toBukkit(Property property) { + return new ProfileProperty(property.name(), property.value(), property.signature()); + } +@@ -346,6 +368,13 @@ public class CraftPlayerProfile implements PlayerProfile, SharedPlayerProfile { + return new Property(property.getName(), property.getValue(), property.getSignature()); + } + ++ // Leaf start - Configurable vanilla username check ++ public static GameProfile asAuthlibCopyCustomValidation(PlayerProfile profile) { ++ CraftPlayerProfile craft = ((CraftPlayerProfile) profile); ++ return asAuthlibCopy(craft.cloneCustom()); ++ } ++ // Leaf end - Configurable vanilla username check ++ + public static GameProfile asAuthlibCopy(PlayerProfile profile) { + CraftPlayerProfile craft = ((CraftPlayerProfile) profile); + return craft.getGameProfile(); diff --git a/camellia-server/src/main/java/abomination/LinearRegionFile.java b/camellia-server/src/main/java/abomination/LinearRegionFile.java new file mode 100644 index 0000000..d89ee0b --- /dev/null +++ b/camellia-server/src/main/java/abomination/LinearRegionFile.java @@ -0,0 +1,621 @@ +package abomination; + +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import com.github.luben.zstd.ZstdInputStream; +import com.github.luben.zstd.ZstdOutputStream; +import com.mojang.logging.LogUtils; +import moe.monnairealms.camellia.data.RegionFile; +import net.jpountz.lz4.LZ4Compressor; +import net.jpountz.lz4.LZ4Factory; +import net.jpountz.lz4.LZ4FastDecompressor; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFileVersion; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import net.openhft.hashing.LongHashFunction; +import org.slf4j.Logger; + +import javax.annotation.Nullable; +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; +import java.util.concurrent.locks.ReentrantLock; + +// LinearRegionFile_implementation_version_0_5byXymb +// Just gonna use this string to inform other forks about updates ;-) +public class LinearRegionFile implements RegionFile { + private static final long SUPERBLOCK = 0xc3ff13183cca9d9aL; + private static final byte VERSION = 3; + private static final int HEADER_SIZE = 27; + private static final int FOOTER_SIZE = 8; + private static final Logger LOGGER = LogUtils.getLogger(); + + private byte[][] bucketBuffers; + private final byte[][] buffer = new byte[1024][]; + private final int[] bufferUncompressedSize = new int[1024]; + + private final long[] chunkTimestamps = new long[1024]; + private final Object markedToSaveLock = new Object(); + + private final LZ4Compressor compressor; + private final LZ4FastDecompressor decompressor; + + private boolean markedToSave = false; + private boolean close = false; + + public final ReentrantLock fileLock = new ReentrantLock(true); + public Path regionFile; + + private final int compressionLevel; + private int gridSize = 8; + private int bucketSize = 4; + private final Thread bindThread; + + public Path getRegionFile() { + return this.regionFile; + } + + public ReentrantLock getFileLock() { + return this.fileLock; + } + + private int chunkToBucketIdx(int chunkX, int chunkZ) { + int bx = chunkX / bucketSize, bz = chunkZ / bucketSize; + return bx * gridSize + bz; + } + + private void openBucket(int chunkX, int chunkZ) { + chunkX = Math.floorMod(chunkX, 32); + chunkZ = Math.floorMod(chunkZ, 32); + int idx = chunkToBucketIdx(chunkX, chunkZ); + + if (bucketBuffers == null) return; + if (bucketBuffers[idx] != null) { + try { + ByteArrayInputStream bucketByteStream = new ByteArrayInputStream(bucketBuffers[idx]); + ZstdInputStream zstdStream = new ZstdInputStream(bucketByteStream); + ByteBuffer bucketBuffer = ByteBuffer.wrap(zstdStream.readAllBytes()); + + int bx = chunkX / bucketSize, bz = chunkZ / bucketSize; + + for (int cx = 0; cx < 32 / gridSize; cx++) { + for (int cz = 0; cz < 32 / gridSize; cz++) { + int chunkIndex = (bx * (32 / gridSize) + cx) + (bz * (32 / gridSize) + cz) * 32; + + int chunkSize = bucketBuffer.getInt(); + long timestamp = bucketBuffer.getLong(); + this.chunkTimestamps[chunkIndex] = timestamp; + + if (chunkSize > 0) { + byte[] chunkData = new byte[chunkSize - 8]; + bucketBuffer.get(chunkData); + + int maxCompressedLength = this.compressor.maxCompressedLength(chunkData.length); + byte[] compressed = new byte[maxCompressedLength]; + int compressedLength = this.compressor.compress(chunkData, 0, chunkData.length, compressed, 0, maxCompressedLength); + byte[] finalCompressed = new byte[compressedLength]; + System.arraycopy(compressed, 0, finalCompressed, 0, compressedLength); + + // TODO: Optimization - return the requested chunk immediately to save on one LZ4 decompression + this.buffer[chunkIndex] = finalCompressed; + this.bufferUncompressedSize[chunkIndex] = chunkData.length; + } + } + } + } catch (IOException ex) { + throw new RuntimeException("Region file corrupted: " + regionFile + " bucket: " + idx); + // TODO: Make sure the server crashes instead of corrupting the world + } + bucketBuffers[idx] = null; + } + } + + public boolean regionFileOpen = false; + + private synchronized void openRegionFile() { + if (regionFileOpen) return; + regionFileOpen = true; + + File regionFile = new File(this.regionFile.toString()); + + if (!regionFile.canRead()) { + this.bindThread.start(); + return; + } + + try { + byte[] fileContent = Files.readAllBytes(this.regionFile); + ByteBuffer buffer = ByteBuffer.wrap(fileContent); + + long superBlock = buffer.getLong(); + if (superBlock != SUPERBLOCK) + throw new RuntimeException("Invalid superblock: " + superBlock + " file " + this.regionFile); + + byte version = buffer.get(); + if (version == 1 || version == 2) { + parseLinearV1(buffer); + } else if (version == 3) { + parseLinearV2(buffer); + } else { + throw new RuntimeException("Invalid version: " + version + " file " + this.regionFile); + } + + this.bindThread.start(); + } catch (IOException e) { + throw new RuntimeException("Failed to open region file " + this.regionFile, e); + } + } + + private void parseLinearV1(ByteBuffer buffer) throws IOException { + final int HEADER_SIZE = 32; + final int FOOTER_SIZE = 8; + + // Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused. + buffer.position(buffer.position() + 11); + + int dataCount = buffer.getInt(); + long fileLength = this.regionFile.toFile().length(); + if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE) { + throw new IOException("Invalid file length: " + this.regionFile + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE)); + } + + buffer.position(buffer.position() + 8); // Skip data hash (Long): Unused. + + byte[] rawCompressed = new byte[dataCount]; + buffer.get(rawCompressed); + + ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(rawCompressed); + ZstdInputStream zstdInputStream = new ZstdInputStream(byteArrayInputStream); + ByteBuffer decompressedBuffer = ByteBuffer.wrap(zstdInputStream.readAllBytes()); + + int[] starts = new int[1024]; + for (int i = 0; i < 1024; i++) { + starts[i] = decompressedBuffer.getInt(); + decompressedBuffer.getInt(); // Skip timestamps (Int): Unused. + } + + for (int i = 0; i < 1024; i++) { + if (starts[i] > 0) { + int size = starts[i]; + byte[] chunkData = new byte[size]; + decompressedBuffer.get(chunkData); + + int maxCompressedLength = this.compressor.maxCompressedLength(size); + byte[] compressed = new byte[maxCompressedLength]; + int compressedLength = this.compressor.compress(chunkData, 0, size, compressed, 0, maxCompressedLength); + byte[] finalCompressed = new byte[compressedLength]; + System.arraycopy(compressed, 0, finalCompressed, 0, compressedLength); + + this.buffer[i] = finalCompressed; + this.bufferUncompressedSize[i] = size; + this.chunkTimestamps[i] = getTimestamp(); // Use current timestamp as we don't have the original + } + } + } + + private void parseLinearV2(ByteBuffer buffer) throws IOException { + buffer.getLong(); // Skip newestTimestamp (Long) + gridSize = buffer.get(); + if (gridSize != 1 && gridSize != 2 && gridSize != 4 && gridSize != 8 && gridSize != 16 && gridSize != 32) + throw new RuntimeException("Invalid grid size: " + gridSize + " file " + this.regionFile); + bucketSize = 32 / gridSize; + + buffer.getInt(); // Skip region_x (Int) + buffer.getInt(); // Skip region_z (Int) + + boolean[] chunkExistenceBitmap = deserializeExistenceBitmap(buffer); + + while (true) { + byte featureNameLength = buffer.get(); + if (featureNameLength == 0) break; + byte[] featureNameBytes = new byte[featureNameLength]; + buffer.get(featureNameBytes); + String featureName = new String(featureNameBytes); + int featureValue = buffer.getInt(); + // System.out.println("NBT Feature: " + featureName + " = " + featureValue); + } + + int[] bucketSizes = new int[gridSize * gridSize]; + byte[] bucketCompressionLevels = new byte[gridSize * gridSize]; + long[] bucketHashes = new long[gridSize * gridSize]; + for (int i = 0; i < gridSize * gridSize; i++) { + bucketSizes[i] = buffer.getInt(); + bucketCompressionLevels[i] = buffer.get(); + bucketHashes[i] = buffer.getLong(); + } + + bucketBuffers = new byte[gridSize * gridSize][]; + for (int i = 0; i < gridSize * gridSize; i++) { + if (bucketSizes[i] > 0) { + bucketBuffers[i] = new byte[bucketSizes[i]]; + buffer.get(bucketBuffers[i]); + long rawHash = LongHashFunction.xx().hashBytes(bucketBuffers[i]); + if (rawHash != bucketHashes[i]) throw new IOException("Region file hash incorrect " + this.regionFile); + } + } + + long footerSuperBlock = buffer.getLong(); + if (footerSuperBlock != SUPERBLOCK) + throw new IOException("Footer superblock invalid " + this.regionFile); + } + + public LinearRegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync, int compressionLevel) throws IOException { + this(storageKey, directory, path, RegionFileVersion.getSelected(), dsync, compressionLevel); + } + + public LinearRegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync, int compressionLevel) throws IOException { + Runnable flushCheck = () -> { + while (!close) { + synchronized (saveLock) { + if (markedToSave && activeSaveThreads < SAVE_THREAD_MAX_COUNT) { + activeSaveThreads++; + Runnable flushOperation = () -> { + try { + flush(); + } catch (IOException ex) { + LOGGER.error("Region file {} flush failed", this.regionFile.toAbsolutePath(), ex); + } finally { + synchronized (saveLock) { + activeSaveThreads--; + } + } + }; + + Thread saveThread = USE_VIRTUAL_THREAD ? + Thread.ofVirtual().name("Linear IO - " + LinearRegionFile.this.hashCode()).unstarted(flushOperation) : + Thread.ofPlatform().name("Linear IO - " + LinearRegionFile.this.hashCode()).unstarted(flushOperation); + saveThread.setPriority(Thread.NORM_PRIORITY - 3); + saveThread.start(); + } + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(SAVE_DELAY_MS)); + } + }; + this.bindThread = USE_VIRTUAL_THREAD ? Thread.ofVirtual().unstarted(flushCheck) : Thread.ofPlatform().unstarted(flushCheck); + this.bindThread.setName("Linear IO Schedule - " + this.hashCode()); + this.regionFile = path; + this.compressionLevel = compressionLevel; + + this.compressor = LZ4Factory.fastestInstance().fastCompressor(); + this.decompressor = LZ4Factory.fastestInstance().fastDecompressor(); + } + + private synchronized void markToSave() { + synchronized (markedToSaveLock) { + markedToSave = true; + } + } + + private synchronized boolean isMarkedToSave() { + synchronized (markedToSaveLock) { + if (markedToSave) { + markedToSave = false; + return true; + } + return false; + } + } + + public static int SAVE_THREAD_MAX_COUNT = 6; + public static int SAVE_DELAY_MS = 100; + public static boolean USE_VIRTUAL_THREAD = true; + private static final Object saveLock = new Object(); + private static int activeSaveThreads = 0; + + /*public void run() { + while (!close) { + synchronized (saveLock) { + if (markedToSave && activeSaveThreads < SAVE_THREAD_MAX_COUNT) { + activeSaveThreads++; + Thread saveThread = new Thread(() -> { + try { + flush(); + } catch (IOException ex) { + LOGGER.error("Region file " + this.regionFile.toAbsolutePath() + " flush failed", ex); + } finally { + synchronized (saveLock) { + activeSaveThreads--; + } + } + }, "RegionFileFlush"); + saveThread.setPriority(Thread.NORM_PRIORITY - 3); + saveThread.start(); + } + } + LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(SAVE_DELAY_MS)); + } + }*/ + + public synchronized boolean doesChunkExist(ChunkPos pos) throws Exception { + openRegionFile(); + throw new Exception("doesChunkExist is a stub"); + } + + public synchronized void flush() throws IOException { + if (!isMarkedToSave()) return; + + openRegionFile(); + + long timestamp = getTimestamp(); + + long writeStart = System.nanoTime(); + File tempFile = new File(regionFile.toString() + ".tmp"); + FileOutputStream fileStream = new FileOutputStream(tempFile); + DataOutputStream dataStream = new DataOutputStream(fileStream); + + dataStream.writeLong(SUPERBLOCK); + dataStream.writeByte(VERSION); + dataStream.writeLong(timestamp); + dataStream.writeByte(gridSize); + + String fileName = regionFile.getFileName().toString(); + String[] parts = fileName.split("\\."); + int regionX = 0; + int regionZ = 0; + try { + if (parts.length >= 4) { + regionX = Integer.parseInt(parts[1]); + regionZ = Integer.parseInt(parts[2]); + } else { + LOGGER.warn("Unexpected file name format: " + fileName); + } + } catch (NumberFormatException e) { + LOGGER.error("Failed to parse region coordinates from file name: " + fileName, e); + } + + dataStream.writeInt(regionX); + dataStream.writeInt(regionZ); + + boolean[] chunkExistenceBitmap = new boolean[1024]; + for (int i = 0; i < 1024; i++) { + chunkExistenceBitmap[i] = (this.bufferUncompressedSize[i] > 0); + } + writeSerializedExistenceBitmap(dataStream, chunkExistenceBitmap); + + writeNBTFeatures(dataStream); + + int bucketMisses = 0; + byte[][] buckets = new byte[gridSize * gridSize][]; + for (int bx = 0; bx < gridSize; bx++) { + for (int bz = 0; bz < gridSize; bz++) { + if (bucketBuffers != null && bucketBuffers[bx * gridSize + bz] != null) { + buckets[bx * gridSize + bz] = bucketBuffers[bx * gridSize + bz]; + continue; + } + bucketMisses++; + + ByteArrayOutputStream bucketStream = new ByteArrayOutputStream(); + ZstdOutputStream zstdStream = new ZstdOutputStream(bucketStream, this.compressionLevel); + DataOutputStream bucketDataStream = new DataOutputStream(zstdStream); + + boolean hasData = false; + for (int cx = 0; cx < 32 / gridSize; cx++) { + for (int cz = 0; cz < 32 / gridSize; cz++) { + int chunkIndex = (bx * 32 / gridSize + cx) + (bz * 32 / gridSize + cz) * 32; + if (this.bufferUncompressedSize[chunkIndex] > 0) { + hasData = true; + byte[] chunkData = new byte[this.bufferUncompressedSize[chunkIndex]]; + this.decompressor.decompress(this.buffer[chunkIndex], 0, chunkData, 0, this.bufferUncompressedSize[chunkIndex]); + bucketDataStream.writeInt(chunkData.length + 8); + bucketDataStream.writeLong(this.chunkTimestamps[chunkIndex]); + bucketDataStream.write(chunkData); + } else { + bucketDataStream.writeInt(0); + bucketDataStream.writeLong(this.chunkTimestamps[chunkIndex]); + } + } + } + bucketDataStream.close(); + + if (hasData) { + buckets[bx * gridSize + bz] = bucketStream.toByteArray(); + } + } + } + + for (int i = 0; i < gridSize * gridSize; i++) { + dataStream.writeInt(buckets[i] != null ? buckets[i].length : 0); + dataStream.writeByte(this.compressionLevel); + long rawHash = 0; + if (buckets[i] != null) { + rawHash = LongHashFunction.xx().hashBytes(buckets[i]); + } + dataStream.writeLong(rawHash); + } + + for (int i = 0; i < gridSize * gridSize; i++) { + if (buckets[i] != null) { + dataStream.write(buckets[i]); + } + } + + dataStream.writeLong(SUPERBLOCK); + + dataStream.flush(); + fileStream.getFD().sync(); + fileStream.getChannel().force(true); // Ensure atomicity on Btrfs + dataStream.close(); + + fileStream.close(); + Files.move(tempFile.toPath(), this.regionFile, StandardCopyOption.REPLACE_EXISTING); +//System.out.println("writeStart REGION FILE FLUSH " + (System.nanoTime() - writeStart) + " misses: " + bucketMisses); + } + + private void writeNBTFeatures(DataOutputStream dataStream) throws IOException { + // writeNBTFeature(dataStream, "example", 1); + dataStream.writeByte(0); // End of NBT features + } + + private void writeNBTFeature(DataOutputStream dataStream, String featureName, int featureValue) throws IOException { + byte[] featureNameBytes = featureName.getBytes(); + dataStream.writeByte(featureNameBytes.length); + dataStream.write(featureNameBytes); + dataStream.writeInt(featureValue); + } + + public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Abomination - prevent chunk dupe + + public synchronized void write(ChunkPos pos, ByteBuffer buffer) { + openRegionFile(); + openBucket(pos.x(), pos.z()); + try { + byte[] b = toByteArray(new ByteArrayInputStream(buffer.array())); + int uncompressedSize = b.length; + + if (uncompressedSize > MAX_CHUNK_SIZE) { + LOGGER.error("Chunk dupe attempt " + this.regionFile); + clear(pos); + } else { + int maxCompressedLength = this.compressor.maxCompressedLength(b.length); + byte[] compressed = new byte[maxCompressedLength]; + int compressedLength = this.compressor.compress(b, 0, b.length, compressed, 0, maxCompressedLength); + b = new byte[compressedLength]; + System.arraycopy(compressed, 0, b, 0, compressedLength); + + int index = getChunkIndex(pos.x(), pos.z()); + this.buffer[index] = b; + this.chunkTimestamps[index] = getTimestamp(); + this.bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())] = uncompressedSize; + } + } catch (IOException e) { + LOGGER.error("Chunk write IOException " + e + " " + this.regionFile); + } + markToSave(); + } + + public DataOutputStream getChunkDataOutputStream(ChunkPos pos) { + openRegionFile(); + openBucket(pos.x(), pos.z()); + return new DataOutputStream(new BufferedOutputStream(new ChunkBuffer(pos))); + } + + @Override + public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) throws IOException { + final DataOutputStream out = this.getChunkDataOutputStream(pos); + + return new MoonriseRegionFileIO.RegionDataController.WriteData( + data, MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, + out, regionFile -> out.close() + ); + } + + private class ChunkBuffer extends ByteArrayOutputStream { + + private final ChunkPos pos; + + public ChunkBuffer(ChunkPos chunkcoordintpair) { + super(); + this.pos = chunkcoordintpair; + } + + public void close() throws IOException { + ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); + LinearRegionFile.this.write(this.pos, bytebuffer); + } + } + + private byte[] toByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + byte[] tempBuffer = new byte[4096]; + + int length; + while ((length = in.read(tempBuffer)) >= 0) { + out.write(tempBuffer, 0, length); + } + + return out.toByteArray(); + } + + @Nullable + public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) { + openRegionFile(); + openBucket(pos.x(), pos.z()); + + if (this.bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())] != 0) { + byte[] content = new byte[bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())]]; + this.decompressor.decompress(this.buffer[getChunkIndex(pos.x(), pos.z())], 0, content, 0, bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())]); + return new DataInputStream(new ByteArrayInputStream(content)); + } + return null; + } + + public synchronized void clear(ChunkPos pos) { + openRegionFile(); + openBucket(pos.x(), pos.z()); + int i = getChunkIndex(pos.x(), pos.z()); + this.buffer[i] = null; + this.bufferUncompressedSize[i] = 0; + this.chunkTimestamps[i] = 0; + markToSave(); + } + + public synchronized boolean hasChunk(ChunkPos pos) { + openRegionFile(); + openBucket(pos.x(), pos.z()); + return this.bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())] > 0; + } + + public synchronized void close() throws IOException { + openRegionFile(); + close = true; + try { + flush(); + } catch (IOException e) { + throw new IOException("Region flush IOException " + e + " " + this.regionFile); + } + } + + private static int getChunkIndex(int x, int z) { + return (x & 31) + ((z & 31) << 5); + } + + private static int getTimestamp() { + return (int) (System.currentTimeMillis() / 1000L); + } + + public boolean recalculateHeader() { + return false; + } + + public void setOversized(int x, int z, boolean something) { + } + + public CompoundTag getOversizedData(int x, int z) throws IOException { + throw new IOException("getOversizedData is a stub " + this.regionFile); + } + + public boolean isOversized(int x, int z) { + return false; + } + + public Path getPath() { + return this.regionFile; + } + + private boolean[] deserializeExistenceBitmap(ByteBuffer buffer) { + boolean[] result = new boolean[1024]; + for (int i = 0; i < 128; i++) { + byte b = buffer.get(); + for (int j = 0; j < 8; j++) { + result[i * 8 + j] = ((b >> (7 - j)) & 1) == 1; + } + } + return result; + } + + private void writeSerializedExistenceBitmap(DataOutputStream out, boolean[] bitmap) throws IOException { + for (int i = 0; i < 128; i++) { + byte b = 0; + for (int j = 0; j < 8; j++) { + if (bitmap[i * 8 + j]) { + b |= (1 << (7 - j)); + } + } + out.writeByte(b); + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/com/kiocg/ChunkHot.java b/camellia-server/src/main/java/com/kiocg/ChunkHot.java new file mode 100644 index 0000000..387bdaf --- /dev/null +++ b/camellia-server/src/main/java/com/kiocg/ChunkHot.java @@ -0,0 +1,90 @@ +package com.kiocg; + +import java.util.Arrays; + +public class ChunkHot { + // 热度统计总区间数量 + private static final int TIMES_LENGTH = 10; + // 当前统计区间下标 + private int index = -1; + + // 热度统计区间 + private final long[] times = new long[TIMES_LENGTH]; + // 存放临时的区间数值 + // 用于修正正在统计的当前区间热度没有计入总值的问题 + private long temp; + // 所有区间的热度总值 + private long total; + + // 用于每个具体统计的计算 + private long nanos; + // 当前统计是否进行中 + private volatile boolean started = false; + + /** + * 更新区间下标 + */ + public void nextTick() { + this.index = ++this.index % TIMES_LENGTH; + } + + /** + * 开始统计一个新区间 + */ + public void start() { + started = true; + temp = times[this.index]; + times[this.index] = 0L; + } + + public boolean isStarted() { + return this.started; + } + + /** + * 结束当前区间的统计 + * 将统计值更新入热度总值 + */ + public void stop() { + started = false; + total -= temp; + total += times[this.index]; + } + + /** + * 开始一个具体统计 + */ + public void startTicking() { + if (!started) return; + nanos = System.nanoTime(); + } + + /** + * 结束一个具体统计 + * 将统计值计入当前热度区间 + */ + public void stopTickingAndCount() { + if (!started) return; + // 定义一个具体统计的最大值为 1,000,000 + // 有时候某个具体统计的计算值会在某1刻飙升,可能是由于保存数据到磁盘? + times[this.index] += Math.min(System.nanoTime() - nanos, 1000000L); + } + + /** + * 清空统计 (当区块卸载时) + */ + public void clear() { + started = false; + Arrays.fill(times, 0L); + temp = 0L; + total = 0L; + nanos = 0L; + } + + /** + * @return 获取区块热度平均值 + */ + public long getAverage() { + return total / ((long) TIMES_LENGTH * 20L); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityLimits.java b/camellia-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityLimits.java new file mode 100644 index 0000000..cca144d --- /dev/null +++ b/camellia-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityLimits.java @@ -0,0 +1,158 @@ +/* + * This file is part of Kaiiju (https://github.com/KaiijuMC/Kaiiju) + * + * Kaiiju 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. + * + * Kaiiju 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 Kaiiju. If not, see . + */ + +package dev.kaiijumc.kaiiju; + +import com.google.common.base.Throwables; +import com.mojang.logging.LogUtils; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ClassInfo; +import io.github.classgraph.ScanResult; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.world.entity.Entity; +import org.bukkit.Bukkit; +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; + +@SuppressWarnings("unused") +public class KaiijuEntityLimits { + private static final Logger LOGGER = LogUtils.getLogger(); + private static final File CONFIG_FOLDER = new File("luminol_config"); + + protected static final String HEADER = + "Per region entity limits for Kaiiju.\n" + + "If there are more of particular entity type in a region than limit, entity ticking will be throttled.\n" + + "Example: for Wither limit 100 & 300 Withers in a region -> 100 Withers tick every tick & every Wither ticks every 3 ticks.\n" + + "Available entities: GlowSquid, Ambient, Bat, Animal, Bee, Cat, Chicken, Cod, Cow, Dolphin, Fish, FishSchool, Fox, Golem, IronGolem, " + + "MushroomCow, Ocelot, Panda, Parrot, Perchable, Pig, PolarBear, PufferFish, Rabbit, Salmon, Sheep, Snowman, Squid, TropicalFish, Turtle, " + + "WaterAnimal, Wolf, Allay, Axolotl, Camel, Frog, Tadpole, Goat, Horse, HorseAbstract, HorseChestedAbstract, HorseDonkey, HorseMule, " + + "HorseSkeleton, HorseZombie, Llama, LlamaTrader, Sniffer, EnderCrystal, EnderDragon, Wither, ArmorStand, Hanging, ItemFrame, Leash, " + + "Painting, GlowItemFrame, FallingBlock, Item, TNTPrimed, Blaze, CaveSpider, Creeper, Drowned, Enderman, Endermite, Evoker, Ghast, " + + "GiantZombie, Guardian, GuardianElder, IllagerAbstract, IllagerIllusioner, IllagerWizard, MagmaCube, Monster, MonsterPatrolling, Phantom, " + + "ZombifiedPiglin, Pillager, Ravager, Shulker, Silverfish, Skeleton, SkeletonAbstract, SkeletonStray, SkeletonWither, Slime, Spider, Strider, Vex, " + + "Vindicator, Witch, Zoglin, Zombie, ZombieHusk, ZombieVillager, Hoglin, Piglin, PiglinAbstract, PiglinBrute, Warden, Villager, " + + "VillagerTrader, Arrow, DragonFireball, Egg, EnderPearl, EnderSignal, EvokerFangs, Fireball, FireballFireball, Fireworks, FishingHook, " + + "LargeFireball, LlamaSpit, Potion, Projectile, ProjectileThrowable, ShulkerBullet, SmallFireball, Snowball, SpectralArrow, ThrownExpBottle, " + + "ThrownTrident, TippedArrow, WitherSkull, Raider, ChestBoat, Boat, MinecartAbstract, MinecartChest, MinecartCommandBlock, MinecartContainer, " + + "MinecartFurnace, MinecartHopper, MinecartMobSpawner, MinecartRideable, MinecartTNT\n"; + protected static final File ENTITY_LIMITS_FILE = new File(CONFIG_FOLDER, "kaiiju_entity_limits.yml"); + public static YamlConfiguration entityLimitsConfig; + public static boolean enabled = false; + + protected static Map, EntityLimit> entityLimits; + + static final String ENTITY_PREFIX = "Entity"; + + public static void init() { + init(true); + } + + private static void init(boolean setup) { + entityLimitsConfig = new YamlConfiguration(); + + if (ENTITY_LIMITS_FILE.exists()) { + try { + entityLimitsConfig.load(ENTITY_LIMITS_FILE); + } catch (InvalidConfigurationException ex) { + Bukkit.getLogger().log(Level.SEVERE, "Could not load kaiiju_entity_limits.yml, please correct your syntax errors", ex); + throw Throwables.propagate(ex); + } catch (IOException ignore) { + } + } else { + if (setup) { + entityLimitsConfig.options().header(HEADER); + entityLimitsConfig.options().copyDefaults(true); + entityLimitsConfig.set("enabled", enabled); + entityLimitsConfig.set("Axolotl.limit", 1000); + entityLimitsConfig.set("Axolotl.removal", 2000); + try { + entityLimitsConfig.save(ENTITY_LIMITS_FILE); + } catch (IOException ex) { + Bukkit.getLogger().log(Level.SEVERE, "Could not save " + ENTITY_LIMITS_FILE, ex); + } + } + } + + enabled = entityLimitsConfig.getBoolean("enabled"); + + entityLimits = new Object2ObjectOpenHashMap<>(); + try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("net.minecraft.world.entity").scan()) { + Map entityClasses = new HashMap<>(); + for (ClassInfo classInfo : scanResult.getAllClasses()) { + Class entityClass = Class.forName(classInfo.getName()); + if (Entity.class.isAssignableFrom(entityClass)) { + String entityName = extractEntityName(entityClass.getSimpleName()); + entityClasses.put(entityName, classInfo); + } + } + + for (String key : entityLimitsConfig.getKeys(false)) { + if (key.equals("enabled")) { + continue; + } + + if (!entityClasses.containsKey(key)) { + LOGGER.error("Unknown entity '" + key + "' in kaiiju-entity-limits.yml, skipping"); + continue; + } + int limit = entityLimitsConfig.getInt(key + ".limit"); + int removal = entityLimitsConfig.getInt(key + ".removal"); + + if (limit < 1) { + LOGGER.error(key + " has a limit less than the minimum of 1, ignoring"); + continue; + } + if (removal <= limit && removal != -1) { + LOGGER.error(key + " has a removal limit that is less than or equal to its limit, setting removal to limit * 10"); + removal = limit * 10; + } + + entityLimits.put((Class) Class.forName(entityClasses.get(key).getName()), new EntityLimit(limit, removal)); + } + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + + public static EntityLimit getEntityLimit(Entity entity) { + return entityLimits.get(entity.getClass()); + } + + private static String extractEntityName(String input) { + int prefixLength = ENTITY_PREFIX.length(); + + if (input.length() <= prefixLength || !input.startsWith(ENTITY_PREFIX)) { + return input; + } else { + return input.substring(prefixLength); + } + } + + public record EntityLimit(int limit, int removal) { + @Override + public String toString() { + return "EntityLimit{limit=" + limit + ", removal=" + removal + "}"; + } + } +} diff --git a/camellia-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityThrottler.java b/camellia-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityThrottler.java new file mode 100644 index 0000000..330c206 --- /dev/null +++ b/camellia-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityThrottler.java @@ -0,0 +1,101 @@ +/* + * This file is part of Kaiiju (https://github.com/KaiijuMC/Kaiiju) + * + * Kaiiju 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. + * + * Kaiiju 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 Kaiiju. If not, see . + */ + +package dev.kaiijumc.kaiiju; + +import io.papermc.paper.threadedregions.RegionizedWorldData; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.world.entity.Entity; + +public class KaiijuEntityThrottler { + private static class TickInfo { + int currentTick; + int continueFrom; + int toTick; + int toRemove; + } + + public static class EntityThrottlerReturn { + public boolean skip; + public boolean remove; + } + + private final Object2ObjectOpenHashMap entityLimitTickInfoMap = new Object2ObjectOpenHashMap<>(); + + public void tickLimiterStart() { + for (TickInfo tickInfo : entityLimitTickInfoMap.values()) { + tickInfo.currentTick = 0; + } + } + + public EntityThrottlerReturn tickLimiterShouldSkip(Entity entity) { + EntityThrottlerReturn retVal = new EntityThrottlerReturn(); + if (entity.isRemoved()) return retVal; + KaiijuEntityLimits.EntityLimit entityLimit = KaiijuEntityLimits.getEntityLimit(entity); + + if (entityLimit != null) { + TickInfo tickInfo = entityLimitTickInfoMap.computeIfAbsent(entityLimit, el -> { + TickInfo newTickInfo = new TickInfo(); + newTickInfo.toTick = entityLimit.limit(); + return newTickInfo; + }); + + tickInfo.currentTick++; + if (tickInfo.currentTick <= tickInfo.toRemove && entityLimit.removal() > 0) { + retVal.skip = false; + retVal.remove = true; + return retVal; + } + + if (tickInfo.currentTick < tickInfo.continueFrom) { + retVal.skip = true; + return retVal; + } + if (tickInfo.currentTick - tickInfo.continueFrom < tickInfo.toTick) { + retVal.skip = false; + return retVal; + } + retVal.skip = true; + return retVal; + } else { + retVal.skip = false; + return retVal; + } + } + + public void tickLimiterFinish(RegionizedWorldData regionizedWorldData) { + for (var entry : entityLimitTickInfoMap.entrySet()) { + KaiijuEntityLimits.EntityLimit entityLimit = entry.getKey(); + TickInfo tickInfo = entry.getValue(); + + int additionals = 0; + int nextContinueFrom = tickInfo.continueFrom + tickInfo.toTick; + if (nextContinueFrom >= tickInfo.currentTick) { + additionals = entityLimit.limit() - (tickInfo.currentTick - tickInfo.continueFrom); + nextContinueFrom = 0; + } + tickInfo.continueFrom = nextContinueFrom; + tickInfo.toTick = entityLimit.limit() + additionals; + + if (tickInfo.toRemove == 0 && tickInfo.currentTick > entityLimit.removal()) { + tickInfo.toRemove = tickInfo.currentTick - entityLimit.removal(); + } else if (tickInfo.toRemove != 0) { + tickInfo.toRemove = 0; + } + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/me/titaniumtown/ArrayConstants.java b/camellia-server/src/main/java/me/titaniumtown/ArrayConstants.java new file mode 100644 index 0000000..5318ebf --- /dev/null +++ b/camellia-server/src/main/java/me/titaniumtown/ArrayConstants.java @@ -0,0 +1,21 @@ +// This is a part from Leaf(https://github.com/Winds-Studio/Leaf/blob/ver/26.2/leaf-server/src/main/java/me/titaniumtown/ArrayConstants.java) +// Original License: https://github.com/Winds-Studio/Leaf/blob/ver/26.2/LICENSE.md + +// Gale - JettPack - reduce array allocations + +package me.titaniumtown; + +public final class ArrayConstants { + + public static final Object[] emptyObjectArray = new Object[0]; + public static final short[] emptyShortArray = new short[0]; + public static final int[] emptyIntArray = new int[0]; + public static final int[] zeroSingletonIntArray = new int[]{0}; + public static final byte[] emptyByteArray = new byte[0]; + public static final String[] emptyStringArray = new String[0]; + public static final long[] emptyLongArray = new long[0]; + public static final org.bukkit.entity.Entity[] emptyBukkitEntityArray = new org.bukkit.entity.Entity[0]; + public static final net.minecraft.world.entity.Entity[] emptyEntityArray = new net.minecraft.world.entity.Entity[0]; + //public static final net.minecraft.server.level.ServerLevel[] emptyServerLevelArray = new net.minecraft.server.level.ServerLevel[0]; + public static final net.minecraft.tags.TagKey[] emptyTagKeyArray = new net.minecraft.tags.TagKey[0]; // Leaf - Optimize isEyeInFluid +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/CommandRegister.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/CommandRegister.java new file mode 100644 index 0000000..356acf7 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/CommandRegister.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.commands; + +import moe.monnairealms.camellia.commands.bar.BarCommand; + +public class CommandRegister { + /** + * Register commands after config loading + * This method is called after system configuration is fully loaded, + * used to register commands that depend on complete configuration + */ + public static void register() { + new BarCommand().register(); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/BarCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/BarCommand.java new file mode 100644 index 0000000..43c39f6 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/BarCommand.java @@ -0,0 +1,52 @@ +package moe.monnairealms.camellia.commands.bar; + +import moe.monnairealms.camellia.commands.bar.sub.ToggleCommand; +import moe.monnairealms.camellia.enums.EnumBarType; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.RootNode; + +public class BarCommand extends RootNode { + private static final String PERM_BASE = "luminol.commands.bar"; + + public BarCommand() { + super("bar", PERM_BASE); + children( + new BarSubcommand(EnumBarType.TPS), + new BarSubcommand(EnumBarType.MEMORY), + new BarSubcommand(EnumBarType.REGION) + ); + } + + public static boolean hasPermission(@NotNull CommandSender sender, String... subcommand) { + return hasPermission(PERM_BASE, sender, subcommand); + } + + @Override + public void register() { + super.register(); + children.forEach(child -> { + if (child instanceof BarSubcommand barSubcommand) { + barSubcommand.getChildren().forEach(subChild -> { + if (subChild instanceof ToggleCommand toggleCommand) { + toggleCommand.register(); + } + }); + } + }); + } + + @Override + public void unregister() { + super.unregister(); + children.forEach(child -> { + if (child instanceof BarSubcommand barSubcommand) { + barSubcommand.getChildren().forEach(subChild -> { + if (subChild instanceof ToggleCommand toggleCommand) { + toggleCommand.unregister(); + } + }); + } + }); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/BarSubcommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/BarSubcommand.java new file mode 100644 index 0000000..360cf6f --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/BarSubcommand.java @@ -0,0 +1,35 @@ +package moe.monnairealms.camellia.commands.bar; + +import io.papermc.paper.command.brigadier.CommandSourceStack; +import moe.monnairealms.camellia.commands.bar.sub.ConfigEditCommand; +import moe.monnairealms.camellia.commands.bar.sub.ToggleCommand; +import moe.monnairealms.camellia.enums.EnumBarType; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.CommandNode; +import org.leavesmc.leaves.command.LiteralNode; + +import java.util.List; + +public class BarSubcommand extends LiteralNode { + public BarSubcommand(EnumBarType barType) { + super(barType.getCommandName()); + children( + new ToggleCommand(barType), + new ConfigEditCommand(barType) + ); + } + + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return hasPermission(source.getSender()); + } + + protected boolean hasPermission(CommandSender sender) { + return BarCommand.hasPermission(sender, this.name); + } + + public List getChildren() { + return children; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/sub/ConfigEditCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/sub/ConfigEditCommand.java new file mode 100644 index 0000000..cac38bc --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/sub/ConfigEditCommand.java @@ -0,0 +1,74 @@ +package moe.monnairealms.camellia.commands.bar.sub; + +import com.mojang.brigadier.arguments.BoolArgumentType; +import moe.monnairealms.camellia.api.config.CamelliaConfigsInstance; +import moe.monnairealms.camellia.config.ConfigManager; +import moe.monnairealms.camellia.config.modules.function.MembarConfig; +import moe.monnairealms.camellia.config.modules.function.RegionBarConfig; +import moe.monnairealms.camellia.config.modules.function.TpsBarConfig; +import moe.monnairealms.camellia.enums.EnumBarType; +import moe.monnairealms.camellia.functions.bars.TickableStatusBarList; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; +import org.leavesmc.leaves.command.LiteralNode; + +public class ConfigEditCommand extends LiteralNode { + private final EnumBarType barType; + + public ConfigEditCommand(EnumBarType barType) { + super("config"); + this.barType = barType; + children( + BooleanArgument::new + ); + } + + private class BooleanArgument extends ArgumentNode { + protected BooleanArgument() { + super("boolean", BoolArgumentType.bool()); + } + + // TODO + @Contract(pure = true) + private static boolean isEnabledInGlobal(@NonNull EnumBarType type) { + return switch (type) { + case TPS -> TpsBarConfig.tpsbarEnabled; + case MEMORY -> MembarConfig.memoryBarEnabled; + case REGION -> RegionBarConfig.regionbarEnabled; + }; + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + boolean enabled = isEnabledInGlobal(barType); + + boolean value = context.getArgument(BooleanArgument.class); + if (value == enabled) { + context.getSender().sendMessage( + Component + .text("Bar type with " + barType.getName() + " was already " + (value ? "enabled" : "disabled") + "!") + .color(TextColor.color(255, 0, 0))); + } else { + CamelliaConfigsInstance config = ConfigManager.getConfigs(barType.getConfigOrigin()); + if (config.setConfig(barType.getConfigPath(), value)) { + + context.getSender().sendMessage( + Component + .text("Bar type with " + barType.getName() + (value ? " enabled" : " disabled") + " successfully!") + .color(TextColor.color(0, 255, 0)) + ); + + config.reloadAsync(true).thenAccept(_ -> { + TickableStatusBarList.raiseGlobalReload(); + }); + } + } + return true; + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/sub/ToggleCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/sub/ToggleCommand.java new file mode 100644 index 0000000..4aa2576 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/bar/sub/ToggleCommand.java @@ -0,0 +1,127 @@ +package moe.monnairealms.camellia.commands.bar.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import io.papermc.paper.command.brigadier.PaperCommands; +import moe.monnairealms.camellia.commands.bar.BarCommand; +import moe.monnairealms.camellia.enums.EnumBarType; +import moe.monnairealms.camellia.functions.bars.TickableStatusBarList; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; +import org.leavesmc.leaves.command.LiteralNode; + +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +public class ToggleCommand extends LiteralNode { + private final EnumBarType barType; + + public ToggleCommand(EnumBarType barType) { + super("toggle"); + this.barType = barType; + children( + PlayerArg::new + ); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + if (!(context.getSender() instanceof Player player)) { + context.getSender().sendMessage(Component.text("Only player can display bars!").color(TextColor.color(255, 0, 0))); + return true; + } + return execute0(context, player); + } + + public boolean execute0(@NotNull CommandContext context, Player player) { + final TickableStatusBarList barList = ((CraftPlayer) player).getHandle().statusBarList; + + boolean enabled = barList.isEnabled(this.barType); + + if (!enabled) { + context.getSender().sendMessage(Component.text("Bar type with " + this.barType.getName() + " was already disabled!").color(TextColor.color(255, 0, 0))); + return true; + } + + if (barList.isVisible(this.barType)) { + context.getSender().sendMessage(Component.text("Disabled Bar type with " + this.barType.getName() + " for " + player.getName()).color(TextColor.color(0, 255, 0))); + barList.setVisible(this.barType, false); + return true; + } + + context.getSender().sendMessage(Component.text("Enabled Bar type with " + this.barType.getName() + " for " + player.getName()).color(TextColor.color(0, 255, 0))); + barList.setVisible(this.barType, true); + return true; + } + + private class PlayerArg extends ArgumentNode { + protected PlayerArg() { + super("player", StringArgumentType.string()); + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + Bukkit.getServer().getOnlinePlayers().forEach(player -> builder.suggest(player.getName())); + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String name = context.getArgument(PlayerArg.class); + Player player = Bukkit.getServer().getPlayer(name); + if (player == null) { + player = Bukkit.getServer().getPlayer(UUID.fromString(name)); + if (player == null) { + context.getSender().sendMessage(Component.text("Player " + name + " was not found!").color(TextColor.color(255, 0, 0))); + return true; + } + } + return execute0(context, player); + } + } + + protected ArgumentBuilder compile0() { + ArgumentBuilder builder = Commands.literal(this.barType.getCommandName()).requires(this::requires); + + if (canExecute()) { + builder = builder.executes(mojangCtx -> { + CommandContext ctx = new CommandContext(mojangCtx); + return execute(ctx) ? 1 : 0; + }); + } + + return builder; + } + + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return BarCommand.hasPermission(source.getSender(), this.barType.getName(), this.name); + } + + @SuppressWarnings("unchecked") + public void register() { // register for old version command + PaperCommands.INSTANCE.setValid(); + PaperCommands.INSTANCE.getDispatcher().register((LiteralArgumentBuilder) compile0()); + PaperCommands.INSTANCE.invalidate(); + Bukkit.getOnlinePlayers().forEach(Player::updateCommands); + } + + public void unregister() { // unregister for old version command + PaperCommands.INSTANCE.setValid(); + PaperCommands.INSTANCE.getDispatcher().getRoot().removeCommand(this.barType.getCommandName()); + PaperCommands.INSTANCE.invalidate(); + Bukkit.getOnlinePlayers().forEach(Player::updateCommands); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/ConfigCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/ConfigCommand.java new file mode 100644 index 0000000..6cf57d0 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/ConfigCommand.java @@ -0,0 +1,37 @@ +package moe.monnairealms.camellia.commands.config; + +import moe.monnairealms.camellia.commands.config.sub.*; +import moe.monnairealms.camellia.config.ConfigsInstance; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.RootNode; + +public class ConfigCommand extends RootNode { + public final ConfigsInstance config; + public final String name; + private final String PERM_BASE; + + public ConfigCommand(String name, String commandName, ConfigsInstance config) { + super(commandName, name + ".commands." + name + "config"); + this.name = name; + this.PERM_BASE = name + ".commands." + name + "config"; + this.config = config; + children( + new ReloadCommand(this), + new SetCommand(this), + new ResetCommand(this), + new OpenGuiCommand(this), + new SubmitCommand(this), + new CleanCommand(this), + new ResetCommentsCommand(this) + ); + } + + public boolean hasPermission(@NotNull CommandSender sender, String... subcommand) { + return hasPermission(PERM_BASE, sender, subcommand); + } + + public String getCommandName() { + return super.name; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/ConfigSubcommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/ConfigSubcommand.java new file mode 100644 index 0000000..4099a02 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/ConfigSubcommand.java @@ -0,0 +1,24 @@ +package moe.monnairealms.camellia.commands.config; + +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.LiteralNode; + +public abstract class ConfigSubcommand extends LiteralNode { + protected final ConfigCommand parent; + + protected ConfigSubcommand(String name, ConfigCommand parent) { + super(name); + this.parent = parent; + } + + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return hasPermission(source.getSender()); + } + + protected boolean hasPermission(CommandSender sender) { + return parent.hasPermission(sender, this.name); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/CleanCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/CleanCommand.java new file mode 100644 index 0000000..ec4f56e --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/CleanCommand.java @@ -0,0 +1,69 @@ +package moe.monnairealms.camellia.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import moe.monnairealms.camellia.commands.config.ConfigCommand; +import moe.monnairealms.camellia.commands.config.ConfigSubcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; + +import java.util.concurrent.CompletableFuture; + +public class CleanCommand extends ConfigSubcommand { + public CleanCommand(ConfigCommand parent) { + super("clean", parent); + children( + new PathArgument(parent) + ); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + context.getSender().sendMessage( + Component + .text("If you want to clean up useless items in the configuration file, please use /" + parent.getCommandName() + " clean confirm") + .color(TextColor.color(255, 0, 0)) + ); + return true; + } + + static class PathArgument extends ArgumentNode { + protected final ConfigCommand parent; + + PathArgument(ConfigCommand parent) { + super("confirm", StringArgumentType.string()); + this.parent = parent; + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + builder.suggest("confirm"); + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + String confirm = context.getArgument(PathArgument.class); + if (!"confirm".equals(confirm)) { + context.getSender().sendMessage( + Component + .text("Please use /" + parent.getCommandName() + " clean confirm to confirm!") + .color(TextColor.color(255, 0, 0)) + ); + return true; + } + parent.config.clean(); + context.getSender().sendMessage( + Component + .text("Clean up in the configuration file successfully!") + .color(TextColor.color(0, 255, 0)) + ); + return true; + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/OpenGuiCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/OpenGuiCommand.java new file mode 100644 index 0000000..7b35ab1 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/OpenGuiCommand.java @@ -0,0 +1,83 @@ +package moe.monnairealms.camellia.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import moe.monnairealms.camellia.commands.config.ConfigCommand; +import moe.monnairealms.camellia.commands.config.ConfigSubcommand; +import moe.monnairealms.camellia.utils.dialog.ConfigCommandDialog; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import net.minecraft.world.entity.player.Player; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; + +import java.util.concurrent.CompletableFuture; + +import static org.leavesmc.leaves.command.CommandUtils.getListClosestMatchingLast; + +public class OpenGuiCommand extends ConfigSubcommand { + public OpenGuiCommand(ConfigCommand parent) { + super("open-gui", parent); + children( + new PathArgument(parent) + ); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + if (context.getSender() instanceof CraftPlayer cPlayer) { + final Player player = cPlayer.getHandle(); + ConfigCommandDialog.openGui(player, parent.getCommandName(), parent.config); + } else { + context.getSender().sendMessage( + Component + .text("Only player can use this command!") + .color(TextColor.color(255, 0, 0)) + ); + } + return true; + } + + static class PathArgument extends ArgumentNode { + protected final ConfigCommand parent; + + PathArgument(ConfigCommand parent) { + super("path", StringArgumentType.string()); + this.parent = parent; + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + String path = context.getArgumentOrDefault(PathArgument.class, ""); + int dotIndex = path.lastIndexOf("."); + builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2); + if (dotIndex == -1) builder.suggest("full"); + for (String s : getListClosestMatchingLast( + path.substring(dotIndex + 1), + parent.config.completeConfigPath(path) + )) { + builder.suggest(s.substring(path.lastIndexOf('.') + 1)); + } + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + if (context.getSender() instanceof CraftPlayer cPlayer) { + final Player player = cPlayer.getHandle(); + ConfigCommandDialog.openGui(player, parent.getCommandName(), parent.config, context.getArgument(PathArgument.class)); + } else { + context.getSender().sendMessage( + Component + .text("Only player can use this command!") + .color(TextColor.color(255, 0, 0)) + ); + } + return true; + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ReloadCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ReloadCommand.java new file mode 100644 index 0000000..0ceceaa --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ReloadCommand.java @@ -0,0 +1,25 @@ +package moe.monnairealms.camellia.commands.config.sub; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import moe.monnairealms.camellia.commands.config.ConfigCommand; +import moe.monnairealms.camellia.commands.config.ConfigSubcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.CommandContext; + +public class ReloadCommand extends ConfigSubcommand { + public ReloadCommand(ConfigCommand parent) { + super("reload", parent); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + parent.config.reloadAsync(true).thenAccept(nullValue -> context.getSender().sendMessage( + Component + .text("Reloaded config file!") + .color(TextColor.color(0, 255, 0)) + )); + return true; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ResetCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ResetCommand.java new file mode 100644 index 0000000..2c2ee56 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ResetCommand.java @@ -0,0 +1,59 @@ +package moe.monnairealms.camellia.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import moe.monnairealms.camellia.commands.config.ConfigCommand; +import moe.monnairealms.camellia.commands.config.ConfigSubcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; + +import java.util.concurrent.CompletableFuture; + +import static org.leavesmc.leaves.command.CommandUtils.getListClosestMatchingLast; + +public class ResetCommand extends ConfigSubcommand { + public ResetCommand(ConfigCommand parent) { + super("reset", parent); + children(new PathArgument(parent)); + } + + static class PathArgument extends ArgumentNode { + protected final ConfigCommand parent; + + PathArgument(ConfigCommand parent) { + super("path", StringArgumentType.string()); + this.parent = parent; + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + String path = context.getArgumentOrDefault(PathArgument.class, ""); + int dotIndex = path.lastIndexOf("."); + builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2); + for (String s : getListClosestMatchingLast( + path.substring(dotIndex + 1), + parent.config.completeConfigPath(path) + )) { + builder.suggest(s.substring(path.lastIndexOf('.') + 1)); + } + + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String path = context.getArgumentOrDefault(PathArgument.class, ""); + parent.config.resetConfig(path); + parent.config.reloadAsync(true).thenAccept(nullValue -> context.getSender().sendMessage( + Component + .text("Reset Config " + path + " to " + parent.config.getConfig(path) + " successfully!") + .color(TextColor.color(0, 255, 0)) + )); + return true; + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ResetCommentsCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ResetCommentsCommand.java new file mode 100644 index 0000000..342cf7d --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/ResetCommentsCommand.java @@ -0,0 +1,68 @@ +package moe.monnairealms.camellia.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import moe.monnairealms.camellia.commands.config.ConfigCommand; +import moe.monnairealms.camellia.commands.config.ConfigSubcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; + +import java.util.concurrent.CompletableFuture; + +public class ResetCommentsCommand extends ConfigSubcommand { + public ResetCommentsCommand(ConfigCommand parent) { + super("reset-comments", parent); + children( + new PathArgument(parent) + ); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + context.getSender().sendMessage( + Component + .text("If you want to reset comments to default in the configuration file, please use /" + parent.getCommandName() + " reset-comments confirm") + .color(TextColor.color(255, 0, 0)) + ); + return true; + } + + static class PathArgument extends ArgumentNode { + protected final ConfigCommand parent; + + PathArgument(ConfigCommand parent) { + super("confirm", StringArgumentType.string()); + this.parent = parent; + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + builder.suggest("confirm"); + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException { + String confirm = context.getArgument(CleanCommand.PathArgument.class); + if (!confirm.equals("confirm")) { + context.getSender().sendMessage( + Component + .text("Please use /" + parent.getCommandName() + " reset-comments confirm to confirm!") + .color(TextColor.color(255, 0, 0)) + ); + return true; + } + parent.config.reloadAsync(false).thenAccept(nullValue -> context.getSender().sendMessage( + Component + .text("Reset comments to default in the configuration file!") + .color(TextColor.color(0, 255, 0)) + )); + return true; + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/SetCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/SetCommand.java new file mode 100644 index 0000000..5d3db12 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/SetCommand.java @@ -0,0 +1,123 @@ +package moe.monnairealms.camellia.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import moe.monnairealms.camellia.commands.config.ConfigCommand; +import moe.monnairealms.camellia.commands.config.ConfigSubcommand; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; + +import java.util.Objects; +import java.util.concurrent.CompletableFuture; + +import static org.leavesmc.leaves.command.CommandUtils.getListClosestMatchingLast; + +public class SetCommand extends ConfigSubcommand { + public SetCommand(ConfigCommand parent) { + super("set", parent); + children(new PathArgument(parent)); + } + + static class PathArgument extends ArgumentNode { + protected final ConfigCommand parent; + + PathArgument(ConfigCommand parent) { + super("path", StringArgumentType.string()); + this.parent = parent; + children( + new ValueArgument(parent) + ); + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + String path = context.getArgumentOrDefault(PathArgument.class, ""); + int dotIndex = path.lastIndexOf("."); + builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2); + for (String s : getListClosestMatchingLast( + path.substring(dotIndex + 1), + parent.config.completeConfigPath(path) + )) { + builder.suggest(s.substring(path.lastIndexOf('.') + 1)); + } + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String path = context.getArgumentOrDefault(PathArgument.class, ""); + context.getSender().sendMessage( + Component + .text("Config " + path + " is " + parent.config.getConfig(path) + "!") + .color(TextColor.color(0, 255, 0)) + ); + return true; + } + + private class ValueArgument extends ArgumentNode { + private final ConfigCommand parent; + + private ValueArgument(ConfigCommand parent) { + super("value", StringArgumentType.greedyString()); + this.parent = parent; + } + + @Override + protected CompletableFuture getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) { + String path = context.getArgument(PathArgument.class); + if (!parent.config.getAllConfigPaths("").contains(path)) { + return builder + .suggest("", net.minecraft.network.chat.Component.literal("This config path does not exist.")) + .buildFuture(); + } + Object value = parent.config.getConfigOrigin(path); + String[] suggestions = parent.config.getConfigSuggestions(path); + builder.suggest(value.toString(), net.minecraft.network.chat.Component.literal("Default value") + .withStyle(style -> style.withColor(net.minecraft.network.chat.TextColor.fromLegacyFormat(net.minecraft.ChatFormatting.GRAY)))); + if (suggestions == null) { + if (value instanceof Boolean) { + builder.suggest(String.valueOf(!(Boolean) value)); + } else if (value instanceof Enum enumValue) { + Enum[] values = enumValue.getClass().getEnumConstants(); + for (Enum enumValue1 : values) { + if (enumValue1 == value) continue; + builder.suggest(enumValue1.name()); + } + } + } else { + for (String s : suggestions) { + if (!Objects.equals(s, value.toString())) { + builder.suggest(s); + } + } + } + + return builder.buildFuture(); + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String path = context.getArgument(PathArgument.class); + String value = context.getArgument(ValueArgument.class); + if (parent.config.setConfig(path, value)) { + parent.config.reloadAsync(true).thenAccept(nullValue -> context.getSender().sendMessage( + Component + .text("Set Config " + path + " to " + value + " successfully!") + .color(TextColor.color(0, 255, 0)) + )); + } else { + context.getSender().sendMessage( + Component + .text("Failed to set config " + path + " to " + value + "!") + .color(TextColor.color(255, 0, 0)) + ); + } + return true; + } + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/SubmitCommand.java b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/SubmitCommand.java new file mode 100644 index 0000000..93d8236 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/commands/config/sub/SubmitCommand.java @@ -0,0 +1,37 @@ +package moe.monnairealms.camellia.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import moe.monnairealms.camellia.commands.config.ConfigCommand; +import moe.monnairealms.camellia.commands.config.ConfigSubcommand; +import moe.monnairealms.camellia.utils.dialog.ConfigCommandDialog; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.ArgumentNode; +import org.leavesmc.leaves.command.CommandContext; + +import java.util.Arrays; + +public class SubmitCommand extends ConfigSubcommand { + public SubmitCommand(ConfigCommand parent) { + super("submit", parent); + children( + new PathArgument(parent) + ); + } + + static class PathArgument extends ArgumentNode { + protected final ConfigCommand parent; + + PathArgument(ConfigCommand parent) { + super("path", StringArgumentType.greedyString()); + this.parent = parent; + } + + @Override + protected boolean execute(@NotNull CommandContext context) { + String content = context.getRange().get(context.getInput()); + String[] args = org.apache.commons.lang3.StringUtils.split(content, ' '); + ConfigCommandDialog.processSubmit(context.getSender(), parent.config, Arrays.copyOfRange(args, 2, args.length)); + return true; + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigManager.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigManager.java new file mode 100644 index 0000000..5f7e7d5 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigManager.java @@ -0,0 +1,145 @@ +package moe.monnairealms.camellia.config; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import moe.monnairealms.camellia.commands.CommandRegister; +import moe.monnairealms.camellia.config.flags.TransformedConfig; +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class ConfigManager { + private static boolean initialized = false; + private static final ConfigsInstanceBuilder builder = new ConfigsInstanceBuilder(); + private static final Map configfiles = new HashMap<>(); + private static final Collection runnableBeforeFinalLoad = new ConcurrentLinkedQueue<>(); + private static final Map needTransformedConfigs = new ConcurrentHashMap<>(); + // String[]: + // 0 -> origin key + // 1 -> target key + // 2 -> origin full path + // 3 -> target full path + + public static void initConfigs() { + registerConfig("camellia", builder.of("camellia", "moe.monnairealms.camellia.config.modules")); + preLoad(); + } + + public static void registerConfig(String name, ConfigsInstance config) { + configfiles.put(name, config); + } + + public static void preLoad() { + CompletableFuture[] futures = configfiles.values().stream() + .map(config -> CompletableFuture.runAsync(() -> { + try { + config.preLoadConfig(); + } catch (IOException e) { + throw new RuntimeException("Failed to preload config", e); + } + })) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).join(); + acceptTransformedConfigs(); + } + + public static void loadConfigFiles() { + runTaskBeforeFinalLoad(); + // Finalize loading + CompletableFuture[] futures = configfiles.values().stream() + .map(config -> CompletableFuture.runAsync(config::finalizeLoadConfig)) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).join(); + CommandRegister.register(); // register command after config loaded to enable some command didn't depend on config files + initialized = true; + } + + public static void registerRunnableBeforeFinalLoad(Runnable runnable) { + if (initialized) return; + runnableBeforeFinalLoad.add(runnable); + } + + public static void registerTransformedConfig(@NotNull String origin, @NotNull String target, @NotNull String originKey, @NotNull String targetKey, TransformedConfig transformedConfig) { + if (initialized) return; + needTransformedConfigs.put(transformedConfig, new String[]{origin, target, originKey, targetKey}); + } + + public static ConfigsInstance getConfigs(String name) { + return configfiles.get(name); + } + + public static ConfigsInstanceBuilder getBuilder() { + return builder; + } + + static void runTaskBeforeFinalLoad() { + runnableBeforeFinalLoad.forEach(Runnable::run); + runnableBeforeFinalLoad.clear(); + } + + public static void reApplyStagedConfigs() { + CompletableFuture[] futures = configfiles.values().stream() + .map(config -> CompletableFuture.runAsync(config::reApplyStagedConfigs)) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).join(); + } + + public static void saveConfigs() { + saveConfigs(true); + } + + public static void saveConfigs(boolean async) { + if (async) { + CompletableFuture[] futures = configfiles.values().stream() + .map(config -> CompletableFuture.runAsync(config::saveConfigs)) + .toArray(CompletableFuture[]::new); + CompletableFuture.allOf(futures).join(); + } else { + configfiles.values().forEach(ConfigsInstance::saveConfigs); + } + } + + public static void acceptTransformedConfigs() { + Set toReload = new HashSet<>(); + for (Map.Entry entry : needTransformedConfigs.entrySet()) { + String[] config = entry.getValue(); + TransformedConfig transformedConfig = entry.getKey(); + ConfigsInstance origin = getConfigs(config[0]); + ConfigsInstance target = getConfigs(config[1]); + if (origin == null || target == null) continue; + CommentedFileConfig originConfig = origin.getFileInstance(); + CommentedFileConfig targetConfig = target.getFileInstance(); + + final String oldConfigKeyName = config[2]; + final String newConfigKeyName = config[3]; + Object oldValue = originConfig.get(oldConfigKeyName); + if (oldValue != null) { + boolean success = true; + if (transformedConfig.transform()) { + try { + for (Class logic : transformedConfig.transformLogic()) { + oldValue = logic.getDeclaredConstructor().newInstance().transform(oldValue); + } + oldValue = new DefaultTransformLogic().transform(oldValue); + targetConfig.set(newConfigKeyName, oldValue); + if (transformedConfig.transformComments()) { + targetConfig.setComment(newConfigKeyName, originConfig.getComment(oldConfigKeyName)); + } + } catch (Exception e) { + success = false; + target.logger.error("Failed to transform removed config {}!", transformedConfig.name()); + } + } + + if (success) origin.removeConfig(oldConfigKeyName, transformedConfig.directory()); + } + toReload.add(target); + toReload.add(origin); + } + toReload.forEach(ConfigsInstance::saveConfigs); + needTransformedConfigs.clear(); // free space when all done + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigsInstance.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigsInstance.java new file mode 100644 index 0000000..b342e50 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigsInstance.java @@ -0,0 +1,1065 @@ +package moe.monnairealms.camellia.config; + +import com.electronwill.nightconfig.core.UnmodifiableConfig; +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.mojang.logging.LogUtils; +import io.papermc.paper.threadedregions.RegionizedServer; +import moe.monnairealms.camellia.api.config.CamelliaConfigsInstance; +import moe.monnairealms.camellia.api.config.ConfigDataPair; +import moe.monnairealms.camellia.commands.config.ConfigCommand; +import moe.monnairealms.camellia.config.flags.*; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import moe.monnairealms.camellia.utils.ClassLoadUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; + +/** + * Configuration instance manager that handles loading, reloading and managing configuration modules. + */ +public class ConfigsInstance implements CamelliaConfigsInstance { + public final Logger logger = LogUtils.getClassLogger(); + + // Basic configuration properties + private final ClassLoader loader; + private final File baseConfigFolder; + private final File baseConfigFile; + private final String name; // Used to transform config to another config system + private final String commandName; // Used to register command + private final String pack; // Used to find all classes + + // Storage collections + private final Map> allInstanced = new HashMap<>(); // add exception to map to store exceptions + private final Map stagedConfigMap = new HashMap<>(); + private final Map defaultvalueMap = new HashMap<>(); + private final Map suggestionsMap = new HashMap<>(); + + // Constants and state flags + public boolean alreadyInit = false; + private CommentedFileConfig configFileInstance = null; + + /** + * Private constructor to create a configuration instance + */ + protected ConfigsInstance(@NotNull ClassLoader loader, @NotNull File base, @NotNull String name, @NotNull String file_name, + @NotNull String command_name, @NotNull String pack) { + this.loader = loader; + this.baseConfigFolder = base; + this.name = name; + this.pack = pack; + this.commandName = command_name; + this.baseConfigFile = new File(base, file_name); + } + + // Lifecycle management methods + // ======================================================================== + + /** + * Setup the configuration latch by registering the command + */ + public void setupLatch() { + ConfigCommand command = new ConfigCommand(name, commandName, this); + command.register(); + alreadyInit = true; + } + + /** + * Reload configuration with default settings (keeping comments) + */ + public void reload() { + reload(true); + } + + /** + * Reload configuration with option to keep comments + */ + public void reload(boolean keepComments) { + reload(keepComments, true); + } + + public void reload(boolean keepComments, boolean needGlobal) { + if (needGlobal) { + RegionizedServer.ensureGlobalTickThread("Reload " + baseConfigFile.getName() + " off global region thread!"); + } + runUnloadTasks(); + dropAllInstanced(); + try { + preLoadConfig(keepComments); + finalizeLoadConfig(); + } catch (Exception e) { + logger.error("Fail to load config file of {}.", name, e); + } + } + + /** + * Initialize configuration if this config not register in server load stage + */ + @Override + public void initialize() throws IOException { + preLoadConfig(true); + ConfigManager.acceptTransformedConfigs(); + ConfigManager.runTaskBeforeFinalLoad(); + finalizeLoadConfig(); + } + + /** + * Reload configuration asynchronously + */ + public @NotNull CompletableFuture reloadAsync(boolean keepComments) { + return CompletableFuture.runAsync(() -> reload(keepComments), task -> RegionizedServer.getInstance().addTask(() -> { + try { + task.run(); + } catch (Exception e) { + logger.error("Fail to reload config of {}", name, e); + } + })); + } + + /** + * Clear all instantiated modules + */ + public void dropAllInstanced() { + allInstanced.clear(); + } + + /** + * Run unload tasks for all modules + */ + public void runUnloadTasks() { + for (IConfigModule module : allInstanced.keySet()) { + module.onUnloaded(configFileInstance); + } + } + + /** + * Finalize configuration loading by calling onLoaded for all modules + */ + public void finalizeLoadConfig() { + for (Map.Entry> entry : allInstanced.entrySet()) { + entry.getKey().onLoaded(configFileInstance, entry.getValue()); + } + // if the config load with exceptions but allowed, remove exceptions from the map + allInstanced.replaceAll((_, _) -> null); + setupLatch(); + } + + /** + * Re-apply staged configuration values to module fields after beforeFinalLoad tasks + * This ensures that any config changes made in beforeFinalLoad are properly reflected + */ + public void reApplyStagedConfigs() { + if (stagedConfigMap.isEmpty()) { + return; + } + + // For each staged config value, update the corresponding field + for (Map.Entry> entry : allInstanced.entrySet()) { + IConfigModule module = entry.getKey(); + Field[] fields = module.getClass().getDeclaredFields(); + + for (Field field : fields) { + int modifiers = field.getModifiers(); + if (!(Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers))) { + continue; + } + + ConfigInfo configInfo = field.getAnnotation(ConfigInfo.class); + if (configInfo == null || field.getAnnotation(DoNotLoad.class) != null) { + continue; + } + + // Build full configuration key + ConfigClassInfo classInfo = getConfigClassInfo(module); + if (classInfo == null) continue; + + final List keys = new ArrayList<>(); + String name = classInfo.category().getBaseKeyName(); + if (name != null) keys.add(name); + keys.addAll(List.of(classInfo.directory())); + keys.add(classInfo.name()); + keys.addAll(List.of(configInfo.directory())); + keys.add(configInfo.name()); + final String fullConfigKeyName = String.join(".", keys); + + // Check if this config has a staged value + if (stagedConfigMap.containsKey(fullConfigKeyName)) { + try { + field.setAccessible(true); + Object stagedValue = getActualConfigValue(fullConfigKeyName); + if (stagedValue != null) { + stagedValue = tryTransform(field, stagedValue); + field.set(null, stagedValue); + } + } catch (Exception e) { + logger.error("Failed to re-apply staged config value for {}", fullConfigKeyName, e); + } + } + } + } + } + + // Configuration loading methods + // ======================================================================== + + /** + * Preload configuration with default settings (keeping comments) + */ + public void preLoadConfig() throws IOException { + preLoadConfig(true); + } + + /** + * Preload configuration with option to keep comments + */ + public void preLoadConfig(boolean keepComments) throws IOException { + baseConfigFolder.mkdirs(); + + if (!baseConfigFile.exists()) { + baseConfigFile.createNewFile(); + } + if (configFileInstance != null) { + configFileInstance.close(); + } + + configFileInstance = CommentedFileConfig.of(baseConfigFile); + configFileInstance.load(); + + try { + instanceAllModule(); + loadAllModules(keepComments); + } catch (Exception e) { + logger.error("Failed to load config modules!", e); + throw new RuntimeException(e); + } + + saveConfigs(); + } + + /** + * Load all configuration modules + */ + private void loadAllModules(boolean keepComments) { + Map> stagedMap = new HashMap<>(); + for (IConfigModule instanced : allInstanced.keySet()) { + Set exceptions = loadForSingle(instanced, keepComments); + if (exceptions != null) { + stagedMap.put(instanced, exceptions); + } + } + + loadCategoryComments(); // load base key comment + + allInstanced.putAll(stagedMap); + } + + /** + * Load config category comments + */ + private void loadCategoryComments() { + for (EnumConfigCategory category : EnumConfigCategory.values()) { + String key = category.getBaseKeyName(); + if (key == null) continue; + String comment = category.getKeyComment(); + if (comment == null) continue; + if (!completeConfigPath(key).isEmpty()) { + String comment0 = configFileInstance.getComment(key); + if (comment0 == null) { + configFileInstance.setComment(key, comment); + } + } + } + } + + /** + * Instantiate all configuration modules + */ + private void instanceAllModule() throws NoSuchMethodException, InvocationTargetException, + InstantiationException, IllegalAccessException { + for (Class clazz : ClassLoadUtil.getClasses(pack, loader)) { + if (IConfigModule.class.isAssignableFrom(clazz)) { + allInstanced.put((IConfigModule) clazz.getConstructor().newInstance(), null); + } + } + } + + /** + * Load configuration for a single module + */ + private @Nullable Set loadForSingle(@NotNull IConfigModule singleConfigModule, boolean keepComments) { + ConfigClassInfo configClassInfo = getConfigClassInfo(singleConfigModule); + if (configClassInfo == null) { + return null; + } + + // Build configuration path and handle class comments + final List category = buildConfigCategoryPath(configClassInfo); + final String fullConfigBasePath = String.join(".", category); + handleClassLevelComments(configClassInfo, fullConfigBasePath); + + // Process each field in the module + Field[] fields = singleConfigModule.getClass().getDeclaredFields(); + Set exception = new HashSet<>(); + for (Field field : fields) { + try { + processConfigField(field, singleConfigModule, category, keepComments); + } catch (Exception e) { + exception.add(e); + } + } + return exception.isEmpty() ? null : exception; + } + + /** + * Get ConfigClassInfo annotation from a config module + */ + private ConfigClassInfo getConfigClassInfo(IConfigModule singleConfigModule) { + return singleConfigModule.getClass().getAnnotation(ConfigClassInfo.class); + } + + /** + * Build configuration category path from ConfigClassInfo annotation + */ + private List buildConfigCategoryPath(ConfigClassInfo configClassInfo) { + final List category = new ArrayList<>(); + String name = configClassInfo.category().getBaseKeyName(); + if (name != null) category.add(name); + category.addAll(List.of(configClassInfo.directory())); + category.add(configClassInfo.name()); + return category; + } + + /** + * Handle class-level comments for configuration + */ + private void handleClassLevelComments(ConfigClassInfo configClassInfo, String fullConfigBasePath) { + final String comment = configFileInstance.getComment(fullConfigBasePath); + if (comment == null || comment.isBlank()) { + String comments0 = configClassInfo.comments(); + if (!comments0.isBlank()) { + configFileInstance.setComment(fullConfigBasePath, comments0); + } + } + } + + /** + * Process a single configuration field + */ + private void processConfigField(Field field, @NotNull IConfigModule singleConfigModule, + List category, boolean keepComments) throws IllegalAccessException { + int modifiers = field.getModifiers(); + if (!(Modifier.isStatic(modifiers) && !Modifier.isFinal(modifiers))) { + return; + } + + // Check for special annotations + boolean skipLoad = field.getAnnotation(DoNotLoad.class) != null; + boolean doNotReload = alreadyInit && field.getAnnotation(HotReloadUnsupported.class) != null; + ConfigInfo configInfo = field.getAnnotation(ConfigInfo.class); + + if (skipLoad || configInfo == null) { + return; + } + + // Build full configuration key + final List keys = new ArrayList<>(category); + keys.addAll(List.of(configInfo.directory())); + keys.add(configInfo.name()); + final String fullConfigKeyName = String.join(".", keys); + + field.setAccessible(true); + Object currentValue = field.get(null); + if (currentValue instanceof Enum) { + currentValue = ((Enum) currentValue).name(); + } + boolean removed = getConfigClassInfo(singleConfigModule).category() == EnumConfigCategory.REMOVED; + + // Store default value if not initialized + if (!alreadyInit && !removed) { + defaultvalueMap.put(fullConfigKeyName, currentValue); + } + + // Handle missing or removed configurations + if (!configFileInstance.contains(fullConfigKeyName) || removed) { + handleMissingOrRemovedConfig(field, fullConfigKeyName, configInfo, removed); + } else { + // Handle existing configurations + handleExistingConfig(field, fullConfigKeyName, configInfo, doNotReload, keepComments); + } + + // handle tasks need processed before config finalized + ConfigManager.registerRunnableBeforeFinalLoad(singleConfigModule::beforeFinalLoad); + } + + /** + * Handle missing or removed configuration entries + */ + private void handleMissingOrRemovedConfig(Field field, String fullConfigKeyName, + ConfigInfo configInfo, boolean removed) throws IllegalAccessException { + // Process transformed configurations + processTransformedConfigs(field, fullConfigKeyName, configInfo, removed); + + // Handle removed configurations + if (removed) { + configFileInstance.remove("removed"); + return; + } + + // Skip if value already exists + if (configFileInstance.get(fullConfigKeyName) != null) { + return; + } + + // Validate default value + Object currentValue = field.get(null); + if (currentValue instanceof Enum) { + currentValue = ((Enum) currentValue).name(); + } else if (currentValue instanceof List list) { + if (!list.isEmpty() && list.getFirst() instanceof Enum) { + List stringList = new ArrayList<>(); + for (Object item : list) { + if (item instanceof Enum e) { + stringList.add(e.name()); + } else { + stringList.add(item.toString()); + } + } + currentValue = stringList; + } + } + if (currentValue == null) { + throw new UnsupportedOperationException("Config " + configInfo.name() + "tried to add an null default value!"); + } + + // Add default value with comments + final String comments = configInfo.comments(); + if (!comments.isBlank()) { + configFileInstance.setComment(fullConfigKeyName, comments); + } + + configFileInstance.add(fullConfigKeyName, currentValue); + } + + /** + * Process transformed configurations + */ + private void processTransformedConfigs(Field field, String fullConfigKeyName, + ConfigInfo configInfo, boolean removed) { + for (TransformedConfig transformedConfig : field.getAnnotationsByType(TransformedConfig.class)) { + final String oldConfigKeyName = String.join(".", transformedConfig.directory()) + "." + transformedConfig.name(); + + if (!Objects.equals(transformedConfig.originInstance(), "")) { + ConfigManager.registerTransformedConfig(transformedConfig.originInstance(), name, + oldConfigKeyName, fullConfigKeyName, transformedConfig); + } else { + Object oldValue = configFileInstance.get(oldConfigKeyName); + if (oldValue != null) { + boolean success = true; + + if (transformedConfig.transform() && !removed) { + try { + for (Class logic : transformedConfig.transformLogic()) { + oldValue = logic.getDeclaredConstructor().newInstance().transform(oldValue); + } + configFileInstance.set(fullConfigKeyName, oldValue); + } catch (Exception e) { + success = false; + logger.error("Failed to transform removed config {}!", transformedConfig.name()); + } + + if (transformedConfig.transformComments()) { + configFileInstance.setComment(fullConfigKeyName, + configFileInstance.getComment(oldConfigKeyName)); + } + } + + if (success) { + removeConfig(oldConfigKeyName, transformedConfig.directory()); + } + + final String comments = configInfo.comments(); + if (!comments.isBlank()) { + configFileInstance.setComment(fullConfigKeyName, comments); + } + + if (!removed && configFileInstance.get(fullConfigKeyName) != null) { + break; + } + } + } + } + } + + /** + * Handle existing configuration entries + */ + private void handleExistingConfig(Field field, String fullConfigKeyName, + ConfigInfo configInfo, boolean doNotReload, + boolean keepComments) throws IllegalAccessException, IllegalFormatConversionException { + // Handle existing configurations + Object actuallyValue = getActualConfigValue(fullConfigKeyName, field); + + IllegalFormatConversionException e0 = null; + + // Transform value if needed + try { + actuallyValue = tryTransform(field, actuallyValue); + configFileInstance.set(fullConfigKeyName, actuallyValue); + } catch (IllegalFormatConversionException e) { + if (configInfo.allowAutoReset()) resetConfig(fullConfigKeyName); + e0 = e; + logger.error("Failed to transform config {}, reset to default!", fullConfigKeyName); + } + + // Update field value if hot reload is supported + if (!doNotReload) { + field.set(null, actuallyValue); + } + + // Handle comments + if (!keepComments) { + final String comments = configInfo.comments(); + configFileInstance.setComment(fullConfigKeyName, comments); + } + + // Store suggestions for command completion + if (!alreadyInit) { + CommandSuggestions commandSuggestions = field.getAnnotation(CommandSuggestions.class); + if (commandSuggestions != null) { + suggestionsMap.put(fullConfigKeyName, commandSuggestions.suggest()); + } + } + if (e0 != null) throw e0; + } + + /** + * Get the actual configuration value, handling staged values + */ + private Object getActualConfigValue(String fullConfigKeyName) { + Object actuallyValue; + if (stagedConfigMap.containsKey(fullConfigKeyName)) { + actuallyValue = stagedConfigMap.get(fullConfigKeyName); + if (actuallyValue == null) { + actuallyValue = defaultvalueMap.get(fullConfigKeyName); + } + if (actuallyValue instanceof String v) { + actuallyValue = parseListFromString(v); + } + stagedConfigMap.remove(fullConfigKeyName); + } else { + actuallyValue = configFileInstance.get(fullConfigKeyName); + } + return actuallyValue; + } + + private Object getActualConfigValue(String fullConfigKeyName, Field field) { + Object value = getActualConfigValue(fullConfigKeyName); + if (field != null && value instanceof List list && !list.isEmpty() && list.getFirst() instanceof String) { + Class elementType = getListElementType(field); + if (elementType != null && elementType.isEnum()) { + List transformedList = new ArrayList<>(); + for (Object item : list) { + String enumValue = item.toString(); + Object enumConstant = Arrays.stream(elementType.getEnumConstants()) + .filter(e -> ((Enum) e).name().equalsIgnoreCase(enumValue)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No enum constant " + elementType.getSimpleName() + "." + enumValue)); + transformedList.add(enumConstant); + } + return Collections.unmodifiableList(transformedList); + } + } + return value; + } + + // Configuration manipulation methods + // ======================================================================== + + /** + * Remove a configuration entry + */ + public void removeConfig(String name, String[] keys) { + configFileInstance.remove(name); + Object configAtPath = configFileInstance.get(String.join(".", keys)); + if (configAtPath instanceof UnmodifiableConfig && ((UnmodifiableConfig) configAtPath).isEmpty() || configAtPath == null) { + removeConfig(keys); + } + } + + /** + * Remove a configuration entry + */ + public void removeConfig(String key) { + // split on literal dot to get path segments + removeConfig(key, key.split("\\.")); + } + + /** + * Recursively remove configuration entries + */ + public void removeConfig(String[] keys) { + configFileInstance.remove(String.join(".", keys)); + Object configAtPath = configFileInstance.get(String.join(".", Arrays.copyOfRange(keys, 1, keys.length))); + if (configAtPath instanceof UnmodifiableConfig && ((UnmodifiableConfig) configAtPath).isEmpty()) { + removeConfig(Arrays.copyOfRange(keys, 1, keys.length)); + } + } + + /** + * Set configuration value by keys array + */ + public boolean setConfig(String[] keys, Object value) { + return setConfig(String.join(".", keys), value); + } + + /** + * Parse a list from string representation + */ + public Object parseListFromString(String input) { + if (input.startsWith("[") && input.endsWith("]")) { + String content = input.substring(1, input.length() - 1).trim(); + + if (content.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + boolean inQuotes = false; + boolean escapeNext = false; + + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + + if (escapeNext) { + current.append(c); + escapeNext = false; + } else if (c == '\\') { + escapeNext = true; + } else if (c == '"') { + inQuotes = !inQuotes; + } else if (c == ',' && !inQuotes) { + result.add(current.toString().trim()); + current = new StringBuilder(); + } else { + current.append(c); + } + } + + if (!current.isEmpty()) { + result.add(current.toString().trim()); + } + + return result.stream().map(s -> { + if (s.startsWith("\"") && s.endsWith("\"") && s.length() >= 2) { + return s.substring(1, s.length() - 1); + } + return s; + }).collect(Collectors.toList()); + } + return input; + } + + /** + * Convert a list to its string representation + */ + public String parseStringFromList(List list) { + String ret; + if (list.isEmpty()) { + return "[]"; + } + if (list.getFirst() instanceof String) { + ret = list.stream() + .map(obj -> { + String str = obj.toString(); + if (str.contains(",") || str.contains("\"") || str.contains(" ") || str.contains("[")) { + str = str.replace("\"", "\\\""); + return "\"" + str + "\""; + } + return str; + }) + .collect(Collectors.joining(", ", "[", "]")); + } else if (list.getFirst() instanceof Number) { + ret = list.stream() + .map(Object::toString) + .collect(Collectors.joining(", ", "[", "]")); + } else if (list.getFirst() instanceof Enum) { + ret = list.stream() + .map(obj -> { + String str = ((Enum) obj).name(); + return "\"" + str + "\""; + }) + .collect(Collectors.joining(", ", "[", "]")); + } else { + ret = list.stream() + .map(obj -> { + String str; + try { + str = (String) list.getFirst().getClass().getMethod("transformInList").invoke(obj); + } catch (Exception e) { + str = null; + } + return "\"" + str + "\""; + }) + .collect(Collectors.joining(", ", "[", "]")); + } + return ret; + } + + /** + * Set configuration value by key + */ + public boolean setConfig(String key, Object value) { + if (configFileInstance.contains(key) && configFileInstance.get(key) != null) { + stagedConfigMap.put(key, value); + return true; + } + return false; + } + + /** + * Attempt to transform a value to target type + */ + private Object tryTransform(Field field, Object value) throws IllegalAccessException { + Class targetType = field.get(null).getClass(); + if (!targetType.isAssignableFrom(value.getClass())) { + try { + if (targetType == Integer.class) { + value = Integer.parseInt(value.toString()); + } else if (targetType == Double.class) { + value = Double.parseDouble(value.toString()); + } else if (targetType == Boolean.class) { + value = Boolean.parseBoolean(value.toString()); + } else if (targetType == Long.class) { + value = Long.parseLong(value.toString()); + } else if (targetType == Float.class) { + value = Float.parseFloat(value.toString()); + } else if (targetType == String.class) { + value = value.toString(); + } else if (targetType.isEnum()) { + String enumValue = value.toString(); + // ignore case to match enum + value = Arrays.stream(targetType.getEnumConstants()) + .filter(e -> ((Enum) e).name().equalsIgnoreCase(enumValue)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No enum constant " + targetType.getSimpleName() + "." + enumValue)); + } else if (List.class.isAssignableFrom(targetType) && value instanceof List valueList) { + if (!valueList.isEmpty() && valueList.getFirst() instanceof String) { + Class elementType = getListElementType(field); + + if (elementType != null && elementType.isEnum()) { + List transformedList = new ArrayList<>(); + for (Object item : valueList) { + String enumValue = item.toString(); + Object enumConstant = Arrays.stream(elementType.getEnumConstants()) + .filter(e -> ((Enum) e).name().equalsIgnoreCase(enumValue)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No enum constant " + elementType.getSimpleName() + "." + enumValue)); + transformedList.add(enumConstant); + } + value = Collections.unmodifiableList(transformedList); + } + } + } + } catch (Exception e) { + logger.error("Failed to transform value {}!", value); + throw new IllegalFormatConversionExceptionWithOrigin((char) 0, targetType, value); + } + } + return value; + } + + private Class getListElementType(Field field) { + java.lang.reflect.Type genericType = field.getGenericType(); + if (genericType instanceof java.lang.reflect.ParameterizedType parameterizedType) { + java.lang.reflect.Type[] typeArguments = parameterizedType.getActualTypeArguments(); + if (typeArguments.length > 0 && typeArguments[0] instanceof Class) { + return (Class) typeArguments[0]; + } + } + return null; + } + + /** + * Save configuration to file + */ + public void saveConfigs() { + configFileInstance.save(); + } + + /** + * Reset configuration by keys array + */ + public void resetConfig(String[] keys) { + resetConfig(String.join(".", keys)); + } + + /** + * Reset configuration by key + */ + public void resetConfig(String key) { + stagedConfigMap.put(key, null); + } + + // Configuration retrieval methods + // ======================================================================== + + /** + * Get default configuration value as string + */ + public String getDefaultConfig(String key) { + return defaultvalueMap.get(key).toString(); + } + + /** + * Get configuration value as string by keys array + */ + public String getConfig(String[] keys) { + return getConfig(String.join(".", keys)); + } + + /** + * Get configuration value as string by key + */ + public String getConfig(String key) { + return getConfigOrigin(key).toString(); + } + + /** + * Get original configuration value by keys array + */ + public T getConfigOrigin(String[] keys) { + return getConfigOrigin(String.join(".", keys)); + } + + /** + * Get original configuration value by key + */ + public T getConfigOrigin(String key) { + return configFileInstance.get(key); + } + + /** + * Get configuration suggestions by keys array + */ + public String[] getConfigSuggestions(String[] keys) { + return getConfigSuggestions(String.join(".", keys)); + } + + /** + * Get configuration suggestions by key + */ + public String[] getConfigSuggestions(String key) { + return suggestionsMap.get(key); + } + + /** + * Get the underlying configuration file instance + */ + public CommentedFileConfig getFileInstance() { + return configFileInstance; + } + + // Configuration path completion methods + // ======================================================================== + + /** + * Complete configuration path based on partial path + */ + public List completeConfigPath(String partialPath) { + List allPaths = getAllConfigPaths(partialPath); + List result = new ArrayList<>(); + + for (String path : allPaths) { + String remaining = path.substring(partialPath.length()); + if (remaining.isEmpty()) continue; + + int dotIndex = remaining.indexOf('.'); + String suggestion = (dotIndex == -1) + ? path + : partialPath + remaining.substring(0, dotIndex); + + if (!result.contains(suggestion)) { + result.add(suggestion); + } + } + return result; + } + + /** + * Get single configuration entries under a key + */ + public List getSingleConfig(String key) { + List list = new ArrayList<>(); + if (!key.endsWith(".")) { + key += "."; + } + List checkList = completeConfigPath(key); + for (String check : checkList) { + if (completeConfigPath(check + ".").isEmpty()) { + list.add(check); + } + } + return list; + } + + /** + * Complete configuration path with specific dot index + */ + public List completeConfigPath(String partialPath, int dotIndex) { + List allPaths = getAllConfigPaths(partialPath); + Set resultSet = new HashSet<>(); + + for (String path : allPaths) { + String remaining = path.substring(partialPath.length()); + if (remaining.isEmpty()) continue; + + String fullPath = partialPath + remaining; + String[] parts = fullPath.split("\\."); + + if (dotIndex == -1 || dotIndex < parts.length) { + StringBuilder suggestionBuilder = new StringBuilder(); + for (int i = 0; i <= dotIndex; i++) { + if (i > 0) { + suggestionBuilder.append("."); + } + suggestionBuilder.append(parts[i]); + } + String suggestion = suggestionBuilder.toString(); + resultSet.add(suggestion); + } + } + + return new ArrayList<>(resultSet); + } + + /** + * Get all configuration paths starting with current path + */ + public List getAllConfigPaths(String currentPath) { + return defaultvalueMap.keySet().stream() + .filter(k -> k.startsWith(currentPath)) + .toList(); + } + + // Data retrieval methods + // ======================================================================== + + /** + * Get all configuration data + */ + public Set getAllData() { + return getData("", false, false); + } + + /** + * Get configuration data with specified prefix + */ + public Set getData(String prefix) { + return getData(prefix, false, false); + } + + /** + * Get all configuration data with comments + */ + public Set getAllDataFull() { + return getData("", true, true); + } + + /** + * Get configuration data with comments and specified prefix + */ + public Set getDataFull(String prefix) { + return getData(prefix, true, true); + } + + /** + * Get configuration data with specified prefix + */ + public Set getData(String prefix, boolean _comment, boolean _withSuggestions) { + List keys = getAllConfigPaths(prefix); + return getData(keys, _comment, _withSuggestions); + } + + /** + * Get configuration data for specified keys + */ + public Set getData(List list) { + return getData(list, false, false); + } + + /** + * Get configuration data with comments for specified keys + */ + public Set getDataWithComment(List list) { + return getData(list, true, false); + } + + public Set getDataFull(List list) { + return getData(list, true, true); + } + + /** + * Get configuration data for specified keys + */ + public Set getData(List list, boolean _comment, boolean _withSuggestions) { + Set result = new HashSet<>(); + for (String key : list) { + Object valueOrigin = configFileInstance.get(key); + Object value = valueOrigin; + if (value instanceof List list1) { + value = parseStringFromList(list1); + } else if (value instanceof Enum) { + value = ((Enum) value).name(); + } + String comment = null; + if (_comment) { + comment = configFileInstance.getComment(key); + if (comment == null || comment.isEmpty()) { + comment = null; + } + } + String[] suggestions = null; + if (_withSuggestions) { + suggestions = getConfigSuggestions(key); + } + + if (suggestions == null) { + if (valueOrigin instanceof Enum enumValue) { + Enum[] values = enumValue.getClass().getEnumConstants(); + suggestions = new String[values.length]; + for (Enum enumValue1 : values) { + suggestions[enumValue1.ordinal()] = enumValue1.name(); + } + } + } + result.add(new ConfigDataPair(key, value, comment, suggestions)); + } + return result; + } + + /** + * Clean up unused configuration entries + */ + public void clean() { + Map validValues = new HashMap<>(); + Map validComments = new HashMap<>(); + for (String key : defaultvalueMap.keySet()) { + validValues.put(key, configFileInstance.get(key)); + validComments.put(key, configFileInstance.getComment(key)); + } + configFileInstance.clear(); + validValues.forEach(configFileInstance::set); + validComments.forEach(configFileInstance::setComment); + saveConfigs(); + } + +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigsInstanceBuilder.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigsInstanceBuilder.java new file mode 100644 index 0000000..94cf9bd --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/ConfigsInstanceBuilder.java @@ -0,0 +1,82 @@ +package moe.monnairealms.camellia.config; + +import moe.monnairealms.camellia.api.config.CamelliaConfigBuilder; +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +public class ConfigsInstanceBuilder implements CamelliaConfigBuilder { + // Factory methods for creating ConfigsInstance objects + public ConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull String name, + @NotNull String pack + ) { + return this.of(loader, new File(name + "_config"), name, pack); + } + + public ConfigsInstance of( + @NotNull String name, + @NotNull String pack + ) { + return this.of(MinecraftServer.class.getClassLoader(), name, pack); + } + + public ConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull File base, + @NotNull String name, + @NotNull String pack + ) { + return this.of(loader, base, name, name + "_global_config.toml", pack); + } + + public ConfigsInstance of( + @NotNull File base, + @NotNull String name, + @NotNull String pack + ) { + return this.of(MinecraftServer.class.getClassLoader(), base, name, pack); + } + + public ConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull File base, + @NotNull String name, + @NotNull String file_name, + @NotNull String pack + ) { + return this.of(loader, base, name, file_name, name + "config", pack); + } + + public ConfigsInstance of( + @NotNull File base, + @NotNull String name, + @NotNull String file_name, + @NotNull String pack + ) { + return this.of(MinecraftServer.class.getClassLoader(), base, name, file_name, pack); + } + + public ConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull File base, + @NotNull String name, + @NotNull String file_name, + @NotNull String command_name, + @NotNull String pack + ) { + return new ConfigsInstance(loader, base, name, file_name, command_name, pack); + } + + public ConfigsInstance of( + @NotNull File base, + @NotNull String name, + @NotNull String file_name, + @NotNull String command_name, + @NotNull String pack + ) { + return new ConfigsInstance(MinecraftServer.class.getClassLoader(), base, name, file_name, command_name, pack); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/DefaultTransformLogic.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/DefaultTransformLogic.java new file mode 100644 index 0000000..4886fd5 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/DefaultTransformLogic.java @@ -0,0 +1,7 @@ +package moe.monnairealms.camellia.config; + +public class DefaultTransformLogic { + public Object transform(Object obj) { + return obj; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/IConfigModule.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/IConfigModule.java new file mode 100644 index 0000000..2c0744a --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/IConfigModule.java @@ -0,0 +1,27 @@ +package moe.monnairealms.camellia.config; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +public interface IConfigModule { + default void beforeFinalLoad() { + } + + default void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + } + + default void onUnloaded(CommentedFileConfig configInstance) { + } + + default T get(String keyName, T defaultValue, @NotNull CommentedFileConfig config) { + if (!config.contains(keyName)) { + config.set(keyName, defaultValue); + return defaultValue; + } + + return config.get(keyName); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/IllegalFormatConversionExceptionWithOrigin.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/IllegalFormatConversionExceptionWithOrigin.java new file mode 100644 index 0000000..41f98b4 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/IllegalFormatConversionExceptionWithOrigin.java @@ -0,0 +1,24 @@ +package moe.monnairealms.camellia.config; + +import java.util.IllegalFormatConversionException; + +public class IllegalFormatConversionExceptionWithOrigin extends IllegalFormatConversionException { + private final Object origin; + + /** + * Constructs an instance of this class with the mismatched conversion and + * the corresponding argument class. + * + * @param c Inapplicable conversion + * @param arg Class of the mismatched argument + * @param originalValue The original value + */ + public IllegalFormatConversionExceptionWithOrigin(char c, Class arg, Object originalValue) { + super(c, arg); + origin = originalValue; + } + + public Object getOrigin() { + return origin; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/CommandSuggestions.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/CommandSuggestions.java new file mode 100644 index 0000000..9c38b2f --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/CommandSuggestions.java @@ -0,0 +1,9 @@ +package moe.monnairealms.camellia.config.flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandSuggestions { + String[] suggest(); +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/ConfigClassInfo.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/ConfigClassInfo.java new file mode 100644 index 0000000..8901e26 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/ConfigClassInfo.java @@ -0,0 +1,17 @@ +package moe.monnairealms.camellia.config.flags; + +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigClassInfo { + EnumConfigCategory category(); + + String name(); + + String[] directory() default {}; + + String comments() default ""; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/ConfigInfo.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/ConfigInfo.java new file mode 100644 index 0000000..260dfcc --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/ConfigInfo.java @@ -0,0 +1,15 @@ +package moe.monnairealms.camellia.config.flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface ConfigInfo { + String name(); + + String[] directory() default {}; + + String comments() default ""; + + boolean allowAutoReset() default true; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/DoNotLoad.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/DoNotLoad.java new file mode 100644 index 0000000..1982c5b --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/DoNotLoad.java @@ -0,0 +1,8 @@ +package moe.monnairealms.camellia.config.flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface DoNotLoad { +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/HotReloadUnsupported.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/HotReloadUnsupported.java new file mode 100644 index 0000000..fcb4f64 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/HotReloadUnsupported.java @@ -0,0 +1,8 @@ +package moe.monnairealms.camellia.config.flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface HotReloadUnsupported { +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/TransformedConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/TransformedConfig.java new file mode 100644 index 0000000..9a19dba --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/flags/TransformedConfig.java @@ -0,0 +1,28 @@ +package moe.monnairealms.camellia.config.flags; + +import moe.monnairealms.camellia.config.DefaultTransformLogic; + +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(TransformedConfig.List.class) +public @interface TransformedConfig { + String name(); + + String[] directory(); + + String originInstance() default ""; + + boolean transform() default true; + + boolean transformComments() default true; + + Class[] transformLogic() default {}; + + @Retention(RetentionPolicy.RUNTIME) + @interface List { + TransformedConfig[] value(); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/CommandConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/CommandConfig.java new file mode 100644 index 0000000..465e632 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/CommandConfig.java @@ -0,0 +1,25 @@ +package moe.monnairealms.camellia.config.modules.experiment; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.HotReloadUnsupported; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.EXPERIMENT, name = "command") +public class CommandConfig implements IConfigModule { + @ConfigInfo(name = "enable_data_command") + @HotReloadUnsupported + public static boolean data = false; + @ConfigInfo(name = "enable_command_block", comments = """ + Force to enable command blocks. + ATTENTION: WOULD CAUSE SERVER CRASHING AS SOME THREADING ISSUE!!! + DO NOT ENABLE UNLESS YOU KNOW WHAT YOU ARE DOING!!! + """) + public static boolean commandBlock = false; + @ConfigInfo(name = "enable_tick_command", comments = """ + Only freeze/unfreeze/step/query command is allowed if you enabled it. + WARN: This should disabled in production environment! + """) + public static boolean tick = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/DisableAsyncCatcherConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/DisableAsyncCatcherConfig.java new file mode 100644 index 0000000..9689a18 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/DisableAsyncCatcherConfig.java @@ -0,0 +1,17 @@ +package moe.monnairealms.camellia.config.modules.experiment; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.EXPERIMENT, name = "disable_async_catchers") +public class DisableAsyncCatcherConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Disable async catcher to prevent some crashes caused by some plugins which supports folia but has issuable logics. + ATTENTION: Would cause region deadlock when getChunkAt was incorrectly called! + See: https://github.com/PaperMC/Folia/issues/280 which is resolved in folia(https://github.com/PaperMC/Folia/commit/2e7bc0721af95196c85500c7bb136aeea0bc12ce) + DO NOT ENABLE UNLESS YOU KNOW WHAT YOU ARE DOING!!! + """) + public static boolean enabled = false; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/DisableEntityCatchConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/DisableEntityCatchConfig.java new file mode 100644 index 0000000..815d1fd --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/experiment/DisableEntityCatchConfig.java @@ -0,0 +1,15 @@ +package moe.monnairealms.camellia.config.modules.experiment; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.EXPERIMENT, name = "disable_entity_exception_catchers") +public class DisableEntityCatchConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + If this config enabled, the server will crash directly when entity ticking has some errors instead of removing the entity to keep server running. + It could prevent entity disappearing but may cause more server crashes. + DO NOT ENABLE UNLESS YOU KNOW WHAT YOU ARE DOING!!!""") + public static boolean enabled = false; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/CollisionBehaviorConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/CollisionBehaviorConfig.java new file mode 100644 index 0000000..870c79f --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/CollisionBehaviorConfig.java @@ -0,0 +1,22 @@ +package moe.monnairealms.camellia.config.modules.fixes; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.CommandSuggestions; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumCollisionBehaviorMode; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "collision_behavior") +public class CollisionBehaviorConfig implements IConfigModule { + @CommandSuggestions(suggest = {"VANILLA", "BLOCK_SHAPE_VANILLA", "PAPER"}) + @ConfigInfo(name = "mode", comments = + """ + Decides which collision logics will be used(Moonrise and Paper modified this for optimization but would also break some vanilla behaviours at the same time). + Would be useful for fixing improper behaviours of some huge redstone machines + Available Value: + VANILLA + BLOCK_SHAPE_VANILLA + PAPER""") + public static EnumCollisionBehaviorMode behaviorMode = EnumCollisionBehaviorMode.BLOCK_SHAPE_VANILLA; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/FoliaEntityMovingFixConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/FoliaEntityMovingFixConfig.java new file mode 100644 index 0000000..7a91a8e --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/FoliaEntityMovingFixConfig.java @@ -0,0 +1,21 @@ +package moe.monnairealms.camellia.config.modules.fixes; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "fix_high_velocity_issue") +public class FoliaEntityMovingFixConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = + """ + A simple fix of an issue on folia\s + (Sometimes the entity would\s + have a large moment that cross the\s + different tick regions, and it would\s + make the server crashed) but sometimes it might doesn't work""") + public static boolean enabled = false; + + @ConfigInfo(name = "warn_on_detected") + public static boolean warnOnDetected = false; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/ForceCleanupEntityBrainMemoryConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/ForceCleanupEntityBrainMemoryConfig.java new file mode 100644 index 0000000..1c988db --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/ForceCleanupEntityBrainMemoryConfig.java @@ -0,0 +1,18 @@ +package moe.monnairealms.camellia.config.modules.fixes; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "force_cleanup_drop_non_owned_entity_memory_module", comments = "This config is a temporary fix for those incorrect owned data in the memory of each mob, for more you can see https://github.com/PaperMC/Folia/issues/203") +public class ForceCleanupEntityBrainMemoryConfig implements IConfigModule { + @ConfigInfo(name = "enabled_for_entity", comments = "When enabled, the entity's brain will clean the memory which is typed of entity and not belong to current tickregion") + public static boolean enabledForEntity = false; + + @ConfigInfo(name = "enabled_for_block_pos", comments = "When enabled, the entity's brain will clean the memory which is typed of block_pos and not belong to current tickregion") + public static boolean enabledForBlockPos = false; + + @ConfigInfo(name = "enabled_for_position_tracker", comments = "When enabled, the entity's brain will clean the memory which is typed of position_tracker and not belong to current tickregion") + public static boolean enabledForPositionTracker = false; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/ItemMultitaskConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/ItemMultitaskConfig.java new file mode 100644 index 0000000..c8b822c --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/ItemMultitaskConfig.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.fixes; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "item_multitask") +public class ItemMultitaskConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Prevent the server from interrupting the state of items + during block interactions or hotbar slot changes.""") + public static boolean enabled = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/LongCommandSupportConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/LongCommandSupportConfig.java new file mode 100644 index 0000000..2315e63 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/LongCommandSupportConfig.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.fixes; + +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "long_command_support") +public class LongCommandSupportConfig { + @ConfigInfo(name = "enabled", comments = """ + Some long commands can be run through the dialog command, + but paper has prohibited it. + Enable this to fix this problem.""") + public static boolean enabled = true; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/POIRangeFixes.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/POIRangeFixes.java new file mode 100644 index 0000000..42eb939 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/POIRangeFixes.java @@ -0,0 +1,15 @@ +package moe.monnairealms.camellia.config.modules.fixes; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(name = "poi_range_fixes", category = EnumConfigCategory.FIXES) +public class POIRangeFixes implements IConfigModule { + @ConfigInfo(name = "do_not_compete_poi_if_unloaded", comments = """ + Do not compete POI if it's unloaded + Related with https://github.com/PaperMC/Folia/issues/292 + """) + public static boolean doNotCompetePOIIfUnloaded = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/PathfindingFixesConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/PathfindingFixesConfig.java new file mode 100644 index 0000000..8b92b48 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/PathfindingFixesConfig.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.fixes; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "pathfinding_fixes") +public class PathfindingFixesConfig implements IConfigModule { + @ConfigInfo(name = "break_down_pathfinding_when_out_of_region", comments = "Recompute path or stop pathfinding when it's touching the blocks out of current tick region") + public static boolean breakDownPathfindingWhenOutOfRegion = false; + @ConfigInfo(name = "do_not_pathfind_to_not_owned_targets", comments = "Skip pathfinding target when it's out of current tick region") + public static boolean doNotPathfindToNotOwnedTargets = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/UnsafeTeleportationConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/UnsafeTeleportationConfig.java new file mode 100644 index 0000000..cec7013 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/fixes/UnsafeTeleportationConfig.java @@ -0,0 +1,15 @@ +package moe.monnairealms.camellia.config.modules.fixes; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "allow_unsafe_teleportation") +public class UnsafeTeleportationConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Allow non player entities enter end portals if enabled. + If you want to use sand duping,please turn on this. + Warning: This would cause some unsafe issues, you could learn more on : https://github.com/PaperMC/Folia/issues/297""") + public static boolean enabled = false; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/MembarConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/MembarConfig.java new file mode 100644 index 0000000..753c8fd --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/MembarConfig.java @@ -0,0 +1,50 @@ +package moe.monnairealms.camellia.config.modules.function; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.DoNotLoad; +import moe.monnairealms.camellia.enums.EnumBarType; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import moe.monnairealms.camellia.enums.EnumStatusBarDisplay; +import moe.monnairealms.camellia.functions.bars.TickableStatusBarList; +import net.kyori.adventure.bossbar.BossBar; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "membar") +public class MembarConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean memoryBarEnabled = false; + @ConfigInfo(name = "format") + public static String memBarFormat = "Memory usage : MB/MB"; + @ConfigInfo(name = "bar_color_list") + public static List barColors = List.of(BossBar.Color.GREEN, BossBar.Color.YELLOW, BossBar.Color.RED, BossBar.Color.PURPLE); + @ConfigInfo(name = "memory_color_list") + public static List memColors = List.of("", "", "", ""); + @ConfigInfo(name = "update_interval_ticks") + public static int updateInterval = 15; + @ConfigInfo(name = "display", comments = "Available displays: BOSS_BAR, ACTION_BAR, TAB_LIST") + public static EnumStatusBarDisplay display = EnumStatusBarDisplay.BOSS_BAR; + + @DoNotLoad + private static boolean inited = false; + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + TickableStatusBarList.raiseGlobalReload(EnumBarType.MEMORY); + + if (!inited) { // command has moved to CommandRegister + inited = true; + } + } + + @Override + public void onUnloaded(CommentedFileConfig configInstance) { + Bukkit.getCommandMap().getKnownCommands().remove("luminol:membar"); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/PortalRateLimiterConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/PortalRateLimiterConfig.java new file mode 100644 index 0000000..645c68d --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/PortalRateLimiterConfig.java @@ -0,0 +1,66 @@ +package moe.monnairealms.camellia.config.modules.function; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.HotReloadUnsupported; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import net.objecthunter.exp4j.Expression; +import net.objecthunter.exp4j.ExpressionBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ConfigClassInfo(name = "portal_rate_limit", category = EnumConfigCategory.FUNCTION) +public class PortalRateLimiterConfig implements IConfigModule { + @ConfigInfo(name = "enable", comments = "Whether or not to limit the portal rate when entity goes into portals") + @HotReloadUnsupported + public static boolean enabled = false; + + @ConfigInfo(name = "maximum_portal_teleports_per_tick", comments = """ + Decides how much portal teleportation should be handled within a tick in a single tick region,when exceed, + the portal teleportation will be pushed into the next tick + + Note: set to -1 to use custom expressions""") + @HotReloadUnsupported + public static int maxPortalTeleportsPerTick = 200; + + @ConfigInfo(name = "maximum_portal_teleports_per_tick_expression", comments = """ + If the fixed limit is not enough for use, you could define your own expression to dynamically limit the + portal rate. + + Available variables(all is of current tickregion): e (ticking_entity_count) + c (ticking_chunk_count) + p (player_count) + Example: 50 * (1 + sqrt(x/1000) + c/200 + p/5) + """) + @HotReloadUnsupported + public static String maxPortalTeleportsExpression = "50 * (1 + sqrt(e/1000) + c/200 + p/5)"; + + // use this to prevent reallocation + private static final String VARIABLE_TICKING_ENTITY_CONT = "e"; + private static final String VARIABLE_TICKING_CHUNK_CONT = "c"; + private static final String VARIABLE_PLAYER_CONT = "p"; + + @Nullable + public static Expression getExpressionIfConfigured() { + if (maxPortalTeleportsPerTick != -1) { + return null; + } + + return new ExpressionBuilder(maxPortalTeleportsExpression) + .variables( + VARIABLE_PLAYER_CONT, + VARIABLE_TICKING_CHUNK_CONT, + VARIABLE_TICKING_ENTITY_CONT + ) + .build(); + } + + public static int computeExpression(@NotNull Expression expression, int entityCount, int chunkCount, int playerCount) { + expression.setVariable(VARIABLE_TICKING_ENTITY_CONT, entityCount); + expression.setVariable(VARIABLE_TICKING_CHUNK_CONT, chunkCount); + expression.setVariable(VARIABLE_PLAYER_CONT, playerCount); + + return (int) expression.evaluate(); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/RegionBarConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/RegionBarConfig.java new file mode 100644 index 0000000..4368d78 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/RegionBarConfig.java @@ -0,0 +1,50 @@ +package moe.monnairealms.camellia.config.modules.function; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.DoNotLoad; +import moe.monnairealms.camellia.enums.EnumBarType; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import moe.monnairealms.camellia.enums.EnumStatusBarDisplay; +import moe.monnairealms.camellia.functions.bars.TickableStatusBarList; +import net.kyori.adventure.bossbar.BossBar; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "regionbar") +public class RegionBarConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean regionbarEnabled = false; + @ConfigInfo(name = "format") + public static String regionBarFormat = "Util: Chunks: Players: Entities: "; + @ConfigInfo(name = "bar_color_list") + public static List barColors = List.of(BossBar.Color.GREEN, BossBar.Color.YELLOW, BossBar.Color.RED, BossBar.Color.PURPLE); + @ConfigInfo(name = "util_color_list") + public static List utilColors = List.of("", "", "", ""); + @ConfigInfo(name = "update_interval_ticks") + public static int updateInterval = 15; + @ConfigInfo(name = "display", comments = "Available displays: BOSS_BAR, ACTION_BAR, TAB_LIST") + public static EnumStatusBarDisplay display = EnumStatusBarDisplay.BOSS_BAR; + + @DoNotLoad + private static boolean inited = false; + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + TickableStatusBarList.raiseGlobalReload(EnumBarType.REGION); + + if (!inited) { // command has moved to CommandRegister + inited = true; + } + } + + @Override + public void onUnloaded(CommentedFileConfig configInstance) { + Bukkit.getCommandMap().getKnownCommands().remove("luminol:regionbar"); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/RegionFormatConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/RegionFormatConfig.java new file mode 100644 index 0000000..8a06db3 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/RegionFormatConfig.java @@ -0,0 +1,81 @@ +package moe.monnairealms.camellia.config.modules.function; + +import abomination.LinearRegionFile; +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.IllegalFormatConversionExceptionWithOrigin; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.DoNotLoad; +import moe.monnairealms.camellia.config.flags.HotReloadUnsupported; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import moe.monnairealms.camellia.enums.EnumRegionFormat; +import moe.monnairealms.camellia.utils.BufferedLinearRegionFileFlusher; +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "region_format") +public class RegionFormatConfig implements IConfigModule { + @HotReloadUnsupported + @ConfigInfo(name = "format", allowAutoReset = false, comments = "Available choices: MCA, B_LINEAR, LINEAR_V2") + public static EnumRegionFormat regionFormat = EnumRegionFormat.MCA; + @HotReloadUnsupported + @ConfigInfo(name = "linear_compression_level", comments = "Decides the compression level of the region file(Only works for LINEAR_V2 and B_LINEAR)") + public static int linearCompressionLevel = 1; + @HotReloadUnsupported + @ConfigInfo(name = "linear_io_thread_count", comments = "Decides the worker thread count of linear(Only works for LINEAR_V2)") + public static int linearIoThreadCount = 6; + @HotReloadUnsupported + @ConfigInfo(name = "linear_io_flush_delay_ms", comments = "Decides when it will be flushed to the region file when it has been marked to save for n(default is 100) milliseconds(Only works for LINEAR_V2)") + public static int linearIoFlushDelayMs = 100; + @HotReloadUnsupported + @ConfigInfo(name = "blinear_io_flush_delay_ms", comments = "Decides when it will be flushed to the region file when there has been no write operations for n(default is 3000) milliseconds(Only works for B_LINEAR)") + public static int blinearIoFlushDelayMs = 3000; + @HotReloadUnsupported + @ConfigInfo(name = "blinear_io_thread_count", comments = "Decides the worker thread count of buffered linear(Only works for B_LINEAR)") + public static int blinearIoThreadCount = 6; + @HotReloadUnsupported + @ConfigInfo(name = "linear_use_virtual_thread", comments = "Decides if it could use virtual threads for linear format(Only works for LINEAR_V2)") + public static boolean linearUseVirtualThread = true; + + @DoNotLoad + public static BufferedLinearRegionFileFlusher blinearFlusher = null; + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set exs) { + if (exs != null) { + for (Exception e : exs) { + if (e instanceof IllegalFormatConversionExceptionWithOrigin) { + throw new RuntimeException("Invalid region format: " + ((IllegalFormatConversionExceptionWithOrigin) e).getOrigin().toString()); + } + } + } + + if (regionFormat == EnumRegionFormat.LINEAR_V2) { + checkCompressionLevel(); + + LinearRegionFile.SAVE_DELAY_MS = linearIoFlushDelayMs; + LinearRegionFile.SAVE_THREAD_MAX_COUNT = linearIoThreadCount; + LinearRegionFile.USE_VIRTUAL_THREAD = linearUseVirtualThread; + } + + if (regionFormat == EnumRegionFormat.B_LINEAR) { + blinearFlusher = new BufferedLinearRegionFileFlusher(blinearIoThreadCount, 20, blinearIoFlushDelayMs); + + checkCompressionLevel(); + + // we don't need to consider that it will be reloaded more than once as this config is unreloadable + Runtime.getRuntime().addShutdownHook(new Thread(() -> blinearFlusher.shutdown())); + } + } + + private static void checkCompressionLevel() { + if (RegionFormatConfig.linearCompressionLevel > 23 || RegionFormatConfig.linearCompressionLevel < 1) { + MinecraftServer.LOGGER.error("Linear or BufferedLinear region compression level should be between 1 and 22 in config: {}", RegionFormatConfig.linearCompressionLevel); + MinecraftServer.LOGGER.error("Falling back to compression level 1."); + RegionFormatConfig.linearCompressionLevel = 1; + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/SecureSeedConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/SecureSeedConfig.java new file mode 100644 index 0000000..c19f290 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/SecureSeedConfig.java @@ -0,0 +1,41 @@ +package moe.monnairealms.camellia.config.modules.function; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.HotReloadUnsupported; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +import java.security.SecureRandom; +import java.util.Base64; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "secure_seed") +public class SecureSeedConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Once you enable secure seed, all ores and structures are generated with 1024-bit seed + instead of using 64-bit seed in vanilla, making traditional seed cracking impossible. + Note: If you use V1 it will be vulnerable to terrain elevation attacks. + ***** WARN: You need keep it enabled if your old world are also using secure seed! Or it will kill your save *****""") + @HotReloadUnsupported + public static boolean enabled = false; + + @ConfigInfo(name = "version", comments = """ + Version 1: Blake2b (insecure, reversible with a GPU/ASIC cluster in minutes with enough entropy) + Version 2: Blake3 with salt key derivation (recommended, irreversible) + ***** WARN: Switching versions will cause chunk errors! *****""") + @HotReloadUnsupported + public static int version = 1; + + @ConfigInfo(name = "salt", comments = """ + Auto-generated 256-bit salt for V2 cryptographic operations. + Generated once on first startup - DO NOT SHARE THIS OR MODIFY (MODIFYING THIS WILL CAUSE CHUNK ERRORS)! + Used with Blake3 keyed hash to make seed irreversible.""") + @HotReloadUnsupported + public static String salt = generateSalt(); + + private static String generateSalt() { + byte[] saltBytes = new byte[32]; + new SecureRandom().nextBytes(saltBytes); + return Base64.getEncoder().encodeToString(saltBytes); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/TpsBarConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/TpsBarConfig.java new file mode 100644 index 0000000..99c8a12 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/TpsBarConfig.java @@ -0,0 +1,58 @@ +package moe.monnairealms.camellia.config.modules.function; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.DoNotLoad; +import moe.monnairealms.camellia.enums.EnumBarType; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import moe.monnairealms.camellia.enums.EnumStatusBarDisplay; +import moe.monnairealms.camellia.functions.bars.TickableStatusBarList; +import net.kyori.adventure.bossbar.BossBar; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "tpsbar") +public class TpsBarConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean tpsbarEnabled = false; + @ConfigInfo(name = "format") + public static String tpsBarFormat = "TPS: MSPT: Ping: ms ChunkHot: "; + @ConfigInfo(name = "bar_color_list") + public static List barColors = List.of(BossBar.Color.GREEN, BossBar.Color.YELLOW, BossBar.Color.RED, BossBar.Color.PURPLE); + @ConfigInfo(name = "tps_color_list") + public static List tpsColors = List.of("", "", "", ""); + @ConfigInfo(name = "ping_color_list") + public static List pingColors = List.of("", "", "", ""); + @ConfigInfo(name = "chunkhot_color_list") + public static List chunkHotColors = List.of("", "", "", ""); + @ConfigInfo(name = "update_interval_ticks") + public static int updateInterval = 15; + @ConfigInfo(name = "precision_of_tps_value", comments = "Example(if tps is 20.00000000)(value -> result): 2 -> 20.00, 1 -> 20.0") + public static int precisionOfTPS = 2; + @ConfigInfo(name = "precision_of_mspt_value", comments = "Example(if mspt is 20.00000000)(value -> result): 2 -> 20.00, 1 -> 20.0") + public static int precisionOfMSPT = 2; + @ConfigInfo(name = "display", comments = "Available displays: BOSS_BAR, ACTION_BAR, TAB_LIST") + public static EnumStatusBarDisplay display = EnumStatusBarDisplay.BOSS_BAR; + + @DoNotLoad + private static boolean inited = false; + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + TickableStatusBarList.raiseGlobalReload(EnumBarType.TPS); + + if (!inited) { // command has moved to CommandRegister + inited = true; + } + } + + @Override + public void onUnloaded(CommentedFileConfig configInstance) { + Bukkit.getCommandMap().getKnownCommands().remove("luminol:tpsbar"); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/TripwireBehaviorConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/TripwireBehaviorConfig.java new file mode 100644 index 0000000..f67aa88 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/function/TripwireBehaviorConfig.java @@ -0,0 +1,21 @@ +package moe.monnairealms.camellia.config.modules.function; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import moe.monnairealms.camellia.enums.EnumTripwireBehavior; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "tripwire_dupe") +public class TripwireBehaviorConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean enabled = false; + + @ConfigInfo(name = "behavior_mode", comments = + """ + Available Value: + VANILLA20 + VANILLA21 + MIXED""") + public static EnumTripwireBehavior behaviorMode = EnumTripwireBehavior.VANILLA21; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/DisableWarningConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/DisableWarningConfig.java new file mode 100644 index 0000000..25037ef --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/DisableWarningConfig.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.misc; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "disable_warning") +public class DisableWarningConfig implements IConfigModule { + @ConfigInfo(name = "disable_heightmap_warning", comments = + """ + Disable heightmap-check's warning""") + public static boolean disableHeightmapWarning = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/FoliaWatchogConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/FoliaWatchogConfig.java new file mode 100644 index 0000000..e4fee6a --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/FoliaWatchogConfig.java @@ -0,0 +1,12 @@ +package moe.monnairealms.camellia.config.modules.misc; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "folia_watchdog") +public class FoliaWatchogConfig implements IConfigModule { + @ConfigInfo(name = "tick_region_time_out_ms", comments = "Decides the interval of the watchdog prints the threads dumps of tickregions in stuck") + public static int tickRegionTimeOutMs = 5000; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/PaperPacketLimiterConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/PaperPacketLimiterConfig.java new file mode 100644 index 0000000..ddd9b1d --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/PaperPacketLimiterConfig.java @@ -0,0 +1,15 @@ +package moe.monnairealms.camellia.config.modules.misc; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "force_disable_packet_limiter_of_paper", comments = + "Force and fully disable all packet limiters of Paper, which is used to prevent from kicking by using some quick crafting mods but \n" + + "has negative impacts on security" +) +public class PaperPacketLimiterConfig implements IConfigModule { + @ConfigInfo(name = "force_disable") + public static boolean forceDisable = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/SavePortalTicketsConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/SavePortalTicketsConfig.java new file mode 100644 index 0000000..1dd4121 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/SavePortalTicketsConfig.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.misc; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "save_portal_tickets") +public class SavePortalTicketsConfig implements IConfigModule { + @ConfigInfo(name = "do_save", comments = "whether or not to save the portal tickets when server stopping," + + " this would make it acts like mc before 1.21.5," + + " and won't auto active the portal chunk loader when server started again.") + public static boolean doSave = true; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/ServerModNameConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/ServerModNameConfig.java new file mode 100644 index 0000000..622b1b4 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/ServerModNameConfig.java @@ -0,0 +1,15 @@ +package moe.monnairealms.camellia.config.modules.misc; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "server_mod_name") +public class ServerModNameConfig implements IConfigModule { + @ConfigInfo(name = "name", comments = "Decides the server mod name shown in your F3 debug screen.") + public static String serverModName = "Camellia"; + + @ConfigInfo(name = "vanilla_spoof", comments = "Ignore any plugin's modification and server mod name set in this config block, only force sending brand name of vanilla") + public static boolean fakeVanilla = false; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/UsernameCheckConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/UsernameCheckConfig.java new file mode 100644 index 0000000..499e793 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/misc/UsernameCheckConfig.java @@ -0,0 +1,63 @@ +package moe.monnairealms.camellia.config.modules.misc; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.mojang.logging.LogUtils; +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.DoNotLoad; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.Set; +import java.util.regex.Pattern; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "username_checks") +public class UsernameCheckConfig implements IConfigModule { + @DoNotLoad + private static final Logger LOGGER = LogUtils.getLogger(); + + @ConfigInfo(name = "enabled", comments = "Decide whether the username checks are enabled, \n" + + " you could disable it if your players are using Chinese username but also notification any security impacts caused by disabling it") + public static boolean enabled = true; + @ConfigInfo(name = "enforce_skull_validation", comments = """ + Enforce skull validation, preventing skulls with invalid names from disconnecting the client. + """) + public static boolean enforceSkullValidation = true; + @ConfigInfo(name = "allow_old_player_join", comments = """ + Allow old players to join the server after the username regex is changed, + even if their names don't meet the new requirements. + """) + public static boolean allowOldPlayersJoin = false; + + @DoNotLoad + private static final String defaultUsernameCheckRegex = "^[a-zA-Z0-9_.]*$"; + @ConfigInfo(name = "username_check_regex", comments = """ + Use username regex to validate usernames, + allowing only characters specified in the regex. + """) + public static final String usernameCheckRegex = defaultUsernameCheckRegex; + + @DoNotLoad + public static Pattern usernameRegex; + + public static boolean useCustomUsernameRegex() { + return !usernameCheckRegex.equals(defaultUsernameCheckRegex); + } + + public static boolean shouldSkipNonPlayerNameCheck() { // helper + return !enabled || !usernameCheckRegex.equals(defaultUsernameCheckRegex); + } + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + try { + usernameRegex = Pattern.compile(usernameCheckRegex); + } catch (Exception ex) { + LOGGER.error("Failed to parse regex! Falling back to default", ex); + + usernameRegex = Pattern.compile(defaultUsernameCheckRegex); + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/AsyncProtocolChangeConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/AsyncProtocolChangeConfig.java new file mode 100644 index 0000000..e959fb1 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/AsyncProtocolChangeConfig.java @@ -0,0 +1,15 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "use_async_protocol_switching") +public class AsyncProtocolChangeConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Uses async protocol preparation for mc. + Warn: Due to the packet sequence was changed by this optimization, it might be\s + uncompatible with some plugins(ViaVersion etc.)""") + public static boolean enabled = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/CpuAffinityConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/CpuAffinityConfig.java new file mode 100644 index 0000000..d22b8f5 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/CpuAffinityConfig.java @@ -0,0 +1,122 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.mojang.logging.LogUtils; +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.DoNotLoad; +import moe.monnairealms.camellia.config.flags.HotReloadUnsupported; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import moe.monnairealms.camellia.utils.AffinityRunnableWrapper; +import net.openhft.affinity.Affinity; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; + +import java.util.BitSet; +import java.util.List; +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "cpu_affinity") +public class CpuAffinityConfig implements IConfigModule { + @HotReloadUnsupported + @ConfigInfo(name = "enabled_for_tickregion", comments = "Using this you could pin the threads of tick region scheduler(Following are the same) to cpu cores listed in the config 'tickregion_affinity' following, \n" + + "which is useful for those CPU with P and E cores (such as 12/13/14 gen Intel Core CPUs and so on.)") + public static boolean enabledForTickRegion = false; + @HotReloadUnsupported + @ConfigInfo(name = "enable_for_chunksystem_worker") + public static boolean enabledForChunkSystemWorker = false; + @HotReloadUnsupported + @ConfigInfo(name = "enable_for_chunksystem_io") + public static boolean enabledForChunkSystemIo = false; + + @HotReloadUnsupported + @ConfigInfo(name = "tickregion_affinity", comments = "The core number you want the tick region threads to bind on") + public static List tickRegionAffinity = Affinity.getAffinity() + .stream() + .mapToObj(String::valueOf) + .toList(); + @HotReloadUnsupported + @ConfigInfo(name = "chunksystem_worker_affinity") + public static List chunkSystemWorkerAffinity = Affinity.getAffinity() + .stream() + .mapToObj(String::valueOf) + .toList(); + @HotReloadUnsupported + @ConfigInfo(name = "chunksystem_io_affinity") + public static List chunkSystemIoAffinity = Affinity.getAffinity() + .stream() + .mapToObj(String::valueOf) + .toList(); + + @DoNotLoad + private static boolean inited = false; + @DoNotLoad + private static final Logger LOGGER = LogUtils.getLogger(); + @DoNotLoad + public static AffinityRunnableWrapper tickRegionRunnableWrapper; + @DoNotLoad + public static AffinityRunnableWrapper chunkSystemWorkerRunnableWrapper; + @DoNotLoad + public static AffinityRunnableWrapper chunkSystemIoRunnableWrapper; + + public static Runnable wrapForTickRegion(Runnable in) { + return tickRegionRunnableWrapper == null ? in : tickRegionRunnableWrapper.wrap(in); + } + + public static Runnable wrapForChunkSystemWorker(Runnable in) { + return chunkSystemWorkerRunnableWrapper == null ? in : chunkSystemWorkerRunnableWrapper.wrap(in); + } + + public static Runnable wrapForChunkSystemIo(Runnable in) { + return chunkSystemIoRunnableWrapper == null ? in : chunkSystemIoRunnableWrapper.wrap(in); + } + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + if (enabledForTickRegion) { + tickRegionRunnableWrapper = new AffinityRunnableWrapper("tick_region", parseAffinity(tickRegionAffinity)); + LOGGER.info("Tick region thread now bound to: {}", tickRegionRunnableWrapper.getAffinity()); + } + + if (enabledForChunkSystemIo) { + chunkSystemIoRunnableWrapper = new AffinityRunnableWrapper("chunk_system_io", parseAffinity(chunkSystemIoAffinity)); + LOGGER.info("Chunk system I/O thread now bound to: {}", chunkSystemIoRunnableWrapper.getAffinity()); + } + + if (enabledForChunkSystemWorker) { + chunkSystemWorkerRunnableWrapper = new AffinityRunnableWrapper("chunk_system_worker", parseAffinity(chunkSystemWorkerAffinity)); + LOGGER.info("Chunk system worker thread now bound to: {}", chunkSystemIoRunnableWrapper.getAffinity()); + } + + if (!inited) { + inited = true; + } + } + + private @NonNull BitSet parseAffinity(@NonNull List affinity) { + int maxAvailable = Runtime.getRuntime().availableProcessors(); + BitSet affinitySet = new BitSet(affinity.size()); + affinity.stream() + .mapToInt(str -> { + try { + return Integer.parseInt(str); + } catch (NumberFormatException ignored) { + LOGGER.warn("Unable to parse cpu id {} to a valid number, falling back to 0.", str); + return 0; + } + }) + .distinct() + .filter(cpuId -> { + if (cpuId >= 0 && cpuId < maxAvailable) { + return true; + } else { + LOGGER.warn("Invalid cpu id {}, ignoring.", cpuId); + return false; + } + }) + .forEach(affinitySet::set); + return affinitySet; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/EntityGoalSelectorInactiveTickConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/EntityGoalSelectorInactiveTickConfig.java new file mode 100644 index 0000000..4611354 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/EntityGoalSelectorInactiveTickConfig.java @@ -0,0 +1,15 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "throttle_goal_selector_tick_in_inactive_tick", comments = + "Throttles the AI goal selector in entity inactive ticks. \n" + + "This can improve performance by a few percent, but has minor gameplay implications." +) +public class EntityGoalSelectorInactiveTickConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean enabled = false; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/GaleVariableEntityWakeupConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/GaleVariableEntityWakeupConfig.java new file mode 100644 index 0000000..d35042f --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/GaleVariableEntityWakeupConfig.java @@ -0,0 +1,17 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "variable_entity_waking_up") +public class GaleVariableEntityWakeupConfig implements IConfigModule { + @ConfigInfo(name = "entity_wakeup_duration_ratio_standard_deviation", comments = """ + If this value is set to any value > 0, waking up inactive entities happens spread over time, instead of many entities at once. This makes entities feel and behave more natural. + This setting is the coefficient of variation, or σ / μ (the ratio of the standard deviation to the mean) of the inactivity duration. + + In other words, this setting is the value σ, so that the regular inactivity duration will be multiplied by a factor normal_distribution(μ = 1, σ). + If a value ≤ 0 is given, variable entity wake-up is disabled.""") + public static double entityWakeUpDurationRatioStandardDeviation = 0.2; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/KaiijuEntityLimiterConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/KaiijuEntityLimiterConfig.java new file mode 100644 index 0000000..86a69d7 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/KaiijuEntityLimiterConfig.java @@ -0,0 +1,18 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import dev.kaiijumc.kaiiju.KaiijuEntityLimits; +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "kaiiju_entity_limiter") +public class KaiijuEntityLimiterConfig implements IConfigModule { + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + KaiijuEntityLimits.init(); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/LeavesSleepingBlockEntityConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/LeavesSleepingBlockEntityConfig.java new file mode 100644 index 0000000..801fb68 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/LeavesSleepingBlockEntityConfig.java @@ -0,0 +1,17 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.HotReloadUnsupported; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "lithium_sleeping_block_entity") +public class LeavesSleepingBlockEntityConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Use sleeping blocking optimizations from lithium,\s + on luminol the hopper optimizations of paper were totally removed and replaced by those of lithium\s + and it's turned on by default""") + @HotReloadUnsupported + public static boolean enabled = true; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/LobotomizeVillageConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/LobotomizeVillageConfig.java new file mode 100644 index 0000000..0eefe76 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/LobotomizeVillageConfig.java @@ -0,0 +1,16 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "lobotomize_villager", comments = "Lobotomizes the villager if it cannot move (Does not disable trading)") +public class LobotomizeVillageConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean villagerLobotomizeEnabled = false; + @ConfigInfo(name = "check_interval", comments = "The interval in ticks to check if a villager is lobotomized ") + public static int villagerLobotomizeCheckInterval = 100; + @ConfigInfo(name = "wait_until_trade_locked", comments = "Wait until a villager has been traded with before lobotomizing") + public static boolean villagerLobotomizeWaitUntilTradeLocked = false; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/OptimizedDragonRespawnConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/OptimizedDragonRespawnConfig.java new file mode 100644 index 0000000..1f2f072 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/OptimizedDragonRespawnConfig.java @@ -0,0 +1,12 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "end_dragon") +public class OptimizedDragonRespawnConfig implements IConfigModule { + @ConfigInfo(name = "optimized_dragon_respawn") + public static boolean optimizedRespawn = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/PetalReduceSensorWorkConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/PetalReduceSensorWorkConfig.java new file mode 100644 index 0000000..67a1bd5 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/PetalReduceSensorWorkConfig.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "reduce_sensor_work", comments = "When it is enabled, it will delete the line of sight cache less often and use a faster nearby comparison.") +public class PetalReduceSensorWorkConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean enabled = true; + @ConfigInfo(name = "delay_ticks", comments = "The interval of each entity to drop the cache(in ticks)") + public static int delayTicks = 10; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/ProjectileChunkReduceConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/ProjectileChunkReduceConfig.java new file mode 100644 index 0000000..e7c3694 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/ProjectileChunkReduceConfig.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "projectile") +public class ProjectileChunkReduceConfig implements IConfigModule { + @ConfigInfo(name = "max-loads-per-tick", comments = "Controls how many chunks are allowed to be sync loaded by projectiles in a tick.") + public static int maxProjectileLoadsPerTick; + @ConfigInfo(name = "max-loads-per-projectile", comments = "Controls how many chunks a projectile can load in its lifetime before it gets automatically removed.") + public static int maxProjectileLoadsPerProjectile; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/SIMDConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/SIMDConfig.java new file mode 100644 index 0000000..ea92725 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/optimizations/SIMDConfig.java @@ -0,0 +1,45 @@ +package moe.monnairealms.camellia.config.modules.optimizations; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.mojang.logging.LogUtils; +import gg.pufferfish.pufferfish.simd.SIMDDetection; +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.config.flags.DoNotLoad; +import moe.monnairealms.camellia.enums.EnumConfigCategory; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "use_simd") +public class SIMDConfig implements IConfigModule { + @DoNotLoad + private static final Logger LOGGER = LogUtils.getLogger(); + @ConfigInfo(name = "enabled") + public static boolean enabled = true; + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set e) { + if (!enabled) { + return; + } + + // Attempt to detect vectorization + try { + SIMDDetection.isEnabled = SIMDDetection.canEnable(LOGGER); + } catch (NoClassDefFoundError | Exception ignored) { + ignored.printStackTrace(); + } + + if (SIMDDetection.isEnabled) { + LOGGER.info("SIMD operations detected as functional. Will replace some operations with faster versions."); + } else { + LOGGER.warn("SIMD operations are available for your server, but are not configured!"); + LOGGER.warn("To enable additional optimizations, add \"--add-modules=jdk.incubator.vector\" to your startup flags, BEFORE the \"-jar\"."); + LOGGER.warn("If you have already added this flag, then SIMD operations are not supported on your JVM or CPU."); + LOGGER.warn("Debug: Java: {}, test run: {}", System.getProperty("java.version"), SIMDDetection.testRun); + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/removed/RemovedConfig.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/removed/RemovedConfig.java new file mode 100644 index 0000000..c8795ce --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/removed/RemovedConfig.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.removed; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.REMOVED, name = "removed_config") +public class RemovedConfig implements IConfigModule { + @ConfigInfo(name = "removed", comments = + """ + RemovedConfig redirect to here, no any function.""") + public static boolean enabled = true; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/unsupported/DisableCheckForFoliaSupported.java b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/unsupported/DisableCheckForFoliaSupported.java new file mode 100644 index 0000000..51d191b --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/config/modules/unsupported/DisableCheckForFoliaSupported.java @@ -0,0 +1,14 @@ +package moe.monnairealms.camellia.config.modules.unsupported; + +import moe.monnairealms.camellia.config.IConfigModule; +import moe.monnairealms.camellia.config.flags.ConfigClassInfo; +import moe.monnairealms.camellia.config.flags.ConfigInfo; +import moe.monnairealms.camellia.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.UNSUPPORTED, name = "disable_check_for_folia_supported") +public class DisableCheckForFoliaSupported implements IConfigModule { + @ConfigInfo(name = "disable_for_paper", comments = """ + Disable check for folia-supported for spigot/bukkit/paper plugin. + ATTENTION: No support will be provided if you enabled this.""") + public static boolean disableForPaper = false; +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/data/BufferedLinearRegionFile.java b/camellia-server/src/main/java/moe/monnairealms/camellia/data/BufferedLinearRegionFile.java new file mode 100644 index 0000000..b5e0f37 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/data/BufferedLinearRegionFile.java @@ -0,0 +1,1441 @@ +package moe.monnairealms.camellia.data; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; +import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO; +import com.github.luben.zstd.Zstd; +import com.github.luben.zstd.ZstdInputStream; +import moe.monnairealms.camellia.utils.BufferedLinearRegionFileFlusher; +import net.jpountz.lz4.LZ4Compressor; +import net.jpountz.lz4.LZ4Factory; +import net.jpountz.lz4.LZ4FastDecompressor; +import net.jpountz.xxhash.XXHash32; +import net.jpountz.xxhash.XXHashFactory; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionFileStorage; +import net.openhft.hashing.LongHashFunction; +import org.apache.commons.lang3.Validate; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jspecify.annotations.NonNull; + +import java.io.*; +import java.lang.invoke.VarHandle; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +public class BufferedLinearRegionFile implements moe.monnairealms.camellia.data.RegionFile { + private static final double SWAP_FILE_AUTO_COMPACT_PERCENT = 3.0 / 5.0; // 60 % + private static final long SWAP_FILE_AUTO_COMPACT_SIZE = 1024 * 1024; // 1 MiB + + private static final long SWAP_FILE_SUPER_BLOCK = 0x1145141919810L; + private static final int SWAP_FILE_HASH_SEED = 0x0721; // ~(∠・ω< )⌒★ + private static final byte SWAP_FILE_VERSION = 0x02; // ver 2.0 + + private static final long MASTER_FILE_SUPER_BLOCK = -0x200812250269L; + private static final byte MASTER_FILE_VERSION = 0x02; // ver 2.0 + private static final byte MASTER_FILE_VERSION_BUCKET = 0x03; // ver 3.0 + + private static final long LINEAR_FILE_SUPER_BLOCK = 0xc3ff13183cca9d9aL; + + private static final int BUCKET_SHIFT = 6; + private static final int BUCKET_SIZE = 1 << BUCKET_SHIFT; + private static final int BUCKET_COUNT = 1024 / BUCKET_SIZE; + + private static final long MAX_SIZE_PER_CHUNK = RegionFile.MAX_CHUNK_SIZE; + + private static final StandardOpenOption[] SWAP_FILE_CHANNEL_OPTIONS = new StandardOpenOption[]{ + StandardOpenOption.CREATE, + StandardOpenOption.WRITE, + StandardOpenOption.READ, + StandardOpenOption.DELETE_ON_CLOSE + }; + + private static final class Bucket { + private final Object lock = new Object(); + + private final AtomicLong writeEpoch = new AtomicLong(); + private final AtomicLong syncedEpoch = new AtomicLong(); + private volatile boolean loaded = false; + } + + private final Bucket[] buckets = new Bucket[BUCKET_COUNT]; + + private final Path masterFilePath; + private final Path swapFilePath; + + private final ReadWriteLock regionObjectLock = new ReentrantReadWriteLock(); + private final XXHash32 xxHash32 = XXHashFactory.fastestInstance().hash32(); + private Sector[] sectors = new Sector[1024]; + private long currentAcquiredIndex = this.headerSize(); + private int xxHash32Seed = SWAP_FILE_HASH_SEED; + private FileChannel swapFileChannel; + + private final byte compressionLevel; + private final LinearMasterFileParser masterFileParser = new LinearMasterFileParser(); + private final CompressingOps compressingOps = new CompressingOps(); + + // managed by VarHandles following + private boolean closed = false; + private boolean beingSynced = false; + private boolean synced = false; + private long lastWritten = System.nanoTime(); + + private static final VarHandle CLOSED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "closed", boolean.class); + private static final VarHandle SYNCED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "synced", boolean.class); + private static final VarHandle BEING_SYNCED_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "beingSynced", boolean.class); + private static final VarHandle LAST_WRITTEN_HANDLE = ConcurrentUtil.getVarHandle(BufferedLinearRegionFile.class, "lastWritten", long.class); + + private final BufferedLinearRegionFileFlusher flusher; + + public BufferedLinearRegionFile(Path masterFilePath, int compressionLevel, @NotNull BufferedLinearRegionFileFlusher flusher) throws IOException { + this.masterFilePath = masterFilePath; + this.swapFilePath = Path.of(this.masterFilePath.toString() + ".swp"); + + Validate.inclusiveBetween(1, 22, compressionLevel); + + for (int i = 0; i < this.buckets.length; i++) { + this.buckets[i] = new Bucket(); + } + + this.compressionLevel = (byte) compressionLevel; + + this.cleanUpSwapFile(); + this.initSwapFile(); + this.tryLoadOldBlinearMasterFileData(); + + this.flusher = flusher; + this.flusher.addFile(this); + } + + private static void writeFullyAt(FileChannel channel, @NonNull ByteBuffer buf, long startOffset) throws IOException { + long offset = startOffset; + while (buf.hasRemaining()) { + offset += channel.write(buf, offset); + } + } + + private static void readFullyAt(FileChannel channel, @NonNull ByteBuffer buf, long startOffset) throws IOException { + long offset = startOffset; + while (buf.hasRemaining()) { + final int read = channel.read(buf, offset); + if (read < 0) throw new EOFException("Unexpected EOF at offset " + offset); + offset += read; + } + } + + private void cleanUpSwapFile() throws IOException { + Files.deleteIfExists(this.swapFilePath); + } + + private void ensureBucketLoaded(int chunkIndex) throws IOException { + final int bucketIndex = chunkIndex >> BUCKET_SHIFT; + final Bucket bucket = this.buckets[bucketIndex]; + + // bucket lock -> master read lock -> swap write lock + synchronized (bucket.lock) { + if (bucket.loaded) { + return; + } + + this.masterFileParser.loadBucketsFor(this.masterFilePath, bucketIndex); + bucket.loaded = true; + } + } + + private long markBucketDirty(int chunkIndex) { + return this.markBucketDirtyByIndex(chunkIndex >> BUCKET_SHIFT); + } + + private long markBucketDirtyByIndex(int bucketIndex) { + final Bucket bucket = this.buckets[bucketIndex]; + + return bucket.writeEpoch.incrementAndGet(); + } + + private long getBucketWriteEpoch(int bucketIndex) { + final Bucket bucket = this.buckets[bucketIndex]; + + return bucket.writeEpoch.get(); + } + + private long getBucketSyncedEpoch(int bucketIndex) { + final Bucket bucket = this.buckets[bucketIndex]; + + return bucket.syncedEpoch.get(); + } + + private void markBucketSynced(int bucketIndex, long syncedEpoch) { + final Bucket bucket = this.buckets[bucketIndex]; + + bucket.syncedEpoch.accumulateAndGet(syncedEpoch, Math::max); + } + + private boolean isBucketDirty(int bucketIndex) { + final Bucket bucket = this.buckets[bucketIndex]; + + return bucket.writeEpoch.get() != bucket.syncedEpoch.get(); + } + + public boolean markAsBeingSynced() { + return BEING_SYNCED_HANDLE.compareAndSet(this, false, true); + } + + + public long getLastWritten() { + return (long) LAST_WRITTEN_HANDLE.getVolatile(this); + } + + public boolean shouldSync() { + return !((boolean) SYNCED_HANDLE.getVolatile(this)); + } + + public boolean softReadLock() { + // not done close logic yet + return this.regionObjectLock.readLock().tryLock(); + } + + public void releaseReadLock() { + this.regionObjectLock.readLock().unlock(); + } + + public boolean isClosedRaw() { + return (boolean) CLOSED_HANDLE.getVolatile(this); + } + + public boolean isClosed() { + this.regionObjectLock.readLock().lock(); + try { + return (boolean) CLOSED_HANDLE.getVolatile(this); + } finally { + this.regionObjectLock.readLock().unlock(); + } + } + + public void syncIfNeeded() throws IOException { + // the sync operation is just coping the data from swap file to the master file + // so we could acquire read lock simply so that we won't block any other read operations + try { + // skip if closed already + if (this.isClosed()) { + return; + } + + this.syncToMasterFile(); + } finally { + BEING_SYNCED_HANDLE.setVolatile(this, false); // mark as not being synced + } + } + + private void syncToMasterFile() throws IOException { + // prevent multiple syncs in the same time + if (!SYNCED_HANDLE.compareAndSet(this, false, true)) { + return; + } + + try { + // this.masterFileParser.writeMainFile(this.masterFilePath); + this.masterFileParser.writeMainFileBucketed(this.masterFilePath); + } catch (Throwable e) { + // set back + SYNCED_HANDLE.setVolatile(this, false); + + throw new IOException("Failed to sync to master file!", e); + } + } + + private void tryLoadOldBlinearMasterFileData() throws IOException { + this.masterFileParser.tryParseMainFileOld(this.masterFilePath); + } + + private void initSwapFile() throws IOException { + this.swapFileChannel = FileChannel.open( + this.swapFilePath, + SWAP_FILE_CHANNEL_OPTIONS + ); + + // fill default sectors + for (int i = 0; i < 1024; i++) { + this.sectors[i] = new Sector(i, this.headerSize(), 0); + } + } + + private void recalculateAcquiredIndex() { + long newValue = this.headerSize(); + + for (Sector sector : this.sectors) { + if (sector.hasData()) { + newValue = Math.max(newValue, sector.offset + sector.length); + } + } + + this.currentAcquiredIndex = newValue; + } + + private void writeSwapFileHeaders(boolean forceFile, boolean forceMeta) throws IOException { + final ByteBuffer buffer = ByteBuffer.allocate(this.headerSize()); + + buffer.putLong(SWAP_FILE_SUPER_BLOCK); // Magic + buffer.put(SWAP_FILE_VERSION); // Version + buffer.putInt(this.xxHash32Seed); // XXHash32 seed + buffer.putLong(this.currentAcquiredIndex); // Acquired index + + for (Sector sector : this.sectors) { + // encode each sector + buffer.put(sector.getEncoded()); + } + + buffer.flip(); + + writeFullyAt(this.swapFileChannel, buffer, 0); + + if (forceFile) { + this.swapFileChannel.force(forceMeta); + } + } + + private int sectorSize() { + return this.sectors.length * Sector.sizeOfSingle(); + } + + private int headerSize() { + int result = 0; + + result += Long.BYTES; // Magic + result += Byte.BYTES; // Version + result += Integer.BYTES; // XXHash32 seed + result += Long.BYTES; // Acquired index + result += this.sectorSize(); // Sectors + + return result; + } + + private void flushInternal() throws IOException { + boolean initiallySyncRequired; + + this.regionObjectLock.writeLock().lock(); + try { + if (this.isClosedRaw()) { + return; + } + + long spareSize = this.currentAcquiredIndex; + + spareSize -= this.headerSize(); + for (Sector sector : this.sectors) { + // skip no data sectors + if (!sector.hasData()) { + continue; + } + + spareSize -= sector.length; + } + + long sectorSize = 0; + for (Sector sector : this.sectors) { + // skip no data sectors + if (!sector.hasData()) { + continue; + } + + sectorSize += sector.length; + } + + final boolean compactRequested = spareSize > SWAP_FILE_AUTO_COMPACT_SIZE && (double) spareSize > ((double) sectorSize) * SWAP_FILE_AUTO_COMPACT_PERCENT; + + // try auto compact to clean the garbage area + if (compactRequested) { + // do compact + this.compactSwapFile(); + } + + // prevent syncing after compact because it could be time costing sometimes + initiallySyncRequired = !Files.exists(this.masterFilePath) && !compactRequested; + } finally { + this.regionObjectLock.writeLock().unlock(); + } + + if (initiallySyncRequired) { + this.syncToMasterFile(); + } + } + + private void closeInternal() throws IOException { + this.syncIfNeeded(); + + this.regionObjectLock.writeLock().lock(); + try { + this.markClosed(); + + this.swapFileChannel.close(); + } finally { + this.regionObjectLock.writeLock().unlock(); + } + } + + private void markClosed() throws IOException { + if (!CLOSED_HANDLE.compareAndSet(this, false, true)) { + throw new IOException("Already closed!"); + } + + this.flusher.removeFile(this); + } + + private void compactSwapFile() throws IOException { + this.writeSwapFileHeaders(true, true); // save headers for compact + + final Sector[] newSectorsToBeReplaced = new Sector[this.sectors.length]; + + for (int i = 0; i < this.sectors.length; i++) { + final Sector old = this.sectors[i]; + + if (old.hasData()) { + newSectorsToBeReplaced[i] = old; + continue; + } + + // note: + // we reset length to 0 and this would make length <= newLength(which is >= 0) is always true. + // so that the following write operation wouldn't override the data of other sectors + // see the write method in Sector class + newSectorsToBeReplaced[i] = new Sector(i, 0, 0); + } + + long newAcquiredIndex; + + final Path targetTemp = new File(this.swapFilePath.toString() + ".tmp").toPath(); + + try (FileChannel tempChannel = FileChannel.open( + targetTemp, + StandardOpenOption.CREATE_NEW, + StandardOpenOption.WRITE, + StandardOpenOption.READ, + StandardOpenOption.TRUNCATE_EXISTING + )) { + long offsetPointer = this.headerSize(); + tempChannel.position(offsetPointer); + + for (Sector sector : newSectorsToBeReplaced) { + // skip cleared or no data-contained sectors + if (!sector.hasData()) { + continue; + } + + // transfer to target + sector.transferTo(this.swapFileChannel, tempChannel); + + // recalculate the offset and length + final Sector newRecalculated = new Sector(sector.index, offsetPointer, sector.length); + newRecalculated.hasData = true; + + offsetPointer += sector.length; + newSectorsToBeReplaced[sector.index] = newRecalculated; // update sector infos + } + + tempChannel.force(true); + + newAcquiredIndex = offsetPointer; + } catch (Throwable ex) { + // recalculate acquired index + this.recalculateAcquiredIndex(); + // delete the target temp file + Files.deleteIfExists(targetTemp); + // fast-fail + // note: we don't block new write operations here as this is recoverable + throw new IOException("Failed to compact swap file!", ex); + } + + this.swapFileChannel.close(); + + // replace swap file + try { + Files.move( + targetTemp, + this.swapFilePath, + StandardCopyOption.REPLACE_EXISTING, + StandardCopyOption.ATOMIC_MOVE + ); + } catch (Throwable e) { + // atomic move might be unsupported on some file systems, so give it an attempt to retry without atomic move + try { + Files.move( + targetTemp, + this.swapFilePath, + StandardCopyOption.REPLACE_EXISTING + ); + } catch (Throwable ex) { + // now we are totally failed + e.addSuppressed(ex); + + // delete file that failed to replace + Files.deleteIfExists(targetTemp); + // recalculate acquired index + this.recalculateAcquiredIndex(); + // reopen closed channel + this.reopenSwapFileChannel(); + // fast-fail + this.markClosed(); // prevent new writing & sync operations + throw new IOException("Failed to replace original swap file!", e); + } + } + + + try { + // reopen file channel + this.reopenSwapFileChannel(); + + // replace with recalculated file headers + this.sectors = newSectorsToBeReplaced; + this.currentAcquiredIndex = newAcquiredIndex; + + // flush to file + this.writeSwapFileHeaders(true, true); + } catch (Throwable ex) { + // we are totally failed here, + // directly mark as closed as the swap file is already replaced, and we failed to update the + // data which is still in the memory + // + // which means we might write any data into any incorrect indexed sectors which will blow the whole data + this.markClosed(); + throw new IOException(ex); + } + } + + private void reopenSwapFileChannel() throws IOException { + if (this.swapFileChannel.isOpen()) { + this.swapFileChannel.close(); + } + + this.swapFileChannel = FileChannel.open( + this.swapFilePath, + SWAP_FILE_CHANNEL_OPTIONS + ); + } + + private void writeChunkDataRaw(int index, ByteBuffer chunkData, boolean skipSync) throws IOException { + final ByteBuffer committed = this.compressingOps.compress(chunkData); // run compression out of lock + + this.regionObjectLock.writeLock().lock(); + try { + final Sector sector = this.sectors[index]; + + sector.store(committed, this.swapFileChannel); + if (!skipSync) { + this.markBucketDirty(index); + } + } finally { + this.regionObjectLock.writeLock().unlock(); + } + + if (skipSync) { + return; + } + + this.markAsToSync(); + } + + private @Nullable ByteBuffer readChunkDataRaw(int index) throws IOException { + return this.readChunkDataRaw(index, true); + } + + private @Nullable ByteBuffer readChunkDataRaw(int index, boolean acquireLock) throws IOException { + final ByteBuffer raw; + + this.regionObjectLock.readLock().lock(); + try { + final Sector sector = this.sectors[index]; + + if (!sector.hasData()) { + return null; + } + + raw = sector.read(this.swapFileChannel); + } finally { + this.regionObjectLock.readLock().unlock(); + } + + return this.compressingOps.decompress(raw); + } + + private void clearChunkData(int index) throws IOException { + this.ensureBucketLoaded(index); + + this.regionObjectLock.writeLock().lock(); + try { + final Sector sector = this.sectors[index]; + + sector.clear(); + this.markBucketDirty(index); + } finally { + this.regionObjectLock.writeLock().unlock(); + } + + this.markAsToSync(); + } + + private void markAsToSync() { + SYNCED_HANDLE.setVolatile(this, false); // mark as unsynced + LAST_WRITTEN_HANDLE.setVolatile(this, System.nanoTime()); // update last written time + } + + private static int getChunkIndex(int x, int z) { + return (x & 31) + ((z & 31) << 5); + } + + private boolean hasData(int index) throws IOException { + this.ensureBucketLoaded(index); + + this.regionObjectLock.readLock().lock(); + try { + return this.sectors[index].hasData(); + } finally { + this.regionObjectLock.readLock().unlock(); + } + } + + private void writeChunk(int x, int z, @NotNull ByteBuffer data) throws IOException { + final int chunkIndex = getChunkIndex(x, z); + + if (data.remaining() > MAX_SIZE_PER_CHUNK) { + throw new RegionFileStorage.RegionFileSizeException("Writing too large chunk, limit : " + MAX_SIZE_PER_CHUNK + " but got : " + data.remaining()); + } + + final int oldPositionOfData = data.position(); + final int xxHash32OfData = this.xxHash32.hash(data, this.xxHash32Seed); + data.position(oldPositionOfData); + + // uncompressed length(int) + timestamp(long) + xxhash32(int) + final ByteBuffer chunkSectionBuilder = ByteBuffer.allocate(data.remaining() + 4 + 8 + 4); + + chunkSectionBuilder.putInt(data.remaining()); // Length(int) + chunkSectionBuilder.putLong(System.currentTimeMillis()); // Timestamp(long) + chunkSectionBuilder.putInt(xxHash32OfData); // xxHash32 of the original data(int) + chunkSectionBuilder.put(data); // Data(bytes) + chunkSectionBuilder.flip(); + + this.writeChunkDataRaw(chunkIndex, chunkSectionBuilder, false); + } + + private @Nullable ByteBuffer readChunk(int x, int z) throws IOException { + final int chunkIndex = getChunkIndex(x, z); + + this.ensureBucketLoaded(chunkIndex); + + final ByteBuffer data = this.readChunkDataRaw(chunkIndex); + + if (data == null) { + return null; + } + + final int length = data.getInt(); // compressed length(int) + final long timestamp = data.getLong(); // TODO use this timestamp(long) for something? + final int dataXXHash32 = data.getInt(); // XXHash32 for validation(int) + + final IOException xxHash32CheckFailedEx = this.checkXXHash32(dataXXHash32, data); + if (xxHash32CheckFailedEx != null) { + throw xxHash32CheckFailedEx; // prevent from loading + } + + return data; + } + + private @Nullable IOException checkXXHash32(long originalXXHash32, @NotNull ByteBuffer input) { + final int oldPositionOfInput = input.position(); + final int currentXXHash32 = this.xxHash32.hash(input, this.xxHash32Seed); + input.position(oldPositionOfInput); + + if (originalXXHash32 != currentXXHash32) { + return new IOException("XXHash32 check failed ! Expected: " + originalXXHash32 + ",but got: " + currentXXHash32); + } + + return null; + } + + @Override + public Path getPath() { + return this.masterFilePath; + } + + @Override + public DataInputStream getChunkDataInputStream(@NotNull ChunkPos pos) throws IOException { + final ByteBuffer data = this.readChunk(pos.x(), pos.z()); + + if (data == null) { + return null; + } + + return new DataInputStream(new ByteBufferInputStream(data)); + } + + @Override + public boolean doesChunkExist(@NotNull ChunkPos pos) throws IOException { + return this.hasData(getChunkIndex(pos.x(), pos.z())); + } + + @Override + public DataOutputStream getChunkDataOutputStream(ChunkPos pos) { + return new DataOutputStream(new ChunkBufferHelper(pos)); + } + + @Override + public void clear(@NotNull ChunkPos pos) throws IOException { + this.clearChunkData(getChunkIndex(pos.x(), pos.z())); + } + + @Override + public boolean hasChunk(@NotNull ChunkPos pos) { + try { + return this.hasData(getChunkIndex(pos.x(), pos.z())); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void write(@NotNull ChunkPos pos, ByteBuffer buf) throws IOException { + + final int chunkIndex = getChunkIndex(pos.x(), pos.z()); + + this.ensureBucketLoaded(chunkIndex); + + this.writeChunk(pos.x(), pos.z(), buf); + } + + // MCC 的玩意,这东西也用不上给Linear了() + @Override + public CompoundTag getOversizedData(int x, int z) { + return null; + } + + @Override + public boolean isOversized(int x, int z) { + return false; + } + + @Override + public boolean recalculateHeader() { + return false; + } + + @Override + public void setOversized(int x, int z, boolean oversized) { + + } + // MCC end + + @Override + public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) { + final DataOutputStream out = this.getChunkDataOutputStream(pos); + + return new MoonriseRegionFileIO.RegionDataController.WriteData( + data, MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, + out, regionFile -> out.close() + ); + } + + @Override + public void flush() throws IOException { + this.flushInternal(); + } + + @Override + public void close() throws IOException { + this.closeInternal(); + } + + public static class ByteBufferInputStream extends InputStream { + protected final ByteBuffer internal; + + public ByteBufferInputStream(ByteBuffer buf) { + this.internal = buf; + } + + @Override + public int available() { + return this.internal.remaining(); + } + + @Override + public int read() throws IOException { + return this.internal.hasRemaining() ? (this.internal.get() & 0xFF) : -1; + } + + @Override + public int read(byte @NotNull [] bytes, int off, int len) throws IOException { + if (!this.internal.hasRemaining()) return -1; + len = Math.min(len, this.internal.remaining()); + this.internal.get(bytes, off, len); + return len; + } + } + + // here we use this tool to prevent the swap file goes too large + // sometimes when a region contains all chunks, it might be very huge without any compressions(around 100MiB) + private static class CompressingOps { + private final LZ4Compressor lz4Compressor = LZ4Factory.fastestInstance().fastCompressor(); + private final LZ4FastDecompressor lz4Decompressor = LZ4Factory.fastestInstance().fastDecompressor(); + + public @NotNull ByteBuffer compress(@NotNull ByteBuffer in) { + final int bufferLenToAllocate = this.lz4Compressor.maxCompressedLength(in.remaining()); + final ByteBuffer result = ByteBuffer.allocate(bufferLenToAllocate + 4); + + result.putInt(in.remaining()); + this.lz4Compressor.compress(in, result); + + return result.flip(); + } + + public @NotNull ByteBuffer decompress(@NotNull ByteBuffer flippedIn) { + final int originalLen = flippedIn.getInt(); + final byte[] raw = new byte[flippedIn.remaining()]; + flippedIn.get(raw); + + final byte[] decompressed = new byte[originalLen]; + this.lz4Decompressor.decompress(raw, decompressed); + + return ByteBuffer.wrap(decompressed); + } + } + + public class Sector { + private final int index; + private long offset; + private long length; + private boolean hasData = false; + + private Sector(int index, long offset, long length) { + this.index = index; + this.offset = offset; + this.length = length; + } + + public void transferTo(@NotNull FileChannel source, @NotNull FileChannel target) throws IOException { + long transferred = 0; + while (transferred < this.length) { + transferred += source.transferTo( + this.offset + transferred, + this.length - transferred, + target); + } + } + + public @NotNull ByteBuffer read(@NotNull FileChannel channel) throws IOException { + final ByteBuffer result = ByteBuffer.allocate((int) this.length); + + readFullyAt(channel, result, this.offset); + + result.flip(); + return result; + } + + public void store(@NotNull ByteBuffer newData, @NotNull FileChannel channel) throws IOException { + final long oldLength = this.length; + final long newDataLength = newData.remaining(); + + this.hasData = true; + this.length = newDataLength; + + // data is smaller or its length equals to the local buffer we hold, write it directly + if (newDataLength <= oldLength) { + writeFullyAt(channel, newData, this.offset); + + return; + } + + // or we will append to the end of file + this.offset = BufferedLinearRegionFile.this.currentAcquiredIndex; + + BufferedLinearRegionFile.this.currentAcquiredIndex += this.length; + + writeFullyAt(channel, newData, this.offset); + } + + private @NotNull ByteBuffer getEncoded() { + final ByteBuffer buffer = ByteBuffer.allocate(sizeOfSingle()); + + buffer.putLong(this.offset); + buffer.putLong(this.length); + buffer.put((byte) (this.hasData ? 1 : 0)); + buffer.flip(); + + return buffer; + } + + public void restoreFrom(@NotNull ByteBuffer buffer) { + this.offset = buffer.getLong(); + this.length = buffer.getLong(); + this.hasData = buffer.get() == 1; + + if (this.length < 0 || this.offset < 0) { + throw new IllegalStateException("Invalid sector data: " + this); + } + } + + public void clear() { + this.hasData = false; + } + + public boolean hasData() { + return this.hasData; + } + + static int sizeOfSingle() { + // offset + length hasData + return Long.BYTES * 2 + 1; + } + } + + private class ChunkBufferHelper extends ByteArrayOutputStream { + private final ChunkPos pos; + + private ChunkBufferHelper(ChunkPos pos) { + this.pos = pos; + } + + @Override + public void close() throws IOException { + ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); + + final int chunkIndex = getChunkIndex(this.pos.x(), this.pos.z()); + + BufferedLinearRegionFile.this.ensureBucketLoaded(chunkIndex); + BufferedLinearRegionFile.this.writeChunk(this.pos.x(), this.pos.z(), bytebuffer); + + BufferedLinearRegionFile.this.flushInternal(); + } + } + + private class LinearMasterFileParser { + // V3 new format layout: + // [0, 14): header — superblock(8) + version(1) + compressionLevel(1) + xxHash32Seed(4) + // [14, 142): position table — BUCKET_COUNT(16) × long(8) each; 0 = no data for that bucket + // [526, EOF): bucket data — originalLen(int) + compressedLen(int) + compressedData + private static final long V3_POS_TABLE_OFFSET = 14L; + private static final int V3_POS_TABLE_SIZE = BUCKET_COUNT * Long.BYTES; // 128 + private static final long V3_DATA_AREA_OFFSET = V3_POS_TABLE_OFFSET + V3_POS_TABLE_SIZE; // 142 + + private final ReadWriteLock masterFileLock = new ReentrantReadWriteLock(); + + public void writeMainFileBucketed(@NotNull Path mainFile) throws IOException { + final Path tmpFilePath = Path.of(mainFile + ".tmp"); + final long[] syncedBucketEpochs = new long[BUCKET_COUNT]; + final long[] newPositionTable = new long[BUCKET_COUNT]; + + // note: there is no necessary hold the write lock for this stuff + // as the truly write operations only happens on replacing the master file (see the file move call below this hunk) + // and we had CAS flags to prevent multiple synchronization happening at the same time + + // Open old file to copy non-dirty buckets + long[] oldPositionTable = null; + FileChannel oldChannel = null; + + this.masterFileLock.writeLock().lock(); + try { + if (Files.exists(mainFile)) { + try { + oldChannel = FileChannel.open(mainFile, StandardOpenOption.READ); + if (oldChannel.size() >= V3_DATA_AREA_OFFSET) { + final ByteBuffer hdr = ByteBuffer.allocate(14); + readFullyAt(oldChannel, hdr, 0); + hdr.flip(); + if (hdr.getLong() == MASTER_FILE_SUPER_BLOCK && hdr.get() == MASTER_FILE_VERSION_BUCKET) { + oldPositionTable = this.parseOffsetTable(oldChannel); + } else { + oldChannel.close(); + oldChannel = null; + } + } else { + oldChannel.close(); + oldChannel = null; + } + } catch (Throwable e) { + if (oldChannel != null) { + try { + oldChannel.close(); + } catch (IOException e2) { + e.addSuppressed(e2); + } + } + + throw new RuntimeException(e); + } + } + + BufferedLinearRegionFile.this.regionObjectLock.readLock().lock(); + try (FileChannel outChannel = FileChannel.open(tmpFilePath, + StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { + + // Write header (14 bytes) + final ByteBuffer header = ByteBuffer.allocate(14); + header.putLong(MASTER_FILE_SUPER_BLOCK); + header.put(MASTER_FILE_VERSION_BUCKET); + header.put(BufferedLinearRegionFile.this.compressionLevel); + header.putInt(BufferedLinearRegionFile.this.xxHash32Seed); + header.flip(); + writeFullyAt(outChannel, header, 0); + + // Write position table placeholder (all zeros, filled in at the end) + writeFullyAt(outChannel, ByteBuffer.allocate(V3_POS_TABLE_SIZE), V3_POS_TABLE_OFFSET); + + long dataOffset = V3_DATA_AREA_OFFSET; + + for (int bucketIndex = 0; bucketIndex < BUCKET_COUNT; bucketIndex++) { + final long bucketWriteEpoch = BufferedLinearRegionFile.this.getBucketWriteEpoch(bucketIndex); + final boolean isBucketDirty = bucketWriteEpoch != BufferedLinearRegionFile.this.getBucketSyncedEpoch(bucketIndex); + + if (isBucketDirty) { + final int baseChunk = bucketIndex << BUCKET_SHIFT; + final ByteArrayOutputStream rawBuf = new ByteArrayOutputStream(); + final DataOutputStream rawOut = new DataOutputStream(rawBuf); + boolean hasAny = false; + + for (int i = 0; i < BUCKET_SIZE; i++) { + // swap read lock + final ByteBuffer data = BufferedLinearRegionFile.this.readChunkDataRaw(baseChunk + i, false); + + // note: null -> no data contained + if (data == null) { + rawOut.writeInt(0); + } else { + final byte[] arr = new byte[data.remaining()]; + data.get(arr); + rawOut.writeInt(arr.length); + rawOut.write(arr); + hasAny = true; + } + } + rawOut.flush(); + + if (hasAny) { + final byte[] raw = rawBuf.toByteArray(); + final byte[] compressed = Zstd.compress(raw, BufferedLinearRegionFile.this.compressionLevel); + + newPositionTable[bucketIndex] = dataOffset; + + final ByteBuffer bucketBuf = ByteBuffer.allocate(8 + compressed.length); + bucketBuf.putInt(raw.length); // original (uncompressed) length + bucketBuf.putInt(compressed.length); // compressed length + bucketBuf.put(compressed); + bucketBuf.flip(); + writeFullyAt(outChannel, bucketBuf, dataOffset); + dataOffset += bucketBuf.limit(); + } + // else: newPositionTable[bucketIndex] stays 0 + + syncedBucketEpochs[bucketIndex] = bucketWriteEpoch; + } else { + // Not dirty: copy bytes from old file if available + if (oldPositionTable != null && oldPositionTable[bucketIndex] != 0) { + final long oldOffset = oldPositionTable[bucketIndex]; + final ByteBuffer lensBuf = ByteBuffer.allocate(8); + readFullyAt(oldChannel, lensBuf, oldOffset); + lensBuf.flip(); + lensBuf.getInt(); // skip originalLen + final int compressedLen = lensBuf.getInt(); + + final long bucketTotalSize = 8L + compressedLen; + newPositionTable[bucketIndex] = dataOffset; + outChannel.position(dataOffset); + long remaining = bucketTotalSize; + long srcPos = oldOffset; + while (remaining > 0) { + final long transferred = oldChannel.transferTo(srcPos, remaining, outChannel); + srcPos += transferred; + remaining -= transferred; + } + dataOffset += bucketTotalSize; + } + } + } + + // Write the finalized position table + final ByteBuffer posTableBuf = ByteBuffer.allocate(V3_POS_TABLE_SIZE); + for (final long pos : newPositionTable) { + posTableBuf.putLong(pos); + } + posTableBuf.flip(); + writeFullyAt(outChannel, posTableBuf, V3_POS_TABLE_OFFSET); + + outChannel.force(true); + } finally { + BufferedLinearRegionFile.this.regionObjectLock.readLock().unlock(); + + if (oldChannel != null) { + oldChannel.close(); + } + } + + try { + Files.move(tmpFilePath, mainFile, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); + } catch (Throwable e) { + + try { + Files.move(tmpFilePath, mainFile, StandardCopyOption.REPLACE_EXISTING); + } catch (Throwable ex) { + Files.deleteIfExists(tmpFilePath); + + e.addSuppressed(ex); + + throw new IOException("Failed to replace master file!", e); + } + } + } finally { + this.masterFileLock.writeLock().unlock(); + } + + for (int i = 0; i < syncedBucketEpochs.length; i++) { + final long syncedEpoch = syncedBucketEpochs[i]; + if (syncedEpoch != 0L) { + BufferedLinearRegionFile.this.markBucketSynced(i, syncedEpoch); + } + } + } + + private void loadBucketsFor(Path file, int bucketIndex) throws IOException { + final int beginChunkIndex = bucketIndex << BUCKET_SHIFT; + + this.masterFileLock.readLock().lock(); + + try { + if (!Files.exists(file)) { + return; + } + + try (FileChannel channel = FileChannel.open(file, StandardOpenOption.READ)) { + if (channel.size() < V3_DATA_AREA_OFFSET) { + return; + } + + final ByteBuffer headerBuf = ByteBuffer.allocate(14); + readFullyAt(channel, headerBuf, 0); + headerBuf.flip(); + + final long superblock = headerBuf.getLong(); + if (superblock != MASTER_FILE_SUPER_BLOCK) + throw new IOException("Invalid superblock " + superblock + "!"); + + final byte version = headerBuf.get(); + if (version != MASTER_FILE_VERSION_BUCKET) + throw new IOException("Unknown version: " + version); + + // compressionLevel and hashSeed consumed but not used here + headerBuf.get(); + headerBuf.getInt(); + + final long[] posTable = this.parseOffsetTable(channel); + + // New format: jump directly to bucket data + final long bucketDataOffset = posTable[bucketIndex]; + if (bucketDataOffset == 0) return; + + final ByteBuffer lensBuf = ByteBuffer.allocate(8); + readFullyAt(channel, lensBuf, bucketDataOffset); + lensBuf.flip(); + final int originalLen = lensBuf.getInt(); + final int compressedLen = lensBuf.getInt(); + + final byte[] compressedData = new byte[compressedLen]; + readFullyAt(channel, ByteBuffer.wrap(compressedData), bucketDataOffset + 8); + + final ByteBuffer decompressed = ByteBuffer.wrap(Zstd.decompress(compressedData, originalLen)); + this.loadChunksFromBucketData(decompressed, beginChunkIndex); + } + } finally { + this.masterFileLock.readLock().unlock(); + } + } + + private long @NonNull [] parseOffsetTable(FileChannel channel) throws IOException { + final ByteBuffer buf = ByteBuffer.allocate(V3_POS_TABLE_SIZE); + readFullyAt(channel, buf, V3_POS_TABLE_OFFSET); + buf.flip(); + + final long[] table = new long[BUCKET_COUNT]; + + Arrays.fill(table, 0L); + for (int i = 0; i < BUCKET_COUNT; i++) { + final long pos = buf.getLong(); + table[i] = pos; + } + + return table; + } + + private void loadChunksFromBucketData(ByteBuffer decompressed, int beginChunkIndex) throws IOException { + for (int chunkIndex = beginChunkIndex; chunkIndex < beginChunkIndex + BUCKET_SIZE; chunkIndex++) { + final int chunkSectionDataSize = decompressed.getInt(); + if (chunkSectionDataSize <= 0) continue; + + final byte[] chunkSectionData = new byte[chunkSectionDataSize]; + decompressed.get(chunkSectionData); + + BufferedLinearRegionFile.this.writeChunkDataRaw(chunkIndex, ByteBuffer.wrap(chunkSectionData), true); + } + } + + private void parseLinearV2(@NonNull DataInputStream ioStream, Path file) throws IOException { + try (ioStream) { + ioStream.readLong(); // Skip newestTimestamp (Long) + + byte gridSize = ioStream.readByte(); + if (gridSize != 1 && gridSize != 2 && gridSize != 4 && gridSize != 8 && gridSize != 16 && gridSize != 32) + throw new RuntimeException("Invalid grid size: " + gridSize + " file " + file); + int bucketSize = 32 / gridSize; + + ioStream.readInt(); // Skip region_x (Int) + ioStream.readInt(); // Skip region_z (Int) + + ioStream.skipBytes(128); // Skip existence bitmap + + // Skip NBT features + while (true) { + byte featureNameLength = ioStream.readByte(); + if (featureNameLength == 0) break; + byte[] featureNameBytes = new byte[featureNameLength]; + ioStream.readFully(featureNameBytes); + ioStream.readInt(); // featureValue + } + + // Read bucket metadata + int totalBuckets = gridSize * gridSize; + int[] bucketSizes = new int[totalBuckets]; + byte[] bucketCompressionLevels = new byte[totalBuckets]; + long[] bucketHashes = new long[totalBuckets]; + for (int i = 0; i < totalBuckets; i++) { + bucketSizes[i] = ioStream.readInt(); + bucketCompressionLevels[i] = ioStream.readByte(); + bucketHashes[i] = ioStream.readLong(); + } + + // Read and decompress each bucket, load chunks into swap + for (int bx = 0; bx < gridSize; bx++) { + for (int bz = 0; bz < gridSize; bz++) { + int bucketIdx = bx * gridSize + bz; + + if (bucketSizes[bucketIdx] <= 0) continue; + + byte[] compressedBucket = new byte[bucketSizes[bucketIdx]]; + ioStream.readFully(compressedBucket); + + long rawHash = LongHashFunction.xx().hashBytes(compressedBucket); + if (rawHash != bucketHashes[bucketIdx]) { + throw new IOException("Region file hash incorrect for bucket " + bucketIdx + " in " + file); + } + + ByteArrayInputStream bucketByteStream = new ByteArrayInputStream(compressedBucket); + ZstdInputStream zstdStream = new ZstdInputStream(bucketByteStream); + ByteBuffer bucketBuffer = ByteBuffer.wrap(zstdStream.readAllBytes()); + zstdStream.close(); + + for (int cx = 0; cx < bucketSize; cx++) { + for (int cz = 0; cz < bucketSize; cz++) { + int chunkX = bx * bucketSize + cx; + int chunkZ = bz * bucketSize + cz; + int chunkIndex = chunkX + chunkZ * 32; + + int chunkSize = bucketBuffer.getInt(); + long timestamp = bucketBuffer.getLong(); + + if (chunkSize > 0) { + // chunkSize includes the 8 bytes of timestamp already written + int dataLen = chunkSize - 8; + byte[] chunkData = new byte[dataLen]; + bucketBuffer.get(chunkData); + + // Mark bucket as loaded. writeChunk() bumps the bucket epoch so it gets synced to the new master format. + final int blinearBucketIndex = chunkIndex >> BUCKET_SHIFT; + final Bucket bucket = BufferedLinearRegionFile.this.buckets[blinearBucketIndex]; + + synchronized (bucket.lock) { + bucket.loaded = true; + } + + // Use writeChunk to go through the full path (adds length + timestamp + xxhash header) + BufferedLinearRegionFile.this.writeChunk(chunkX, chunkZ, ByteBuffer.wrap(chunkData)); + } + } + } + } + } + + // Footer validation + long footerSuperBlock = ioStream.readLong(); + if (footerSuperBlock != LINEAR_FILE_SUPER_BLOCK) { + throw new IOException("Footer superblock invalid " + file); + } + } + } + + private boolean tryParseBlinearV2(@NotNull DataInputStream ioStream, Path file) throws IOException { + final byte version = ioStream.readByte(); + + // we will parse dynamically (V3) + if (version == MASTER_FILE_VERSION_BUCKET) { + ioStream.close(); + return false; + } + + if (version != MASTER_FILE_VERSION) + throw new RuntimeException("Invalid version: " + version + " in " + file); + + // Skip newestTimestamp (Long) + Compression level (Byte): Unused. + ioStream.skipBytes(9); + + try (final ZstdInputStream decompressStream = new ZstdInputStream(ioStream)) { + // only used as a helper stream + // the parent stream will be closed in the try-catch block upper + final DataInputStream decompressedStreamHelper = new DataInputStream(decompressStream); + + for (int index = 0; index < 1024; index++) { + int size = decompressedStreamHelper.readInt(); // len + + if (size > 0) { + byte[] sectorData = new byte[size]; + decompressedStreamHelper.readFully(sectorData, 0, size); // data + + final ByteBuffer sectorDataNioBuffer = ByteBuffer.wrap(sectorData); + + final int bucketIndex = index >> BUCKET_SHIFT; + final Bucket bucket = BufferedLinearRegionFile.this.buckets[bucketIndex]; + + synchronized (bucket.lock) { + bucket.loaded = true; + } + + BufferedLinearRegionFile.this.writeChunkDataRaw(index, sectorDataNioBuffer, false); + } + } + } + + return true; + } + + @Contract(value = "_ -> new", pure = true) + public static int @NotNull [] coordinatesFromIndex(int chunkIndex) { + int x = chunkIndex & 31; + int z = (chunkIndex >> 5) & 31; + return new int[]{x, z}; + } + + private void parseLinearV1(@NotNull DataInputStream ioStream) throws IOException { + // Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused. + ioStream.skipBytes(11); + // Skip chunk data len(Int)(Unused). + ioStream.skipBytes(4); + // Skip data hash (Long): Unused. + ioStream.skipBytes(8); + + try (final ZstdInputStream decompressedStream = new ZstdInputStream(ioStream)) { + // only used as a helper stream + // the parent stream will be closed in the try-catch block upper + final DataInputStream bufferHelper = new DataInputStream(decompressedStream); + + final int[] chunkStarts = new int[1024]; + for (int i = 0; i < 1024; i++) { + chunkStarts[i] = bufferHelper.readInt(); + bufferHelper.skipBytes(4); // Skip timestamps (Int): Unused. + } + + for (int i = 0; i < 1024; i++) { + if (chunkStarts[i] > 0) { + int size = chunkStarts[i]; + byte[] chunkData = new byte[size]; + bufferHelper.readFully(chunkData); + + final ByteBuffer chunkDataNioBuffer = ByteBuffer.wrap(chunkData); + + final int[] posByAxis = coordinatesFromIndex(i); + + final int x = posByAxis[0]; + final int z = posByAxis[1]; + + + final int bucketIndex = i >> BUCKET_SHIFT; + final Bucket bucket = BufferedLinearRegionFile.this.buckets[bucketIndex]; + + synchronized (bucket.lock) { + bucket.loaded = true; + } + + BufferedLinearRegionFile.this.writeChunk(x, z, chunkDataNioBuffer); + } + } + } + } + + // won't and need not to hold any region locks as we are calling this in a safe point (initially newed) + public void tryParseMainFileOld(@NotNull Path mainFilePath) throws IOException { + final File file = mainFilePath.toFile(); + + if (!file.exists() || !file.canRead()) { + return; + } + + // those streams will be closed in the parse logic, or we will close it manually + final FileInputStream fileStream = new FileInputStream(file); + final DataInputStream rawDataStream = new DataInputStream(fileStream); + + boolean oldParsed = false; + final long superBlock; + try { + superBlock = rawDataStream.readLong(); + + if (superBlock == MASTER_FILE_SUPER_BLOCK) { + oldParsed = this.tryParseBlinearV2(rawDataStream, mainFilePath); + + // false -> v3 -> closed in parse block + if (!oldParsed) { + return; + } + } + + if (superBlock == LINEAR_FILE_SUPER_BLOCK) { + final byte version = rawDataStream.readByte(); + + if (version == 1 || version == 2) { + this.parseLinearV1(rawDataStream); + + oldParsed = true; + } + + if (version == 3) { + this.parseLinearV2(rawDataStream, mainFilePath); + + oldParsed = true; + } + } + + } catch (Throwable ex) { + try { + rawDataStream.close(); + } catch (IOException ex2) { + ex.addSuppressed(ex2); + } + + throw new IOException("Failed to parse master file: " + mainFilePath, ex); + } + + // old parsed, remove the original file, and we will recreate it as we sync + if (oldParsed) { + // immediately do sync operation + BufferedLinearRegionFile.this.syncToMasterFile(); + return; + } + + // anyone non-matched, close stream and throw the error + rawDataStream.close(); + + throw new IOException("Unknown or unsupported super block : " + superBlock); + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/data/RegionFile.java b/camellia-server/src/main/java/moe/monnairealms/camellia/data/RegionFile.java new file mode 100644 index 0000000..ae2cc94 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/data/RegionFile.java @@ -0,0 +1,44 @@ +package moe.monnairealms.camellia.data; + + +import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.level.ChunkPos; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Path; + +public interface RegionFile extends ChunkSystemRegionFile, AutoCloseable { + Path getPath(); + + DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException; + + boolean doesChunkExist(ChunkPos pos) throws Exception; + + DataOutputStream getChunkDataOutputStream(ChunkPos pos) throws IOException; + + void flush() throws IOException; + + void clear(ChunkPos pos) throws IOException; + + boolean hasChunk(ChunkPos pos); + + void close() throws IOException; + + void write(ChunkPos pos, ByteBuffer buf) throws IOException; + + CompoundTag getOversizedData(int x, int z) throws IOException; + + boolean isOversized(int x, int z); + + boolean recalculateHeader() throws IOException; + + void setOversized(int x, int z, boolean oversized) throws IOException; + + default int getRecalculateCount() { + return 0; + } // Luminol - Configurable region file format +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumBarType.java b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumBarType.java new file mode 100644 index 0000000..0dfd268 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumBarType.java @@ -0,0 +1,92 @@ +package moe.monnairealms.camellia.enums; + +import com.mojang.datafixers.util.Pair; +import moe.monnairealms.camellia.functions.bars.TickableStatusBar; +import moe.monnairealms.camellia.functions.bars.impl.Membar; +import moe.monnairealms.camellia.functions.bars.impl.RegionBar; +import moe.monnairealms.camellia.functions.bars.impl.TpsBar; +import net.minecraft.world.entity.player.Player; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +import java.util.Map; +import java.util.function.Supplier; + +public enum EnumBarType { + TPS( + TpsBar.class, + "tps", + "function.tpsbar.enabled", + TpsBar::buildSettings + ), + MEMORY( + Membar.class, + "memory", + "membar", + "function.membar.enabled", + Membar::buildSettings + ), + REGION( + RegionBar.class, + "region", + "function.regionbar.enabled", + RegionBar::buildSettings + ); + + private final Class clazz; + private final String name; + private final String commandName; + private final String configPath; + private final String configOrigin; + private final Supplier> settingsProvider; + + EnumBarType(Class clazz, String name, String configPath, Supplier> settingsProvider) { + this(clazz, name, name + "bar", configPath, settingsProvider); + } + + EnumBarType(Class clazz, String name, Pair configPath, Supplier> settingsProvider) { + this(clazz, name, name + "bar", configPath, settingsProvider); + } + + EnumBarType(Class clazz, String name, String commandName, String configPath, Supplier> settingsProvider) { + this(clazz, name, commandName, new Pair<>("luminol", configPath), settingsProvider); + } + + EnumBarType(Class clazz, String name, String commandName, @NonNull Pair configPath, Supplier> settingsProvider) { + this.clazz = clazz; + this.name = name; + this.commandName = commandName; + this.configPath = configPath.getSecond(); + this.configOrigin = configPath.getFirst(); + this.settingsProvider = settingsProvider; + } + + @NotNull + public TickableStatusBar newBar(Player player) { + try { + return this.clazz.getConstructor(Player.class).newInstance(player); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public Map getSettings() { + return this.settingsProvider.get(); + } + + public String getCommandName() { + return this.commandName; + } + + public String getName() { + return this.name; + } + + public String getConfigOrigin() { + return this.configOrigin; + } + + public String getConfigPath() { + return this.configPath; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumCollisionBehaviorMode.java b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumCollisionBehaviorMode.java new file mode 100644 index 0000000..ee05143 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumCollisionBehaviorMode.java @@ -0,0 +1,7 @@ +package moe.monnairealms.camellia.enums; + +public enum EnumCollisionBehaviorMode { + VANILLA, + BLOCK_SHAPE_VANILLA, + PAPER +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumConfigCategory.java b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumConfigCategory.java new file mode 100644 index 0000000..ba5d334 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumConfigCategory.java @@ -0,0 +1,33 @@ +package moe.monnairealms.camellia.enums; + +public enum EnumConfigCategory { + OPTIMIZATIONS("optimizations"), // optimize performance feature + FIXES("fixes"), // fix not vanilla or other bugs caused by folia/paper + MISC("misc"), // unknown classify features + FUNCTION("function"), // new functions + EXPERIMENT("experiment"), // experimental features + UNSUPPORTED("unsupported"), // features we do not want anyone to use + REMOVED("removed"), // removed config + ROOT(null); + + private final String baseKeyName; + private final String keyComment; + + EnumConfigCategory(String baseKeyName, String keyComment) { + this.baseKeyName = baseKeyName; + this.keyComment = keyComment; + } + + EnumConfigCategory(String baseKeyName) { + this.baseKeyName = baseKeyName; + this.keyComment = null; + } + + public String getBaseKeyName() { + return this.baseKeyName; + } + + public String getKeyComment() { + return this.keyComment; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumRegionFormat.java b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumRegionFormat.java new file mode 100644 index 0000000..c1b5836 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumRegionFormat.java @@ -0,0 +1,29 @@ +package moe.monnairealms.camellia.enums; + +import abomination.LinearRegionFile; +import moe.monnairealms.camellia.config.modules.function.RegionFormatConfig; +import moe.monnairealms.camellia.data.BufferedLinearRegionFile; +import moe.monnairealms.camellia.utils.RegionFileFactory; +import net.minecraft.world.level.chunk.storage.RegionFile; + +public enum EnumRegionFormat { + MCA("mca", (info) -> new RegionFile(info.info(), info.filePath(), info.folder(), info.sync())), + LINEAR_V2("linear", (info) -> new LinearRegionFile(info.info(), info.filePath(), info.folder(), info.sync(), RegionFormatConfig.linearCompressionLevel)), + B_LINEAR("b_linear", (info) -> new BufferedLinearRegionFile(info.filePath(), RegionFormatConfig.linearCompressionLevel, RegionFormatConfig.blinearFlusher)); + + private final String argument; + private final RegionFileFactory creator; + + EnumRegionFormat(String argument, RegionFileFactory creator) { + this.argument = argument; + this.creator = creator; + } + + public RegionFileFactory getCreator() { + return this.creator; + } + + public String getArgument() { + return this.argument; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumStatusBarDisplay.java b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumStatusBarDisplay.java new file mode 100644 index 0000000..d5b097d --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumStatusBarDisplay.java @@ -0,0 +1,21 @@ +package moe.monnairealms.camellia.enums; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.Nullable; + +public enum EnumStatusBarDisplay { + BOSS_BAR, + ACTION_BAR, + TAB_LIST; + + @Contract(pure = true) + public static @Nullable EnumStatusBarDisplay fromOrdinal(int ordinal) { + EnumStatusBarDisplay[] values = values(); + + if (ordinal < 0 || ordinal >= values.length) { + return null; + } + + return values[ordinal]; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumTripwireBehavior.java b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumTripwireBehavior.java new file mode 100644 index 0000000..9cef178 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/enums/EnumTripwireBehavior.java @@ -0,0 +1,7 @@ +package moe.monnairealms.camellia.enums; + +public enum EnumTripwireBehavior { + VANILLA20, + VANILLA21, + MIXED +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/TickableStatusBar.java b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/TickableStatusBar.java new file mode 100644 index 0000000..d1da6f5 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/TickableStatusBar.java @@ -0,0 +1,194 @@ +package moe.monnairealms.camellia.functions.bars; + +import moe.monnairealms.camellia.enums.EnumStatusBarDisplay; +import net.kyori.adventure.bossbar.BossBar; +import net.kyori.adventure.text.Component; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public abstract class TickableStatusBar { + public static final String SETTING_KEY_UPDATE_INTERVALS = "update_intervals"; + public static final String SETTING_KEY_ENABLED = "enabled"; + public static final String SETTING_DISPLAY = "display"; + public static final String SETTING_ALLOW_PLAYER_DISPLAY_SWITCH = "allow_player_display_switch"; + + protected final Player player; + private BossBar bar = null; + + private long tickedCount = 0; + private boolean lastIsVisible = false; + private EnumStatusBarDisplay lastDisplay; + + private boolean visible = false; + private boolean enabled = false; + private boolean allowPlayerDisplaySwitch = false; + + private int updateIntervalInTicks; + private EnumStatusBarDisplay display; + private EnumStatusBarDisplay storedDisplay; + + public TickableStatusBar(Player player) { + this.player = player; + } + + public EnumStatusBarDisplay getDisplay() { + return this.display; + } + + /** + * Handle the update of the status bar display update + * + * @param bar the bossbar instance if the display mode is BOSS_BAR, else it's null + * @param owner the player that is displayed for + * @see EnumStatusBarDisplay + */ + public abstract void updateDisplay(@Nullable BossBar bar, Player owner); + + public void handleDisplayUpdate(Player owner, EnumStatusBarDisplay old, EnumStatusBarDisplay newDisplay) { + if (old == EnumStatusBarDisplay.TAB_LIST && newDisplay != EnumStatusBarDisplay.TAB_LIST) { + final CraftPlayer apiOwner = (CraftPlayer) owner.getBukkitEntity(); + + // reset the display + apiOwner.sendPlayerListFooter(Component.empty()); + } + } + + public BossBar newBar() { + return BossBar.bossBar(Component.text(""), 0.0F, BossBar.Color.PURPLE, BossBar.Overlay.NOTCHED_20); + } + + public void initSettings(@NotNull Map settings) { + this.applySettings(settings); + + // actual we need sync it here + this.lastDisplay = this.display; + } + + public void applySettings(@NotNull Map settings) { + this.updateIntervalInTicks = (int) settings.getOrDefault(SETTING_KEY_UPDATE_INTERVALS, 20); + this.enabled = (boolean) settings.getOrDefault(SETTING_KEY_ENABLED, false); + this.allowPlayerDisplaySwitch = (boolean) settings.getOrDefault(SETTING_ALLOW_PLAYER_DISPLAY_SWITCH, false); + + // pre init(the value might not be initialized if it's a new player) + if (this.storedDisplay == null) { + this.storedDisplay = (EnumStatusBarDisplay) settings.getOrDefault(SETTING_DISPLAY, EnumStatusBarDisplay.BOSS_BAR); + } + + // pre init(the value might not be initialized if it's a new player) + // also force update when custom switch is not allowed + if (this.display == null || !this.allowPlayerDisplaySwitch) { + this.display = (EnumStatusBarDisplay) settings.getOrDefault(SETTING_DISPLAY, EnumStatusBarDisplay.BOSS_BAR); + } + } + + public boolean isEnabled() { + return this.enabled; + } + + protected void tick() { + final CraftPlayer apiPlayer = (CraftPlayer) this.player.getBukkitEntity(); + boolean barUpdateRequired = this.tickedCount % this.updateIntervalInTicks == 0; + + final boolean isActualVisible = this.visible && this.enabled; + + final boolean usesBossbarBefore = this.lastDisplay == EnumStatusBarDisplay.BOSS_BAR; + final boolean usesBossbar = this.display == EnumStatusBarDisplay.BOSS_BAR; + + // reduce allocations + if (this.bar == null && usesBossbar) { + this.bar = this.newBar(); + } + + // handle display updates + // bossbar -> other + if (usesBossbarBefore && !usesBossbar) { + apiPlayer.hideBossBar(this.bar); + } + // other -> bossbar + if (!usesBossbarBefore && usesBossbar) { + apiPlayer.showBossBar(this.bar); + } + // sync display state + if (this.lastDisplay != this.display) { + this.handleDisplayUpdate(this.player, this.lastDisplay, this.display); + + this.lastDisplay = this.display; + } + + // handle visible state upgrade / downgrade + // visible -> invisible + if (this.lastIsVisible && !isActualVisible) { + if (usesBossbar) { + // go hide + apiPlayer.hideBossBar(this.bar); + } + } + + // invisible -> visible + if (!this.lastIsVisible && isActualVisible) { + if (usesBossbar) { + // make it visible then + apiPlayer.showBossBar(this.bar); + } + + // force update once + this.updateDisplay(this.bar, this.player); + // skip unnecessary updates + barUpdateRequired = false; + } + + // refresh the old value after sync + if (this.lastIsVisible != isActualVisible) { + this.lastIsVisible = isActualVisible; + } + + // we only updates the displayed value when it's visible + if (isActualVisible) { + if (barUpdateRequired) { + this.updateDisplay(this.bar, this.player); + } + } + + this.tickedCount++; + } + + // note: the visible update is performed by the tick logic, we don't actively update it + public void setVisible(boolean visible) { + this.visible = visible; + } + + public boolean isVisible() { + return this.visible; + } + + public void store(@NotNull ValueOutput output) { + output.putBoolean("visible", this.visible); + output.putByte("display", (byte) this.storedDisplay.ordinal()); + } + + public void load(@NotNull ValueInput input) { + this.visible = input.getBooleanOr("visible", false); + + EnumStatusBarDisplay display = EnumStatusBarDisplay.fromOrdinal(input.getByteOr("display", (byte) 0)); + // null -> not found + // also we only change it when custom switch is enabled + if (display == null) { + // init (by default it's that configured value) + this.storedDisplay = this.display; + } else { + // value is present, sync + this.storedDisplay = display; + + // then sync to mainline if custom switch is allowed + if (this.allowPlayerDisplaySwitch) { + this.display = display; + } + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/TickableStatusBarList.java b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/TickableStatusBarList.java new file mode 100644 index 0000000..2260c38 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/TickableStatusBarList.java @@ -0,0 +1,171 @@ +package moe.monnairealms.camellia.functions.bars; + +import com.mojang.logging.LogUtils; +import moe.monnairealms.camellia.enums.EnumBarType; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.players.PlayerList; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.storage.ValueInput; +import net.minecraft.world.level.storage.ValueOutput; +import org.jspecify.annotations.NonNull; +import org.slf4j.Logger; + +import java.util.EnumMap; +import java.util.Map; + +public class TickableStatusBarList { + private static final Logger LOGGER = LogUtils.getLogger(); + + private final EnumMap managedBars = new EnumMap<>(EnumBarType.class); + private final Player player; + + public TickableStatusBarList(Player player) { + this.player = player; + + for (EnumBarType type : EnumBarType.values()) { + final TickableStatusBar bar = type.newBar(this.player); + + bar.initSettings(type.getSettings()); + + this.managedBars.put(type, bar); + } + } + + public static void raiseGlobalReload(EnumBarType type) { + final MinecraftServer server = MinecraftServer.getServer(); + if (server == null) { + return; + } + + final PlayerList playerList = server.getPlayerList(); + if (playerList == null) { + return; + } + + for (Player playerInList : playerList.getPlayers()) { + playerInList.getBukkitEntity().taskScheduler.scheduleOrExecute(_ -> playerInList.statusBarList.reload(type)); + } + } + + public static void raiseGlobalReload() { + final MinecraftServer server = MinecraftServer.getServer(); + if (server == null) { + return; + } + + final PlayerList playerList = server.getPlayerList(); + if (playerList == null) { + return; + } + + for (Player playerInList : playerList.getPlayers()) { + playerInList.getBukkitEntity().taskScheduler.scheduleOrExecute(_ -> playerInList.statusBarList.reloadAll()); + } + } + + public void reloadAll() { + for (EnumBarType type : EnumBarType.values()) { + this.reload(type); + } + } + + public void reload(EnumBarType type) { + final TickableStatusBar bar = this.managedBars.get(type); + if (bar == null) { + LOGGER.warn("Reloading a non-existed bar {} !", type); + return; + } + + bar.applySettings(type.getSettings()); + } + + public void tick() { + for (TickableStatusBar bar : this.managedBars.values()) { + bar.tick(); + } + } + + public void load(@NonNull ValueInput input) { + final ValueInput statusBarsInput = input + .child("luminol") + .flatMap(luminol -> luminol.child("status_bars")) + .orElse(null); // I hate these optionals (x) + + // does not have this data + if (statusBarsInput == null) { + return; + } + + for (Map.Entry barEntry : this.managedBars.entrySet()) { + final EnumBarType type = barEntry.getKey(); + final TickableStatusBar bar = barEntry.getValue(); + final String categoryName = type.getName(); + + if (bar != null) { + final ValueInput inputOfThisBar = statusBarsInput.child(categoryName).orElse(null); + + // does not have this data + if (inputOfThisBar == null) { + continue; + } + + bar.load(inputOfThisBar); + continue; + } + + LOGGER.warn("Skipping loading null status bar {} !", categoryName); + } + } + + public void save(@NonNull ValueOutput output) { + final ValueOutput statusBarsOutput = output + .child("luminol") + .child("status_bars"); + + for (Map.Entry barEntry : this.managedBars.entrySet()) { + final EnumBarType type = barEntry.getKey(); + final TickableStatusBar bar = barEntry.getValue(); + final String categoryName = type.getName(); + + if (bar != null) { + final ValueOutput outOfThisBar = statusBarsOutput.child(categoryName); + + bar.store(outOfThisBar); + continue; + } + + LOGGER.warn("Skipping storing null status bar {} !", categoryName); + } + } + + public void setVisible(EnumBarType type, boolean visible) { + final TickableStatusBar bar = this.managedBars.get(type); + + if (bar == null) { + LOGGER.warn("Bar with type {} does not exist! Skipping visibility updates.", type); + return; + } + + bar.setVisible(visible); + } + + public boolean isVisible(EnumBarType type) { + final TickableStatusBar bar = this.managedBars.get(type); + + if (bar == null) { + return false; + } + + return bar.isVisible(); + } + + public boolean isEnabled(EnumBarType type) { + final TickableStatusBar bar = this.managedBars.get(type); + + if (bar == null) { + return false; + } + + return bar.isEnabled(); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/Membar.java b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/Membar.java new file mode 100644 index 0000000..6d46da1 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/Membar.java @@ -0,0 +1,110 @@ +package moe.monnairealms.camellia.functions.bars.impl; + +import moe.monnairealms.camellia.config.modules.function.MembarConfig; +import moe.monnairealms.camellia.enums.EnumStatusBarDisplay; +import moe.monnairealms.camellia.functions.bars.TickableStatusBar; +import net.kyori.adventure.bossbar.BossBar; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.minecraft.world.entity.player.Player; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; +import org.jspecify.annotations.NonNull; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryUsage; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class Membar extends TickableStatusBar { + public Membar(Player player) { + super(player); + } + + public static @NonNull @UnmodifiableView Map buildSettings() { + final HashMap ret = new HashMap<>(); + + ret.put(TickableStatusBar.SETTING_KEY_ENABLED, MembarConfig.memoryBarEnabled); + ret.put(TickableStatusBar.SETTING_DISPLAY, MembarConfig.display); + ret.put(TickableStatusBar.SETTING_KEY_UPDATE_INTERVALS, MembarConfig.updateInterval); + + return Collections.unmodifiableMap(ret); + } + + @Override + public void updateDisplay(@Nullable BossBar bar, @NonNull Player owner) { + final EnumStatusBarDisplay display = this.getDisplay(); + final CraftPlayer apiOwner = (CraftPlayer) owner.getBukkitEntity(); + + final MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage(); + long used = heap.getUsed(); + long xmx = heap.getMax(); + + double percent = Math.clamp((float) used / xmx, 0.0F, 1.0F); + final Component message = MiniMessage.miniMessage().deserialize( + MembarConfig.memBarFormat, + Placeholder.component("used", getMemoryComponent(used, xmx)), + Placeholder.component("available", getMaxMemComponent(xmx)) + ); + + switch (display) { + case BOSS_BAR -> bar.name(message).color(barColorForMemory(percent)).progress((float) percent); + + case ACTION_BAR -> apiOwner.sendActionBar(message); + + case TAB_LIST -> apiOwner.sendPlayerListFooter(message); + + default -> throw new IllegalStateException(); + } + } + + private static @NotNull Component getMaxMemComponent(double max) { + final BossBar.Color colorBukkit = BossBar.Color.GREEN; + final String colorString = colorBukkit.name(); + + final String content = "<%s>"; + final String replaced = String.format(content, colorString, colorString); + + return MiniMessage.miniMessage().deserialize(replaced, Placeholder.parsed("text", String.format("%.2f", max / (1024 * 1024)))); + } + + private static @NotNull Component getMemoryComponent(long used, long max) { + return MiniMessage.miniMessage().deserialize(textPlaceholderForMemory(Math.clamp((float) used / max, 0.0F, 1.0F)), Placeholder.parsed("text", String.format("%.2f", (double) used / (1024 * 1024)))); + } + + private static BossBar.Color barColorForMemory(double memPercent) { + if (memPercent == -1) { + return MembarConfig.barColors.get(3); + } + + if (memPercent <= 50) { + return MembarConfig.barColors.get(0); + } + + if (memPercent <= 70) { + return MembarConfig.barColors.get(1); + } + + return MembarConfig.barColors.get(2); + } + + private static String textPlaceholderForMemory(double memPercent) { + if (memPercent == -1) { + return MembarConfig.memColors.get(3); + } + + if (memPercent <= 50) { + return MembarConfig.memColors.get(0); + } + + if (memPercent <= 70) { + return MembarConfig.memColors.get(1); + } + + return MembarConfig.memColors.get(2); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/RegionBar.java b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/RegionBar.java new file mode 100644 index 0000000..f8afaf0 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/RegionBar.java @@ -0,0 +1,129 @@ +package moe.monnairealms.camellia.functions.bars.impl; + +import ca.spottedleaf.common.time.TickData; +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import io.papermc.paper.threadedregions.TickRegions; +import moe.monnairealms.camellia.config.modules.function.RegionBarConfig; +import moe.monnairealms.camellia.enums.EnumStatusBarDisplay; +import moe.monnairealms.camellia.functions.bars.TickableStatusBar; +import net.kyori.adventure.bossbar.BossBar; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.minecraft.world.entity.player.Player; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; +import org.jspecify.annotations.NonNull; + +import java.text.DecimalFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class RegionBar extends TickableStatusBar { + private final ThreadLocal ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.0")); + + public RegionBar(Player player) { + super(player); + } + + public static @NonNull @UnmodifiableView Map buildSettings() { + final HashMap ret = new HashMap<>(); + + ret.put(TickableStatusBar.SETTING_KEY_ENABLED, RegionBarConfig.regionbarEnabled); + ret.put(TickableStatusBar.SETTING_DISPLAY, RegionBarConfig.display); + ret.put(TickableStatusBar.SETTING_KEY_UPDATE_INTERVALS, RegionBarConfig.updateInterval); + + return Collections.unmodifiableMap(ret); + } + + @Override + public void updateDisplay(@Nullable BossBar bar, @NonNull Player owner) { + final EnumStatusBarDisplay display = this.getDisplay(); + final CraftPlayer apiOwner = (CraftPlayer) owner.getBukkitEntity(); + + final ThreadedRegionizer.ThreadedRegion region = TickRegionScheduler.getCurrentRegion(); + final TickData.TickReportData reportData = region.getData().getRegionSchedulingHandle().getTickReport5s(System.nanoTime()); + final TickRegions.RegionStats regionStats = region.getData().getRegionStats(); + + final double utilisation = reportData.utilisation(); + final int chunkCount = regionStats.getChunkCount(); + final int playerCount = regionStats.getPlayerCount(); + final int entityCount = regionStats.getEntityCount(); + + final double utilisationPercent = utilisation * 100.0; + final String formattedUtil = ONE_DECIMAL_PLACES.get().format(utilisationPercent); + final Component message = MiniMessage.miniMessage().deserialize( + RegionBarConfig.regionBarFormat, + Placeholder.component("util", getUtilComponent(formattedUtil)), + Placeholder.component("chunks", getChunksComponent(chunkCount)), + Placeholder.component("players", getPlayersComponent(playerCount)), + Placeholder.component("entities", getEntitiesComponent(entityCount)) + ); + + switch (display) { + case ACTION_BAR -> apiOwner.sendActionBar(message); + + case BOSS_BAR -> + bar.name(message).color(barColorForUtil(utilisationPercent)).progress((float) Math.clamp(utilisation, 0, 1.0)); + + case TAB_LIST -> apiOwner.sendPlayerListFooter(message); + + default -> throw new IllegalStateException(); + } + } + + private static @NotNull Component getEntitiesComponent(int entities) { + final String content = ""; + return MiniMessage.miniMessage().deserialize(content, Placeholder.parsed("text", String.valueOf(entities))); + } + + private static @NotNull Component getPlayersComponent(int players) { + final String content = ""; + return MiniMessage.miniMessage().deserialize(content, Placeholder.parsed("text", String.valueOf(players))); + } + + private static @NotNull Component getChunksComponent(int chunks) { + final String content = ""; + return MiniMessage.miniMessage().deserialize(content, Placeholder.parsed("text", String.valueOf(chunks))); + } + + private static @NotNull Component getUtilComponent(String formattedUtil) { + return MiniMessage.miniMessage().deserialize(textPlaceholderForUtil(Double.parseDouble(formattedUtil)), Placeholder.parsed("text", formattedUtil + "%")); + } + + private static BossBar.Color barColorForUtil(double util) { + if (util > 100) { + return RegionBarConfig.barColors.get(3); + } + + if (util >= 70) { + return RegionBarConfig.barColors.get(2); + } + + if (util >= 50) { + return RegionBarConfig.barColors.get(1); + } + + return RegionBarConfig.barColors.get(0); + } + + private static String textPlaceholderForUtil(double util) { + if (util > 100) { + return RegionBarConfig.utilColors.get(3); + } + + if (util >= 70) { + return RegionBarConfig.utilColors.get(2); + } + + if (util >= 50) { + return RegionBarConfig.utilColors.get(1); + } + + return RegionBarConfig.utilColors.get(0); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/TpsBar.java b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/TpsBar.java new file mode 100644 index 0000000..4c63d80 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/functions/bars/impl/TpsBar.java @@ -0,0 +1,167 @@ +package moe.monnairealms.camellia.functions.bars.impl; + +import ca.spottedleaf.common.time.TickData; +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickRegionScheduler; +import io.papermc.paper.threadedregions.TickRegions; +import moe.monnairealms.camellia.config.modules.function.TpsBarConfig; +import moe.monnairealms.camellia.enums.EnumStatusBarDisplay; +import moe.monnairealms.camellia.functions.bars.TickableStatusBar; +import net.kyori.adventure.bossbar.BossBar; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.minimessage.MiniMessage; +import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder; +import net.minecraft.world.entity.player.Player; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.UnmodifiableView; +import org.jspecify.annotations.NonNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class TpsBar extends TickableStatusBar { + public TpsBar(Player player) { + super(player); + } + + public static @NonNull @UnmodifiableView Map buildSettings() { + final HashMap ret = new HashMap<>(); + + ret.put(TickableStatusBar.SETTING_KEY_ENABLED, TpsBarConfig.tpsbarEnabled); + ret.put(TickableStatusBar.SETTING_DISPLAY, TpsBarConfig.display); + ret.put(TickableStatusBar.SETTING_KEY_UPDATE_INTERVALS, TpsBarConfig.updateInterval); + + return Collections.unmodifiableMap(ret); + } + + @Override + public void updateDisplay(@Nullable BossBar bar, @NotNull Player owner) { + final EnumStatusBarDisplay display = this.getDisplay(); + final CraftPlayer apiOwner = (CraftPlayer) owner.getBukkitEntity(); + + final ThreadedRegionizer.ThreadedRegion region = TickRegionScheduler.getCurrentRegion(); + final TickData.TickReportData reportData = region.getData().getRegionSchedulingHandle().getTickReport5s(System.nanoTime()); + final TickData.SegmentData tpsData = reportData.tpsData().segmentAll(); + + final double tps = tpsData.average(); + final double mspt = reportData.timePerTickData().segmentAll().average() / 1.0E6; + + final Component message = MiniMessage.miniMessage().deserialize( + TpsBarConfig.tpsBarFormat, + Placeholder.component("tps", getTpsComponent(tps)), + Placeholder.component("mspt", getMsptComponent(mspt)), + Placeholder.component("ping", getPingComponent(apiOwner.getPing())), + Placeholder.component("chunkhot", getChunkHotComponent(apiOwner.getNearbyChunkHot())) + ); + + switch (display) { + case ACTION_BAR -> apiOwner.sendActionBar(message); + + case BOSS_BAR -> + bar.name(message).color(barColorForTps(tps)).progress((float) Math.clamp(mspt / 50, 0, (float) 1)); + + case TAB_LIST -> apiOwner.sendPlayerListFooter(message); + + default -> throw new IllegalStateException(); + } + } + + private static @NotNull Component getPingComponent(int ping) { + return MiniMessage.miniMessage().deserialize(textPlaceholderForPing(ping), Placeholder.parsed("text", String.valueOf(ping))); + } + + private static @NotNull Component getMsptComponent(double mspt) { + return MiniMessage.miniMessage().deserialize(textPlaceholderForMspt(mspt), Placeholder.parsed("text", String.format("%." + TpsBarConfig.precisionOfMSPT + "f", mspt))); + } + + private static @NotNull Component getChunkHotComponent(long chunkHot) { + return MiniMessage.miniMessage().deserialize(textPlaceholderForChunkHot(chunkHot), Placeholder.parsed("text", String.valueOf(chunkHot))); + } + + private static @NotNull Component getTpsComponent(double tps) { + return MiniMessage.miniMessage().deserialize(textPlaceholderForTps(tps), Placeholder.parsed("text", String.format("%." + TpsBarConfig.precisionOfTPS + "f", tps))); + } + + private static String textPlaceholderForPing(int ping) { + if (ping == -1) { + return TpsBarConfig.pingColors.get(3); + } + + if (ping <= 80) { + return TpsBarConfig.pingColors.get(0); + } + + if (ping <= 160) { + return TpsBarConfig.pingColors.get(1); + } + + return TpsBarConfig.pingColors.get(2); + } + + private static String textPlaceholderForChunkHot(long chunkHot) { + if (chunkHot == -1) { + return TpsBarConfig.chunkHotColors.get(3); + } + + if (chunkHot <= 300000L) { + return TpsBarConfig.chunkHotColors.get(0); + } + + if (chunkHot <= 500000L) { + return TpsBarConfig.chunkHotColors.get(1); + } + + return TpsBarConfig.chunkHotColors.get(2); + } + + private static String textPlaceholderForMspt(double mspt) { + if (mspt == -1) { + return TpsBarConfig.tpsColors.get(3); + } + + if (mspt <= 25) { + return TpsBarConfig.tpsColors.get(0); + } + + if (mspt <= 50) { + return TpsBarConfig.tpsColors.get(1); + } + + return TpsBarConfig.tpsColors.get(2); + } + + private static BossBar.Color barColorForTps(double tps) { + if (tps == -1) { + return TpsBarConfig.barColors.get(3); + } + + if (tps >= 19) { + return TpsBarConfig.barColors.get(0); + } + + if (tps >= 15) { + return TpsBarConfig.barColors.get(1); + } + + return TpsBarConfig.barColors.get(2); + } + + private static String textPlaceholderForTps(double tps) { + if (tps == -1) { + return TpsBarConfig.tpsColors.get(3); + } + + if (tps >= 19) { + return TpsBarConfig.tpsColors.get(0); + } + + if (tps >= 15) { + return TpsBarConfig.tpsColors.get(1); + } + + return TpsBarConfig.tpsColors.get(2); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/AffinityRunnableWrapper.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/AffinityRunnableWrapper.java new file mode 100644 index 0000000..bcc87ba --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/AffinityRunnableWrapper.java @@ -0,0 +1,34 @@ +package moe.monnairealms.camellia.utils; + +import net.openhft.affinity.Affinity; +import org.jetbrains.annotations.NotNull; + +import java.util.BitSet; + +public class AffinityRunnableWrapper { + private final BitSet affinity; + private final String name; + + public AffinityRunnableWrapper(String name, BitSet affinity) { + this.name = name; + this.affinity = affinity; + } + + public Runnable wrap(Runnable original) { + return () -> { + Affinity.setAffinity(this.affinity); + + original.run(); + }; + } + + @NotNull + public String getName() { + return this.name; + } + + @NotNull + public BitSet getAffinity() { + return this.affinity; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/BufferedLinearRegionFileFlusher.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/BufferedLinearRegionFileFlusher.java new file mode 100644 index 0000000..9499c19 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/BufferedLinearRegionFileFlusher.java @@ -0,0 +1,133 @@ +package moe.monnairealms.camellia.utils; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.mojang.logging.LogUtils; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectArraySet; +import moe.monnairealms.camellia.data.BufferedLinearRegionFile; +import org.apache.commons.lang3.Validate; +import org.slf4j.Logger; + +import java.io.IOException; +import java.util.List; +import java.util.Set; +import java.util.concurrent.*; + +public class BufferedLinearRegionFileFlusher implements Runnable { + private static final Logger logger = LogUtils.getLogger(); + + private final Set inManagement = new ObjectArraySet<>(); + private final ScheduledFuture flusherChecker; + private final Executor ioWorkerPool; + private final long flushOfWriteTimeoutMs; + + public BufferedLinearRegionFileFlusher(int nIoThreads, long checkIntervalMs, long flushOfWriteTimeoutMs) { + Validate.isTrue(nIoThreads > 0, "Number of I/O threads must > 0!"); + Validate.isTrue(checkIntervalMs > 0, "Check interval must > 0"); + Validate.isTrue(flushOfWriteTimeoutMs > 0, "Flush of write timeout must > 0"); + + this.ioWorkerPool = Executors.newFixedThreadPool(nIoThreads, new ThreadFactoryBuilder() + .setNameFormat("BufferedLinearRegionFile I/O Worker %d") + .setDaemon(true) + .build() + ); + this.flusherChecker = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder() + .setNameFormat("BufferedLinearRegionFile Flusher Checker") + .setDaemon(true) + .build()) + .scheduleWithFixedDelay(this, checkIntervalMs, checkIntervalMs, TimeUnit.MILLISECONDS); + this.flushOfWriteTimeoutMs = flushOfWriteTimeoutMs; + } + + public void shutdown() { + this.flusherChecker.cancel(false); + + ((ExecutorService) this.ioWorkerPool).shutdown(); + for (; ; ) { + try { + if (((ExecutorService) this.ioWorkerPool).awaitTermination(100, TimeUnit.MILLISECONDS)) { + break; + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } + + @Override + public void run() { + final long currentNanos = System.nanoTime(); + final BufferedLinearRegionFile[] copied; + + synchronized (this) { + copied = this.inManagement.toArray(new BufferedLinearRegionFile[0]); + } + + final List toRemove = new ObjectArrayList<>(); + for (BufferedLinearRegionFile file : copied) { + // try acquiring the read lock + if (!file.softReadLock()) { + // if the read lock is unacquirable, it might mean there is another operations is processing(might be a writing operation) + continue; + } + + boolean closed; + + try { + // check if the file is closed + closed = file.isClosedRaw(); + } finally { + file.releaseReadLock(); + } + + if (closed) { + // add to pending remove list so that we could clean the closed file correctly + toRemove.add(file); + continue; + } + + // skip non sync-required files + if (!file.shouldSync()) { + continue; + } + + final long lastWriteNanos = file.getLastWritten(); + final long timeElapsed = (currentNanos - lastWriteNanos) / 1_000_000; // Convert to milliseconds + + // if deadline(timeout) reached + if (timeElapsed >= this.flushOfWriteTimeoutMs) { + // already marked to flush + if (!file.markAsBeingSynced()) { + continue; + } + + this.ioWorkerPool.execute(() -> { + try { + file.syncIfNeeded(); + } catch (IOException e) { + logger.error("Failed to sync master file: ", e); + } + }); + } + } + + synchronized (this) { + // clean closed files + for (BufferedLinearRegionFile file : toRemove) { + this.inManagement.remove(file); + } + } + } + + public void removeFile(BufferedLinearRegionFile fileToRemove) { + synchronized (this) { + this.inManagement.remove(fileToRemove); + } + } + + public void addFile(BufferedLinearRegionFile fileToAdd) { + synchronized (this) { + this.inManagement.add(fileToAdd); + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/ClassLoadUtil.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/ClassLoadUtil.java new file mode 100644 index 0000000..1448963 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/ClassLoadUtil.java @@ -0,0 +1,97 @@ +package moe.monnairealms.camellia.utils; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class ClassLoadUtil { + public static @NotNull Collection> getClasses(String pack, ClassLoader loader) { + Set> classes = new HashSet<>(); + String packageDirName = pack.replace('.', '/'); + Enumeration dirs; + + try { + dirs = loader.getResources(packageDirName); + while (dirs.hasMoreElements()) { + URL url = dirs.nextElement(); + String protocol = url.getProtocol(); + if ("file".equals(protocol)) { + String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8); + findClassesInPackageByFile(pack, filePath, classes); + } else if ("jar".equals(protocol)) { + JarFile jar; + try { + jar = ((JarURLConnection) url.openConnection()).getJarFile(); + Enumeration entries = jar.entries(); + findClassesInPackageByJar(pack, entries, packageDirName, classes); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + return classes; + } + + private static void findClassesInPackageByFile(String packageName, String packagePath, Set> classes) { + File dir = new File(packagePath); + + if (!dir.exists() || !dir.isDirectory()) { + return; + } + + File[] dirfiles = dir.listFiles((file) -> file.isDirectory() || file.getName().endsWith(".class")); + if (dirfiles != null) { + for (File file : dirfiles) { + if (file.isDirectory()) { + findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), classes); + } else { + String className = file.getName().substring(0, file.getName().length() - 6); + try { + classes.add(Class.forName(packageName + '.' + className)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + } + } + + private static void findClassesInPackageByJar(String packageName, Enumeration entries, String packageDirName, Set> classes) { + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.charAt(0) == '/') { + name = name.substring(1); + } + if (name.startsWith(packageDirName)) { + int idx = name.lastIndexOf('/'); + if (idx != -1) { + packageName = name.substring(0, idx).replace('/', '.'); + } + if (name.endsWith(".class") && !entry.isDirectory()) { + String className = name.substring(packageName.length() + 1, name.length() - 6); + try { + classes.add(Class.forName(packageName + '.' + className)); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + } + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/NullPlugin.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/NullPlugin.java new file mode 100644 index 0000000..00f315e --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/NullPlugin.java @@ -0,0 +1,152 @@ +package moe.monnairealms.camellia.utils; + +import org.bukkit.Server; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.configuration.file.FileConfiguration; +import org.bukkit.generator.BiomeProvider; +import org.bukkit.generator.ChunkGenerator; +import org.bukkit.plugin.PluginBase; +import org.bukkit.plugin.PluginDescriptionFile; +import org.bukkit.plugin.PluginLoader; +import org.bukkit.plugin.PluginLogger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +public class NullPlugin extends PluginBase { + private final String pluginName; + private boolean enabled = true; + private PluginDescriptionFile pdf; + + public NullPlugin() { + this.pluginName = "Minecraft"; + pdf = new PluginDescriptionFile(pluginName, "1.0", "nms"); + } + + @Override + public File getDataFolder() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginDescriptionFile getDescription() { + return pdf; + } + + // Paper start + @Override + public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() { + return pdf; + } + + @Override + public FileConfiguration getConfig() { + throw new UnsupportedOperationException("Not supported."); + } + // Paper end + + @Override + public InputStream getResource(String filename) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveDefaultConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void saveResource(String resourcePath, boolean replace) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void reloadConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginLogger getLogger() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginLoader getPluginLoader() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public Server getServer() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public void onDisable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void onLoad() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void onEnable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean isNaggable() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void setNaggable(boolean canNag) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public @Nullable BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean onCommand(CommandSender sender, Command command, String label, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public List onTabComplete(CommandSender sender, Command command, String alias, String[] args) { + throw new UnsupportedOperationException("Not supported."); + } + + // Paper start - lifecycle events + @Override + public @NotNull io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager getLifecycleManager() { + throw new UnsupportedOperationException("Not supported."); + } + // Paper end - lifecycle events +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RateThrottler.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RateThrottler.java new file mode 100644 index 0000000..86aa834 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RateThrottler.java @@ -0,0 +1,87 @@ +package moe.monnairealms.camellia.utils; + +import org.jetbrains.annotations.NotNull; + +public class RateThrottler { + private static final byte STATE_NOT_BEGIN = 0; + private static final byte STATE_RECORDING = 1; + private static final byte STATE_DESTROYED = 2; + + private byte status; + private int recordedThisTick = 0; + + private int totallyTicked = 0; + private int totalCount = 0; + + private void checkDestroyed() { + if (this.status == STATE_DESTROYED) { + throw new IllegalStateException("Already destroyed!"); + } + } + + public void increase() { + this.recordedThisTick++; + } + + public void mergeWith(@NotNull RateThrottler other) { + this.checkDestroyed(); + other.checkDestroyed(); + + this.totallyTicked += other.totallyTicked; + this.totalCount += other.totalCount; + } + + public void splitInto(@NotNull RateThrottler other) { + this.checkDestroyed(); + other.checkDestroyed(); + + other.totalCount = this.totalCount; + other.totallyTicked = this.totallyTicked; + } + + public double getAvgCount() { + return (double) this.totalCount / Math.min(this.totallyTicked, 1); + } + + public int getCountThisTick() { + return this.recordedThisTick; + } + + public boolean isOutOfRate(int expected) { + return this.recordedThisTick >= expected; + } + + public void destroy() { + if (this.status == STATE_DESTROYED) { + throw new IllegalStateException("Already destroyed!"); + } + + this.status = STATE_DESTROYED; + } + + public void begin() { + this.checkDestroyed(); + + if (this.status == STATE_RECORDING) { + throw new IllegalStateException("Attempt to begin a already recording throttler!"); + } + + this.status = STATE_RECORDING; + } + + public void done() { + this.checkDestroyed(); + + if (this.status == STATE_NOT_BEGIN) { + throw new IllegalStateException("Attempt to done a already done or new throttler!"); + + } + + this.status = STATE_NOT_BEGIN; + + this.totallyTicked++; + this.totalCount += this.recordedThisTick; + + this.recordedThisTick = 0; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RegionCreatorInfo.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RegionCreatorInfo.java new file mode 100644 index 0000000..ca4b090 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RegionCreatorInfo.java @@ -0,0 +1,8 @@ +package moe.monnairealms.camellia.utils; + +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; + +import java.nio.file.Path; + +public record RegionCreatorInfo(RegionStorageInfo info, Path filePath, Path folder, boolean sync) { +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RegionFileFactory.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RegionFileFactory.java new file mode 100644 index 0000000..ebf63b4 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/RegionFileFactory.java @@ -0,0 +1,10 @@ +package moe.monnairealms.camellia.utils; + +import moe.monnairealms.camellia.data.RegionFile; + +import java.io.IOException; + +@FunctionalInterface +public interface RegionFileFactory { + RegionFile newFile(RegionCreatorInfo info) throws IOException; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/ConfigCommandDialog.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/ConfigCommandDialog.java new file mode 100644 index 0000000..4781b8d --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/ConfigCommandDialog.java @@ -0,0 +1,117 @@ +package moe.monnairealms.camellia.utils.dialog; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import moe.monnairealms.camellia.config.ConfigsInstance; +import net.kyori.adventure.text.format.TextColor; +import net.minecraft.commands.functions.StringTemplate; +import net.minecraft.network.chat.Component; +import net.minecraft.server.dialog.action.CommandTemplate; +import net.minecraft.server.dialog.action.ParsedTemplate; +import net.minecraft.world.entity.player.Player; +import org.bukkit.command.CommandSender; + +import java.lang.reflect.Type; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class ConfigCommandDialog { + public static void openGui(Player player, String name, ConfigsInstance config) { + openGui(player, name, config, ""); + } + + public static void openGui(Player player, String name, ConfigsInstance config, String[] args) { + openGui(player, name, config, args.length == 1 ? "" : args[1]); + } + + public static void openGui(Player player, String name, ConfigsInstance config, String prefix) { + if (prefix.equals("full")) { + player.openDialog( + ConfigDialogUtil.createHolder( + name, + config.getAllDataFull(), + name + " submit " + )); + return; + } + + // Get all possible paths at current level + List keyList = config.completeConfigPath(prefix.isEmpty() ? prefix : prefix + "."); + List keySingleConfigs = config.getSingleConfig(prefix); + keyList.removeAll(keySingleConfigs); + DialogUtil.DialogBuilder builder = new DialogUtil.DialogBuilder(); + + // Add navigation buttons for each sub-path + for (String key : keyList) { + // Check if this key has children or is a valid config node + List childPaths = config.completeConfigPath(key + "."); + List childKeySingleConfigs = config.getSingleConfig(key); + + // Always create button if there are child paths or if it's a valid config node + if (!childPaths.isEmpty() || !childKeySingleConfigs.isEmpty()) { + String raw = name + " open-gui " + key + "$(missing)"; + StringTemplate template = StringTemplate.fromString(raw); + CommandTemplate commandTemplate = new CommandTemplate(new ParsedTemplate(raw, template)); + builder.addButton( + DialogUtil.createButton( + Component.translatable(key), + 300, + Optional.of(commandTemplate) + )); + } + } + + ConfigDialogUtil.addInputs( + config.getDataFull(keySingleConfigs), + name + " submit ", + builder + ); + + // Add "Show all configs" button at root level + if (prefix.isEmpty()) { + String raw = name + " open-gui full$(missing)"; + StringTemplate template = StringTemplate.fromString(raw); + CommandTemplate commandTemplate = new CommandTemplate(new ParsedTemplate(raw, template)); + builder.addButton( + DialogUtil.createButton( + Component.translatable("Show all configs"), + 300, + Optional.of(commandTemplate) + )); + } + + if (builder.getInputCount() == 0) { + builder.addButton( + DialogUtil.createButton( + Component.translatable("Close"), + 300, + Optional.empty() + )); + } + + builder.setTitle(name) + .setPause(false) + .setColumns(1); + player.openDialog( + DialogUtil.transformToHolder( + builder.build() + )); + } + + public static void processSubmit(CommandSender sender, ConfigsInstance config, String[] args) { + String fullText = String.join(" ", args); + Gson gson = new Gson(); + Type type = new TypeToken>() { + }.getType(); + Map map = gson.fromJson(fullText, type); + for (Map.Entry entry : map.entrySet()) { + config.setConfig(entry.getKey(), entry.getValue()); + } + config.reloadAsync(true).thenAccept(nullValue -> sender.sendMessage( + net.kyori.adventure.text.Component + .text("Apply config update successfully!") + .color(TextColor.color(0, 255, 0)) + )); + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/ConfigDialogUtil.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/ConfigDialogUtil.java new file mode 100644 index 0000000..b446f6a --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/ConfigDialogUtil.java @@ -0,0 +1,55 @@ +package moe.monnairealms.camellia.utils.dialog; + +import com.mojang.datafixers.util.Pair; +import moe.monnairealms.camellia.api.config.ConfigDataPair; +import net.minecraft.core.Holder; +import net.minecraft.server.dialog.Dialog; +import org.jetbrains.annotations.NotNull; +import org.jspecify.annotations.NonNull; + +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +public class ConfigDialogUtil { + public static Holder createHolder(String title, Set configs, String commandPrefix) { + return DialogUtil.createHolder(title, generateConfigMap(configs), commandPrefix); + } + + public static DialogUtil.DialogBuilder addInputs(Set configs, String commandPrefix, @NotNull DialogUtil.DialogBuilder builder) { + return DialogUtil.addInputs(generateConfigMap(configs), commandPrefix, builder); + } + + private static @NonNull Map> generateConfigMap(Set configs) { + Map> map = new TreeMap<>(); + for (ConfigDataPair config : configs) { + String key = config.key(); + Object value = config.value(); + String[] suggestions = config.suggestions(); + String comment = config.comment(); + String addition1 = ""; + if (comment != null && !comment.isEmpty()) { + addition1 = "Comments: " + comment; + } + + String addition2 = ""; + + if (suggestions != null && suggestions.length > 0) { + StringBuilder addition = new StringBuilder("Suggestions: "); + boolean first = true; + for (String suggestion : suggestions) { + if (!first) { + addition.append(", "); + } else { + first = false; + } + addition.append(suggestion); + } + addition2 = addition.toString(); + } + String addition = addition1.isEmpty() ? addition2 : addition2.isEmpty() ? addition1 : addition1 + "\n" + addition2; + map.put(key, Pair.of(value, addition)); + } + return map; + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/DialogUtil.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/DialogUtil.java new file mode 100644 index 0000000..a558ebc --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/dialog/DialogUtil.java @@ -0,0 +1,472 @@ +package moe.monnairealms.camellia.utils.dialog; + +import com.mojang.datafixers.util.Pair; +import net.minecraft.commands.functions.StringTemplate; +import net.minecraft.core.Holder; +import net.minecraft.network.chat.Component; +import net.minecraft.server.dialog.*; +import net.minecraft.server.dialog.action.Action; +import net.minecraft.server.dialog.action.CommandTemplate; +import net.minecraft.server.dialog.action.ParsedTemplate; +import net.minecraft.server.dialog.body.DialogBody; +import net.minecraft.server.dialog.input.BooleanInput; +import net.minecraft.server.dialog.input.NumberRangeInput; +import net.minecraft.server.dialog.input.TextInput; +import org.jetbrains.annotations.NotNull; +import org.json.simple.JSONObject; + +import java.util.*; + +public class DialogUtil { + public static Holder createHolder(String title, Map> map, String commandPrefix) { + return transformToHolder( + createDialog(title, map, commandPrefix) + ); + } + + public static Holder createHolder(String title, List list) { + return transformToHolder( + createDialog(title, list) + ); + } + + public static Holder transformToHolder(Dialog dialog) { + return Holder.direct(dialog); + } + + public static MultiActionDialog createDialog(String title, List options) { + DialogBuilder builder = new DialogBuilder(); + for (String option : options) { + builder.addButton( + createButton( + Component.translatable(option), + 300, + Optional.empty() + )); + } + + builder.setTitle(title) + .setPause(false) + .setColumns(1); + + return builder.build(); + } + + public static MultiActionDialog createDialog(String title, Map> map, String commandPrefix) { + return addInputs(map, commandPrefix, new DialogBuilder()) + .setTitle(title) + .setPause(false) + .setColumns(1) + .build(); + } + + public static DialogBuilder addInputs(Map> map, String commandPrefix, @NotNull DialogBuilder builder) { + boolean hasInput = false; + JSONObject valueBuilder = new JSONObject(); + Set usedKeys = new HashSet<>(); + int keyCounter = 0; + + for (Map.Entry> entry : map.entrySet()) { + Object value = entry.getValue().getFirst(); + String label = entry.getKey(); + String key = sanitizeKey(label); + String comment = entry.getValue().getSecond(); + + String originalKey = key; + while (usedKeys.contains(key)) { + key = originalKey + "_" + (++keyCounter); + } + usedKeys.add(key); + + valueBuilder.put(label, "$(" + key + ")"); + + if (comment != null && !comment.isEmpty()) { + String addition = "Any edit in this text input will not save to file.\n" + comment; + String _label = "Additional information of " + label; + String _key = sanitizeKey(_label); + String _originalKey = _key; + while (usedKeys.contains(_key)) { + _key = _originalKey + "_" + (++keyCounter); + } + usedKeys.add(_key); + builder.addInput( + createTextInput( + _label, + _key, + addition, + 300, + true, + 2147483647, + new TextInput.MultilineOptions( + Optional.of(1000), + Optional.of( + (int) (20 * (addition.lines().count() + 1) + ) + ) + ) + ) + ); + } + + switch (value) { + case Boolean boolValue -> { + Input checkbox = createCheckbox(label, key, boolValue, "true", "false"); + builder.addInput(checkbox); + } + case String stringValue -> { + Input textbox = createTextInput(label, key, stringValue, 300, true, 2147483647, null); + builder.addInput(textbox); + } + case Number numberValue -> { + Input numberInput = createTextInput(label, key, numberValue.toString(), 300, true, 2147483647, null); + builder.addInput(numberInput); + } + default -> { + } + } + + hasInput = true; + } + String raw = commandPrefix + valueBuilder.toJSONString() + "$(missing)"; + StringTemplate template = StringTemplate.fromString(raw); + CommandTemplate confirmTemplate = new CommandTemplate(new ParsedTemplate(raw, template)); + if (hasInput) { + builder.addButton(createButton( + Component.translatable("Confirm"), + 300, + Optional.of(confirmTemplate) + )) + .addButton(createButton( + Component.translatable("Cancel"), + 300, + Optional.empty() + )); + } + + return builder; + } + + public static Input createCheckbox(String label, String key, boolean value, String trueText, String falseText) { + return new Input(key, new BooleanInput( + Component.translatable(label), + value, + trueText, + falseText + )); + } + + public static Input createTextInput(String label, String key, String value, int width, boolean labelVisible, int maxLength, TextInput.MultilineOptions multilineOptions) { + return new Input(key, new TextInput( + width, + Component.translatable(label), + labelVisible, + value, + maxLength, + Optional.ofNullable(multilineOptions) + )); + } + + // TODO: number input is not work now + public static Input createNumberInput(String label, String key, Number value, NumberRangeInput.RangeInfo rangeInfo) { + return new Input(key, new NumberRangeInput( + 300, + Component.translatable(label), + value.getClass().getName(), + rangeInfo + )); + } + + public static ActionButton createButton(Component key, int width, Optional action) { + CommonButtonData buttonData = new CommonButtonData( + key, + width + ); + return new ActionButton(buttonData, action); + } + + /** + * Calculate the display width of a string (Chinese characters have width 2, English characters have width 1) + * + * @param text The text to calculate width for + * @return The display width + */ + public static int getTextDisplayWidth(String text) { + int width = 0; + for (char c : text.toCharArray()) { + // Chinese character range + if (c >= '\u4e00' && c <= '\u9fff') { + width += 2; + } else { + width += 1; + } + } + return width; + } + + /** + * Split text into words, preserving spaces and line breaks + * + * @param text The text to split + * @return List of words with their separators + */ + private static List splitIntoWords(String text) { + List words = new ArrayList<>(); + StringBuilder currentWord = new StringBuilder(); + + for (int i = 0; i < text.length(); i++) { + char c = text.charAt(i); + + if (Character.isWhitespace(c)) { + // If we have accumulated a word, add it to list + if (!currentWord.isEmpty()) { + words.add(currentWord.toString()); + currentWord = new StringBuilder(); + } + // Add whitespace as separate "word" + words.add(String.valueOf(c)); + } else { + // Accumulate non-whitespace characters + currentWord.append(c); + } + } + + // Add the last word if exists + if (!currentWord.isEmpty()) { + words.add(currentWord.toString()); + } + + return words; + } + + /** + * Automatically wrap text by words based on display width + * + * @param text Original text + * @param maxWidth Maximum display width per line + * @return Processed text with appropriate line breaks + */ + public static String wrapTextByWordsAndWidth(String text, int maxWidth) { + // If text is null or width is within limit, return as is + if (text == null || getTextDisplayWidth(text) <= maxWidth) { + return text; + } + + List words = splitIntoWords(text); + StringBuilder wrappedText = new StringBuilder(); + StringBuilder currentLine = new StringBuilder(); + int currentWidth = 0; + + for (String word : words) { + int wordWidth = getTextDisplayWidth(word); + + // Handle explicit line breaks + if (word.contains("\n")) { + // Add current line content + wrappedText.append(currentLine); + // Add the word containing newline + wrappedText.append(word); + // Reset for next line + currentLine = new StringBuilder(); + currentWidth = 0; + continue; + } + + // Handle whitespace characters + if (word.matches("\\s+")) { + // If adding space would exceed limit, wrap to next line + if (currentWidth + wordWidth > maxWidth && !currentLine.toString().trim().isEmpty()) { + wrappedText.append(currentLine.toString().trim()).append("\n"); + currentLine = new StringBuilder(); + currentWidth = 0; + // Only add space if it's not leading whitespace on new line + if (!word.equals(" ")) { + currentLine.append(word); + currentWidth = wordWidth; + } + } else { + currentLine.append(word); + currentWidth += wordWidth; + } + continue; + } + + // Handle regular words + // If adding word would exceed limit, wrap to next line + if (currentWidth + wordWidth > maxWidth && !currentLine.toString().trim().isEmpty()) { + wrappedText.append(currentLine.toString().trim()).append("\n"); + currentLine = new StringBuilder(); + currentWidth = 0; + } + + currentLine.append(word); + currentWidth += wordWidth; + } + + // Append the last line if not empty + if (!currentLine.toString().trim().isEmpty()) { + wrappedText.append(currentLine.toString().trim()); + } + + return wrappedText.toString(); + } + + /** + * Automatically wrap text based on display width + * + * @param text Original text + * @param maxWidth Maximum display width + * @return Processed text with line breaks + */ + public static String wrapTextByDisplayWidth(String text, int maxWidth) { + // If text is null or width is within limit, return as is + if (text == null || getTextDisplayWidth(text) <= maxWidth) { + return text; + } + + StringBuilder wrappedText = new StringBuilder(); + StringBuilder currentLine = new StringBuilder(); + int currentWidth = 0; + + for (char c : text.toCharArray()) { + // Determine character width (2 for Chinese, 1 for others) + int charWidth = (c >= '\u4e00' && c <= '\u9fff') ? 2 : 1; + + if (currentWidth + charWidth > maxWidth && !currentLine.isEmpty()) { + wrappedText.append(currentLine.toString().trim()).append("\n"); + currentLine = new StringBuilder(); + currentWidth = 0; + } + + currentLine.append(c); + currentWidth += charWidth; + } + + if (!currentLine.isEmpty()) { + wrappedText.append(currentLine.toString().trim()); + } + + return wrappedText.toString(); + } + + private static String sanitizeKey(String originalKey) { + if (originalKey == null || originalKey.isEmpty()) { + return "key_" + System.currentTimeMillis(); // generate a unique key + } + + StringBuilder sanitized = new StringBuilder(); + + char firstChar = originalKey.charAt(0); + if (Character.isDigit(firstChar)) { + sanitized.append("_").append(firstChar); + } else if (isValidKeyChar(firstChar)) { + sanitized.append(firstChar); + } else { + sanitized.append("_"); + } + + for (int i = 1; i < originalKey.length(); i++) { + char c = originalKey.charAt(i); + if (isValidKeyChar(c)) { + sanitized.append(c); + } else { + sanitized.append("_"); + } + } + + String result = sanitized.toString(); + if (result.isEmpty() || Character.isDigit(result.charAt(0))) { + result = "key_" + result; + } + + return result; + } + + private static boolean isValidKeyChar(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + + public static class DialogBuilder { + String title = ""; + Optional externalTitle = Optional.empty(); + boolean canCloseWithEscape = true; + boolean pause = true; + int actionClose = 0; + int columns = 2; + List buttons = new ArrayList<>(); + List inputs = new ArrayList<>(); + List bodies = new ArrayList<>(); + Optional exitButton = Optional.empty(); + + public DialogBuilder setTitle(String title) { + this.title = title; + return this; + } + + public DialogBuilder setExternalTitle(Component externalTitle) { + this.externalTitle = Optional.of(externalTitle); + return this; + } + + public DialogBuilder setCanCloseWithEscape(boolean canCloseWithEscape) { + this.canCloseWithEscape = canCloseWithEscape; + return this; + } + + public DialogBuilder setPause(boolean pause) { + this.pause = pause; + return this; + } + + // 0 for close, 1 for none, 2 for wait + public DialogBuilder setActionClose(int actionClose) { + this.actionClose = actionClose; + return this; + } + + public DialogBuilder setColumns(int columns) { + this.columns = columns; + return this; + } + + public DialogBuilder addButton(ActionButton button) { + this.buttons.add(button); + return this; + } + + public int getButtonCount() { + return this.buttons.size(); + } + + public DialogBuilder addInput(Input input) { + this.inputs.add(input); + return this; + } + + public int getInputCount() { + return this.inputs.size(); + } + + public DialogBuilder addBody(DialogBody body) { + this.bodies.add(body); + return this; + } + + public DialogBuilder setExitButton(ActionButton exitButton) { + this.exitButton = Optional.of(exitButton); + return this; + } + + public MultiActionDialog build() { + CommonDialogData data = new CommonDialogData( + Component.translatable(title), + externalTitle, + canCloseWithEscape, + pause, + DialogAction.values()[actionClose], // if paused you must do something + bodies, + inputs + ); + return new MultiActionDialog(data, buttons, exitButton, columns); + } + } +} diff --git a/camellia-server/src/main/java/moe/monnairealms/camellia/utils/entity/EntityMoveOutOfRegionException.java b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/entity/EntityMoveOutOfRegionException.java new file mode 100644 index 0000000..8c0e909 --- /dev/null +++ b/camellia-server/src/main/java/moe/monnairealms/camellia/utils/entity/EntityMoveOutOfRegionException.java @@ -0,0 +1,30 @@ +package moe.monnairealms.camellia.utils.entity; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.MoverType; +import net.minecraft.world.phys.Vec3; + +public class EntityMoveOutOfRegionException extends RuntimeException { + private final Entity entity; + private final Vec3 movement; + private final MoverType moverType; + + public EntityMoveOutOfRegionException(Entity entity, Vec3 movement, MoverType moverType) { + this.entity = entity; + this.movement = movement; + this.moverType = moverType; + } + + + public Entity getEntity() { + return this.entity; + } + + public Vec3 getMovement() { + return this.movement; + } + + public MoverType getMoverType() { + return this.moverType; + } +} diff --git a/camellia-server/src/main/java/org/dreeam/leaf/util/FastBitRadixSort.java b/camellia-server/src/main/java/org/dreeam/leaf/util/FastBitRadixSort.java new file mode 100644 index 0000000..7541146 --- /dev/null +++ b/camellia-server/src/main/java/org/dreeam/leaf/util/FastBitRadixSort.java @@ -0,0 +1,104 @@ +/** + * This file is a part of Leaf( + * * This file is a part of Leaf(https://github.com/Winds-Studio/Leaf/blob/ver/1.21.11/leaf-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java)) + * Original license: https://github.com/Winds-Studio/Leaf/blob/ver/1.21.11/LICENSE.md + */ +package org.dreeam.leaf.util; + +import net.minecraft.world.entity.Entity; + +public final class FastBitRadixSort { + + private static final int SMALL_ARRAY_THRESHOLD = 6; + private static final long[] LONGS = new long[0]; + private long[] bitsBuffer = LONGS; + + public void sort(Object[] entities, int size, net.minecraft.core.Position target) { + if (size <= 1) { + return; + } + + if (this.bitsBuffer.length < size) { + this.bitsBuffer = new long[size]; + } + double tx = target.x(); + double ty = target.y(); + double tz = target.z(); + for (int i = 0; i < size; i++) { + this.bitsBuffer[i] = Double.doubleToRawLongBits(((Entity) entities[i]).distanceToSqr(tx, ty, tz)); + } + + fastRadixSort(entities, this.bitsBuffer, 0, size - 1, 62); + } + + private static void fastRadixSort( + Object[] ents, + long[] bits, + int low, + int high, + int bit + ) { + if (bit < 0 || low >= high) { + return; + } + + if (high - low <= SMALL_ARRAY_THRESHOLD) { + insertionSort(ents, bits, low, high); + return; + } + + int i = low; + int j = high; + final long mask = 1L << bit; + + while (i <= j) { + while (i <= j && (bits[i] & mask) == 0) { + i++; + } + while (i <= j && (bits[j] & mask) != 0) { + j--; + } + if (i < j) { + swap(ents, bits, i++, j--); + } + } + + if (low < j) { + fastRadixSort(ents, bits, low, j, bit - 1); + } + if (i < high) { + fastRadixSort(ents, bits, i, high, bit - 1); + } + } + + private static void insertionSort( + Object[] ents, + long[] bits, + int low, + int high + ) { + for (int i = low + 1; i <= high; i++) { + int j = i; + Object currentEntity = ents[j]; + long currentBits = bits[j]; + + while (j > low && bits[j - 1] > currentBits) { + ents[j] = ents[j - 1]; + bits[j] = bits[j - 1]; + j--; + } + ents[j] = currentEntity; + bits[j] = currentBits; + } + } + + private static void swap(Object[] ents, long[] bits, int a, int b) { + Object tempEntity = ents[a]; + ents[a] = ents[b]; + ents[b] = tempEntity; + + long tempBits = bits[a]; + bits[a] = bits[b]; + bits[b] = tempBits; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java b/camellia-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java new file mode 100644 index 0000000..7a53e2a --- /dev/null +++ b/camellia-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java @@ -0,0 +1,41 @@ +/** + * This file is a part of Leaf(https://github.com/Winds-Studio/Leaf/blob/ver/1.21.8/leaf-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java) + * Original license: https://github.com/Winds-Studio/Leaf/blob/ver/1.21.8/LICENSE.md + */ +package org.dreeam.leaf.world.biome; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.world.level.biome.Biome; + +import java.util.function.Function; +import java.util.function.Supplier; + +public class PositionalBiomeGetter implements Supplier> { + + private final Function> biomeGetter; + private final BlockPos.MutableBlockPos pos; + private int nextX, nextY, nextZ; + private volatile Holder curBiome; + + public PositionalBiomeGetter(Function> biomeGetter, BlockPos.MutableBlockPos pos) { + this.biomeGetter = biomeGetter; + this.pos = pos; + } + + public void update(int nextX, int nextY, int nextZ) { + this.nextX = nextX; + this.nextY = nextY; + this.nextZ = nextZ; + this.curBiome = null; + } + + @Override + public Holder get() { + Holder biome = curBiome; + if (biome == null) { + curBiome = biome = biomeGetter.apply(pos.set(nextX, nextY, nextZ)); + } + return biome; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/command/ArgumentNode.java b/camellia-server/src/main/java/org/leavesmc/leaves/command/ArgumentNode.java new file mode 100644 index 0000000..b276c8f --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/command/ArgumentNode.java @@ -0,0 +1,83 @@ +/* + * 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.command; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.concurrent.CompletableFuture; + +public abstract class ArgumentNode extends CommandNode { + protected final ArgumentType argumentType; + + protected ArgumentNode(String name, ArgumentType argumentType) { + super(name); + this.argumentType = argumentType; + } + + @SuppressWarnings({"unused", "RedundantThrows"}) + protected CompletableFuture getSuggestions(final CommandContext context, final SuggestionsBuilder builder) throws CommandSyntaxException { + return Suggestions.empty(); + } + + protected boolean overrideSuggestions() { + return isMethodOverridden("getSuggestions", ArgumentNode.class); + } + + @Override + protected ArgumentBuilder compileBase() { + RequiredArgumentBuilder argumentBuilder = Commands.argument(name, argumentType); + + if (overrideSuggestions()) { + argumentBuilder.suggests( + (context, builder) -> getSuggestions(new CommandContext(context), builder) + ); + } + + return argumentBuilder; + } + + public static class ArgumentSuggestions { + @Contract(pure = true) + public static WrappedArgument.@NotNull SuggestionApplier strings(String... values) { + return (context, builder) -> { + for (String s : values) { + builder.suggest(s); + } + }; + } + + @Contract(pure = true) + public static WrappedArgument.@NotNull SuggestionApplier strings(List values) { + return (context, builder) -> { + for (String s : values) { + builder.suggest(s); + } + }; + } + } +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandContext.java b/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandContext.java new file mode 100644 index 0000000..9d13a58 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandContext.java @@ -0,0 +1,142 @@ +/* + * 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.command; + +import com.mojang.brigadier.Command; +import com.mojang.brigadier.RedirectModifier; +import com.mojang.brigadier.context.ParsedCommandNode; +import com.mojang.brigadier.context.StringRange; +import com.mojang.brigadier.tree.CommandNode; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +import static org.leavesmc.leaves.command.CommandNode.getNameForNode; + +@SuppressWarnings({"ClassCanBeRecord", "unused"}) +public class CommandContext { + private final com.mojang.brigadier.context.CommandContext source; + + public CommandContext(com.mojang.brigadier.context.CommandContext source) { + this.source = source; + } + + public com.mojang.brigadier.context.CommandContext getChild() { + return source.getChild(); + } + + public com.mojang.brigadier.context.CommandContext getLastChild() { + return source.getLastChild(); + } + + public Command getCommand() { + return source.getCommand(); + } + + public CommandSourceStack getSource() { + return source.getSource(); + } + + public CommandSender getSender() { + return source.getSource().getSender(); + } + + public @NotNull V getArgument(final String name, final Class clazz) { + return source.getArgument(name, clazz); + } + + public int getInteger(final String name) { + return source.getArgument(name, Integer.class); + } + + public boolean getBoolean(final String name) { + return source.getArgument(name, Boolean.class); + } + + public String getString(final String name) { + return source.getArgument(name, String.class); + } + + @SuppressWarnings("unchecked") + public @NotNull V getArgument(final Class> nodeClass) { + String name = getNameForNode(nodeClass); + return (V) source.getArgument(name, Object.class); + } + + public V getArgumentOrDefault(final Class> nodeClass, final V defaultValue) { + try { + return getArgument(nodeClass); + } catch (IllegalArgumentException e) { + return defaultValue; + } + } + + public V getArgumentOrDefault(final String name, final Class clazz, final V defaultValue) { + try { + return source.getArgument(name, clazz); + } catch (IllegalArgumentException e) { + return defaultValue; + } + } + + public String getStringOrDefault(final String name, final String defaultValue) { + return getArgumentOrDefault(name, String.class, defaultValue); + } + + public int getIntegerOrDefault(final String name, final int defaultValue) { + return getArgumentOrDefault(name, Integer.class, defaultValue); + } + + public float getFloatOrDefault(final String name, final float defaultValue) { + return getArgumentOrDefault(name, Float.class, defaultValue); + } + + public RedirectModifier getRedirectModifier() { + return source.getRedirectModifier(); + } + + public StringRange getRange() { + return source.getRange(); + } + + public String getInput() { + return source.getInput(); + } + + public CommandNode getRootNode() { + return source.getRootNode(); + } + + public List> getNodes() { + return source.getNodes(); + } + + public boolean hasNodes() { + return source.hasNodes(); + } + + public boolean isForked() { + return source.isForked(); + } + + public com.mojang.brigadier.context.CommandContext getMojangContext() { + return source; + } +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandNode.java b/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandNode.java new file mode 100644 index 0000000..335bdc4 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandNode.java @@ -0,0 +1,100 @@ +/* + * 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.command; + +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import java.util.stream.Stream; + +public abstract class CommandNode { + private static final Map, String> class2NameMap = new HashMap<>(); + + protected final String name; + protected final List children = new ArrayList<>(); + + protected CommandNode(String name) { + this.name = name; + class2NameMap.put(getClass(), name); + } + + @SafeVarargs + protected final void children(Supplier... childrenClasses) { + this.children.addAll(Stream.of(childrenClasses).map(Supplier::get).toList()); + } + + protected final void children(CommandNode... childrenClasses) { + this.children.addAll(List.of(childrenClasses)); + } + + protected abstract ArgumentBuilder compileBase(); + + protected boolean execute(CommandContext context) throws CommandSyntaxException { + return true; + } + + public boolean requires(CommandSourceStack source) { + return true; + } + + public String getName() { + return this.name; + } + + protected ArgumentBuilder compile() { + ArgumentBuilder builder = compileBase().requires(this::requires); + + for (CommandNode child : children) { + builder = builder.then(child.compile()); + } + + if (canExecute()) { + builder = builder.executes(mojangCtx -> { + CommandContext ctx = new CommandContext(mojangCtx); + return execute(ctx) ? 1 : 0; + }); + } + + return builder; + } + + protected boolean canExecute() { + return isMethodOverridden("execute", CommandNode.class); + } + + protected boolean isMethodOverridden(String methodName, @NotNull Class baseClass) { + for (Method method : getClass().getDeclaredMethods()) { + if (method.getName().equals(methodName)) { + return method.getDeclaringClass() != baseClass; + } + } + return false; + } + + public static String getNameForNode(Class nodeClass) { + return class2NameMap.get(nodeClass); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandUtils.java b/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandUtils.java new file mode 100644 index 0000000..cf11c85 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/command/CommandUtils.java @@ -0,0 +1,149 @@ +/* + * 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.command; + +import com.google.common.base.Functions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import org.bukkit.Bukkit; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class CommandUtils { + + public static void registerPermissions(String base, @NotNull List children) { + List permissions = new ArrayList<>(); + permissions.add(base); + permissions.addAll(children.stream().map((it) -> base + "." + it.getName()).toList()); + registerPermissions(permissions); + } + + public static void registerPermissions(@NotNull List permissions) { + PluginManager pluginManager = Bukkit.getServer().getPluginManager(); + for (String perm : permissions) { + if (pluginManager.getPermission(perm) == null) { + pluginManager.addPermission(new Permission(perm, PermissionDefault.OP)); + } + } + } + + @DefaultQualifier(NonNull.class) + public static List getListClosestMatchingLast(final String last, final Collection collection) { + if (collection.isEmpty()) { + return Collections.emptyList(); + } + + ArrayList candidates = Lists.newArrayList(); + String lastLower = last.toLowerCase(); + for (String item : Iterables.transform(collection, Functions.toStringFunction())) { + String itemLower = item.toLowerCase(); + if (itemLower.startsWith(lastLower)) { + candidates.add(Candidate.of(item, 0)); + } else if (itemLower.contains(lastLower)) { + candidates.add(Candidate.of(item, damerauLevenshteinDistance(lastLower, itemLower))); + } + } + candidates.sort(Comparator.comparingInt(Candidate::score)); + + List results = new ArrayList<>(candidates.size()); + for (Candidate candidate : candidates) { + results.add(candidate.item); + } + + return results; + } + + /** + * Computes the Dameraur-Levenshtein Distance between two strings. Adapted + * from the algorithm at Wikipedia: Damerau–Levenshtein distance + * + * @param s1 The first string being compared. + * @param s2 The second string being compared. + * @return The number of substitutions, deletions, insertions, and + * transpositions required to get from s1 to s2. + */ + @SuppressWarnings("DuplicatedCode") + private static int damerauLevenshteinDistance(@Nullable String s1, @Nullable String s2) { + if (s1 == null && s2 == null) { + return 0; + } + if (s1 != null && s2 == null) { + return s1.length(); + } + if (s1 == null) { + return s2.length(); + } + + int s1Len = s1.length(); + int s2Len = s2.length(); + int[][] H = new int[s1Len + 2][s2Len + 2]; + + int INF = s1Len + s2Len; + H[0][0] = INF; + for (int i = 0; i <= s1Len; i++) { + H[i + 1][1] = i; + H[i + 1][0] = INF; + } + for (int j = 0; j <= s2Len; j++) { + H[1][j + 1] = j; + H[0][j + 1] = INF; + } + + Map sd = new HashMap<>(); + for (char Letter : (s1 + s2).toCharArray()) { + if (!sd.containsKey(Letter)) { + sd.put(Letter, 0); + } + } + + for (int i = 1; i <= s1Len; i++) { + int DB = 0; + for (int j = 1; j <= s2Len; j++) { + int i1 = sd.get(s2.charAt(j - 1)); + int j1 = DB; + + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + H[i + 1][j + 1] = H[i][j]; + DB = j; + } else { + H[i + 1][j + 1] = Math.min(H[i][j], Math.min(H[i + 1][j], H[i][j + 1])) + 1; + } + + H[i + 1][j + 1] = Math.min(H[i + 1][j + 1], H[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1)); + } + sd.put(s1.charAt(i - 1), i); + } + + return H[s1Len + 1][s2Len + 1]; + } + + private record Candidate(String item, int score) { + @Contract("_, _ -> new") + private static @NotNull Candidate of(String item, int score) { + return new Candidate(item, score); + } + } +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/command/LiteralNode.java b/camellia-server/src/main/java/org/leavesmc/leaves/command/LiteralNode.java new file mode 100644 index 0000000..4495946 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/command/LiteralNode.java @@ -0,0 +1,34 @@ +/* + * 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.command; + +import com.mojang.brigadier.builder.ArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; + +public class LiteralNode extends CommandNode { + + protected LiteralNode(String name) { + super(name); + } + + @Override + protected ArgumentBuilder compileBase() { + return Commands.literal(name); + } +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/command/RootNode.java b/camellia-server/src/main/java/org/leavesmc/leaves/command/RootNode.java new file mode 100644 index 0000000..cc3163b --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/command/RootNode.java @@ -0,0 +1,73 @@ +/* + * 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.command; + +import com.mojang.brigadier.builder.ArgumentBuilder; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.PaperCommands; +import org.bukkit.Bukkit; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import static org.leavesmc.leaves.command.CommandUtils.registerPermissions; + +public abstract class RootNode extends LiteralNode { + private final String permissionBase; + + public RootNode(String name, String permissionBase) { + super(name); + this.permissionBase = permissionBase; + } + + @Override + protected ArgumentBuilder compile() { + registerPermissions(permissionBase, this.children); + return super.compile(); + } + + public static boolean hasPermission(String permissionBase, @NotNull CommandSender sender, String... subcommand) { + if (sender.hasPermission(permissionBase)) return true; + String currentPermission = permissionBase; + for (String sub : subcommand) { + currentPermission += "." + sub; + if (sender.hasPermission(currentPermission)) return true; + } + return false; + } + + @Override + public boolean requires(@NotNull CommandSourceStack source) { + return children.stream().anyMatch(child -> child.requires(source)); + } + + @SuppressWarnings("unchecked") + public void register() { + PaperCommands.INSTANCE.setValid(); + PaperCommands.INSTANCE.getDispatcher().register((LiteralArgumentBuilder) compile()); + PaperCommands.INSTANCE.invalidate(); + Bukkit.getOnlinePlayers().forEach(org.bukkit.entity.Player::updateCommands); + } + + public void unregister() { + PaperCommands.INSTANCE.setValid(); + PaperCommands.INSTANCE.getDispatcher().getRoot().removeCommand(name); + PaperCommands.INSTANCE.invalidate(); + Bukkit.getOnlinePlayers().forEach(org.bukkit.entity.Player::updateCommands); + } +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/command/WrappedArgument.java b/camellia-server/src/main/java/org/leavesmc/leaves/command/WrappedArgument.java new file mode 100644 index 0000000..5e4bb87 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/command/WrappedArgument.java @@ -0,0 +1,85 @@ +/* + * 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.command; + +import com.mojang.brigadier.arguments.ArgumentType; +import com.mojang.brigadier.builder.RequiredArgumentBuilder; +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import io.papermc.paper.command.brigadier.CommandSourceStack; +import io.papermc.paper.command.brigadier.Commands; + +import java.util.concurrent.CompletableFuture; + +public class WrappedArgument { + private final String name; + private final ArgumentType type; + private AsyncSuggestionProvider asyncSuggestionProvider = null; + private SuggestionApplier suggestionApplier = null; + private boolean optional = false; + + public WrappedArgument(String name, ArgumentType type) { + this.name = name; + this.type = type; + } + + public WrappedArgument suggestsAsync(AsyncSuggestionProvider provider) { + this.asyncSuggestionProvider = provider; + return this; + } + + public WrappedArgument suggests(SuggestionApplier provider) { + this.suggestionApplier = provider; + return this; + } + + public WrappedArgument setOptional(boolean optional) { + this.optional = optional; + return this; + } + + public boolean isOptional() { + return optional; + } + + public RequiredArgumentBuilder compile() { + RequiredArgumentBuilder builder = Commands.argument(name, type); + if (asyncSuggestionProvider != null) { + builder.suggests((context, b) -> + asyncSuggestionProvider.getSuggestions(new CommandContext(context), b) + ); + } else if (suggestionApplier != null) { + builder.suggests((context, b) -> { + suggestionApplier.applySuggestions(new CommandContext(context), b); + return CompletableFuture.completedFuture(b.build()); + }); + } + return builder; + } + + @FunctionalInterface + public interface SuggestionApplier { + void applySuggestions(final CommandContext context, final SuggestionsBuilder builder) throws CommandSyntaxException; + } + + @FunctionalInterface + public interface AsyncSuggestionProvider { + CompletableFuture getSuggestions(final CommandContext context, final SuggestionsBuilder builder) throws CommandSyntaxException; + } +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java new file mode 100644 index 0000000..42e7368 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java @@ -0,0 +1,47 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.api.inventory; + +public interface LithiumCooldownReceivingInventory { + + /** + * To be implemented by modded inventories that want to receive hopper-like transfer cooldowns from lithium's (!) + * item transfers. Hopper-like transfer cooldown means a cooldown that is only set if the hopper was empty before + * the transfer. + * NOTE: Lithium does not replace all of vanilla's item transfers. Mod authors still need to implement + * their own hooks for vanilla code even when they require users to install Lithium. + * + * @param currentTime tick time of the item transfer. + */ + default void setTransferCooldown(long currentTime) { + } + + + /** + * To be implemented by modded inventories that want to receive hopper-like transfer cooldowns from lithium's (!) + * item transfers. Hopper-like transfer cooldown means a cooldown that is only set if the hopper was empty before + * the transfer. + * NOTE: Lithium does not replace all of vanilla's item transfers. Mod authors still need to implement + * their own hooks for vanilla code even when they require users to install Lithium. + * + * @return Whether this inventory wants to receive transfer cooldowns from lithium's code + */ + default boolean canReceiveTransferCooldown() { + return false; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java new file mode 100644 index 0000000..a03e559 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java @@ -0,0 +1,33 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.api.inventory; + +import net.minecraft.core.Direction; +import net.minecraft.world.item.ItemStack; + +public interface LithiumDefaultedList { + /** + * Call this method when the behavior of + * {@link net.minecraft.world.Container#canPlaceItem(int, ItemStack)} + * {@link net.minecraft.world.WorldlyContainer#canPlaceItemThroughFace(int, ItemStack, Direction)} + * {@link net.minecraft.world.WorldlyContainer#canTakeItemThroughFace(int, ItemStack, Direction)} + * or similar functionality changed. + * This method will not need to be called if this change in behavior is triggered by a change of the stack list contents. + */ + void changedInteractionConditions(); +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java new file mode 100644 index 0000000..3d047d9 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java @@ -0,0 +1,78 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.api.inventory; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.Container; +import net.minecraft.world.entity.vehicle.ContainerEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; + +/** + * Provides the ability for mods to allow Lithium's hopper optimizations to access their inventories' for item transfers. + * This exists because Lithium's optimized hopper logic will only interact with inventories more efficiently than + * vanilla if the stack list can be directly accessed and replaced with Lithium's custom stack list. + * It is not required to implement this interface, but doing so will allow the mod's inventories to benefit from + * Lithium's optimizations. + *

+ * This interface should be implemented by your {@link Container} or + * {@link net.minecraft.world.WorldlyContainer} type to access the stack list. + *

+ * An inventory must not extend {@link net.minecraft.world.level.block.entity.BlockEntity} if it has a supporting block that + * implements {@link ContainerEntity}. + *

+ * The hopper interaction behavior of a LithiumInventory should only change if the content of the inventory + * stack list also changes. For example, an inventory which only accepts an item if it already contains an item of the + * same type would work fine (changing the acceptance condition only happens when changing the inventory contents here). + * However, an inventory which accepts an item only if a certain block is near its position will need to signal this + * change to hoppers by calling {@link LithiumDefaultedList#changedInteractionConditions()}. + * + * @author 2No2Name + */ +public interface LithiumInventory extends Container { + + /** + * Getter for the inventory stack list of this inventory. + * + * @return inventory stack list + */ + NonNullList getInventoryLithium(); + + /** + * Setter for the inventory stack list of this inventory. + * Used to replace the stack list with Lithium's custom stack list. + * + * @param inventory inventory stack list + */ + void setInventoryLithium(NonNullList inventory); + + /** + * Generates the loot like a hopper access would do in vanilla. + *

+ * If a modded inventory has custom loot generation code, it will be required to override this + * loot generation method. Otherwise, its loot may be generated too late. + */ + default void generateLootLithium() { + if (this instanceof RandomizableContainerBlockEntity) { + ((RandomizableContainerBlockEntity) this).unpackLootTable(null); + } + if (this instanceof ContainerEntity) { + ((ContainerEntity) this).unpackChestVehicleLootTable(null); + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java new file mode 100644 index 0000000..f30d34b --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java @@ -0,0 +1,39 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.api.inventory; + +public interface LithiumTransferConditionInventory { + + /** + * Implement this method to signal that the inventory requires a stack size of 1 for item insertion tests. + * Lithium's hopper optimization transfers a single item, but to avoid excessive copying of item stacks, it passes + * the original stack to the inventory's insertion test. If the inventory requires a stack size of 1 for this test, + * the stack should be copied. However, lithium cannot detect whether the copy is necessary and this method is meant + * to signal this requirement. When the method is not implemented even though it is required, Lithium's hopper + * optimizations may not transfer items correctly to this inventory. + *

+ * The only vanilla inventory that requires this is the Chiseled Bookshelf. Mods with such special inventories + * should implement this method in the inventories' class. + * (It is not required to implement this interface, just the method is enough.) + * + * @return whether the inventory requires a stack size of 1 for item insertion tests + */ + default boolean lithium$itemInsertionTestRequiresStackSize1() { + return false; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java new file mode 100644 index 0000000..fa291e4 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java @@ -0,0 +1,23 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +public interface SetBlockStateHandlingBlockEntity { + default void lithium$handleSetBlockState() { + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java new file mode 100644 index 0000000..b520461 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java @@ -0,0 +1,23 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +public interface SetChangedHandlingBlockEntity { + default void lithium$handleSetChanged() { + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java new file mode 100644 index 0000000..1f65c34 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java @@ -0,0 +1,29 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.state.BlockState; + +public interface ShapeUpdateHandlingBlockBehaviour { + + default void lithium$handleShapeUpdate(LevelReader world, BlockState myBlockState, BlockPos myPos, BlockPos posFrom, BlockState newState) { + } + +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java new file mode 100644 index 0000000..22eea64 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java @@ -0,0 +1,77 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import org.jetbrains.annotations.NotNull; + +public class SleepUntilTimeBlockEntityTickInvoker implements TickingBlockEntity { + + private final BlockEntity sleepingBlockEntity; + private long sleepUntilTickExclusive; + private final TickingBlockEntity delegate; + + public SleepUntilTimeBlockEntityTickInvoker(BlockEntity sleepingBlockEntity, long sleepUntilTickExclusive, TickingBlockEntity delegate) { + this.sleepingBlockEntity = sleepingBlockEntity; + this.sleepUntilTickExclusive = sleepUntilTickExclusive; + this.delegate = delegate; + } + + @Override + public void tick() { + //noinspection ConstantConditions + long tickTime = this.sleepingBlockEntity.getLevel().getRedstoneGameTime(); // Luminol - Regionized threading for sleeping block entity + if (tickTime >= this.sleepUntilTickExclusive) { + ((SleepingBlockEntity) this.sleepingBlockEntity).setTicker(this.delegate); + this.delegate.tick(); + } + } + + @Override + public boolean isRemoved() { + return this.sleepingBlockEntity.isRemoved(); + } + + @Override + @NotNull + public BlockPos getPos() { + return this.sleepingBlockEntity.getBlockPos(); + } + + @Override + @NotNull + public String getType() { + //noinspection ConstantConditions + return net.minecraft.core.registries.BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(this.sleepingBlockEntity.getType()).toString(); + } + + @Override + @NotNull + public BlockEntity getTileEntity() { + return this.sleepingBlockEntity; + } + + @Override + public void updateTicksForLithium(long redstoneGameTimeOffset) { + this.sleepUntilTickExclusive += redstoneGameTimeOffset; + + this.delegate.updateTicksForLithium(redstoneGameTimeOffset); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java new file mode 100644 index 0000000..66a56dc --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java @@ -0,0 +1,102 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; + +public interface SleepingBlockEntity { + TickingBlockEntity SLEEPING_BLOCK_ENTITY_TICKER = new TickingBlockEntity() { + public void tick() { + } + + public boolean isRemoved() { + return false; + } + + public BlockPos getPos() { + return null; + } + + public String getType() { + return ""; + } + + @Override + public BlockEntity getTileEntity() { + return null; + } + }; + + LevelChunk.RebindableTickingBlockEntityWrapper lithium$getTickWrapper(); + + void lithium$setTickWrapper(LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper); + + TickingBlockEntity lithium$getSleepingTicker(); + + void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker); + + default boolean lithium$startSleeping() { + if (this.isSleeping()) { + return false; + } + + LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = this.lithium$getTickWrapper(); + if (tickWrapper == null) { + return false; + } + this.lithium$setSleepingTicker(tickWrapper.ticker); + tickWrapper.rebind(SleepingBlockEntity.SLEEPING_BLOCK_ENTITY_TICKER); + return true; + } + + default void sleepOnlyCurrentTick() { + TickingBlockEntity sleepingTicker = this.lithium$getSleepingTicker(); + LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = this.lithium$getTickWrapper(); + if (sleepingTicker == null) { + sleepingTicker = tickWrapper.ticker; + } + Level world = ((BlockEntity) this).getLevel(); + tickWrapper.rebind(new SleepUntilTimeBlockEntityTickInvoker((BlockEntity) this, world.getRedstoneGameTime() + 1, sleepingTicker)); + this.lithium$setSleepingTicker(null); + } + + default void wakeUpNow() { + TickingBlockEntity sleepingTicker = this.lithium$getSleepingTicker(); + if (sleepingTicker == null) { + return; + } + this.setTicker(sleepingTicker); + this.lithium$setSleepingTicker(null); + } + + default void setTicker(TickingBlockEntity delegate) { + LevelChunk.RebindableTickingBlockEntityWrapper tickWrapper = this.lithium$getTickWrapper(); + if (tickWrapper == null) { + return; + } + tickWrapper.rebind(delegate); + } + + default boolean isSleeping() { + return this.lithium$getSleepingTicker() != null; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java new file mode 100644 index 0000000..e65b7f8 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java @@ -0,0 +1,47 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking; + +import org.leavesmc.leaves.lithium.common.hopper.LithiumStackList; + +/** + * Interface for Objects that can emit various inventory change events. This does not mean that the inventory + * creates those events - this requirement is met by InventoryChangeTracker. This distinction is needed due + * to modded inventories being able to inherit from BaseContainerBlockEntity, which does not guarantee the creation + * of the required events but implements most of the inventory change listening. + * The forwarding methods below are helpers, it is not recommended to call them from outside InventoryChangeTracker.java + */ +public interface InventoryChangeEmitter { + void lithium$emitStackListReplaced(); + + void lithium$emitRemoved(); + + void lithium$emitContentModified(); + + void lithium$emitFirstComparatorAdded(); + + void lithium$forwardContentChangeOnce(InventoryChangeListener inventoryChangeListener, LithiumStackList stackList, InventoryChangeTracker thisTracker); + + void lithium$forwardMajorInventoryChanges(InventoryChangeListener inventoryChangeListener); + + void lithium$stopForwardingMajorInventoryChanges(InventoryChangeListener inventoryChangeListener); + + default void emitCallbackReplaced() { + this.lithium$emitRemoved(); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java new file mode 100644 index 0000000..041b2ee --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java @@ -0,0 +1,32 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking; + +import net.minecraft.world.Container; + +public interface InventoryChangeListener { + default void handleStackListReplaced(Container inventory) { + this.lithium$handleInventoryRemoved(inventory); + } + + void lithium$handleInventoryContentModified(Container inventory); + + void lithium$handleInventoryRemoved(Container inventory); + + boolean lithium$handleComparatorAdded(Container inventory); +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java new file mode 100644 index 0000000..455f758 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java @@ -0,0 +1,34 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking; + +import org.leavesmc.leaves.lithium.common.hopper.LithiumStackList; + +public interface InventoryChangeTracker extends InventoryChangeEmitter { + default void listenForContentChangesOnce(LithiumStackList stackList, InventoryChangeListener inventoryChangeListener) { + this.lithium$forwardContentChangeOnce(inventoryChangeListener, stackList, this); + } + + default void listenForMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { + this.lithium$forwardMajorInventoryChanges(inventoryChangeListener); + } + + default void stopListenForMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { + this.lithium$stopForwardingMajorInventoryChanges(inventoryChangeListener); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java new file mode 100644 index 0000000..6c69839 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java @@ -0,0 +1,26 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking; + +import net.minecraft.core.Direction; + +public interface ComparatorTracker { + void lithium$onComparatorAdded(Direction direction, int offset); + + boolean lithium$hasAnyComparatorNearby(); +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java new file mode 100644 index 0000000..09ed7de --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java @@ -0,0 +1,63 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.Container; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.leavesmc.leaves.lithium.common.util.DirectionConstants; + +public class ComparatorTracking { + + public static void notifyNearbyBlockEntitiesAboutNewComparator(Level world, BlockPos pos) { + BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); + for (Direction searchDirection : DirectionConstants.HORIZONTAL) { + for (int searchOffset = 1; searchOffset <= 2; searchOffset++) { + searchPos.set(pos); + searchPos.move(searchDirection, searchOffset); + BlockState blockState = world.getBlockState(searchPos); + if (blockState.getBlock() instanceof EntityBlock) { + BlockEntity blockEntity = world.lithium$getLoadedExistingBlockEntity(searchPos); + if (blockEntity instanceof Container) { + blockEntity.lithium$onComparatorAdded(searchDirection, searchOffset); + } + } + } + } + } + + public static boolean findNearbyComparators(Level world, BlockPos pos) { + BlockPos.MutableBlockPos searchPos = new BlockPos.MutableBlockPos(); + for (Direction searchDirection : DirectionConstants.HORIZONTAL) { + for (int searchOffset = 1; searchOffset <= 2; searchOffset++) { + searchPos.set(pos); + searchPos.move(searchDirection, searchOffset); + BlockState blockState = world.getBlockState(searchPos); + if (blockState.is(Blocks.COMPARATOR)) { + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java new file mode 100644 index 0000000..4889fd0 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java @@ -0,0 +1,4 @@ +package org.leavesmc.leaves.lithium.common.hopper; + +public interface BlockStateOnlyInventory { +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java new file mode 100644 index 0000000..7890837 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java @@ -0,0 +1,114 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.world.level.block.entity.BlockEntity; + +/** + * Pattern of comparator updates that the given inventory is sending when a hopper + * unsuccessfully attempts to take items from it. This pattern is independent from + * the hopper, given that it fails to take items. + * Background: A hopper trying to take items will take one item out of each slot and + * put it back right after. Some types of inventories will send comparator updates + * every time a hopper does that. Multiple consecutive comparator updates + * are not distinguishable from a single one and can therefore be omitted. + *

+ * A hopper failing to take items can be predicted by testing whether the inventory + * modification counter {@link LithiumStackList} of the hopper or the inventory above + * have changed since just before the previous attempt to take items. + *

+ * Decrementing and immediately incrementing the signal strength of an inventory + * cannot be distinguished from setting the signal strength to 0 temporarily. + * + * @author 2No2Name + */ +public enum ComparatorUpdatePattern { + NO_UPDATE { + //example: empty inventory, inventory that does not send comparator updates on hopper pull attempt + + @Override + public ComparatorUpdatePattern thenUpdate() { + return UPDATE; + } + + @Override + public ComparatorUpdatePattern thenDecrementUpdateIncrementUpdate() { + return DECREMENT_UPDATE_INCREMENT_UPDATE; + } + }, + UPDATE { + //example: inventory with items, but removing any single item does not change the signal strength + @Override + public void apply(BlockEntity blockEntity, LithiumStackList stackList) { + blockEntity.setChanged(); + } + + @Override + public ComparatorUpdatePattern thenDecrementUpdateIncrementUpdate() { + return UPDATE_DECREMENT_UPDATE_INCREMENT_UPDATE; + } + }, + DECREMENT_UPDATE_INCREMENT_UPDATE { + //example: inventory with items, but removing the first item reduces the signal strength + @Override + public void apply(BlockEntity blockEntity, LithiumStackList stackList) { + stackList.setReducedSignalStrengthOverride(); + blockEntity.setChanged(); + stackList.clearSignalStrengthOverride(); + blockEntity.setChanged(); + } + + @Override + public boolean isChainable() { + return false; + } + }, + UPDATE_DECREMENT_UPDATE_INCREMENT_UPDATE { + //example: inventory with items, removing the first item does not reduce the signal strength, + // but there is another item that will reduce the signal strength when removed + @Override + public void apply(BlockEntity blockEntity, LithiumStackList stackList) { + blockEntity.setChanged(); + stackList.setReducedSignalStrengthOverride(); + blockEntity.setChanged(); + stackList.clearSignalStrengthOverride(); + blockEntity.setChanged(); + } + + @Override + public boolean isChainable() { + return false; + } + }; + + public void apply(BlockEntity blockEntity, LithiumStackList stackList) { + + } + + public ComparatorUpdatePattern thenUpdate() { + return this; + } + + public ComparatorUpdatePattern thenDecrementUpdateIncrementUpdate() { + return this; + } + + public boolean isChainable() { + return true; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java new file mode 100644 index 0000000..b5212f8 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java @@ -0,0 +1,29 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +public class HopperCachingState { + + public enum BlockInventory { + UNKNOWN, // No information cached + BLOCK_STATE, // Known to be Composter-like inventory (inventory from block, but not block entity, only depends on block state) + BLOCK_ENTITY, // Known to be BlockEntity inventory without removal tracking capability + REMOVAL_TRACKING_BLOCK_ENTITY, // Known to be BlockEntity inventory with removal tracking capability + NO_BLOCK_INVENTORY // Known to be a block without hopper interaction (-> interact with entities instead) + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java new file mode 100644 index 0000000..f85a1a2 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java @@ -0,0 +1,168 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.world.CompoundContainer; +import net.minecraft.world.Container; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity; +import net.minecraft.world.level.chunk.LevelChunk; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.lithium.common.util.DirectionConstants; +import org.leavesmc.leaves.lithium.common.world.WorldHelper; + +import java.util.Map; + +public class HopperHelper { + + public static boolean tryMoveSingleItem(Container to, ItemStack stack, @Nullable Direction fromDirection) { + ItemStack transferChecker; + if (to.lithium$itemInsertionTestRequiresStackSize1()) { + transferChecker = stack.copy(); + transferChecker.setCount(1); + } else { + transferChecker = stack; + } + + WorldlyContainer toSided = to instanceof WorldlyContainer ? ((WorldlyContainer) to) : null; + if (toSided != null && fromDirection != null) { + int[] slots = toSided.getSlotsForFace(fromDirection); + + for (int slotIndex = 0; slotIndex < slots.length; ++slotIndex) { + if (tryMoveSingleItem(to, toSided, stack, transferChecker, slots[slotIndex], fromDirection)) { + return true; //caller needs to take the item from the original inventory and call to.markDirty() + } + } + } else { + int j = to.getContainerSize(); + for (int slot = 0; slot < j; ++slot) { + if (tryMoveSingleItem(to, toSided, stack, transferChecker, slot, fromDirection)) { + return true; //caller needs to take the item from the original inventory and call to.markDirty() + } + } + } + return false; + } + + public static boolean tryMoveSingleItem(Container to, @Nullable WorldlyContainer toSided, ItemStack transferStack, ItemStack transferChecker, int targetSlot, @Nullable Direction fromDirection) { + ItemStack toStack = to.getItem(targetSlot); + if (to.canPlaceItem(targetSlot, transferChecker) && (toSided == null || toSided.canPlaceItemThroughFace(targetSlot, transferChecker, fromDirection))) { + int toCount; + if (toStack.isEmpty()) { + ItemStack singleItem = transferStack.split(1); + to.setItem(targetSlot, singleItem); + return true; //caller needs to call to.markDirty() + } else if (toStack.getMaxStackSize() > (toCount = toStack.getCount()) && to.getMaxStackSize() > toCount && ItemStack.isSameItemSameComponents(toStack, transferStack)) { + transferStack.shrink(1); + toStack.grow(1); + return true; //caller needs to call to.markDirty() + } + } + return false; + } + + private static int calculateReducedSignalStrength(float contentWeight, int inventorySize, int inventoryMaxCountPerStack, int numOccupiedSlots, int itemStackCount, int itemStackMaxCount) { + //contentWeight adaption can include rounding error for non-power of 2 max stack sizes, which do not exist in vanilla anyways + int maxStackSize = Math.min(inventoryMaxCountPerStack, itemStackMaxCount); + int newNumOccupiedSlots = numOccupiedSlots - (itemStackCount == 1 ? 1 : 0); + float newContentWeight = contentWeight - (1f / (float) maxStackSize); + newContentWeight /= (float) inventorySize; + return Mth.floor(newContentWeight * 14.0F) + (newNumOccupiedSlots > 0 ? 1 : 0); + } + + public static ComparatorUpdatePattern determineComparatorUpdatePattern(Container from, LithiumStackList fromStackList) { + if ((from instanceof HopperBlockEntity) || !(from instanceof RandomizableContainerBlockEntity)) { + return ComparatorUpdatePattern.NO_UPDATE; + } + //calculate the signal strength of the inventory, but also keep the content weight variable + float contentWeight = 0f; + int numOccupiedSlots = 0; + + for (int j = 0; j < from.getContainerSize(); ++j) { + ItemStack itemStack = from.getItem(j); + if (!itemStack.isEmpty()) { + int maxStackSize = Math.min(from.getMaxStackSize(), itemStack.getMaxStackSize()); + contentWeight += itemStack.getCount() / (float) maxStackSize; + ++numOccupiedSlots; + } + } + float f = contentWeight; + f /= (float) from.getContainerSize(); + int originalSignalStrength = Mth.floor(f * 14.0F) + (numOccupiedSlots > 0 ? 1 : 0); + + + ComparatorUpdatePattern updatePattern = ComparatorUpdatePattern.NO_UPDATE; + //check the signal strength change when failing to extract from each slot + int[] availableSlots = from instanceof WorldlyContainer ? ((WorldlyContainer) from).getSlotsForFace(Direction.DOWN) : null; + WorldlyContainer sidedInventory = from instanceof WorldlyContainer ? (WorldlyContainer) from : null; + int fromSize = availableSlots != null ? availableSlots.length : from.getContainerSize(); + for (int i = 0; i < fromSize; i++) { + int fromSlot = availableSlots != null ? availableSlots[i] : i; + ItemStack itemStack = fromStackList.get(fromSlot); + if (!itemStack.isEmpty() && (sidedInventory == null || sidedInventory.canTakeItemThroughFace(fromSlot, itemStack, Direction.DOWN))) { + int newSignalStrength = calculateReducedSignalStrength(contentWeight, from.getContainerSize(), from.getMaxStackSize(), numOccupiedSlots, itemStack.getCount(), itemStack.getMaxStackSize()); + if (newSignalStrength != originalSignalStrength) { + updatePattern = updatePattern.thenDecrementUpdateIncrementUpdate(); + } else { + updatePattern = updatePattern.thenUpdate(); + } + if (!updatePattern.isChainable()) { + break; //if the pattern is indistinguishable from all extensions of the pattern, stop iterating + } + } + } + return updatePattern; + } + + public static Container replaceDoubleInventory(Container blockInventory) { + if (blockInventory instanceof CompoundContainer doubleInventory) { + doubleInventory = LithiumDoubleInventory.getLithiumInventory(doubleInventory); + if (doubleInventory != null) { + return doubleInventory; + } + } + return blockInventory; + } + + public static void updateHopperOnUpdateSuppression(Level level, BlockPos pos, int flags, LevelChunk worldChunk, boolean stateChange) { + if ((flags & Block.UPDATE_NEIGHBORS) == 0 && stateChange) { + //No block updates were sent. We need to update nearby hoppers to avoid outdated inventory caches being used + + //Small performance improvement when getting block entities within the same chunk. + Map blockEntities = WorldHelper.areNeighborsWithinSameChunk(pos) ? worldChunk.getBlockEntities() : null; + if (blockEntities == null || !blockEntities.isEmpty()) { + for (Direction direction : DirectionConstants.ALL) { + BlockPos offsetPos = pos.relative(direction); + //Directly get the block entity instead of getting the block state first. Maybe that is faster, maybe not. + BlockEntity hopper = blockEntities != null ? blockEntities.get(offsetPos) : level.lithium$getLoadedExistingBlockEntity(offsetPos); + if (hopper instanceof HopperBlockEntity hopperBlockEntity) { + hopperBlockEntity.lithium$invalidateCacheOnNeighborUpdate(direction == Direction.DOWN); + } + } + } + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java new file mode 100644 index 0000000..897d392 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java @@ -0,0 +1,52 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.core.NonNullList; +import net.minecraft.world.item.ItemStack; +import org.leavesmc.leaves.lithium.api.inventory.LithiumInventory; + +public class InventoryHelper { + public static LithiumStackList getLithiumStackList(LithiumInventory inventory) { + NonNullList stackList = inventory.getInventoryLithium(); + if (stackList instanceof LithiumStackList lithiumStackList) { + return lithiumStackList; + } + return upgradeToLithiumStackList(inventory); + } + + public static LithiumStackList getLithiumStackListOrNull(LithiumInventory inventory) { + NonNullList stackList = inventory.getInventoryLithium(); + if (stackList instanceof LithiumStackList lithiumStackList) { + return lithiumStackList; + } + return null; + } + + private static LithiumStackList upgradeToLithiumStackList(LithiumInventory inventory) { + //generate loot to avoid any problems with directly accessing the inventory slots + //the loot that is generated here is not generated earlier than in vanilla, because vanilla generates loot + //when the hopper checks whether the inventory is empty or full + inventory.generateLootLithium(); + //get the stack list after generating loot, just in case generating loot creates a new stack list + NonNullList stackList = inventory.getInventoryLithium(); + LithiumStackList lithiumStackList = new LithiumStackList(stackList, inventory.getMaxStackSize()); + inventory.setInventoryLithium(lithiumStackList); + return lithiumStackList; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java new file mode 100644 index 0000000..c8ecadf --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java @@ -0,0 +1,190 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.core.Direction; +import net.minecraft.core.NonNullList; +import net.minecraft.world.CompoundContainer; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import org.leavesmc.leaves.lithium.api.inventory.LithiumInventory; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeEmitter; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeListener; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_comparator_tracking.ComparatorTracker; + +public class LithiumDoubleInventory extends CompoundContainer implements LithiumInventory, InventoryChangeTracker, InventoryChangeEmitter, InventoryChangeListener, ComparatorTracker { + + private final LithiumInventory first; + private final LithiumInventory second; + + private LithiumStackList doubleStackList; + + ReferenceOpenHashSet inventoryChangeListeners = null; + ReferenceOpenHashSet inventoryHandlingTypeListeners = null; + + /** + * This method returns the same LithiumDoubleInventory instance for equal (same children in same order) + * doubleInventory parameters until {@link #lithium$emitRemoved()} is called. After that a new LithiumDoubleInventory object + * may be in use. + * + * @param doubleInventory A double inventory + * @return The only non-removed LithiumDoubleInventory instance for the double inventory. Null if not compatible + */ + public static LithiumDoubleInventory getLithiumInventory(CompoundContainer doubleInventory) { + Container vanillaFirst = doubleInventory.container1; + Container vanillaSecond = doubleInventory.container2; + if (vanillaFirst != vanillaSecond && vanillaFirst instanceof LithiumInventory first && vanillaSecond instanceof LithiumInventory second) { + LithiumDoubleInventory newDoubleInventory = new LithiumDoubleInventory(first, second); + LithiumDoubleStackList doubleStackList = LithiumDoubleStackList.getOrCreate( + newDoubleInventory, + InventoryHelper.getLithiumStackList(first), + InventoryHelper.getLithiumStackList(second), + newDoubleInventory.getMaxStackSize() + ); + newDoubleInventory.doubleStackList = doubleStackList; + return doubleStackList.doubleInventory; + } + return null; + } + + private LithiumDoubleInventory(LithiumInventory first, LithiumInventory second) { + super(first, second); + this.first = first; + this.second = second; + } + + @Override + public void lithium$emitContentModified() { + ReferenceOpenHashSet inventoryChangeListeners = this.inventoryChangeListeners; + if (inventoryChangeListeners != null) { + for (InventoryChangeListener inventoryChangeListener : inventoryChangeListeners) { + inventoryChangeListener.lithium$handleInventoryContentModified(this); + } + inventoryChangeListeners.clear(); + } + } + + @Override + public void lithium$emitStackListReplaced() { + ReferenceOpenHashSet listeners = this.inventoryHandlingTypeListeners; + if (listeners != null && !listeners.isEmpty()) { + listeners.forEach(inventoryChangeListener -> inventoryChangeListener.handleStackListReplaced(this)); + } + + this.invalidateChangeListening(); + } + + @Override + public void lithium$emitRemoved() { + ReferenceOpenHashSet listeners = this.inventoryHandlingTypeListeners; + if (listeners != null && !listeners.isEmpty()) { + listeners.forEach(listener -> listener.lithium$handleInventoryRemoved(this)); + } + + this.invalidateChangeListening(); + } + + private void invalidateChangeListening() { + if (this.inventoryChangeListeners != null) { + this.inventoryChangeListeners.clear(); + } + + LithiumStackList lithiumStackList = InventoryHelper.getLithiumStackListOrNull(this); + if (lithiumStackList != null) { + lithiumStackList.removeInventoryModificationCallback(this); + } + } + + @Override + public void lithium$emitFirstComparatorAdded() { + ReferenceOpenHashSet inventoryChangeListeners = this.inventoryChangeListeners; + if (inventoryChangeListeners != null && !inventoryChangeListeners.isEmpty()) { + inventoryChangeListeners.removeIf(inventoryChangeListener -> inventoryChangeListener.lithium$handleComparatorAdded(this)); + } + } + + @Override + public void lithium$forwardContentChangeOnce(InventoryChangeListener inventoryChangeListener, LithiumStackList stackList, InventoryChangeTracker thisTracker) { + if (this.inventoryChangeListeners == null) { + this.inventoryChangeListeners = new ReferenceOpenHashSet<>(1); + } + stackList.setInventoryModificationCallback(thisTracker); + this.inventoryChangeListeners.add(inventoryChangeListener); + + } + + @Override + public void lithium$forwardMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { + if (this.inventoryHandlingTypeListeners == null) { + this.inventoryHandlingTypeListeners = new ReferenceOpenHashSet<>(1); + + ((InventoryChangeTracker) this.first).listenForMajorInventoryChanges(this); + ((InventoryChangeTracker) this.second).listenForMajorInventoryChanges(this); + } + this.inventoryHandlingTypeListeners.add(inventoryChangeListener); + } + + @Override + public void lithium$stopForwardingMajorInventoryChanges(InventoryChangeListener inventoryChangeListener) { + if (this.inventoryHandlingTypeListeners != null) { + this.inventoryHandlingTypeListeners.remove(inventoryChangeListener); + if (this.inventoryHandlingTypeListeners.isEmpty()) { + ((InventoryChangeTracker) this.first).stopListenForMajorInventoryChanges(this); + ((InventoryChangeTracker) this.second).stopListenForMajorInventoryChanges(this); + } + } + } + + @Override + public NonNullList getInventoryLithium() { + return this.doubleStackList; + } + + @Override + public void setInventoryLithium(NonNullList inventory) { + throw new UnsupportedOperationException(); + } + + @Override + public void lithium$handleInventoryContentModified(Container inventory) { + this.lithium$emitContentModified(); + } + + @Override + public void lithium$handleInventoryRemoved(Container inventory) { + this.lithium$emitRemoved(); + } + + @Override + public boolean lithium$handleComparatorAdded(Container inventory) { + this.lithium$emitFirstComparatorAdded(); + return this.inventoryChangeListeners.isEmpty(); + } + + @Override + public void lithium$onComparatorAdded(Direction direction, int offset) { + throw new UnsupportedOperationException("Call onComparatorAdded(Direction direction, int offset) on the inventory half only!"); + } + + @Override + public boolean lithium$hasAnyComparatorNearby() { + return ((ComparatorTracker) this.first).lithium$hasAnyComparatorNearby() || ((ComparatorTracker) this.second).lithium$hasAnyComparatorNearby(); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java new file mode 100644 index 0000000..0c138d3 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java @@ -0,0 +1,162 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.world.CompoundContainer; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; + +/** + * Class to allow DoubleInventory to have LithiumStackList optimizations. + * The objects should be immutable and their state should be limited to the first and second inventory. + * Other state must be managed carefully, as at any time objects of this class may be replaced with new instances. + */ +public class LithiumDoubleStackList extends LithiumStackList { + private final LithiumStackList first; + private final LithiumStackList second; + final LithiumDoubleInventory doubleInventory; + + private long signalStrengthChangeCount; + + public LithiumDoubleStackList(LithiumDoubleInventory doubleInventory, LithiumStackList first, LithiumStackList second, int maxCountPerStack) { + super(maxCountPerStack); + this.first = first; + this.second = second; + this.doubleInventory = doubleInventory; + } + + public static LithiumDoubleStackList getOrCreate(LithiumDoubleInventory doubleInventory, LithiumStackList first, LithiumStackList second, int maxCountPerStack) { + LithiumDoubleStackList parentStackList = first.parent; + if (parentStackList == null || parentStackList != second.parent || parentStackList.first != first || parentStackList.second != second) { + if (parentStackList != null) { + parentStackList.doubleInventory.lithium$emitRemoved(); + } + parentStackList = new LithiumDoubleStackList(doubleInventory, first, second, maxCountPerStack); + first.parent = parentStackList; + second.parent = parentStackList; + } + return parentStackList; + } + + @Override + public long getModCount() { + return this.first.getModCount() + this.second.getModCount(); + } + + @Override + public void changedALot() { + throw new UnsupportedOperationException("Call changed() on the inventory half only!"); + } + + @Override + public void changed() { + throw new UnsupportedOperationException("Call changed() on the inventory half only!"); + } + + @Override + public ItemStack set(int index, ItemStack element) { + if (index >= this.first.size()) { + return this.second.set(index - this.first.size(), element); + } else { + return this.first.set(index, element); + } + } + + @Override + public void add(int slot, ItemStack element) { + throw new UnsupportedOperationException("Call add(int value, ItemStack element) on the inventory half only!"); + } + + @Override + public ItemStack remove(int index) { + throw new UnsupportedOperationException("Call remove(int value, ItemStack element) on the inventory half only!"); + } + + @Override + public void clear() { + this.first.clear(); + this.second.clear(); + } + + @Override + public int getSignalStrength(Container inventory) { + //signal strength override state has to be stored in the halves, because this object may be replaced with a copy at any time + boolean signalStrengthOverride = this.first.hasSignalStrengthOverride() || this.second.hasSignalStrengthOverride(); + if (signalStrengthOverride) { + return 0; + } + int cachedSignalStrength = this.cachedSignalStrength; + if (cachedSignalStrength == -1 || this.getModCount() != this.signalStrengthChangeCount) { + cachedSignalStrength = this.calculateSignalStrength(Integer.MAX_VALUE); + this.signalStrengthChangeCount = this.getModCount(); + this.cachedSignalStrength = cachedSignalStrength; + return cachedSignalStrength; + } + return cachedSignalStrength; + } + + @Override + public void setReducedSignalStrengthOverride() { + this.first.setReducedSignalStrengthOverride(); + this.second.setReducedSignalStrengthOverride(); + } + + @Override + public void clearSignalStrengthOverride() { + this.first.clearSignalStrengthOverride(); + this.second.clearSignalStrengthOverride(); + } + + /** + * @param masterStackList the stacklist of the inventory that comparators read from (double inventory for double chests) + * @param inventory the blockentity / inventory that this stacklist is inside + */ + public void runComparatorUpdatePatternOnFailedExtract(LithiumStackList masterStackList, Container inventory) { + if (inventory instanceof CompoundContainer compoundContainer) { + this.first.runComparatorUpdatePatternOnFailedExtract( + this, compoundContainer.container1 + ); + this.second.runComparatorUpdatePatternOnFailedExtract( + this, compoundContainer.container2 + ); + } + } + + @NotNull + @Override + public ItemStack get(int index) { + return index >= this.first.size() ? this.second.get(index - this.first.size()) : this.first.get(index); + } + + @Override + public int size() { + return this.first.size() + this.second.size(); + } + + public void setInventoryModificationCallback(@NotNull InventoryChangeTracker inventoryModificationCallback) { + this.first.setInventoryModificationCallback(inventoryModificationCallback); + this.second.setInventoryModificationCallback(inventoryModificationCallback); + } + + public void removeInventoryModificationCallback(@NotNull InventoryChangeTracker inventoryModificationCallback) { + this.first.removeInventoryModificationCallback(inventoryModificationCallback); + this.second.removeInventoryModificationCallback(inventoryModificationCallback); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java new file mode 100644 index 0000000..80ec678 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java @@ -0,0 +1,304 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.core.NonNullList; +import net.minecraft.util.Mth; +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.lithium.api.inventory.LithiumDefaultedList; +import org.leavesmc.leaves.lithium.common.block.entity.inventory_change_tracking.InventoryChangeTracker; +import org.leavesmc.leaves.lithium.common.util.change_tracking.ChangeSubscriber; + +public class LithiumStackList extends NonNullList implements LithiumDefaultedList, ChangeSubscriber.CountChangeSubscriber { + final int maxCountPerStack; + + protected int cachedSignalStrength; + private ComparatorUpdatePattern cachedComparatorUpdatePattern; + + private boolean signalStrengthOverride; + + private long modCount; + private int occupiedSlots; + private int fullSlots; + + LithiumDoubleStackList parent; //only used for double chests + + InventoryChangeTracker inventoryModificationCallback; + + public LithiumStackList(NonNullList original, int maxCountPerStack) { + super(original.list, ItemStack.EMPTY); + this.maxCountPerStack = maxCountPerStack; + + this.cachedSignalStrength = -1; + this.cachedComparatorUpdatePattern = null; + this.modCount = 0; + this.signalStrengthOverride = false; + + this.occupiedSlots = 0; + this.fullSlots = 0; + int size = this.size(); + for (int i = 0; i < size; i++) { + ItemStack stack = this.get(i); + if (!stack.isEmpty()) { + this.occupiedSlots++; + if (stack.getMaxStackSize() <= stack.getCount()) { + this.fullSlots++; + } + stack.lithium$subscribe(this, i); + } + } + + this.inventoryModificationCallback = null; + } + + public LithiumStackList(int maxCountPerStack) { + super(null, ItemStack.EMPTY); + this.maxCountPerStack = maxCountPerStack; + this.cachedSignalStrength = -1; + this.inventoryModificationCallback = null; + } + + public long getModCount() { + return this.modCount; + } + + public void changedALot() { + this.changed(); + + //fix the slot mapping of all stacks in the inventory + //fix occupied/full slot counters + this.occupiedSlots = 0; + this.fullSlots = 0; + int size = this.size(); + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < size; i++) { + ItemStack stack = this.get(i); + if (!stack.isEmpty()) { + this.occupiedSlots++; + if (stack.getMaxStackSize() <= stack.getCount()) { + this.fullSlots++; + } + stack.lithium$unsubscribe(this); + } + } + for (int i = 0; i < size; i++) { + ItemStack stack = this.get(i); + if (!stack.isEmpty()) { + stack.lithium$subscribe(this, i); + } + } + + } + + /** + * Method that must be invoked before or after a change of the inventory to update important values. If done too + * early or too late, behavior might be incorrect. + */ + public void changed() { + this.cachedSignalStrength = -1; + this.cachedComparatorUpdatePattern = null; + this.modCount++; + + InventoryChangeTracker inventoryModificationCallback = this.inventoryModificationCallback; + if (inventoryModificationCallback != null) { + this.inventoryModificationCallback = null; + inventoryModificationCallback.lithium$emitContentModified(); + } + } + + @Override + public ItemStack set(int index, ItemStack element) { + ItemStack previous = super.set(index, element); + + //Handle vanilla's item stack resurrection in HopperBlockEntity extract(Hopper hopper, Inventory inventory, int slot, Direction side): + // Item stacks are set to 0 items, then back to 1. Then inventory.set(index, element) is called. + // At this point, the LithiumStackList unsubscribed from the stack when it reached 0. + // Handle: If the previous == element, and the stack is not subscribed, we handle it as if an empty stack was replaced. + if (previous == element && !element.isEmpty()) { + boolean notSubscribed = previous.lithium$isSubscribedWithData(this, index); + if (!notSubscribed) { + previous = ItemStack.EMPTY; + } + } + + if (previous != element) { + if (!previous.isEmpty()) { + previous.lithium$unsubscribeWithData(this, index); + } + if (!element.isEmpty()) { + element.lithium$subscribe(this, index); + } + + this.occupiedSlots += (previous.isEmpty() ? 1 : 0) - (element.isEmpty() ? 1 : 0); + this.fullSlots += (element.getCount() >= element.getMaxStackSize() ? 1 : 0) - (previous.getCount() >= previous.getMaxStackSize() ? 1 : 0); + this.changed(); + } + + return previous; + } + + @Override + public void add(int slot, ItemStack element) { + super.add(slot, element); + if (!element.isEmpty()) { + element.lithium$subscribe(this, this.indexOf(element)); + } + this.changedALot(); + } + + @Override + public ItemStack remove(int index) { + ItemStack previous = super.remove(index); + if (!previous.isEmpty()) { + previous.lithium$unsubscribeWithData(this, index); + } + this.changedALot(); + return previous; + } + + @Override + public void clear() { + int size = this.size(); + for (int i = 0; i < size; i++) { + ItemStack stack = this.get(i); + if (!stack.isEmpty()) { + stack.lithium$unsubscribeWithData(this, i); + } + } + super.clear(); + this.changedALot(); + } + + public boolean hasSignalStrengthOverride() { + return this.signalStrengthOverride; + } + + public int getSignalStrength(Container inventory) { + if (this.signalStrengthOverride) { + return 0; + } + int signalStrength = this.cachedSignalStrength; + if (signalStrength == -1) { + return this.cachedSignalStrength = this.calculateSignalStrength(inventory.getContainerSize()); + } + return signalStrength; + } + + /** + * [VanillaCopy] {@link net.minecraft.world.inventory.AbstractContainerMenu#getRedstoneSignalFromContainer(Container)} + * + * @return the signal strength for this inventory + */ + int calculateSignalStrength(int inventorySize) { + int i = 0; + float f = 0.0F; + + inventorySize = Math.min(inventorySize, this.size()); + for (int j = 0; j < inventorySize; ++j) { + ItemStack itemStack = this.get(j); + if (!itemStack.isEmpty()) { + f += org.leavesmc.leaves.util.ItemOverstackUtils.getItemStackSignalStrength(this.maxCountPerStack, itemStack); + ++i; + } + } + + f /= (float) inventorySize; + return Mth.floor(f * 14.0F) + (i > 0 ? 1 : 0); + } + + public void setReducedSignalStrengthOverride() { + this.signalStrengthOverride = true; + } + + public void clearSignalStrengthOverride() { + this.signalStrengthOverride = false; + } + + + /** + * @param masterStackList the stacklist of the inventory that comparators read from (double inventory for double chests) + * @param inventory the blockentity / inventory that this stacklist is inside + */ + public void runComparatorUpdatePatternOnFailedExtract(LithiumStackList masterStackList, Container inventory) { + if (inventory instanceof BlockEntity) { + if (this.cachedComparatorUpdatePattern == null) { + this.cachedComparatorUpdatePattern = HopperHelper.determineComparatorUpdatePattern(inventory, masterStackList); + } + this.cachedComparatorUpdatePattern.apply((BlockEntity) inventory, masterStackList); + } + } + + public boolean maybeSendsComparatorUpdatesOnFailedExtract() { + return this.cachedComparatorUpdatePattern == null || this.cachedComparatorUpdatePattern != ComparatorUpdatePattern.NO_UPDATE; + } + + public int getOccupiedSlots() { + return this.occupiedSlots; + } + + public int getFullSlots() { + return this.fullSlots; + } + + @Override + public void changedInteractionConditions() { + this.changed(); + } + + + public void setInventoryModificationCallback(@NotNull InventoryChangeTracker inventoryModificationCallback) { + if (this.inventoryModificationCallback != null && this.inventoryModificationCallback != inventoryModificationCallback) { + this.inventoryModificationCallback.emitCallbackReplaced(); + } + this.inventoryModificationCallback = inventoryModificationCallback; + } + + public void removeInventoryModificationCallback(@NotNull InventoryChangeTracker inventoryModificationCallback) { + if (this.inventoryModificationCallback != null && this.inventoryModificationCallback == inventoryModificationCallback) { + this.inventoryModificationCallback = null; + } + } + + @Override + public void lithium$notify(@Nullable ItemStack publisher, int subscriberData) { + //Item component changes: LithiumStackList does not care about this + } + + @Override + public void lithium$forceUnsubscribe(ItemStack publisher, int subscriberData) { + throw new UnsupportedOperationException("Cannot force unsubscribe on a LithiumStackList!"); + } + + @Override + public void lithium$notifyCount(ItemStack stack, int index, int newCount) { + assert stack == this.get(index); + int count = stack.getCount(); + if (newCount <= 0) { + stack.lithium$unsubscribeWithData(this, index); + } + int maxCount = stack.getMaxStackSize(); + this.occupiedSlots -= newCount <= 0 ? 1 : 0; + this.fullSlots += (newCount >= maxCount ? 1 : 0) - (count >= maxCount ? 1 : 0); + + this.changed(); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java new file mode 100644 index 0000000..ab96fa2 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java @@ -0,0 +1,28 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.hopper; + +import net.minecraft.core.Direction; + +public interface UpdateReceiver { + void lithium$invalidateCacheOnNeighborUpdate(boolean above); + + void lithium$invalidateCacheOnUndirectedNeighborUpdate(); + + void lithium$invalidateCacheOnNeighborUpdate(Direction fromDirection); +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java new file mode 100644 index 0000000..16ba4dd --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java @@ -0,0 +1,5 @@ +package org.leavesmc.leaves.lithium.common.tracking.entity; + +public interface ChunkSectionEntityMovementListener { + void handleEntityMovement(); +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java new file mode 100644 index 0000000..962e3af --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java @@ -0,0 +1,75 @@ +package org.leavesmc.leaves.lithium.common.tracking.entity; + +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public abstract class ChunkSectionEntityMovementTracker { + protected long lastChangeTime = 0; + protected final ReferenceOpenHashSet listeners = new ReferenceOpenHashSet<>(); + protected final long sectionKey; + protected final Level level; + protected int userCount = 0; + + public ChunkSectionEntityMovementTracker(long sectionKey, Level level) { + this.level = level; + this.sectionKey = sectionKey; + } + + public void register() { + this.userCount++; + } + + public abstract void unregister(); + + public static void unregister(@NotNull List trackers) { + for (ChunkSectionEntityMovementTracker tracker : trackers) { + tracker.unregister(); + } + } + + public boolean isUnchangedSince(long lastCheckedTime) { + return this.lastChangeTime <= lastCheckedTime; + } + + public static boolean isUnchangedSince(long lastCheckedTime, @NotNull List trackers) { + for (ChunkSectionEntityMovementTracker tracker : trackers) { + if (!tracker.isUnchangedSince(lastCheckedTime)) { + return false; + } + } + return true; + } + + public void listenToEntityMovementOnce(ChunkSectionEntityMovementListener listener) { + this.listeners.add(listener); + } + + public static void listenToEntityMovementOnce(ChunkSectionEntityMovementListener listener, @NotNull List trackers) { + for (ChunkSectionEntityMovementTracker tracker : trackers) { + tracker.listenToEntityMovementOnce(listener); + } + } + + private void setChanged(long atTime) { + if (atTime > this.lastChangeTime) { + this.lastChangeTime = atTime; + } + } + + public void notifyAllListeners(long time) { + if (!listeners.isEmpty()) { + for (ChunkSectionEntityMovementListener listener : listeners) { + listener.handleEntityMovement(); + } + listeners.clear(); + } + setChanged(time); + } + + public void updateTicks(final long fromTickOffset, final long fromRedstoneTimeOffset) { + this.lastChangeTime += fromRedstoneTimeOffset; + } +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java new file mode 100644 index 0000000..cfd7977 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java @@ -0,0 +1,73 @@ +package org.leavesmc.leaves.lithium.common.tracking.entity; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Container; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.lithium.common.util.tuples.WorldSectionBox; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ChunkSectionInventoryEntityTracker extends ChunkSectionEntityMovementTracker { + public ChunkSectionInventoryEntityTracker(long sectionKey, Level level) { + super(sectionKey, level); + } + + @Override + public void unregister() { + this.userCount--; + if (this.userCount <= 0) { + this.level.getCurrentWorldData().containerEntityMovementTrackerMap.remove(this.sectionKey); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static @NotNull List getEntities(@NotNull Level level, AABB boundingBox) { + return level.getEntitiesOfClass((Class) Container.class, boundingBox, EntitySelector.CONTAINER_ENTITY_SELECTOR); + } + + public static @NotNull List registerAt(ServerLevel world, AABB interactionArea) { + WorldSectionBox worldSectionBox = WorldSectionBox.entityAccessBox(world, interactionArea); + + if (worldSectionBox.chunkX1() == worldSectionBox.chunkX2() && + worldSectionBox.chunkY1() == worldSectionBox.chunkY2() && + worldSectionBox.chunkZ1() == worldSectionBox.chunkZ2()) { + return Collections.singletonList(registerAt( + CoordinateUtils.getChunkSectionKey(worldSectionBox.chunkX1(), worldSectionBox.chunkY1(), worldSectionBox.chunkZ1()), + world + )); + } + + List trackers = new ArrayList<>(); + + for (int x = worldSectionBox.chunkX1(); x <= worldSectionBox.chunkX2(); x++) { + for (int y = worldSectionBox.chunkY1(); y <= worldSectionBox.chunkY2(); y++) { + for (int z = worldSectionBox.chunkZ1(); z <= worldSectionBox.chunkZ2(); z++) { + trackers.add(registerAt(CoordinateUtils.getChunkSectionKey(x, y, z), world)); + } + } + } + + return trackers; + } + + private static @NotNull ChunkSectionInventoryEntityTracker registerAt(long key, Level level) { + ChunkSectionInventoryEntityTracker tracker = level.getCurrentWorldData().containerEntityMovementTrackerMap.computeIfAbsent( + key, + k -> new ChunkSectionInventoryEntityTracker(key, level) + ); + tracker.register(); + return tracker; + } + + // Luminol start - region threading for lithium sleeping block entity + public boolean hasUser() { + return this.userCount > 0; + } + // Luminol end +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java new file mode 100644 index 0000000..b75c4f7 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java @@ -0,0 +1,73 @@ +package org.leavesmc.leaves.lithium.common.tracking.entity; + +import ca.spottedleaf.moonrise.common.util.CoordinateUtils; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.EntitySelector; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.lithium.common.util.tuples.WorldSectionBox; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ChunkSectionItemEntityMovementTracker extends ChunkSectionEntityMovementTracker { + public ChunkSectionItemEntityMovementTracker(long sectionKey, Level level) { + super(sectionKey, level); + } + + @Override + public void unregister() { + this.userCount--; + if (this.userCount <= 0) { + this.level.getCurrentWorldData().itemEntityMovementTrackerMap.remove(this.sectionKey); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static @NotNull List getEntities(@NotNull Level level, AABB boundingBox) { + return level.getEntitiesOfClass((Class) ItemEntity.class, boundingBox, EntitySelector.ENTITY_STILL_ALIVE); + } + + public static @NotNull List registerAt(ServerLevel world, AABB interactionArea) { + WorldSectionBox worldSectionBox = WorldSectionBox.entityAccessBox(world, interactionArea); + + if (worldSectionBox.chunkX1() == worldSectionBox.chunkX2() && + worldSectionBox.chunkY1() == worldSectionBox.chunkY2() && + worldSectionBox.chunkZ1() == worldSectionBox.chunkZ2()) { + return Collections.singletonList(registerAt( + CoordinateUtils.getChunkSectionKey(worldSectionBox.chunkX1(), worldSectionBox.chunkY1(), worldSectionBox.chunkZ1()), + world + )); + } + + List trackers = new ArrayList<>(); + + for (int x = worldSectionBox.chunkX1(); x <= worldSectionBox.chunkX2(); x++) { + for (int y = worldSectionBox.chunkY1(); y <= worldSectionBox.chunkY2(); y++) { + for (int z = worldSectionBox.chunkZ1(); z <= worldSectionBox.chunkZ2(); z++) { + trackers.add(registerAt(CoordinateUtils.getChunkSectionKey(x, y, z), world)); + } + } + } + + return trackers; + } + + private static @NotNull ChunkSectionItemEntityMovementTracker registerAt(long key, Level level) { + ChunkSectionItemEntityMovementTracker tracker = level.getCurrentWorldData().itemEntityMovementTrackerMap.computeIfAbsent( + key, + k -> new ChunkSectionItemEntityMovementTracker(key, level) + ); + tracker.register(); + return tracker; + } + + // Luminol start - region threading for lithium sleeping block entity + public boolean hasUser() { + return this.userCount > 0; + } + // Luminol end +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java new file mode 100644 index 0000000..817be4b --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java @@ -0,0 +1,33 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.util; + +import net.minecraft.core.Direction; + +/** + * Pre-initialized constants to avoid unnecessary allocations. + */ +public final class DirectionConstants { + private DirectionConstants() { + } + + public static final Direction[] ALL = Direction.values(); + public static final Direction[] VERTICAL = {Direction.DOWN, Direction.UP}; + public static final Direction[] HORIZONTAL = {Direction.WEST, Direction.EAST, Direction.NORTH, Direction.SOUTH}; + public static final byte[] HORIZONTAL_OPPOSITE_INDICES = {1, 0, 3, 2}; +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java new file mode 100644 index 0000000..cd0a987 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java @@ -0,0 +1,34 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.util.change_tracking; + +import net.minecraft.world.item.ItemStack; + +public interface ChangePublisher { + void lithium$subscribe(ChangeSubscriber subscriber, int subscriberData); + + int lithium$unsubscribe(ChangeSubscriber subscriber); + + default void lithium$unsubscribeWithData(ChangeSubscriber subscriber, int index) { + throw new UnsupportedOperationException("Only implemented for ItemStacks"); + } + + default boolean lithium$isSubscribedWithData(ChangeSubscriber subscriber, int subscriberData) { + throw new UnsupportedOperationException("Only implemented for ItemStacks"); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java new file mode 100644 index 0000000..3cbb275 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java @@ -0,0 +1,211 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.util.change_tracking; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; + +public interface ChangeSubscriber { + + static ChangeSubscriber combine(ChangeSubscriber prevSubscriber, int prevSData, @NotNull ChangeSubscriber newSubscriber, int newSData) { + if (prevSubscriber == null) { + return newSubscriber; + } else if (prevSubscriber instanceof Multi) { + ArrayList> subscribers = new ArrayList<>(((Multi) prevSubscriber).subscribers); + IntArrayList subscriberDatas = new IntArrayList(((Multi) prevSubscriber).subscriberDatas); + subscribers.add(newSubscriber); + subscriberDatas.add(newSData); + return new Multi<>(subscribers, subscriberDatas); + } else { + ArrayList> subscribers = new ArrayList<>(); + IntArrayList subscriberDatas = new IntArrayList(); + subscribers.add(prevSubscriber); + subscriberDatas.add(prevSData); + subscribers.add(newSubscriber); + subscriberDatas.add(newSData); + return new Multi<>(subscribers, subscriberDatas); + } + } + + static ChangeSubscriber without(ChangeSubscriber prevSubscriber, ChangeSubscriber removedSubscriber) { + return without(prevSubscriber, removedSubscriber, 0, false); + } + + static ChangeSubscriber without(ChangeSubscriber prevSubscriber, ChangeSubscriber removedSubscriber, int removedSubscriberData, boolean matchData) { + if (prevSubscriber == removedSubscriber) { + return null; + } else if (prevSubscriber instanceof Multi multi) { + int index = multi.indexOf(removedSubscriber, removedSubscriberData, matchData); + if (index != -1) { + if (multi.subscribers.size() == 2) { + return multi.subscribers.get(1 - index); + } else { + ArrayList> subscribers = new ArrayList<>(multi.subscribers); + IntArrayList subscriberDatas = new IntArrayList(multi.subscriberDatas); + subscribers.remove(index); + subscriberDatas.removeInt(index); + + return new Multi<>(subscribers, subscriberDatas); + } + } else { + return prevSubscriber; + } + } else { + return prevSubscriber; + } + } + + static int dataWithout(ChangeSubscriber prevSubscriber, ChangeSubscriber removedSubscriber, int subscriberData) { + return dataWithout(prevSubscriber, removedSubscriber, subscriberData, 0, false); + } + + static int dataWithout(ChangeSubscriber prevSubscriber, ChangeSubscriber removedSubscriber, int subscriberData, int removedSubscriberData, boolean matchData) { + if (prevSubscriber instanceof Multi multi) { + int index = multi.indexOf(removedSubscriber, removedSubscriberData, matchData); + if (index != -1) { + if (multi.subscribers.size() == 2) { + return multi.subscriberDatas.getInt(1 - index); + } else { + return subscriberData; + } + } else { + return subscriberData; + } + } + return prevSubscriber == removedSubscriber ? 0 : subscriberData; + } + + static int dataOf(ChangeSubscriber subscribers, ChangeSubscriber subscriber, int subscriberData) { + return subscribers instanceof Multi multi ? multi.subscriberDatas.getInt(multi.subscribers.indexOf(subscriber)) : subscriberData; + } + + static boolean containsSubscriber(ChangeSubscriber subscriber, int subscriberData, ChangeSubscriber subscriber1, int subscriberData1) { + if (subscriber instanceof Multi multi) { + return multi.indexOf(subscriber1, subscriberData1, true) != -1; + } + return subscriber == subscriber1 && subscriberData == subscriberData1; + } + + + /** + * Notify the subscriber that the publisher will be changed immediately after this call. + * + * @param publisher The publisher that is about to change + * @param subscriberData The data associated with the subscriber, given when the subscriber was added + */ + void lithium$notify(@Nullable T publisher, int subscriberData); + + /** + * Notify the subscriber about being unsubscribed from the publisher. Used when the publisher becomes invalid. + * The subscriber should not attempt to unsubscribe itself from the publisher in this method. + * + * @param publisher The publisher unsubscribed from + * @param subscriberData The data associated with the subscriber, given when the subscriber was added + */ + void lithium$forceUnsubscribe(T publisher, int subscriberData); + + interface CountChangeSubscriber extends ChangeSubscriber { + + /** + * Notify the subscriber that the publisher's count data will be changed immediately after this call. + * + * @param publisher The publisher that is about to change + * @param subscriberData The data associated with the subscriber, given when the subscriber was added + * @param newCount The new count of the publisher + */ + void lithium$notifyCount(T publisher, int subscriberData, int newCount); + } + + interface EnchantmentSubscriber extends ChangeSubscriber { + + /** + * Notify the subscriber that the publisher's enchantment data has been changed immediately before this call. + * + * @param publisher The publisher that has changed + * @param subscriberData The data associated with the subscriber, given when the subscriber was added + */ + void lithium$notifyAfterEnchantmentChange(T publisher, int subscriberData); + } + + class Multi implements CountChangeSubscriber, EnchantmentSubscriber { + private final ArrayList> subscribers; + private final IntArrayList subscriberDatas; + + public Multi(ArrayList> subscribers, IntArrayList subscriberDatas) { + this.subscribers = subscribers; + this.subscriberDatas = subscriberDatas; + } + + @Override + public void lithium$notify(T publisher, int subscriberData) { + ArrayList> changeSubscribers = this.subscribers; + for (int i = 0; i < changeSubscribers.size(); i++) { + ChangeSubscriber subscriber = changeSubscribers.get(i); + subscriber.lithium$notify(publisher, this.subscriberDatas.getInt(i)); + } + } + + @Override + public void lithium$forceUnsubscribe(T publisher, int subscriberData) { + ArrayList> changeSubscribers = this.subscribers; + for (int i = 0; i < changeSubscribers.size(); i++) { + ChangeSubscriber subscriber = changeSubscribers.get(i); + subscriber.lithium$forceUnsubscribe(publisher, this.subscriberDatas.getInt(i)); + } + } + + @Override + public void lithium$notifyCount(T publisher, int subscriberData, int newCount) { + ArrayList> changeSubscribers = this.subscribers; + for (int i = 0; i < changeSubscribers.size(); i++) { + ChangeSubscriber subscriber = changeSubscribers.get(i); + if (subscriber instanceof ChangeSubscriber.CountChangeSubscriber countChangeSubscriber) { + countChangeSubscriber.lithium$notifyCount(publisher, this.subscriberDatas.getInt(i), newCount); + } + } + } + + int indexOf(ChangeSubscriber subscriber, int subscriberData, boolean matchData) { + if (!matchData) { + return this.subscribers.indexOf(subscriber); + } else { + for (int i = 0; i < this.subscribers.size(); i++) { + if (this.subscribers.get(i) == subscriber && this.subscriberDatas.getInt(i) == subscriberData) { + return i; + } + } + return -1; + } + } + + @Override + public void lithium$notifyAfterEnchantmentChange(T publisher, int subscriberData) { + ArrayList> changeSubscribers = this.subscribers; + for (int i = 0; i < changeSubscribers.size(); i++) { + ChangeSubscriber subscriber = changeSubscribers.get(i); + if (subscriber instanceof ChangeSubscriber.EnchantmentSubscriber enchantmentSubscriber) { + enchantmentSubscriber.lithium$notifyAfterEnchantmentChange(publisher, this.subscriberDatas.getInt(i)); + } + } + } + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java new file mode 100644 index 0000000..c93b080 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java @@ -0,0 +1,37 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.util.tuples; + +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; + +//Y values use coordinates, not indices (y=0 -> chunkY=0) +//upper bounds are EXCLUSIVE +public record WorldSectionBox(Level world, int chunkX1, int chunkY1, int chunkZ1, int chunkX2, int chunkY2, + int chunkZ2) { + public static WorldSectionBox entityAccessBox(Level world, AABB box) { + int minX = SectionPos.posToSectionCoord(box.minX - 2.0D); + int minY = SectionPos.posToSectionCoord(box.minY - 4.0D); + int minZ = SectionPos.posToSectionCoord(box.minZ - 2.0D); + int maxX = SectionPos.posToSectionCoord(box.maxX + 2.0D) + 1; + int maxY = SectionPos.posToSectionCoord(box.maxY) + 1; + int maxZ = SectionPos.posToSectionCoord(box.maxZ + 2.0D) + 1; + return new WorldSectionBox(world, minX, minY, minZ, maxX, maxY, maxZ); + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java new file mode 100644 index 0000000..7a467f4 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java @@ -0,0 +1,29 @@ +/* + * This file is part of Lithium + * + * Lithium 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. + * + * Lithium 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 Lithium. If not, see . + */ + +package org.leavesmc.leaves.lithium.common.world; + +import net.minecraft.core.BlockPos; + +public class WorldHelper { + public static boolean areNeighborsWithinSameChunk(BlockPos pos) { + int localX = pos.getX() & 15; + int localZ = pos.getZ() & 15; + + return localX > 0 && localZ > 0 && localX < 15 && localZ < 15; + } +} \ No newline at end of file diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java b/camellia-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java new file mode 100644 index 0000000..88a4801 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java @@ -0,0 +1,46 @@ +/* + * 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.util; + +import com.google.common.cache.LoadingCache; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.pattern.BlockInWorld; +import net.minecraft.world.level.block.state.pattern.BlockPattern; + +// Powered by Carpet-AMS-Addition(https://github.com/Minecraft-AMS/Carpet-AMS-Addition) +public class BlockPatternHelper { + public static BlockPattern.BlockPatternMatch partialSearchAround(BlockPattern pattern, Level world, BlockPos pos) { + LoadingCache loadingCache = BlockPattern.createLevelCache(world, false); + int i = Math.max(Math.max(pattern.getWidth(), pattern.getHeight()), pattern.getDepth()); + + for (BlockPos blockPos : BlockPos.betweenClosed(pos, pos.offset(i - 1, 0, i - 1))) { + for (Direction direction : Direction.values()) { + for (Direction direction2 : Direction.values()) { + BlockPattern.BlockPatternMatch result; + if (direction2 == direction || direction2 == direction.getOpposite() || (result = pattern.matches(blockPos, direction, direction2, loadingCache)) == null) { + continue; + } + return result; + } + } + } + return null; + } +} diff --git a/camellia-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java b/camellia-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java new file mode 100644 index 0000000..a5df7e4 --- /dev/null +++ b/camellia-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java @@ -0,0 +1,31 @@ +/* + * 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.util; + +import net.minecraft.world.item.ItemStack; + +// Only used part of the original code was copied +public class ItemOverstackUtils { + public static float getItemStackSignalStrength(int maxStackSize, ItemStack itemStack) { + float result = (float) itemStack.getCount() / Math.min(maxStackSize, itemStack.getMaxStackSize()); + /*if (LeavesConfig.modify.oldMC.allowGrindstoneOverstacking && CurseEnchantedBook.isCursedEnchantedBook(itemStack)) { // Luminol + return result; + }*/ // Luminol + return Math.clamp(result, 0f, 1f); + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..92c8ebb --- /dev/null +++ b/gradle.properties @@ -0,0 +1,17 @@ +group=moe.monnairealms.camellia +mcVersion=26.2 +apiVersion=26.2 +channel=STABLE +clipVersion=3.0.18 +weightVersion=2.0.15 +# true for release, false for skip release, pre for pre-release +release=pre +# true for push to repo, false for skip push repo, auto for detect by release value +pushRepo=auto + +foliaRef=62034198de6d034bed025f34f48e8950b57c77ee + +org.gradle.configuration-cache=true +org.gradle.caching=true +org.gradle.parallel=true +org.gradle.vfs.watch=false diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..1b33c55baabb587c669f562ae36f953de2481846 GIT binary patch literal 43764 zcma&OWmKeVvL#I6?i3D%6z=Zs?ofE*?rw#G$eqJB ziT4y8-Y@s9rkH0Tz>ll(^xkcTl)CY?rS&9VNd66Yc)g^6)JcWaY(5$5gt z8gr3SBXUTN;~cBgz&})qX%#!Fxom2Yau_`&8)+6aSN7YY+pS410rRUU*>J}qL0TnJ zRxt*7QeUqTh8j)Q&iavh<}L+$Jqz))<`IfKussVk%%Ah-Ti?Eo0hQH!rK%K=#EAw0 zwq@@~XNUXRnv8$;zv<6rCRJ6fPD^hfrh;0K?n z=p!u^3xOgWZ%f3+?+>H)9+w^$Tn1e;?UpVMJb!!;f)`6f&4|8mr+g)^@x>_rvnL0< zvD0Hu_N>$(Li7|Jgu0mRh&MV+<}`~Wi*+avM01E)Jtg=)-vViQKax!GeDc!xv$^mL z{#OVBA$U{(Zr8~Xm|cP@odkHC*1R8z6hcLY#N@3E-A8XEvpt066+3t9L_6Zg6j@9Q zj$$%~yO-OS6PUVrM2s)(T4#6=JpI_@Uz+!6=GdyVU?`!F=d;8#ZB@(5g7$A0(`eqY z8_i@3w$0*es5mrSjhW*qzrl!_LQWs4?VfLmo1Sd@Ztt53+etwzAT^8ow_*7Jp`Y|l z*UgSEwvxq+FYO!O*aLf-PinZYne7Ib6ny3u>MjQz=((r3NTEeU4=-i0LBq3H-VJH< z^>1RE3_JwrclUn9vb7HcGUaFRA0QHcnE;6)hnkp%lY1UII#WPAv?-;c?YH}LWB8Nl z{sx-@Z;QxWh9fX8SxLZk8;kMFlGD3Jc^QZVL4nO)1I$zQwvwM&_!kW+LMf&lApv#< zur|EyC|U@5OQuph$TC_ZU`{!vJp`13e9alaR0Dbn5ikLFH7>eIz4QbV|C=%7)F=qo z_>M&5N)d)7G(A%c>}UCrW!Ql_6_A{?R7&CL`;!KOb3 z8Z=$YkV-IF;c7zs{3-WDEFJzuakFbd*4LWd<_kBE8~BFcv}js_2OowRNzWCtCQ6&k z{&~Me92$m*@e0ANcWKuz)?YjB*VoSTx??-3Cc0l2U!X^;Bv@m87eKHukAljrD54R+ zE;@_w4NPe1>3`i5Qy*3^E9x#VB6?}v=~qIprrrd5|DFkg;v5ixo0IsBmik8=Y;zv2 z%Bcf%NE$a44bk^`i4VwDLTbX=q@j9;JWT9JncQ!+Y%2&HHk@1~*L8-{ZpY?(-a9J-1~<1ltr9i~D9`P{XTIFWA6IG8c4;6bFw*lzU-{+?b&%OcIoCiw00n>A1ra zFPE$y@>ebbZlf(sN_iWBzQKDV zmmaLX#zK!@ZdvCANfwV}9@2O&w)!5gSgQzHdk2Q`jG6KD7S+1R5&F)j6QTD^=hq&7 zHUW+r^da^%V(h(wonR(j?BOiC!;y=%nJvz?*aW&5E87qq;2z`EI(f zBJNNSMFF9U{sR-af5{IY&AtoGcoG)Iq-S^v{7+t0>7N(KRoPj;+2N5;9o_nxIGjJ@ z7bYQK)bX)vEhy~VL%N6g^NE@D5VtV+Q8U2%{ji_=6+i^G%xeskEhH>Sqr194PJ$fB zu1y^){?9Vkg(FY2h)3ZHrw0Z<@;(gd_dtF#6y_;Iwi{yX$?asr?0N0_B*CifEi7<6 zq`?OdQjCYbhVcg+7MSgIM|pJRu~`g?g3x?Tl+V}#$It`iD1j+!x+!;wS0+2e>#g?Z z*EA^k7W{jO1r^K~cD#5pamp+o@8&yw6;%b|uiT?{Wa=4+9<}aXWUuL#ZwN1a;lQod zW{pxWCYGXdEq9qAmvAB904}?97=re$>!I%wxPV#|f#@A*Y=qa%zHlDv^yWbR03%V0 zprLP+b(#fBqxI%FiF*-n8HtH6$8f(P6!H3V^ysgd8de-N(@|K!A< z^qP}jp(RaM9kQ(^K(U8O84?D)aU(g?1S8iWwe)gqpHCaFlJxb*ilr{KTnu4_@5{K- z)n=CCeCrPHO0WHz)dDtkbZfUfVBd?53}K>C5*-wC4hpDN8cGk3lu-ypq+EYpb_2H; z%vP4@&+c2p;thaTs$dc^1CDGlPG@A;yGR5@$UEqk6p58qpw#7lc<+W(WR;(vr(D>W z#(K$vE#uBkT=*q&uaZwzz=P5mjiee6>!lV?c}QIX%ZdkO1dHg>Fa#xcGT6~}1*2m9 zkc7l3ItD6Ie~o_aFjI$Ri=C!8uF4!Ky7iG9QTrxVbsQroi|r)SAon#*B*{}TB-?=@ z8~jJs;_R2iDd!$+n$%X6FO&PYS{YhDAS+U2o4su9x~1+U3z7YN5o0qUK&|g^klZ6X zj_vrM5SUTnz5`*}Hyts9ADwLu#x_L=nv$Z0`HqN`Zo=V>OQI)fh01n~*a%01%cx%0 z4LTFVjmW+ipVQv5rYcn3;d2o4qunWUY!p+?s~X~(ost@WR@r@EuDOSs8*MT4fiP>! zkfo^!PWJJ1MHgKS2D_hc?Bs?isSDO61>ebl$U*9*QY(b=i&rp3@3GV@z>KzcZOxip z^dzA~44;R~cnhWz7s$$v?_8y-k!DZys}Q?4IkSyR!)C0j$(Gm|t#e3|QAOFaV2}36 z?dPNY;@I=FaCwylc_;~kXlZsk$_eLkNb~TIl8QQ`mmH&$*zwwR8zHU*sId)rxHu*K z;yZWa8UmCwju%aSNLwD5fBl^b0Ux1%q8YR*uG`53Mi<`5uA^Dc6Ync)J3N7;zQ*75)hf%a@{$H+%S?SGT)ks60)?6j$ zspl|4Ad6@%-r1t*$tT(en!gIXTUDcsj?28ZEzz)dH)SV3bZ+pjMaW0oc~rOPZP@g! zb9E+ndeVO_Ib9c_>{)`01^`ZS198 z)(t=+{Azi11$eu%aU7jbwuQrO`vLOixuh~%4z@mKr_Oc;F%Uq01fA)^W&y+g16e?rkLhTxV!EqC%2}sx_1u7IBq|}Be&7WI z4I<;1-9tJsI&pQIhj>FPkQV9{(m!wYYV@i5h?A0#BN2wqlEwNDIq06|^2oYVa7<~h zI_OLan0Do*4R5P=a3H9`s5*>xU}_PSztg`+2mv)|3nIy=5#Z$%+@tZnr> zLcTI!Mxa`PY7%{;KW~!=;*t)R_sl<^b>eNO@w#fEt(tPMg_jpJpW$q_DoUlkY|uo> z0-1{ouA#;t%spf*7VjkK&$QrvwUERKt^Sdo)5@?qAP)>}Y!h4(JQ!7{wIdkA+|)bv z&8hBwoX4v|+fie}iTslaBX^i*TjwO}f{V)8*!dMmRPi%XAWc8<_IqK1jUsApk)+~R zNFTCD-h>M5Y{qTQ&0#j@I@tmXGj%rzhTW5%Bkh&sSc=$Fv;M@1y!zvYG5P2(2|(&W zlcbR1{--rJ&s!rB{G-sX5^PaM@3EqWVz_y9cwLR9xMig&9gq(voeI)W&{d6j1jh&< zARXi&APWE1FQWh7eoZjuP z;vdgX>zep^{{2%hem;e*gDJhK1Hj12nBLIJoL<=0+8SVEBx7!4Ea+hBY;A1gBwvY<)tj~T=H`^?3>zeWWm|LAwo*S4Z%bDVUe z6r)CH1H!(>OH#MXFJ2V(U(qxD{4Px2`8qfFLG+=a;B^~Te_Z!r3RO%Oc#ZAHKQxV5 zRYXxZ9T2A%NVJIu5Pu7!Mj>t%YDO$T@M=RR(~mi%sv(YXVl`yMLD;+WZ{vG9(@P#e zMo}ZiK^7^h6TV%cG+;jhJ0s>h&VERs=tuZz^Tlu~%d{ZHtq6hX$V9h)Bw|jVCMudd zwZ5l7In8NT)qEPGF$VSKg&fb0%R2RnUnqa){)V(X(s0U zkCdVZe6wy{+_WhZh3qLp245Y2RR$@g-!9PjJ&4~0cFSHMUn=>dapv)hy}|y91ZWTV zCh=z*!S3_?`$&-eZ6xIXUq8RGl9oK0BJw*TdU6A`LJqX9eS3X@F)g$jLkBWFscPhR zpCv8#KeAc^y>>Y$k^=r|K(DTC}T$0#jQBOwB#@`P6~*IuW_8JxCG}J4va{ zsZzt}tt+cv7=l&CEuVtjD6G2~_Meh%p4RGuY?hSt?(sreO_F}8r7Kp$qQdvCdZnDQ zxzc*qchE*E2=WK)^oRNa>Ttj`fpvF-JZ5tu5>X1xw)J@1!IqWjq)ESBG?J|ez`-Tc zi5a}GZx|w-h%5lNDE_3ho0hEXMoaofo#Z;$8|2;EDF&*L+e$u}K=u?pb;dv$SXeQM zD-~7P0i_`Wk$#YP$=hw3UVU+=^@Kuy$>6?~gIXx636jh{PHly_a2xNYe1l60`|y!7 z(u%;ILuW0DDJ)2%y`Zc~hOALnj1~txJtcdD#o4BCT68+8gZe`=^te6H_egxY#nZH&P*)hgYaoJ^qtmpeea`35Fw)cy!w@c#v6E29co8&D9CTCl%^GV|X;SpneSXzV~LXyRn-@K0Df z{tK-nDWA!q38M1~`xUIt_(MO^R(yNY#9@es9RQbY@Ia*xHhD&=k^T+ zJi@j2I|WcgW=PuAc>hs`(&CvgjL2a9Rx zCbZyUpi8NWUOi@S%t+Su4|r&UoU|ze9SVe7p@f1GBkrjkkq)T}X%Qo1g!SQ{O{P?m z-OfGyyWta+UCXH+-+(D^%kw#A1-U;?9129at7MeCCzC{DNgO zeSqsV>W^NIfTO~4({c}KUiuoH8A*J!Cb0*sp*w-Bg@YfBIPZFH!M}C=S=S7PLLcIG zs7K77g~W)~^|+mx9onzMm0qh(f~OsDTzVmRtz=aZTllgR zGUn~_5hw_k&rll<4G=G+`^Xlnw;jNYDJz@bE?|r866F2hA9v0-8=JO3g}IHB#b`hy zA42a0>{0L7CcabSD+F7?pGbS1KMvT{@1_@k!_+Ki|5~EMGt7T%u=79F)8xEiL5!EJ zzuxQ`NBliCoJMJdwu|);zRCD<5Sf?Y>U$trQ-;xj6!s5&w=9E7)%pZ+1Nh&8nCCwM zv5>Ket%I?cxr3vVva`YeR?dGxbG@pi{H#8@kFEf0Jq6~K4>kt26*bxv=P&jyE#e$| zDJB_~imk^-z|o!2njF2hL*|7sHCnzluhJjwLQGDmC)Y9 zr9ZN`s)uCd^XDvn)VirMgW~qfn1~SaN^7vcX#K1G`==UGaDVVx$0BQnubhX|{e z^i0}>k-;BP#Szk{cFjO{2x~LjK{^Upqd&<+03_iMLp0$!6_$@TbX>8U-f*-w-ew1?`CtD_0y_Lo|PfKi52p?`5$Jzx0E8`M0 zNIb?#!K$mM4X%`Ry_yhG5k@*+n4||2!~*+&pYLh~{`~o(W|o64^NrjP?-1Lgu?iK^ zTX6u3?#$?R?N!{599vg>G8RGHw)Hx&=|g4599y}mXNpM{EPKKXB&+m?==R3GsIq?G zL5fH={=zawB(sMlDBJ+{dgb)Vx3pu>L=mDV0{r1Qs{0Pn%TpopH{m(By4;{FBvi{I z$}x!Iw~MJOL~&)p93SDIfP3x%ROjg}X{Sme#hiJ&Yk&a;iR}V|n%PriZBY8SX2*;6 z4hdb^&h;Xz%)BDACY5AUsV!($lib4>11UmcgXKWpzRL8r2Srl*9Y(1uBQsY&hO&uv znDNff0tpHlLISam?o(lOp#CmFdH<6HmA0{UwfU#Y{8M+7od8b8|B|7ZYR9f<#+V|ZSaCQvI$~es~g(Pv{2&m_rKSB2QQ zMvT}$?Ll>V+!9Xh5^iy3?UG;dF-zh~RL#++roOCsW^cZ&({6q|?Jt6`?S8=16Y{oH zp50I7r1AC1(#{b`Aq5cw>ypNggHKM9vBx!W$eYIzD!4KbLsZGr2o8>g<@inmS3*>J zx8oG((8f!ei|M@JZB`p7+n<Q}?>h249<`7xJ?u}_n;Gq(&km#1ULN87CeTO~FY zS_Ty}0TgQhV zOh3T7{{x&LSYGQfKR1PDIkP!WnfC1$l+fs@Di+d4O=eVKeF~2fq#1<8hEvpwuqcaH z4A8u~r^gnY3u6}zj*RHjk{AHhrrDqaj?|6GaVJbV%o-nATw}ASFr!f`Oz|u_QPkR# z0mDudY1dZRlk@TyQ?%Eti=$_WNFtLpSx9=S^be{wXINp%MU?a`F66LNU<c;0&ngifmP9i;bj6&hdGMW^Kf8e6ZDXbQD&$QAAMo;OQ)G zW(qlHh;}!ZP)JKEjm$VZjTs@hk&4{?@+NADuYrr!R^cJzU{kGc1yB?;7mIyAWwhbeA_l_lw-iDVi7wcFurf5 z#Uw)A@a9fOf{D}AWE%<`s1L_AwpZ?F!Vac$LYkp<#A!!`XKaDC{A%)~K#5z6>Hv@V zBEqF(D5?@6r3Pwj$^krpPDCjB+UOszqUS;b2n>&iAFcw<*im2(b3|5u6SK!n9Sg4I z0KLcwA6{Mq?p%t>aW0W!PQ>iUeYvNjdKYqII!CE7SsS&Rj)eIw-K4jtI?II+0IdGq z2WT|L3RL?;GtGgt1LWfI4Ka`9dbZXc$TMJ~8#Juv@K^1RJN@yzdLS8$AJ(>g!U9`# zx}qr7JWlU+&m)VG*Se;rGisutS%!6yybi%B`bv|9rjS(xOUIvbNz5qtvC$_JYY+c& za*3*2$RUH8p%pSq>48xR)4qsp!Q7BEiJ*`^>^6INRbC@>+2q9?x(h0bpc>GaNFi$K zPH$6!#(~{8@0QZk=)QnM#I=bDx5vTvjm$f4K}%*s+((H2>tUTf==$wqyoI`oxI7>C z&>5fe)Yg)SmT)eA(|j@JYR1M%KixxC-Eceknf-;N=jJTwKvk#@|J^&5H0c+%KxHUI z6dQbwwVx3p?X<_VRVb2fStH?HH zFR@Mp=qX%#L3XL)+$PXKV|o|#DpHAoqvj6uQKe@M-mnhCSou7Dj4YuO6^*V`m)1lf z;)@e%1!Qg$10w8uEmz{ENb$^%u}B;J7sDd zump}onoD#!l=agcBR)iG!3AF0-63%@`K9G(CzKrm$VJ{v7^O9Ps7Zej|3m= zVXlR&yW6=Y%mD30G@|tf=yC7-#L!16Q=dq&@beWgaIL40k0n% z)QHrp2Jck#evLMM1RGt3WvQ936ZC9vEje0nFMfvmOHVI+&okB_K|l-;|4vW;qk>n~ z+|kk8#`K?x`q>`(f6A${wfw9Cx(^)~tX7<#TpxR#zYG2P+FY~mG{tnEkv~d6oUQA+ z&hNTL=~Y@rF`v-RZlts$nb$3(OL1&@Y11hhL9+zUb6)SP!;CD)^GUtUpCHBE`j1te zAGud@miCVFLk$fjsrcpjsadP__yj9iEZUW{Ll7PPi<$R;m1o!&Xdl~R_v0;oDX2z^!&8}zNGA}iYG|k zmehMd1%?R)u6R#<)B)1oe9TgYH5-CqUT8N7K-A-dm3hbm_W21p%8)H{O)xUlBVb+iUR}-v5dFaCyfSd zC6Bd7=N4A@+Bna=!-l|*_(nWGDpoyU>nH=}IOrLfS+-d40&(Wo*dDB9nQiA2Tse$R z;uq{`X7LLzP)%Y9aHa4YQ%H?htkWd3Owv&UYbr5NUDAH^<l@Z0Cx%`N+B*i!!1u>D8%;Qt1$ zE5O0{-`9gdDxZ!`0m}ywH!;c{oBfL-(BH<&SQ~smbcobU!j49O^f4&IIYh~f+hK*M zZwTp%{ZSAhMFj1qFaOA+3)p^gnXH^=)`NTYgTu!CLpEV2NF=~-`(}7p^Eof=@VUbd z_9U|8qF7Rueg&$qpSSkN%%%DpbV?8E8ivu@ensI0toJ7Eas^jyFReQ1JeY9plb^{m z&eQO)qPLZQ6O;FTr*aJq=$cMN)QlQO@G&%z?BKUs1&I^`lq>=QLODwa`(mFGC`0H< zOlc*|N?B5&!U6BuJvkL?s1&nsi$*5cCv7^j_*l&$-sBmRS85UIrE--7eD8Gr3^+o? zqG-Yl4S&E;>H>k^a0GdUI(|n1`ws@)1%sq2XBdK`mqrNq_b4N{#VpouCXLzNvjoFv zo9wMQ6l0+FT+?%N(ka*;%m~(?338bu32v26!{r)|w8J`EL|t$}TA4q_FJRX5 zCPa{hc_I(7TGE#@rO-(!$1H3N-C0{R$J=yPCXCtGk{4>=*B56JdXU9cQVwB`6~cQZ zf^qK21x_d>X%dT!!)CJQ3mlHA@ z{Prkgfs6=Tz%63$6Zr8CO0Ak3A)Cv#@BVKr&aiKG7RYxY$Yx>Bj#3gJk*~Ps-jc1l z;4nltQwwT4@Z)}Pb!3xM?+EW0qEKA)sqzw~!C6wd^{03-9aGf3Jmt=}w-*!yXupLf z;)>-7uvWN4Unn8b4kfIza-X=x*e4n5pU`HtgpFFd))s$C@#d>aUl3helLom+RYb&g zI7A9GXLRZPl}iQS*d$Azxg-VgcUr*lpLnbPKUV{QI|bsG{8bLG<%CF( zMoS4pRDtLVYOWG^@ox^h8xL~afW_9DcE#^1eEC1SVSb1BfDi^@g?#f6e%v~Aw>@w- zIY0k+2lGWNV|aA*e#`U3=+oBDmGeInfcL)>*!w|*;mWiKNG6wP6AW4-4imN!W)!hE zA02~S1*@Q`fD*+qX@f3!2yJX&6FsEfPditB%TWo3=HA;T3o2IrjS@9SSxv%{{7&4_ zdS#r4OU41~GYMiib#z#O;zohNbhJknrPPZS6sN$%HB=jUnlCO_w5Gw5EeE@KV>soy z2EZ?Y|4RQDDjt5y!WBlZ(8M)|HP<0YyG|D%RqD+K#e7-##o3IZxS^wQ5{Kbzb6h(i z#(wZ|^ei>8`%ta*!2tJzwMv+IFHLF`zTU8E^Mu!R*45_=ccqI};Zbyxw@U%a#2}%f zF>q?SrUa_a4H9l+uW8JHh2Oob>NyUwG=QH~-^ZebU*R@67DcXdz2{HVB4#@edz?B< z5!rQH3O0>A&ylROO%G^fimV*LX7>!%re{_Sm6N>S{+GW1LCnGImHRoF@csnFzn@P0 zM=jld0z%oz;j=>c7mMwzq$B^2mae7NiG}%>(wtmsDXkWk{?BeMpTrIt3Mizq?vRsf zi_WjNp+61uV(%gEU-Vf0;>~vcDhe(dzWdaf#4mH3o^v{0EWhj?E?$5v02sV@xL0l4 zX0_IMFtQ44PfWBbPYN#}qxa%=J%dlR{O!KyZvk^g5s?sTNycWYPJ^FK(nl3k?z-5t z39#hKrdO7V(@!TU)LAPY&ngnZ1MzLEeEiZznn7e-jLCy8LO zu^7_#z*%I-BjS#Pg-;zKWWqX-+Ly$T!4`vTe5ZOV0j?TJVA*2?*=82^GVlZIuH%9s zXiV&(T(QGHHah=s&7e|6y?g+XxZGmK55`wGV>@1U)Th&=JTgJq>4mI&Av2C z)w+kRoj_dA!;SfTfkgMPO>7Dw6&1*Hi1q?54Yng`JO&q->^CX21^PrU^JU#CJ_qhV zSG>afB%>2fx<~g8p=P8Yzxqc}s@>>{g7}F!;lCXvF#RV)^fyYb_)iKVCz1xEq=fJ| z0a7DMCK*FuP=NM*5h;*D`R4y$6cpW-E&-i{v`x=Jbk_xSn@2T3q!3HoAOB`@5Vg6) z{PW|@9o!e;v1jZ2{=Uw6S6o{g82x6g=k!)cFSC*oemHaVjg?VpEmtUuD2_J^A~$4* z3O7HsbA6wxw{TP5Kk)(Vm?gKo+_}11vbo{Tp_5x79P~#F)ahQXT)tSH5;;14?s)On zel1J>1x>+7;g1Iz2FRpnYz;sD0wG9Q!vuzE9yKi3@4a9Nh1!GGN?hA)!mZEnnHh&i zf?#ZEN2sFbf~kV;>K3UNj1&vFhc^sxgj8FCL4v>EOYL?2uuT`0eDH}R zmtUJMxVrV5H{L53hu3#qaWLUa#5zY?f5ozIn|PkMWNP%n zWB5!B0LZB0kLw$k39=!akkE9Q>F4j+q434jB4VmslQ;$ zKiO#FZ`p|dKS716jpcvR{QJkSNfDVhr2%~eHrW;fU45>>snr*S8Vik-5eN5k*c2Mp zyxvX&_cFbB6lODXznHHT|rsURe2!swomtrqc~w5 zymTM8!w`1{04CBprR!_F{5LB+2_SOuZN{b*!J~1ZiPpP-M;);!ce!rOPDLtgR@Ie1 zPreuqm4!H)hYePcW1WZ0Fyaqe%l}F~Orr)~+;mkS&pOhP5Ebb`cnUt!X_QhP4_4p( z8YKQCDKGIy>?WIFm3-}Br2-N`T&FOi?t)$hjphB9wOhBXU#Hb+zm&We_-O)s(wc`2 z8?VsvU;J>Ju7n}uUb3s1yPx_F*|FlAi=Ge=-kN?1;`~6szP%$3B0|8Sqp%ebM)F8v zADFrbeT0cgE>M0DMV@_Ze*GHM>q}wWMzt|GYC%}r{OXRG3Ij&<+nx9;4jE${Fj_r* z`{z1AW_6Myd)i6e0E-h&m{{CvzH=Xg!&(bLYgRMO_YVd8JU7W+7MuGWNE=4@OvP9+ zxi^vqS@5%+#gf*Z@RVyU9N1sO-(rY$24LGsg1>w>s6ST^@)|D9>cT50maXLUD{Fzf zt~tp{OSTEKg3ZSQyQQ5r51){%=?xlZ54*t1;Ow)zLe3i?8tD8YyY^k%M)e`V*r+vL zPqUf&m)U+zxps+NprxMHF{QSxv}>lE{JZETNk1&F+R~bp{_T$dbXL2UGnB|hgh*p4h$clt#6;NO~>zuyY@C-MD@)JCc5XrYOt`wW7! z_ti2hhZBMJNbn0O-uTxl_b6Hm313^fG@e;RrhIUK9@# z+DHGv_Ow$%S8D%RB}`doJjJy*aOa5mGHVHz0e0>>O_%+^56?IkA5eN+L1BVCp4~m=1eeL zb;#G!#^5G%6Mw}r1KnaKsLvJB%HZL)!3OxT{k$Yo-XrJ?|7{s4!H+S2o?N|^Z z)+?IE9H7h~Vxn5hTis^3wHYuOU84+bWd)cUKuHapq=&}WV#OxHpLab`NpwHm8LmOo zjri+!k;7j_?FP##CpM+pOVx*0wExEex z@`#)K<-ZrGyArK;a%Km`^+We|eT+#MygHOT6lXBmz`8|lyZOwL1+b+?Z$0OhMEp3R z&J=iRERpv~TC=p2-BYLC*?4 zxvPs9V@g=JT0>zky5Poj=fW_M!c)Xxz1<=&_ZcL=LMZJqlnO1P^xwGGW*Z+yTBvbV z-IFe6;(k1@$1;tS>{%pXZ_7w+i?N4A2=TXnGf=YhePg8bH8M|Lk-->+w8Y+FjZ;L=wSGwxfA`gqSn)f(XNuSm>6Y z@|#e-)I(PQ^G@N`%|_DZSb4_pkaEF0!-nqY+t#pyA>{9^*I-zw4SYA1_z2Bs$XGUZbGA;VeMo%CezHK0lO={L%G)dI-+8w?r9iexdoB{?l zbJ}C?huIhWXBVs7oo{!$lOTlvCLZ_KN1N+XJGuG$rh<^eUQIqcI7^pmqhBSaOKNRq zrx~w^?9C?*&rNwP_SPYmo;J-#!G|{`$JZK7DxsM3N^8iR4vvn>E4MU&Oe1DKJvLc~ zCT>KLZ1;t@My zRj_2hI^61T&LIz)S!+AQIV23n1>ng+LUvzv;xu!4;wpqb#EZz;F)BLUzT;8UA1x*6vJ zicB!3Mj03s*kGV{g`fpC?V^s(=JG-k1EMHbkdP4P*1^8p_TqO|;!Zr%GuP$8KLxuf z=pv*H;kzd;P|2`JmBt~h6|GxdU~@weK5O=X&5~w$HpfO}@l-T7@vTCxVOwCkoPQv8 z@aV_)I5HQtfs7^X=C03zYmH4m0S!V@JINm6#(JmZRHBD?T!m^DdiZJrhKpBcur2u1 zf9e4%k$$vcFopK5!CC`;ww(CKL~}mlxK_Pv!cOsFgVkNIghA2Au@)t6;Y3*2gK=5d z?|@1a)-(sQ%uFOmJ7v2iG&l&m^u&^6DJM#XzCrF%r>{2XKyxLD2rgWBD;i(!e4InDQBDg==^z;AzT2z~OmV0!?Z z0S9pX$+E;w3WN;v&NYT=+G8hf=6w0E1$0AOr61}eOvE8W1jX%>&Mjo7&!ulawgzLH zbcb+IF(s^3aj12WSi#pzIpijJJzkP?JzRawnxmNDSUR#7!29vHULCE<3Aa#be}ie~d|!V+ z%l~s9Odo$G&fH!t!+`rUT0T9DulF!Yq&BfQWFZV1L9D($r4H(}Gnf6k3^wa7g5|Ws zj7%d`!3(0bb55yhC6@Q{?H|2os{_F%o=;-h{@Yyyn*V7?{s%Grvpe!H^kl6tF4Zf5 z{Jv1~yZ*iIWL_9C*8pBMQArfJJ0d9Df6Kl#wa}7Xa#Ef_5B7=X}DzbQXVPfCwTO@9+@;A^Ti6il_C>g?A-GFwA0#U;t4;wOm-4oS})h z5&on>NAu67O?YCQr%7XIzY%LS4bha9*e*4bU4{lGCUmO2UQ2U)QOqClLo61Kx~3dI zmV3*(P6F_Tr-oP%x!0kTnnT?Ep5j;_IQ^pTRp=e8dmJtI4YgWd0}+b2=ATkOhgpXe z;jmw+FBLE}UIs4!&HflFr4)vMFOJ19W4f2^W(=2)F%TAL)+=F>IE$=e=@j-*bFLSg z)wf|uFQu+!=N-UzSef62u0-C8Zc7 zo6@F)c+nZA{H|+~7i$DCU0pL{0Ye|fKLuV^w!0Y^tT$isu%i1Iw&N|tX3kwFKJN(M zXS`k9js66o$r)x?TWL}Kxl`wUDUpwFx(w4Yk%49;$sgVvT~n8AgfG~HUcDt1TRo^s zdla@6heJB@JV z!vK;BUMznhzGK6PVtj0)GB=zTv6)Q9Yt@l#fv7>wKovLobMV-+(8)NJmyF8R zcB|_K7=FJGGn^X@JdFaat0uhKjp3>k#^&xE_}6NYNG?kgTp>2Iu?ElUjt4~E-?`Du z?mDCS9wbuS%fU?5BU@Ijx>1HG*N?gIP+<~xE4u=>H`8o((cS5M6@_OK%jSjFHirQK zN9@~NXFx*jS{<|bgSpC|SAnA@I)+GB=2W|JJChLI_mx+-J(mSJ!b)uUom6nH0#2^(L@JBlV#t zLl?j54s`Y3vE^c_3^Hl0TGu*tw_n?@HyO@ZrENxA+^!)OvUX28gDSF*xFtQzM$A+O zCG=n#6~r|3zt=8%GuG} z<#VCZ%2?3Q(Ad#Y7GMJ~{U3>E{5e@z6+rgZLX{Cxk^p-7dip^d29;2N1_mm4QkASo z-L`GWWPCq$uCo;X_BmGIpJFBlhl<8~EG{vOD1o|X$aB9KPhWO_cKiU*$HWEgtf=fn zsO%9bp~D2c@?*K9jVN@_vhR03>M_8h!_~%aN!Cnr?s-!;U3SVfmhRwk11A^8Ns`@KeE}+ zN$H}a1U6E;*j5&~Og!xHdfK5M<~xka)x-0N)K_&e7AjMz`toDzasH+^1bZlC!n()crk9kg@$(Y{wdKvbuUd04N^8}t1iOgsKF zGa%%XWx@WoVaNC1!|&{5ZbkopFre-Lu(LCE5HWZBoE#W@er9W<>R=^oYxBvypN#x3 zq#LC8&q)GFP=5^-bpHj?LW=)-g+3_)Ylps!3^YQ{9~O9&K)xgy zMkCWaApU-MI~e^cV{Je75Qr7eF%&_H)BvfyKL=gIA>;OSq(y z052BFz3E(Prg~09>|_Z@!qj}@;8yxnw+#Ej0?Rk<y}4ghbD569B{9hSFr*^ygZ zr6j7P#gtZh6tMk6?4V$*Jgz+#&ug;yOr>=qdI#9U&^am2qoh4Jy}H2%a|#Fs{E(5r z%!ijh;VuGA6)W)cJZx+;9Bp1LMUzN~x_8lQ#D3+sL{be-Jyeo@@dv7XguJ&S5vrH` z>QxOMWn7N-T!D@1(@4>ZlL^y5>m#0!HKovs12GRav4z!>p(1~xok8+_{| z#Ae4{9#NLh#Vj2&JuIn5$d6t@__`o}umFo(n0QxUtd2GKCyE+erwXY?`cm*h&^9*8 zJ+8x6fRZI-e$CRygofIQN^dWysCxgkyr{(_oBwwSRxZora1(%(aC!5BTtj^+YuevI zx?)H#(xlALUp6QJ!=l9N__$cxBZ5p&7;qD3PsXRFVd<({Kh+mShFWJNpy`N@ab7?9 zv5=klvCJ4bx|-pvOO2-+G)6O?$&)ncA#Urze2rlBfp#htudhx-NeRnJ@u%^_bfw4o z4|{b8SkPV3b>Wera1W(+N@p9H>dc6{cnkh-sgr?e%(YkWvK+0YXVwk0=d`)}*47*B z5JGkEdVix!w7-<%r0JF~`ZMMPe;f0EQHuYHxya`puazyph*ZSb1mJAt^k4549BfS; zK7~T&lRb=W{s&t`DJ$B}s-eH1&&-wEOH1KWsKn0a(ZI+G!v&W4A*cl>qAvUv6pbUR z#(f#EKV8~hk&8oayBz4vaswc(?qw1vn`yC zZQDl2PCB-&Uu@g9ZQHhO+v(W0bNig{-k0;;`+wM@#@J)8r?qOYs#&vUna8ILxN7S{ zp1s41KnR8miQJtJtOr|+qk}wrLt+N*z#5o`TmD1)E&QD(Vh&pjZJ_J*0!8dy_ z>^=@v=J)C`x&gjqAYu`}t^S=DFCtc0MkBU2zf|69?xW`Ck~(6zLD)gSE{7n~6w8j_ zoH&~$ED2k5-yRa0!r8fMRy z;QjBYUaUnpd}mf%iVFPR%Dg9!d>g`01m~>2s))`W|5!kc+_&Y>wD@@C9%>-lE`WB0 zOIf%FVD^cj#2hCkFgi-fgzIfOi+ya)MZK@IZhHT5FVEaSbv-oDDs0W)pA0&^nM0TW zmgJmd7b1R7b0a`UwWJYZXp4AJPteYLH>@M|xZFKwm!t3D3&q~av?i)WvAKHE{RqpD{{%OhYkK?47}+}` zrR2(Iv9bhVa;cDzJ%6ntcSbx7v7J@Y4x&+eWSKZ*eR7_=CVIUSB$^lfYe@g+p|LD{ zPSpQmxx@b$%d!05|H}WzBT4_cq?@~dvy<7s&QWtieJ9)hd4)$SZz}#H2UTi$CkFWW|I)v_-NjuH!VypONC=1`A=rm_jfzQ8Fu~1r8i{q-+S_j$ z#u^t&Xnfi5tZtl@^!fUJhx@~Cg0*vXMK}D{>|$#T*+mj(J_@c{jXBF|rm4-8%Z2o! z2z0o(4%8KljCm^>6HDK!{jI7p+RAPcty_~GZ~R_+=+UzZ0qzOwD=;YeZt*?3%UGdr z`c|BPE;yUbnyARUl&XWSNJ<+uRt%!xPF&K;(l$^JcA_CMH6)FZt{>6ah$|(9$2fc~ z=CD00uHM{qv;{Zk9FR0~u|3|Eiqv9?z2#^GqylT5>6JNZwKqKBzzQpKU2_pmtD;CT zi%Ktau!Y2Tldfu&b0UgmF(SSBID)15*r08eoUe#bT_K-G4VecJL2Pa=6D1K6({zj6 za(2Z{r!FY5W^y{qZ}08+h9f>EKd&PN90f}Sc0ejf%kB4+f#T8Q1=Pj=~#pi$U zp#5rMR%W25>k?<$;$x72pkLibu1N|jX4cWjD3q^Pk3js!uK6h7!dlvw24crL|MZs_ zb%Y%?Fyp0bY0HkG^XyS76Ts*|Giw{31LR~+WU5NejqfPr73Rp!xQ1mLgq@mdWncLy z%8}|nzS4P&`^;zAR-&nm5f;D-%yNQPwq4N7&yULM8bkttkD)hVU>h>t47`{8?n2&4 zjEfL}UEagLUYwdx0sB2QXGeRmL?sZ%J!XM`$@ODc2!y|2#7hys=b$LrGbvvjx`Iqi z&RDDm3YBrlKhl`O@%%&rhLWZ*ABFz2nHu7k~3@e4)kO3%$=?GEFUcCF=6-1n!x^vmu+Ai*amgXH+Rknl6U>#9w;A} zn2xanZSDu`4%%x}+~FG{Wbi1jo@wqBc5(5Xl~d0KW(^Iu(U3>WB@-(&vn_PJt9{1`e9Iic@+{VPc`vP776L*viP{wYB2Iff8hB%E3|o zGMOu)tJX!`qJ}ZPzq7>=`*9TmETN7xwU;^AmFZ-ckZjV5B2T09pYliaqGFY|X#E-8 z20b>y?(r-Fn5*WZ-GsK}4WM>@TTqsxvSYWL6>18q8Q`~JO1{vLND2wg@58OaU!EvT z1|o+f1mVXz2EKAbL!Q=QWQKDZpV|jznuJ}@-)1&cdo z^&~b4Mx{*1gurlH;Vhk5g_cM&6LOHS2 zRkLfO#HabR1JD4Vc2t828dCUG#DL}f5QDSBg?o)IYYi@_xVwR2w_ntlpAW0NWk$F1 z$If?*lP&Ka1oWfl!)1c3fl`g*lMW3JOn#)R1+tfwrs`aiFUgz3;XIJ>{QFxLCkK30 zNS-)#DON3yb!7LBHQJ$)4y%TN82DC2-9tOIqzhZ27@WY^<6}vXCWcR5iN{LN8{0u9 zNXayqD=G|e?O^*ms*4P?G%o@J1tN9_76e}E#66mr89%W_&w4n66~R;X_vWD(oArwj z4CpY`)_mH2FvDuxgT+akffhX0b_slJJ*?Jn3O3~moqu2Fs1oL*>7m=oVek2bnprnW zixkaIFU%+3XhNA@@9hyhFwqsH2bM|`P?G>i<-gy>NflhrN{$9?LZ1ynSE_Mj0rADF zhOz4FnK}wpLmQuV zgO4_Oz9GBu_NN>cPLA=`SP^$gxAnj;WjJnBi%Q1zg`*^cG;Q)#3Gv@c^j6L{arv>- zAW%8WrSAVY1sj$=umcAf#ZgC8UGZGoamK}hR7j6}i8#np8ruUlvgQ$j+AQglFsQQq zOjyHf22pxh9+h#n$21&$h?2uq0>C9P?P=Juw0|;oE~c$H{#RGfa>| zj)Iv&uOnaf@foiBJ}_;zyPHcZt1U~nOcNB{)og8Btv+;f@PIT*xz$x!G?u0Di$lo7 zOugtQ$Wx|C($fyJTZE1JvR~i7LP{ zbdIwqYghQAJi9p}V&$=*2Azev$6K@pyblphgpv8^9bN!?V}{BkC!o#bl&AP!3DAjM zmWFsvn2fKWCfjcAQmE+=c3Y7j@#7|{;;0f~PIodmq*;W9Fiak|gil6$w3%b_Pr6K_ zJEG@&!J%DgBZJDCMn^7mk`JV0&l07Bt`1ymM|;a)MOWz*bh2#d{i?SDe9IcHs7 zjCrnyQ*Y5GzIt}>`bD91o#~5H?4_nckAgotN{2%!?wsSl|LVmJht$uhGa+HiH>;av z8c?mcMYM7;mvWr6noUR{)gE!=i7cZUY7e;HXa221KkRoc2UB>s$Y(k%NzTSEr>W(u z<(4mcc)4rB_&bPzX*1?*ra%VF}P1nwiP5cykJ&W{!OTlz&Td0pOkVp+wc z@k=-Hg=()hNg=Q!Ub%`BONH{ z_=ZFgetj@)NvppAK2>8r!KAgi>#%*7;O-o9MOOfQjV-n@BX6;Xw;I`%HBkk20v`qoVd0)}L6_49y1IhR z_OS}+eto}OPVRn*?UHC{eGyFU7JkPz!+gX4P>?h3QOwGS63fv4D1*no^6PveUeE5% zlehjv_3_^j^C({a2&RSoVlOn71D8WwMu9@Nb@=E_>1R*ve3`#TF(NA0?d9IR_tm=P zOP-x;gS*vtyE1Cm zG0L?2nRUFj#aLr-R1fX*$sXhad)~xdA*=hF3zPZhha<2O$Ps+F07w*3#MTe?)T8|A!P!v+a|ot{|^$q(TX`35O{WI0RbU zCj?hgOv=Z)xV?F`@HKI11IKtT^ocP78cqHU!YS@cHI@{fPD?YXL)?sD~9thOAv4JM|K8OlQhPXgnevF=F7GKD2#sZW*d za}ma31wLm81IZxX(W#A9mBvLZr|PoLnP>S4BhpK8{YV_}C|p<)4#yO{#ISbco92^3 zv&kCE(q9Wi;9%7>>PQ!zSkM%qqqLZW7O`VXvcj;WcJ`2~v?ZTYB@$Q&^CTfvy?1r^ z;Cdi+PTtmQwHX_7Kz?r#1>D zS5lWU(Mw_$B&`ZPmqxpIvK<~fbXq?x20k1~9az-Q!uR78mCgRj*eQ>zh3c$W}>^+w^dIr-u{@s30J=)1zF8?Wn|H`GS<=>Om|DjzC{}Jt?{!fSJe*@$H zg>wFnlT)k#T?LslW zu$^7Uy~$SQ21cE?3Ijl+bLfuH^U5P^$@~*UY#|_`uvAIe(+wD2eF}z_y!pvomuVO; zS^9fbdv)pcm-B@CW|Upm<7s|0+$@@<&*>$a{aW+oJ%f+VMO<#wa)7n|JL5egEgoBv zl$BY(NQjE0#*nv=!kMnp&{2Le#30b)Ql2e!VkPLK*+{jv77H7)xG7&=aPHL7LK9ER z5lfHxBI5O{-3S?GU4X6$yVk>lFn;ApnwZybdC-GAvaznGW-lScIls-P?Km2mF>%B2 zkcrXTk+__hj-3f48U%|jX9*|Ps41U_cd>2QW81Lz9}%`mTDIhE)jYI$q$ma7Y-`>% z8=u+Oftgcj%~TU}3nP8&h7k+}$D-CCgS~wtWvM|UU77r^pUw3YCV80Ou*+bH0!mf0 zxzUq4ed6y>oYFz7+l18PGGzhB^pqSt)si=9M>~0(Bx9*5r~W7sa#w+_1TSj3Jn9mW zMuG9BxN=}4645Cpa#SVKjFst;9UUY@O<|wpnZk$kE+to^4!?0@?Cwr3(>!NjYbu?x z1!U-?0_O?k!NdM^-rIQ8p)%?M+2xkhltt*|l=%z2WFJhme7*2xD~@zk#`dQR$6Lmd zb3LOD4fdt$Cq>?1<%&Y^wTWX=eHQ49Xl_lFUA(YQYHGHhd}@!VpYHHm=(1-O=yfK#kKe|2Xc*9}?BDFN zD7FJM-AjVi)T~OG)hpSWqH>vlb41V#^G2B_EvYlWhDB{Z;Q9-0)ja(O+By`31=biA zG&Fs#5!%_mHi|E4Nm$;vVQ!*>=_F;ZC=1DTPB#CICS5fL2T3XmzyHu?bI;m7D4@#; ztr~;dGYwb?m^VebuULtS4lkC_7>KCS)F@)0OdxZIFZp@FM_pHnJes8YOvwB|++#G( z&dm*OP^cz95Wi15vh`Q+yB>R{8zqEhz5of>Po$9LNE{xS<)lg2*roP*sQ}3r3t<}; zPbDl{lk{pox~2(XY5=qg0z!W-x^PJ`VVtz$git7?)!h>`91&&hESZy1KCJ2nS^yMH z!=Q$eTyRi68rKxdDsdt+%J_&lapa{ds^HV9Ngp^YDvtq&-Xp}60B_w@Ma>_1TTC;^ zpbe!#gH}#fFLkNo#|`jcn?5LeUYto%==XBk6Ik0kc4$6Z+L3x^4=M6OI1=z5u#M%0 z0E`kevJEpJjvvN>+g`?gtnbo$@p4VumliZV3Z%CfXXB&wPS^5C+7of2tyVkMwNWBiTE2 z8CdPu3i{*vR-I(NY5syRR}I1TJOV@DJy-Xmvxn^IInF>Tx2e)eE9jVSz69$6T`M9-&om!T+I znia!ZWJRB28o_srWlAxtz4VVft8)cYloIoVF=pL zugnk@vFLXQ_^7;%hn9x;Vq?lzg7%CQR^c#S)Oc-8d=q_!2ZVH764V z!wDKSgP}BrVV6SfCLZnYe-7f;igDs9t+K*rbMAKsp9L$Kh<6Z;e7;xxced zn=FGY<}CUz31a2G}$Q(`_r~75PzM4l_({Hg&b@d8&jC}B?2<+ed`f#qMEWi z`gm!STV9E4sLaQX+sp5Nu9*;9g12naf5?=P9p@H@f}dxYprH+3ju)uDFt^V{G0APn zS;16Dk{*fm6&BCg#2vo?7cbkkI4R`S9SSEJ=#KBk3rl69SxnCnS#{*$!^T9UUmO#&XXKjHKBqLdt^3yVvu8yn|{ zZ#%1CP)8t-PAz(+_g?xyq;C2<9<5Yy<~C74Iw(y>uUL$+$mp(DRcCWbCKiGCZw@?_ zdomfp+C5xt;j5L@VfhF*xvZdXwA5pcdsG>G<8II-|1dhAgzS&KArcb0BD4ZZ#WfiEY{hkCq5%z9@f|!EwTm;UEjKJsUo696V>h zy##eXYX}GUu%t{Gql8vVZKkNhQeQ4C%n|RmxL4ee5$cgwlU+?V7a?(jI#&3wid+Kz5+x^G!bb#$q>QpR#BZ}Xo5UW^ zD&I`;?(a}Oys7-`I^|AkN?{XLZNa{@27Dv^s4pGowuyhHuXc zuctKG2x0{WCvg_sGN^n9myJ}&FXyGmUQnW7fR$=bj$AHR88-q$D!*8MNB{YvTTEyS zn22f@WMdvg5~o_2wkjItJN@?mDZ9UUlat2zCh(zVE=dGi$rjXF7&}*sxac^%HFD`Y zTM5D3u5x**{bW!68DL1A!s&$2XG@ytB~dX-?BF9U@XZABO`a|LM1X3HWCllgl0+uL z04S*PX$%|^WAq%jkzp~%9HyYIF{Ym?k)j3nMwPZ=hlCg9!G+t>tf0o|J2%t1 ztC+`((dUplgm3`+0JN~}&FRRJ3?l*>Y&TfjS>!ShS`*MwO{WIbAZR#<%M|4c4^dY8 z{Rh;-!qhY=dz5JthbWoovLY~jNaw>%tS4gHVlt5epV8ekXm#==Po$)}mh^u*cE>q7*kvX&gq)(AHoItMYH6^s6f(deNw%}1=7O~bTHSj1rm2|Cq+3M z93djjdomWCTCYu!3Slx2bZVy#CWDozNedIHbqa|otsUl+ut?>a;}OqPfQA05Yim_2 zs@^BjPoFHOYNc6VbNaR5QZfSMh2S*`BGwcHMM(1@w{-4jVqE8Eu0Bi%d!E*^Rj?cR z7qgxkINXZR)K^=fh{pc0DCKtrydVbVILI>@Y0!Jm>x-xM!gu%dehm?cC6ok_msDVA*J#{75%4IZt}X|tIVPReZS#aCvuHkZxc zHVMtUhT(wp09+w9j9eRqz~LtuSNi2rQx_QgQ(}jBt7NqyT&ma61ldD(s9x%@q~PQl zp6N*?=N$BtvjQ_xIT{+vhb1>{pM0Arde0!X-y))A4znDrVx8yrP3B1(7bKPE5jR@5 zwpzwT4cu~_qUG#zYMZ_!2Tkl9zP>M%cy>9Y(@&VoB84#%>amTAH{(hL4cDYt!^{8L z645F>BWO6QaFJ-{C-i|-d%j7#&7)$X7pv#%9J6da#9FB5KyDhkA+~)G0^87!^}AP>XaCSScr;kL;Z%RSPD2CgoJ;gpYT5&6NUK$86$T?jRH=w8nI9Z534O?5fk{kd z`(-t$8W|#$3>xoMfXvV^-A(Q~$8SKDE^!T;J+rQXP71XZ(kCCbP%bAQ1|%$%Ov9_a zyC`QP3uPvFoBqr_+$HenHklqyIr>PU_Fk5$2C+0eYy^~7U&(!B&&P2%7#mBUhM!z> z_B$Ko?{Pf6?)gpYs~N*y%-3!1>o-4;@1Zz9VQHh)j5U1aL-Hyu@1d?X;jtDBNk*vMXPn@ z+u@wxHN*{uHR!*g*4Xo&w;5A+=Pf9w#PeZ^x@UD?iQ&${K2c}UQgLRik-rKM#Y5rdDphdcNTF~cCX&9ViRP}`>L)QA4zNXeG)KXFzSDa6 zd^St;inY6J_i=5mcGTx4_^Ys`M3l%Q==f>{8S1LEHn{y(kbxn5g1ezt4CELqy)~TV6{;VW>O9?5^ ztcoxHRa0jQY7>wwHWcxA-BCwzsP>63Kt&3fy*n#Cha687CQurXaRQnf5wc9o8v7Rw zNwGr2fac;Wr-Ldehn7tF^(-gPJwPt@VR1f;AmKgxN&YPL;j=0^xKM{!wuU|^mh3NE zy35quf}MeL!PU;|{OW_x$TBothLylT-J>_x6p}B_jW1L>k)ps6n%7Rh z96mPkJIM0QFNYUM2H}YF5bs%@Chs6#pEnloQhEl?J-)es!(SoJpEPoMTdgA14-#mC zghayD-DJWtUu`TD8?4mR)w5E`^EHbsz2EjH5aQLYRcF{l7_Q5?CEEvzDo(zjh|BKg z3aJl_n#j&eFHsUw4~lxqnr!6NL*se)6H=A+T1e3xUJGQrd}oSPwSy5+$tt{2t5J5@(lFxl43amsARG74iyNC}uuS zd2$=(r6RdamdGx^eatX@F2D8?U23tDpR+Os?0Gq2&^dF+$9wiWf?=mDWfjo4LfRwL zI#SRV9iSz>XCSgEj!cW&9H-njJopYiYuq|2w<5R2!nZ27DyvU4UDrHpoNQZiGPkp@ z1$h4H46Zn~eqdj$pWrv;*t!rTYTfZ1_bdkZmVVIRC21YeU$iS-*XMNK`#p8Z_DJx| zk3Jssf^XP7v0X?MWFO{rACltn$^~q(M9rMYoVxG$15N;nP)A98k^m3CJx8>6}NrUd@wp-E#$Q0uUDQT5GoiK_R{ z<{`g;8s>UFLpbga#DAf%qbfi`WN1J@6IA~R!YBT}qp%V-j!ybkR{uY0X|x)gmzE0J z&)=eHPjBxJvrZSOmt|)hC+kIMI;qgOnuL3mbNR0g^<%|>9x7>{}>a2qYSZAGPt4it?8 zNcLc!Gy0>$jaU?}ZWxK78hbhzE+etM`67*-*x4DN>1_&{@5t7_c*n(qz>&K{Y?10s zXsw2&nQev#SUSd|D8w7ZD2>E<%g^; zV{yE_O}gq?Q|zL|jdqB^zcx7vo(^})QW?QKacx$yR zhG|XH|8$vDZNIfuxr-sYFR{^csEI*IM#_gd;9*C+SysUFejP0{{z7@P?1+&_o6=7V|EJLQun^XEMS)w(=@eMi5&bbH*a0f;iC~2J74V2DZIlLUHD&>mlug5+v z6xBN~8-ovZylyH&gG#ptYsNlT?-tzOh%V#Y33zlsJ{AIju`CjIgf$@gr8}JugRq^c zAVQ3;&uGaVlVw}SUSWnTkH_6DISN&k2QLMBe9YU=sA+WiX@z)FoSYX`^k@B!j;ZeC zf&**P?HQG6Rk98hZ*ozn6iS-dG}V>jQhb3?4NJB*2F?6N7Nd;EOOo;xR7acylLaLy z9)^lykX39d@8@I~iEVar4jmjjLWhR0d=EB@%I;FZM$rykBNN~jf>#WbH4U{MqhhF6 zU??@fSO~4EbU4MaeQ_UXQcFyO*Rae|VAPLYMJEU`Q_Q_%s2*>$#S^)&7er+&`9L=1 z4q4ao07Z2Vsa%(nP!kJ590YmvrWg+YrgXYs_lv&B5EcoD`%uL79WyYA$0>>qi6ov7 z%`ia~J^_l{p39EY zv>>b}Qs8vxsu&WcXEt8B#FD%L%ZpcVtY!rqVTHe;$p9rbb5O{^rFMB>auLn-^;s+-&P1#h~mf~YLg$8M9 zZ4#87;e-Y6x6QO<{McUzhy(%*6| z)`D~A(TJ$>+0H+mct(jfgL4x%^oC^T#u(bL)`E2tBI#V1kSikAWmOOYrO~#-cc_8! zCe|@1&mN2{*ceeiBldHCdrURk4>V}79_*TVP3aCyV*5n@jiNbOm+~EQ_}1#->_tI@ zqXv+jj2#8xJtW508rzFrYcJxoek@iW6SR@1%a%Bux&;>25%`j3UI`0DaUr7l79`B1 zqqUARhW1^h6=)6?;@v>xrZNM;t}{yY3P@|L}ey@gG( z9r{}WoYN(9TW&dE2dEJIXkyHA4&pU6ki=rx&l2{DLGbVmg4%3Dlfvn!GB>EVaY_%3+Df{fBiqJV>~Xf8A0aqUjgpa} zoF8YXO&^_x*Ej}nw-$-F@(ddB>%RWoPUj?p8U{t0=n>gAI83y<9Ce@Q#3&(soJ{64 z37@Vij1}5fmzAuIUnXX`EYe;!H-yTVTmhAy;y8VZeB#vD{vw9~P#DiFiKQ|kWwGFZ z=jK;JX*A;Jr{#x?n8XUOLS;C%f|zj-7vXtlf_DtP7bpurBeX%Hjwr z4lI-2TdFpzkjgiv!8Vfv`=SP+s=^i3+N~1ELNWUbH|ytVu>EyPN_3(4TM^QE1swRo zoV7Y_g)a>28+hZG0e7g%@2^s>pzR4^fzR-El}ARTmtu!zjZLuX%>#OoU3}|rFjJg} zQ2TmaygxJ#sbHVyiA5KE+yH0LREWr%^C*yR|@gM$nK2P zo}M}PV0v))uJh&33N>#aU376@ZH79u(Yw`EQ2hM3SJs9f99+cO6_pNW$j$L-CtAfe zYfM)ccwD!P%LiBk!eCD?fHCGvgMQ%Q2oT_gmf?OY=A>&PaZQOq4eT=lwbaf}33LCH zFD|)lu{K7$8n9gX#w4~URjZxWm@wlH%oL#G|I~Fb-v^0L0TWu+`B+ZG!yII)w05DU z>GO?n(TN+B=>HdxVDSlIH76pta$_LhbBg;eZ`M7OGcqt||qi zogS72W1IN%=)5JCyOHWoFP7pOFK0L*OAh=i%&VW&4^LF@R;+K)t^S!96?}^+5QBIs zjJNTCh)?)4k^H^g1&jc>gysM`y^8Rm3qsvkr$9AeWwYpa$b22=yAd1t<*{ zaowSEFP+{y?Ob}8&cwfqoy4Pb9IA~VnM3u!trIK$&&0Op#Ql4j>(EW?UNUv#*iH1$ z^j>+W{afcd`{e&`-A{g}{JnIzYib)!T56IT@YEs{4|`sMpW3c8@UCoIJv`XsAw!XC z34|Il$LpW}CIHFC5e*)}00I5{%OL*WZRGzC0?_}-9{#ue?-ug^ zLE|uv-~6xnSs_2_&CN9{9vyc!Xgtn36_g^wI0C4s0s^;8+p?|mm;Odt3`2ZjwtK;l zfd6j)*Fr#53>C6Y8(N5?$H0ma;BCF3HCjUs7rpb2Kf*x3Xcj#O8mvs#&33i+McX zQpBxD8!O{5Y8D&0*QjD=Yhl9%M0)&_vk}bmN_Ud^BPN;H=U^bn&(csl-pkA+GyY0Z zKV7sU_4n;}uR78ouo8O%g*V;79KY?3d>k6%gpcmQsKk&@Vkw9yna_3asGt`0Hmj59 z%0yiF*`jXhByBI9QsD=+>big5{)BGe&+U2gAARGe3ID)xrid~QN_{I>k}@tzL!Md_ z&=7>TWciblF@EMC3t4-WX{?!m!G6$M$1S?NzF*2KHMP3Go4=#ZHkeIv{eEd;s-yD# z_jU^Ba06TZqvV|Yd;Z_sN%$X=!T+&?#p+OQIHS%!LO`Hx0q_Y0MyGYFNoM{W;&@0@ zLM^!X4KhdtsET5G<0+|q0oqVXMW~-7LW9Bg}=E$YtNh1#1D^6Mz(V9?2g~I1( zoz9Cz=8Hw98zVLwC2AQvp@pBeKyidn6Xu0-1SY1((^Hu*-!HxFUPs)yJ+i`^BC>PC zjwd0mygOVK#d2pRC9LxqGc6;Ui>f{YW9Bvb>33bp^NcnZoH~w9(lM5@JiIlfa-6|k ziy31UoMN%fvQfhi8^T+=yrP{QEyb-jK~>$A4SZT-N56NYEbpvO&yUme&pWKs3^94D zH{oXnUTb3T@H+RgzML*lejx`WAyw*?K7B-I(VJx($2!NXYm%3`=F~TbLv3H<{>D?A zJo-FDYdSA-(Y%;4KUP2SpHKAIcv9-ld(UEJE7=TKp|Gryn;72?0LHqAN^fk6%8PCW z{g_-t)G5uCIf0I`*F0ZNl)Z>))MaLMpXgqWgj-y;R+@A+AzDjsTqw2Mo9ULKA3c70 z!7SOkMtZb+MStH>9MnvNV0G;pwSW9HgP+`tg}e{ij0H6Zt5zJ7iw`hEnvye!XbA@!~#%vIkzowCOvq5I5@$3wtc*w2R$7!$*?}vg4;eDyJ_1=ixJuEp3pUS27W?qq(P^8$_lU!mRChT}ctvZz4p!X^ zOSp|JOAi~f?UkwH#9k{0smZ7-#=lK6X3OFEMl7%)WIcHb=#ZN$L=aD`#DZKOG4p4r zwlQ~XDZ`R-RbF&hZZhu3(67kggsM-F4Y_tI^PH8PMJRcs7NS9ogF+?bZB*fcpJ z=LTM4W=N9yepVvTj&Hu~0?*vR1HgtEvf8w%Q;U0^`2@e8{SwgX5d(cQ|1(!|i$km! zvY03MK}j`sff;*-%mN~ST>xU$6Bu?*Hm%l@0dk;j@%>}jsgDcQ)Hn*UfuThz9(ww_ zasV`rSrp_^bp-0sx>i35FzJwA!d6cZ5#5#nr@GcPEjNnFHIrtUYm1^Z$;{d&{hQV9 z6EfFHaIS}46p^5I-D_EcwwzUUuO}mqRh&T7r9sfw`)G^Q%oHxEs~+XoM?8e*{-&!7 z7$m$lg9t9KP9282eke608^Q2E%H-xm|oJ8=*SyEo} z@&;TQ3K)jgspgKHyGiKVMCz>xmC=H5Fy3!=TP)-R3|&1S-B)!6q50wfLHKM@7Bq6E z44CY%G;GY>tC`~yh!qv~YdXw! zSkquvYNs6k1r7>Eza?Vkkxo6XRS$W7EzL&A`o>=$HXgBp{L(i^$}t`NcnAxzbH8Ht z2!;`bhKIh`f1hIFcI5bHI=ueKdzmB9)!z$s-BT4ItyY|NaA_+o=jO%MU5as9 zc2)aLP>N%u>wlaXTK!p)r?+~)L+0eCGb5{8WIk7K52$nufnQ+m8YF+GQc&{^(zh-$ z#wyWV*Zh@d!b(WwXqvfhQX)^aoHTBkc;4ossV3&Ut*k>AI|m+{#kh4B!`3*<)EJVj zwrxK>99v^k4&Y&`Awm>|exo}NvewV%E+@vOc>5>%H#BK9uaE2$vje zWYM5fKuOTtn96B_2~~!xJPIcXF>E_;yO8AwpJ4)V`Hht#wbO3Ung~@c%%=FX4)q+9 z99#>VC2!4l`~0WHs9FI$Nz+abUq# zz`Of97})Su=^rGp2S$)7N3rQCj#0%2YO<R&p>$<#lgXcUj=4H_{oAYiT3 z44*xDn-$wEzRw7#@6aD)EGO$0{!C5Z^7#yl1o;k0PhN=aVUQu~eTQ^Xy{z8Ow6tk83 z4{5xe%(hx)%nD&|e*6sTWH`4W&U!Jae#U4TnICheJmsw{l|CH?UA{a6?2GNgpZLyzU2UlFu1ZVwlALmh_DOs03J^Cjh1im`E3?9&zvNmg(MuMw&0^Lu$(#CJ*q6DjlKsY-RMJ^8yIY|{SQZ*9~CH|u9L z`R78^r=EbbR*_>5?-)I+$6i}G)%mN(`!X72KaV(MNUP7Nv3MS9S|Pe!%N2AeOt5zG zVJ;jI4HZ$W->Ai_4X+`9c(~m=@ek*m`ZQbv3ryI-AD#AH=`x$~WeW~M{Js57(K7(v ze5`};LG|%C_tmd>bkufMWmAo&B+DT9ZV~h(4jg0>^aeAqL`PEUzJJtI8W1M!bQWpv zvN(d}E1@nlYa!L!!A*RN!(Q3F%J?5PvQ0udu?q-T)j3JKV~NL>KRb~w-lWc685uS6 z=S#aR&B8Sc8>cGJ!!--?kwsJTUUm`Jk?7`H z7PrO~xgBrSW2_tTlCq1LH8*!o?pj?qxy8}(=r_;G18POrFh#;buWR0qU24+XUaVZ0 z?(sXcr@-YqvkCmHr{U2oPogHL{r#3r49TeR<{SJX1pcUqyWPrkYz^X8#QW~?F)R5i z>p^!i<;qM8Nf{-fd6!_&V*e_9qP6q(s<--&1Ttj01j0w>bXY7y1W*%Auu&p|XSOH=)V7Bd4fUKh&T1)@cvqhuD-d=?w}O zjI%i(f|thk0Go*!d7D%0^ztBfE*V=(ZIN84f5HU}T9?ulmEYzT5usi=DeuI*d|;M~ zp_=Cx^!4k#=m_qSPBr5EK~E?3J{dWWPH&oCcNepYVqL?nh4D5ynfWip$m*YlZ8r^Z zuFEUL-nW!3qjRCLIWPT0x)FDL7>Yt7@8dA?R2kF@WE>ysMY+)lTsgNM#3VbXVGL}F z1O(>q>2a+_`6r5Xv$NZAnp=Kgnr3)cL(^=8ypEeOf3q8(HGe@7Tt59;yFl||w|mnO zHDxg2G3z8=(6wjj9kbcEY@Z0iOd7Gq5GiPS5% z*sF1J<#daxDV2Z8H>wxOF<;yKzMeTaSOp_|XkS9Sfn6Mpe9UBi1cSTieGG5$O;ZLIIJ60Y>SN4vC?=yE_CWlo(EEE$e4j?z&^FM%kNmRtlbEL^dPPgvs9sbK5fGw*r@ z+!EU@u$T8!nZh?Fdf_qk$VuHk^yVw`h`_#KoS*N%epIIOfQUy_&V}VWDGp3tplMbf z5Se1sJUC$7N0F1-9jdV2mmGK{-}fu|Nv;12jDy0<-kf^AmkDnu6j~TPWOgy1MT68|D z=4=50jVbUKdKaQgD`eWGr3I&^<6uhkjz$YwItY8%Yp9{z4-{6g{73<_b*@XJ4Nm3-3z z?BW3{aY_ccRjb@W1)i5nLg|7BnWS!B`_Uo9CWaE`Ij327QH?i)9A}4Ug4wmxVVa^b z-4+m%-wwOl7cKH7+=x&nrCrbEC)Q$fpg&V83#uEH;C=GNMz`ps@^RxK%T*8%OPnC` z{WO~J%nxYJ`x|N%?&i7?;{_8t^jM&=50HlaOQj8fS}_`moH$c;vI<|cruPFnpT8yU zS%rPOCUSd5Zdb(zwk`hqwTQn)*&n)uYsP*F_(~xEWq}C= zv30kFmZFwJZ@ELVX3?$dXQh|icO7UrL*_5G=I^xXjImz`ZPp>?g#tf(ej~KaIU0algsG!IS09;>?MvqGg#c{i+}qY|{P8W~O%#>|gFd z<1dr$-oxyRGN17yZo1OwLnzwYs0|;IS_nymNB0IlSzPQ%-r`?T=;_XQ^~&#}b|AB} zkNbN5uB?-sUB-T5QLlg%Uk3)uHB;>VIzGe9_J9 zaeISkQm!v(9d(0ML^b9fR^sfHFlH?7Mvddt37OuR{|O0{uv)(&-6<87W4 zyO>s!=cPgP3O&7xxU5DlIPw_o3O>6o6Qb?JWs3qw#p3sBc3g$?Dx zi(6D+DYgV;GrUis-CL%Qe{nvZnwaVXmbhH(|GFh|Q)k=1uvA$I@1DXI7bKlQ@8D6P zS?(*?><>)G49q0wr;NajpxP4W2G)kHl6^=Z>hrNEI4Mwd_$O6$1dXF;Q#hE(-eeW6 zz03GJF%Wl?HO=_ztv5*zRlcU~{+{k%#N59mgm~eK>P!QZ6E?#Cu^2)+K8m@ySvZ*5 z|HDT}BkF@3!l(0%75G=1u2hETXEj!^1Z$!)!lyGXlWD!_vqGE$Z)#cUVBqlORW>0^ zDjyVTxwKHKG|0}j-`;!R-p>}qQfBl(?($7pP<+Y8QE#M8SCDq~k<+>Q^Zf@cT_WdX3~BSe z+|KK|7OL5Hm5(NFP~j>Ct3*$wi0n0!xl=(C61`q&cec@mFlH(sy%+RH<=s)8aAPN`SfJdkAQjdv82G5iRdv8 zh{9wHUZaniSEpslXl^_ODh}mypC?b*9FzLjb~H@3DFSe;D(A-K3t3eOTB(m~I6C;(-lKAvit(70k`%@+O*Ztdz;}|_TS~B?Tpmi=QKC^m_ z2YpEaT3iiz*;T~ap1yiA)a`dKMwu`^UhIUeltNQ1Yjo=q@bI@&3zH?rVUg=IxLy-ni zyxDu%-Fr{H6owTjZU2O5>nDb=q&Jz_TjeSq%!2m40x&U6w~GQ({quPL73IsJS;f`$ zsuhioqCBj(gJ>2hoo)Gou7(WP*pX)f=Y=!=k!&1K?EYY%jJ~X&DnK{^saPQK<1BJ z_A`_{%ZozcB(3w$z^To^6d|XuT@=X~wtW!+{4ID@N{AB~J6AL5vuY>JwvWCNFKsKh zd}@>q@_WV#QZ&UJ0#?X(pXR!oyXOEG3rqzHbCzGLONDb042i$})fM@XF)uSP(DHUc z^&{|$*xe{cs?Gp8=B%RY3L7#$ve$?TWh>MZdxF1zH1v}1z+$Ov#G7?%D)bBCyDe*% zSeKSpETC2V1){II>@UwJi>4uBN+iAx+82E~gb|Cr&8E^i&)A!uv-g?jzH99wU}8+# z$nh>yvb;TwZmS@7LrvuCu_d0-WxFNI&C7%sWuTL%YU!l|I1{|->=dlOeHOCtUO#zkS3ESO8LHV4hTdQL5EdV zuWD33fFPH}HPrW^s$Qn1Xgp&AT6<-He{{4%eIu3rN=iK|9mURdKXfB&Q?qGok%!cs ze53UP{Z!TO-Y@q2;;k2avA3`lm4OoN4@S*k=UA)7H;qZ`d8`XaYFCv?Ba+uGW@r5v z&&{nf(24WSBOhc7!qF^@0cz;XcUynNaj6w2349;s!K{KVqs5yS{ z7VubS`2OzT^5#1~6Tt^RTvt9-J|D2F>y~>2;jeF>g`hx5l%B3H=aLExQihuYngzlnBTYOTHJQMzl>kwqN5JYs)Ej zblA@ntkUS~xi+}y6|(81helS}Q~&VB37qyV|S3Y=><^1wh%msQM?fz z<58MX(=|PSUKCF#)dbhR%D&xgCD?$aR0qen+wpp6 zst}vX18!Be96TD??j1HsHTUx(a&@F?=gT`Q$oJFFyrh^;zgz!(NlAHGn0cJy@us=w zNhC#l5G;H}+>49Nsh12=ZPO2r*2OBQe5kpb&1?*PIBFitK8}FUfb~S-#hKfF0o#&d z#3aPkB$9scYku&kA6{0xHnBV#&Wei5J>5T-XX-gUXEPo+9b7WL=*XESc(3BshL`aj zXp}QIp*40}oWJt*l043e8_5;H5PI5c)U&IEw5dF(4zjX0y_lk9 zAp@!mK>WUqHo)-jop=DoK>&no>kAD=^qIE7qis&_*4~ z6q^EF$D@R~3_xseCG>Ikb6Gfofb$g|75PPyyZN&tiRxqovo_k zO|HA|sgy#B<32gyU9x^&)H$1jvw@qp+1b(eGAb)O%O!&pyX@^nQd^9BQ4{(F8<}|A zhF&)xusQhtoXOOhic=8#Xtt5&slLia3c*a?dIeczyTbC#>FTfiLST57nc3@Y#v_Eg#VUv zT8cKH#f3=1PNj!Oroz_MAR*pow%Y0*6YCYmUy^7`^r|j23Q~^*TW#cU7CHf0eAD_0 zEWEVddxFgQ7=!nEBQ|ibaScslvhuUk^*%b#QUNrEB{3PG@uTxNwW}Bs4$nS9wc(~O zG7Iq>aMsYkcr!9#A;HNsJrwTDYkK8ikdj{M;N$sN6BqJ<8~z>T20{J8Z2rRUuH7~3 z=tgS`AgxbBOMg87UT4Lwge`*Y=01Dvk>)^{Iu+n6fuVX4%}>?3czOGR$0 zpp*wp>bsFFSV`V;r_m+TZns$ZprIi`OUMhe^cLE$2O+pP3nP!YB$ry}2THx2QJs3< za1;>d-AggCarrQ>&Z!d@;mW+!q6eXhb&`GbzUDSxpl8AJ#Cm#tuc)_xh(2NV=5XMs zrf_ozRYO$NkC=pKFX5OH8v1>0i9Z$ec`~Mf+_jQ68spn(CJwclDhEEkH2Qw;${J$clv__nUjn5jA0wCLEnu1j;v!0vB>Ri6m9`;R{JMS%^)4FC zU0Z44+u$I$w=Bj|iu4DT5h~sS`C*zbmX?@-crY}E+hy>}2~C0Nn(EKk@5^qO4@l@! z6O0lr%tzGC`D^)8xU3FnMZVm0kX1sBWhaQyzVoXFWwr%Ny?=2M{5s#5i7fTu3gEkG zc{(Pr$v=;`Y#&`y*J}#M9ux>0?xu!`$9cUKm#Bdd_&S#LPTS?ZPV6zN6>W6JTS~-LfjL{mB=b(KMk3 z2HjBSlJeyUVqDd=Mt!=hpYsvby2GL&3~zm;0{^nZJq+4vb?5HH4wufvr}IX42sHeK zm@x?HN$8TsTavXs)tLDFJtY9b)y~Tl@7z4^I8oUQq4JckH@~CVQ;FoK(+e0XAM>1O z(ei}h?)JQp>)d=6ng-BZF1Z5hsAKW@mXq+hU?r8I(*%`tnIIOXw7V6ZK(T9RFJJe@ zZS!aC+p)Gf2Ujc=a6hx4!A1Th%YH!Lb^xpI!Eu` zmJO{9rw){B1Ql18d%F%da+Tbu1()?o(zT7StYqK6_w`e+fjXq5L^y(0 z09QA6H4oFj59c2wR~{~>jUoDzDdKz}5#onYPJRwa`SUO)Pd4)?(ENBaFVLJr6Kvz= zhTtXqbx09C1z~~iZt;g^9_2nCZ{};-b4dQJbv8HsWHXPVg^@(*!@xycp#R?a|L!+` zY5w))JWV`Gls(=}shH0#r*;~>_+-P5Qc978+QUd>J%`fyn{*TsiG-dWMiJXNgwBaT zJ=wgYFt+1ACW)XwtNx)Q9tA2LPoB&DkL16P)ERWQlY4%Y`-5aM9mZ{eKPUgI!~J3Z zkMd5A_p&v?V-o-6TUa8BndiX?ooviev(DKw=*bBVOW|=zps9=Yl|-R5@yJe*BPzN}a0mUsLn{4LfjB_oxpv(mwq# zSY*%E{iB)sNvWfzg-B!R!|+x(Q|b@>{-~cFvdDHA{F2sFGA5QGiIWy#3?P2JIpPKg6ncI^)dvqe`_|N=8 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..c3f110b --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,57 @@ +pluginManagement { + val weightVersion: String by settings + + repositories { + gradlePluginPortal() + mavenLocal() + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.bacteriawa.com/repository/maven-public/") + } + + plugins { + id("moe.luminolmc.hyacinthusweight.patcher") version weightVersion + id("moe.luminolmc.hyacinthusweight.core") version weightVersion + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +rootProject.name = "camellia" + +for (name in listOf("camellia-api", "camellia-server")) { + include(name) + file(name).mkdirs() +} + +optionalInclude("test-plugin") +optionalInclude("camellia-generator") + +fun optionalInclude(name: String, op: (ProjectDescriptor.() -> Unit)? = null) { + val settingsFile = file("$name.settings.gradle.kts") + if (settingsFile.exists()) { + apply(from = settingsFile) + findProject(":$name")?.let { op?.invoke(it) } + } else { + settingsFile.writeText( + """ + // Uncomment to enable the '$name' project + // include(":$name") + + """.trimIndent() + ) + } +} + +gradle.lifecycle.beforeProject { + val mcVersion = providers.gradleProperty("mcVersion").get().trim() + val camelliaVersionChannel = providers.gradleProperty("channel").get().trim() + val camelliaBuildNumber = providers.environmentVariable("BUILD_NUMBER").orNull?.trim()?.toInt() + val versionString = if (camelliaBuildNumber == null) { + "$mcVersion.local-SNAPSHOT" + } else { + "$mcVersion.build.$camelliaBuildNumber-${camelliaVersionChannel.lowercase()}" + } + version = versionString +} \ No newline at end of file