From 0678cc6e46ac4acf34ca7a1279b6f2b62d61de03 Mon Sep 17 00:00:00 2001 From: Helvetica Volubi Date: Sat, 18 Jul 2026 01:27:14 +0800 Subject: [PATCH] Start hard-fork from Luminol --- build.gradle.kts | 19 +- gradle.properties | 4 +- lophine-api/build.gradle.kts.patch | 47 +- .../features/0001-Rebrand-to-Luminol.patch | 28 + .../features/0002-Luminol-Config-System.patch | 22 + .../features/0003-KioCG-Chunk-API.patch | 28 + ...isable-timings-warn-msg-and-commands.patch | 45 + .../0005-Force-disable-reload-command.patch | 23 + .../features/0006-Pufferfish-Sentry.patch | 62 + .../0007-Pufferfish-SIMD-Utilities.patch | 33 + .../features/0008-Tick-regions-api.patch | 19 + .../features/0009-Leaves-Leaves-plugin.patch | 39 + ...10-Leaves-Fix-SculkCatalyst-exp-skip.patch | 86 + ...11-Purpur-Lobotomize-stuck-villagers.patch | 27 + .../0012-World-load-unload-APIs.patch | 28 + ...ne.patch => 0013-Rebrand-to-Lophine.patch} | 0 ...y-task-id-in-FoliaGlobalRegionSched.patch} | 0 ...h => 0015-Leaves-Add-fakeplayer-api.patch} | 0 ...patch => 0016-Leaves-Replay-Mod-API.patch} | 0 .../pufferfish/sentry/SentryContext.java | 181 ++ .../pufferfish/simd/SIMDChecker.java | 53 + .../pufferfish/simd/SIMDDetection.java | 53 + .../pufferfish/simd/VectorMapPalette.java | 101 + .../me/earthme/luminol/api/RegionStats.java | 28 + .../earthme/luminol/api/ThreadedRegion.java | 61 + .../luminol/api/ThreadedRegionizer.java | 61 + .../earthme/luminol/api/TickRegionData.java | 29 + .../luminol/api/config/ConfigDataPair.java | 11 + .../api/config/LuminolConfigBuilder.java | 82 + .../api/config/LuminolConfigsInstance.java | 203 ++ .../api/entity/EntityTeleportAsyncEvent.java | 71 + .../api/entity/PostEntityPortalEvent.java | 42 + .../api/entity/PreEntityPortalEvent.java | 81 + .../entity/player/PostPlayerRespawnEvent.java | 41 + .../api/portal/EndPlatformCreateEvent.java | 35 + .../luminol/api/portal/PortalLocateEvent.java | 55 + .../leaves/plugin/FeatureManager.java | 14 + .../org/leavesmc/leaves/plugin/Features.java | 15 + lophine-server/build.gradle.kts.patch | 150 +- .../features/0001-Rebrand-to-Lophine.patch | 31 - .../0002-Diff-in-auto-update-patch.patch | 141 - .../0003-Leaves-Item-overstack-util.patch | 190 -- .../features/0004-Leaves-Fakeplayer.patch | 55 - .../features/0005-Leaves-Replay-Mod-API.patch | 18 - .../0006-Update-suppression-event.patch | 18 - .../features/0007-Carpet-features.patch | 26 - .../0008-Carpet-Features-Compatible.patch | 38 - .../features/0001-Luminol-Config-System.patch | 44 + .../0002-Correct-player-respawn-place.patch | 54 + ...-reference-get-in-RegionizedTaskQueu.patch | 19 + ...Correct-thread-unsafe-random-sources.patch | 32 + ...al-logic-of-some-projectile-entities.patch | 58 + ...Configurable-region-format-framework.patch | 388 +++ ...o-not-enable-any-debug-subscriptions.patch | 21 + ...oo-far-poi-chunk-in-poi-compete-scan.patch | 32 + ...xes-around-entity-brain-and-memories.patch | 287 ++ .../0010-Fix-off-region-leashing.patch | 38 + ...sk-returning-of-ServerConfigurationP.patch | 30 + .../0012-Fix-riding-statistics-desync.patch | 64 + ...nder-pearls-when-player-switched-dim.patch | 24 + .../0014-Fix-long-commands-support.patch | 21 + .../0015-Fix-creative-player-item-pick.patch | 19 + ...016-Fix-entity-portal-teleport-speed.patch | 67 + ...-player-auto-saving-ignores-interval.patch | 19 + ...8-Force-disable-builtin-spark-plugin.patch | 98 + ...animals-check-can-teleport-in-an-unl.patch | 24 + ...t-teleprotAsync-calls-in-move-events.patch | 88 + ...-when-teleportation-or-firstly-creat.patch | 47 + ...f-entity-was-moving-to-another-regio.patch | 63 + ...mporarily-fix-teleport-yam-and-pitch.patch | 23 + .../0024-Improved-Server-Health-Report.patch | 288 ++ .../0025-World-load-unload-APIs.patch | 522 ++++ ...aiiju-Do-not-pathfind-outside-region.patch | 23 + ...iiju-Entity-tick-and-removal-limiter.patch | 55 + ...iju-Vanilla-end-portal-teleportation.patch | 81 + ...sync-protocol-switching-optimization.patch | 288 ++ .../0030-Add-config-for-server-mod-name.patch | 19 + ...-Add-config-for-offline-mode-warning.patch | 19 + .../0032-Add-config-for-cpu-affinity.patch | 19 + ...-Add-config-for-unsafe-teleportation.patch | 45 + .../0034-Add-config-for-vanilla-random.patch | 19 + ...0035-Add-config-for-watchdog-timeout.patch | 19 + ...nfig-to-disable-entity-tick-catchers.patch | 18 + ...037-Add-config-for-heightmap-warning.patch | 21 + .../0038-Add-config-gui-support.patch | 19 + ...he-data-command-to-be-enabled-config.patch | 21 + ...-config-for-command-block-command-ex.patch | 19 + ...verify-signature-only-in-online-mode.patch | 20 + ...g-for-whether-to-save-portal-tickets.patch | 19 + ...-Add-back-read-only-datapack-command.patch | 61 + ...r-with-chunkhot-membar-and-regionbar.patch | 360 +++ ...d-config-to-modify-tripwire-behavior.patch | 110 + ...46-Add-config-to-enable-tick-command.patch | 104 + .../0047-Add-config-for-item-multitask.patch | 39 + .../features/0048-Add-tick-regions-api.patch | 63 + ...g-teleportation-event-APIs-for-folia.patch | 149 + .../0050-Add-Portal-rate-limiter.patch | 130 + ...-Add-config-for-waypoint-restoration.patch | 161 + ...-Secure-seed-and-matter-seed-command.patch | 423 +++ .../0053-Add-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 + .../0058-Leaf-fast-bit-radix-sort.patch | 70 + ...-Configurable-vanilla-username-check.patch | 203 ++ ...60-Purpur-Lobotomize-stuck-villagers.patch | 89 + ...fish-Reduce-projectile-chunk-loading.patch | 68 + ...tle-goal-selector-during-inactive-ti.patch | 30 + .../0063-Petal-Reduce-sensor-work.patch | 28 + .../0064-Leaves-Disable-packet-limit.patch | 60 + .../features/0065-Leaves-Vanilla-Hopper.patch | 94 + ...Leaves-Lithium-Sleeping-Block-Entity.patch | 2739 +++++++++++++++++ ...aves-Configurable-collision-behavior.patch | 56 + ...aves-Disable-moved-wrongly-threshold.patch | 50 + ...0069-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 + ...73-Gale-Store-mob-counts-in-an-array.patch | 36 + ...timize-random-calls-in-chunk-ticking.patch | 113 + ...duce-enderman-teleport-chunk-lookups.patch | 58 + .../0076-Gale-Cache-ShapePairKey-hash.patch | 45 + ...on-check-has-physics-before-same-veh.patch | 35 + ...gible-planar-movement-multiplication.patch | 32 + ...9-Gale-Optimize-matching-item-checks.patch | 26 + ...da-and-Optional-allocation-in-Entity.patch | 42 + .../0081-Gale-Reduce-array-allocations.patch | 441 +++ ...0082-Some-optimizations-from-krypton.patch | 80 + ...rver-I18n.patch => 0083-Server-I18n.patch} | 0 ...-Fix-datapack-command-save-function.patch} | 4 +- ...rpur-Barrels-and-enderchests-6-rows.patch} | 0 ...to-disable-some-check-for-operators.patch} | 6 +- ...to-enable-cross-region-damage-trace.patch} | 0 ...d-config-to-enable-save-all-command.patch} | 2 +- ...d-config-to-enable-function-command.patch} | 0 ...config-to-enable-scoreboard-command.patch} | 0 ...dd-config-to-enable-trigger-command.patch} | 0 ...config-to-enable-raytracing-tracker.patch} | 8 +- ...patch => 0093-Add-tick-rate-support.patch} | 0 ... 0094-Modify-merge-ItemEntity-logic.patch} | 0 ...ch => 0095-Old-zombie-reinforcement.patch} | 0 ...0096-Old-leader-zombie-health-logic.patch} | 0 ...tch => 0097-Spawn-invulnerable-time.patch} | 0 ... => 0098-Leaves-Item-overstack-util.patch} | 12 +- ...ch => 0099-Leaves-Old-raid-behavior.patch} | 0 ...-for-Raid-Revert-and-Cross-Region-D.patch} | 0 ...01-Leaves-Leaves-Base-Protocol-Core.patch} | 8 +- ...-Configurable-trading-with-the-void.patch} | 4 +- ...atch => 0103-Leaves-Servux-Protocol.patch} | 4 +- ...avesHooks.patch => 0104-LeavesHooks.patch} | 0 ...ves-Old-Explosion-Damage-Calculator.patch} | 0 ... 0106-Leaves-Redstone-Shears-Wrench.patch} | 0 ... => 0107-Leaves-Syncmatica-Protocol.patch} | 2 +- ....patch => 0108-Leaves-BBOR-Protocol.patch} | 0 ...h => 0109-Leaves-Xaero-Map-Protocol.patch} | 0 ...=> 0110-Leaves-Support-REI-protocol.patch} | 0 ....patch => 0111-Leaves-Jade-Protocol.patch} | 4 +- ...lternative-block-placement-Protocol.patch} | 4 +- ...yer.patch => 0113-Leaves-Fakeplayer.patch} | 26 +- ...patch => 0114-Leaves-Replay-Mod-API.patch} | 18 +- ...=> 0115-Leaves-Creative-fly-no-clip.patch} | 0 ... => 0116-Leaves-Wool-Hopper-Counter.patch} | 0 ...tch => 0117-Global-Entities-Counter.patch} | 12 +- ...aves-Catch-update-suppression-crash.patch} | 10 +- ... 0119-Leaves-CCE-update-suppression.patch} | 0 ...aves-Redstone-ignore-upwards-update.patch} | 0 ...patch => 0121-Instant-Block-Updater.patch} | 4 +- ...rt-TrapDoorBlock-changes-form-Paper.patch} | 0 ...ss-of-item-drops-due-to-update-supp.patch} | 0 ...et-placed-block-on-exception-Do-not.patch} | 0 ...5-Leaves-Old-Block-remove-behaviour.patch} | 0 ...Tweaks-mob-fire-and-explosion-rules.patch} | 0 ...tures.patch => 0127-Carpet-features.patch} | 16 +- ... => 0128-Carpet-Features-Compatible.patch} | 0 ...col.patch => 0129-PCA-sync-protocol.patch} | 0 ...-Add-Vanilla-like-experience-Config.patch} | 0 ...Restore-vanilla-ender-pearl-loading.patch} | 0 .../features/0001-Rebrand-to-Luminol.patch | 1346 ++++++++ .../features/0002-Luminol-Config-System.patch | 28 + ...Correct-thread-unsafe-random-sources.patch | 19 + ...4-Force-disable-builtin-spark-plugin.patch | 59 + .../0005-World-load-unload-APIs.patch | 128 + .../0006-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 + ...-with-chunkhot-membar-and-region-bar.patch | 39 + .../features/0010-Add-tick-regions-api.patch | 22 + ...ockPos-transform-to-Position-support.patch | 23 + .../0012-Add-config-for-cpu-affinity.patch | 28 + ...0013-Leaf-Skip-event-if-no-listeners.patch | 33 + ...-Secure-seed-and-matter-seed-command.patch | 43 + ...-Configurable-vanilla-username-check.patch | 82 + .../features/0016-Leaves-Leaves-plugin.patch | 263 ++ ...17-Purpur-Lobotomize-stuck-villagers.patch | 25 + ...ne.patch => 0018-Rebrand-to-Lophine.patch} | 0 ...rpur-Barrels-and-enderchests-6-rows.patch} | 0 ...y-task-id-in-FoliaGlobalRegionSched.patch} | 0 ...21-Leaves-Leaves-Base-Protocol-Core.patch} | 0 ...avesHooks.patch => 0022-LeavesHooks.patch} | 0 ...ch => 0023-Leaves-Leaves-Fakeplayer.patch} | 10 +- ...patch => 0024-Leaves-Replay-Mod-API.patch} | 6 +- .../main/java/abomination/IRegionFile.java | 43 + .../java/abomination/LinearRegionFile.java | 620 ++++ .../scheduler/SchedulableTickHack.java | 24 + .../src/main/java/com/kiocg/ChunkHot.java | 90 + .../kaiijumc/kaiiju/KaiijuEntityLimits.java | 158 + .../kaiiju/KaiijuEntityThrottler.java | 101 + .../sentry/PufferfishSentryAppender.java | 149 + .../pufferfish/sentry/SentryManager.java | 61 + .../luminol/api/impl/RegionStatsImpl.java | 27 + .../luminol/api/impl/ThreadedRegionImpl.java | 52 + .../api/impl/ThreadedRegionizerImpl.java | 49 + .../luminol/api/impl/TickRegionDataImpl.java | 30 + .../luminol/commands/CommandRegister.java | 14 + .../luminol/commands/bar/BarCommand.java | 52 + .../luminol/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 + .../earthme/luminol/config/ConfigManager.java | 155 + .../luminol/config/ConfigsInstance.java | 1065 +++++++ .../config/ConfigsInstanceBuilder.java | 82 + .../luminol/config/DefaultTransformLogic.java | 7 + .../earthme/luminol/config/IConfigModule.java | 27 + ...alFormatConversionExceptionWithOrigin.java | 24 + .../config/flags/CommandSuggestions.java | 9 + .../luminol/config/flags/ConfigClassInfo.java | 17 + .../luminol/config/flags/ConfigInfo.java | 15 + .../luminol/config/flags/DoNotLoad.java | 8 + .../config/flags/HotReloadUnsupported.java | 8 + .../config/flags/TransformedConfig.java | 28 + .../modules/experiment/CommandConfig.java | 26 + .../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 + .../PreventIncorrectTeleportAsyncConfig.java | 18 + .../fixes/UnsafeTeleportationConfig.java | 15 + .../fixes/VanillaRandomSourceConfig.java | 12 + .../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 + .../config/modules/misc/AutoUpdateConfig.java | 57 + .../modules/misc/DisableWarningConfig.java | 18 + .../modules/misc/FoliaWatchogConfig.java | 12 + .../modules/misc/InorderChatConfig.java | 12 + .../misc/PaperPacketLimiterConfig.java | 15 + .../modules/misc/PublickeyVerifyConfig.java | 12 + .../modules/misc/SavePortalTicketsConfig.java | 14 + .../config/modules/misc/SentryConfig.java | 45 + .../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 + .../PetalReduceSensorWorkConfig.java | 14 + .../ProjectileChunkReduceConfig.java | 14 + .../modules/optimizations/SIMDConfig.java | 45 + .../config/modules/removed/RemovedConfig.java | 14 + .../DisableCheckForFoliaSupported.java | 19 + .../data/BufferedLinearRegionFile.java | 1442 +++++++++ .../me/earthme/luminol/enums/EnumBarType.java | 92 + .../enums/EnumCollisionBehaviorMode.java | 7 + .../luminol/enums/EnumConfigCategory.java | 33 + .../luminol/enums/EnumRegionFormat.java | 29 + .../luminol/enums/EnumStatusBarDisplay.java | 21 + .../luminol/enums/EnumTripwireBehavior.java | 7 + .../functions/bars/TickableStatusBar.java | 194 ++ .../functions/bars/TickableStatusBarList.java | 171 + .../luminol/functions/bars/impl/Membar.java | 110 + .../functions/bars/impl/RegionBar.java | 129 + .../luminol/functions/bars/impl/TpsBar.java | 167 + .../utils/AffinityRunnableWrapper.java | 34 + .../luminol/utils/AutoUpdateHelper.java | 466 +++ .../BufferedLinearRegionFileFlusher.java | 133 + .../earthme/luminol/utils/ClassLoadUtil.java | 97 + .../utils/EntityMoveOutOfRegionException.java | 30 + .../utils/FoliaServerWaypointManager.java | 235 ++ .../luminol/utils/IRegionCreateFunction.java | 10 + .../me/earthme/luminol/utils/NullPlugin.java | 152 + .../earthme/luminol/utils/RateThrottler.java | 87 + .../luminol/utils/RegionCreatorInfo.java | 8 + .../earthme/luminol/utils/TeleportRecord.java | 15 + .../utils/dialog/ConfigCommandDialog.java | 117 + .../utils/dialog/ConfigDialogUtil.java | 55 + .../luminol/utils/dialog/DialogUtil.java | 472 +++ .../thread/LevelAwareRegionScheduler.java | 222 ++ .../thread/LevelSubRegionShutdownThread.java | 258 ++ .../utils/thread/SimpleReferenceRWLock.java | 224 ++ .../java/me/titaniumtown/ArrayConstants.java | 21 + .../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 + .../plugin/MinecraftInternalPlugin.java | 170 + .../leaves/plugin/ServerFeatureManager.java | 31 + .../provider/LeavesPluginProviderFactory.java | 78 + .../configuration/FeaturesConfiguration.java | 38 + .../configuration/LeavesPluginMeta.java | 116 + .../configuration/MixinConfiguration.java | 54 + .../leaves/util/BlockPatternHelper.java | 46 + .../leaves/util/ItemOverstackUtils.java | 195 ++ .../src/main/java/su/plo/matter/Globals.java | 95 + .../src/main/java/su/plo/matter/Hashing.java | 73 + .../main/java/su/plo/matter/HashingV2.java | 519 ++++ .../su/plo/matter/WorldgenCryptoRandom.java | 190 ++ 365 files changed, 28337 insertions(+), 657 deletions(-) create mode 100644 lophine-api/paper-patches/features/0001-Rebrand-to-Luminol.patch create mode 100644 lophine-api/paper-patches/features/0002-Luminol-Config-System.patch create mode 100644 lophine-api/paper-patches/features/0003-KioCG-Chunk-API.patch create mode 100644 lophine-api/paper-patches/features/0004-Disable-timings-warn-msg-and-commands.patch create mode 100644 lophine-api/paper-patches/features/0005-Force-disable-reload-command.patch create mode 100644 lophine-api/paper-patches/features/0006-Pufferfish-Sentry.patch create mode 100644 lophine-api/paper-patches/features/0007-Pufferfish-SIMD-Utilities.patch create mode 100644 lophine-api/paper-patches/features/0008-Tick-regions-api.patch create mode 100644 lophine-api/paper-patches/features/0009-Leaves-Leaves-plugin.patch create mode 100644 lophine-api/paper-patches/features/0010-Leaves-Fix-SculkCatalyst-exp-skip.patch create mode 100644 lophine-api/paper-patches/features/0011-Purpur-Lobotomize-stuck-villagers.patch create mode 100644 lophine-api/paper-patches/features/0012-World-load-unload-APIs.patch rename lophine-api/paper-patches/features/{0001-Rebrand-to-Lophine.patch => 0013-Rebrand-to-Lophine.patch} (100%) rename lophine-api/paper-patches/features/{0002-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch => 0014-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch} (100%) rename lophine-api/paper-patches/features/{0003-Leaves-Add-fakeplayer-api.patch => 0015-Leaves-Add-fakeplayer-api.patch} (100%) rename lophine-api/paper-patches/features/{0004-Leaves-Replay-Mod-API.patch => 0016-Leaves-Replay-Mod-API.patch} (100%) create mode 100644 lophine-api/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java create mode 100644 lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java create mode 100644 lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java create mode 100644 lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/RegionStats.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/ThreadedRegion.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/ThreadedRegionizer.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/TickRegionData.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/config/ConfigDataPair.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/config/LuminolConfigBuilder.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/config/LuminolConfigsInstance.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/entity/EntityTeleportAsyncEvent.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/entity/PostEntityPortalEvent.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/entity/PreEntityPortalEvent.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/entity/player/PostPlayerRespawnEvent.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/portal/EndPlatformCreateEvent.java create mode 100644 lophine-api/src/main/java/me/earthme/luminol/api/portal/PortalLocateEvent.java create mode 100644 lophine-api/src/main/java/org/leavesmc/leaves/plugin/FeatureManager.java create mode 100644 lophine-api/src/main/java/org/leavesmc/leaves/plugin/Features.java delete mode 100644 lophine-server/luminol-patches/features/0001-Rebrand-to-Lophine.patch delete mode 100644 lophine-server/luminol-patches/features/0002-Diff-in-auto-update-patch.patch delete mode 100644 lophine-server/luminol-patches/features/0003-Leaves-Item-overstack-util.patch delete mode 100644 lophine-server/luminol-patches/features/0004-Leaves-Fakeplayer.patch delete mode 100644 lophine-server/luminol-patches/features/0005-Leaves-Replay-Mod-API.patch delete mode 100644 lophine-server/luminol-patches/features/0006-Update-suppression-event.patch delete mode 100644 lophine-server/luminol-patches/features/0007-Carpet-features.patch delete mode 100644 lophine-server/luminol-patches/features/0008-Carpet-Features-Compatible.patch create mode 100644 lophine-server/minecraft-patches/features/0001-Luminol-Config-System.patch create mode 100644 lophine-server/minecraft-patches/features/0002-Correct-player-respawn-place.patch create mode 100644 lophine-server/minecraft-patches/features/0003-Correct-volatile-reference-get-in-RegionizedTaskQueu.patch create mode 100644 lophine-server/minecraft-patches/features/0004-Correct-thread-unsafe-random-sources.patch create mode 100644 lophine-server/minecraft-patches/features/0005-Correct-portal-logic-of-some-projectile-entities.patch create mode 100644 lophine-server/minecraft-patches/features/0006-Configurable-region-format-framework.patch create mode 100644 lophine-server/minecraft-patches/features/0007-Do-not-enable-any-debug-subscriptions.patch create mode 100644 lophine-server/minecraft-patches/features/0008-Do-not-load-too-far-poi-chunk-in-poi-compete-scan.patch create mode 100644 lophine-server/minecraft-patches/features/0009-Fixes-around-entity-brain-and-memories.patch create mode 100644 lophine-server/minecraft-patches/features/0010-Fix-off-region-leashing.patch create mode 100644 lophine-server/minecraft-patches/features/0011-Fix-unpatched-task-returning-of-ServerConfigurationP.patch create mode 100644 lophine-server/minecraft-patches/features/0012-Fix-riding-statistics-desync.patch create mode 100644 lophine-server/minecraft-patches/features/0013-Fix-misbehaved-ender-pearls-when-player-switched-dim.patch create mode 100644 lophine-server/minecraft-patches/features/0014-Fix-long-commands-support.patch create mode 100644 lophine-server/minecraft-patches/features/0015-Fix-creative-player-item-pick.patch create mode 100644 lophine-server/minecraft-patches/features/0016-Fix-entity-portal-teleport-speed.patch create mode 100644 lophine-server/minecraft-patches/features/0017-Fix-player-auto-saving-ignores-interval.patch create mode 100644 lophine-server/minecraft-patches/features/0018-Force-disable-builtin-spark-plugin.patch create mode 100644 lophine-server/minecraft-patches/features/0019-Prevent-tamable-animals-check-can-teleport-in-an-unl.patch create mode 100644 lophine-server/minecraft-patches/features/0020-Prevent-teleprotAsync-calls-in-move-events.patch create mode 100644 lophine-server/minecraft-patches/features/0021-Sync-dragon-part-when-teleportation-or-firstly-creat.patch create mode 100644 lophine-server/minecraft-patches/features/0022-Teleport-async-if-entity-was-moving-to-another-regio.patch create mode 100644 lophine-server/minecraft-patches/features/0023-Temporarily-fix-teleport-yam-and-pitch.patch create mode 100644 lophine-server/minecraft-patches/features/0024-Improved-Server-Health-Report.patch create mode 100644 lophine-server/minecraft-patches/features/0025-World-load-unload-APIs.patch create mode 100644 lophine-server/minecraft-patches/features/0026-Kaiiju-Do-not-pathfind-outside-region.patch create mode 100644 lophine-server/minecraft-patches/features/0027-Kaiiju-Entity-tick-and-removal-limiter.patch create mode 100644 lophine-server/minecraft-patches/features/0028-Kaiiju-Vanilla-end-portal-teleportation.patch create mode 100644 lophine-server/minecraft-patches/features/0029-Async-protocol-switching-optimization.patch create mode 100644 lophine-server/minecraft-patches/features/0030-Add-config-for-server-mod-name.patch create mode 100644 lophine-server/minecraft-patches/features/0031-Add-config-for-offline-mode-warning.patch create mode 100644 lophine-server/minecraft-patches/features/0032-Add-config-for-cpu-affinity.patch create mode 100644 lophine-server/minecraft-patches/features/0033-Add-config-for-unsafe-teleportation.patch create mode 100644 lophine-server/minecraft-patches/features/0034-Add-config-for-vanilla-random.patch create mode 100644 lophine-server/minecraft-patches/features/0035-Add-config-for-watchdog-timeout.patch create mode 100644 lophine-server/minecraft-patches/features/0036-Add-config-to-disable-entity-tick-catchers.patch create mode 100644 lophine-server/minecraft-patches/features/0037-Add-config-for-heightmap-warning.patch create mode 100644 lophine-server/minecraft-patches/features/0038-Add-config-gui-support.patch create mode 100644 lophine-server/minecraft-patches/features/0039-Add-force-the-data-command-to-be-enabled-config.patch create mode 100644 lophine-server/minecraft-patches/features/0040-Add-experimental-config-for-command-block-command-ex.patch create mode 100644 lophine-server/minecraft-patches/features/0041-Add-config-to-verify-signature-only-in-online-mode.patch create mode 100644 lophine-server/minecraft-patches/features/0042-Add-config-for-whether-to-save-portal-tickets.patch create mode 100644 lophine-server/minecraft-patches/features/0043-Add-back-read-only-datapack-command.patch create mode 100644 lophine-server/minecraft-patches/features/0044-Add-tpsbar-with-chunkhot-membar-and-regionbar.patch create mode 100644 lophine-server/minecraft-patches/features/0045-Add-config-to-modify-tripwire-behavior.patch create mode 100644 lophine-server/minecraft-patches/features/0046-Add-config-to-enable-tick-command.patch create mode 100644 lophine-server/minecraft-patches/features/0047-Add-config-for-item-multitask.patch create mode 100644 lophine-server/minecraft-patches/features/0048-Add-tick-regions-api.patch create mode 100644 lophine-server/minecraft-patches/features/0049-Add-missing-teleportation-event-APIs-for-folia.patch create mode 100644 lophine-server/minecraft-patches/features/0050-Add-Portal-rate-limiter.patch create mode 100644 lophine-server/minecraft-patches/features/0051-Add-config-for-waypoint-restoration.patch create mode 100644 lophine-server/minecraft-patches/features/0052-Leaf-Secure-seed-and-matter-seed-command.patch create mode 100644 lophine-server/minecraft-patches/features/0053-Add-secure-seed-V2-with-Blake3.patch create mode 100644 lophine-server/minecraft-patches/features/0054-Leaf-Replace-brain-maps-with-optimized-collection.patch create mode 100644 lophine-server/minecraft-patches/features/0055-Leaf-Remove-useless-creating-stats-json-bases-on-pla.patch create mode 100644 lophine-server/minecraft-patches/features/0056-Leaf-Optimize-PatchedDataComponentMap-equals.patch create mode 100644 lophine-server/minecraft-patches/features/0057-Leaf-Better-checking-for-useless-move-packets.patch create mode 100644 lophine-server/minecraft-patches/features/0058-Leaf-fast-bit-radix-sort.patch create mode 100644 lophine-server/minecraft-patches/features/0059-Leaf-Configurable-vanilla-username-check.patch create mode 100644 lophine-server/minecraft-patches/features/0060-Purpur-Lobotomize-stuck-villagers.patch create mode 100644 lophine-server/minecraft-patches/features/0061-Pufferfish-Reduce-projectile-chunk-loading.patch create mode 100644 lophine-server/minecraft-patches/features/0062-Pufferfish-Throttle-goal-selector-during-inactive-ti.patch create mode 100644 lophine-server/minecraft-patches/features/0063-Petal-Reduce-sensor-work.patch create mode 100644 lophine-server/minecraft-patches/features/0064-Leaves-Disable-packet-limit.patch create mode 100644 lophine-server/minecraft-patches/features/0065-Leaves-Vanilla-Hopper.patch create mode 100644 lophine-server/minecraft-patches/features/0066-Leaves-Lithium-Sleeping-Block-Entity.patch create mode 100644 lophine-server/minecraft-patches/features/0067-Leaves-Configurable-collision-behavior.patch create mode 100644 lophine-server/minecraft-patches/features/0068-Leaves-Disable-moved-wrongly-threshold.patch create mode 100644 lophine-server/minecraft-patches/features/0069-Leaves-Optimized-dragon-respawn.patch create mode 100644 lophine-server/minecraft-patches/features/0070-Gale-Variable-entity-wake-up-duration.patch create mode 100644 lophine-server/minecraft-patches/features/0071-Gale-Replace-AI-attributes-with-optimized-collection.patch create mode 100644 lophine-server/minecraft-patches/features/0072-Gale-Skip-entity-move-if-movement-is-zero.patch create mode 100644 lophine-server/minecraft-patches/features/0073-Gale-Store-mob-counts-in-an-array.patch create mode 100644 lophine-server/minecraft-patches/features/0074-Gale-Optimize-random-calls-in-chunk-ticking.patch create mode 100644 lophine-server/minecraft-patches/features/0075-Gale-Reduce-enderman-teleport-chunk-lookups.patch create mode 100644 lophine-server/minecraft-patches/features/0076-Gale-Cache-ShapePairKey-hash.patch create mode 100644 lophine-server/minecraft-patches/features/0077-Gale-For-collision-check-has-physics-before-same-veh.patch create mode 100644 lophine-server/minecraft-patches/features/0078-Gale-Skip-negligible-planar-movement-multiplication.patch create mode 100644 lophine-server/minecraft-patches/features/0079-Gale-Optimize-matching-item-checks.patch create mode 100644 lophine-server/minecraft-patches/features/0080-Gale-Reduce-lambda-and-Optional-allocation-in-Entity.patch create mode 100644 lophine-server/minecraft-patches/features/0081-Gale-Reduce-array-allocations.patch create mode 100644 lophine-server/minecraft-patches/features/0082-Some-optimizations-from-krypton.patch rename lophine-server/minecraft-patches/features/{0001-Server-I18n.patch => 0083-Server-I18n.patch} (100%) rename lophine-server/minecraft-patches/features/{0002-Fix-datapack-command-save-function.patch => 0084-Fix-datapack-command-save-function.patch} (96%) rename lophine-server/minecraft-patches/features/{0003-Purpur-Barrels-and-enderchests-6-rows.patch => 0085-Purpur-Barrels-and-enderchests-6-rows.patch} (100%) rename lophine-server/minecraft-patches/features/{0004-Add-config-to-disable-some-check-for-operators.patch => 0086-Add-config-to-disable-some-check-for-operators.patch} (96%) rename lophine-server/minecraft-patches/features/{0005-Add-config-to-enable-cross-region-damage-trace.patch => 0087-Add-config-to-enable-cross-region-damage-trace.patch} (100%) rename lophine-server/minecraft-patches/features/{0006-Add-config-to-enable-save-all-command.patch => 0088-Add-config-to-enable-save-all-command.patch} (98%) rename lophine-server/minecraft-patches/features/{0007-Add-config-to-enable-function-command.patch => 0089-Add-config-to-enable-function-command.patch} (100%) rename lophine-server/minecraft-patches/features/{0008-Add-config-to-enable-scoreboard-command.patch => 0090-Add-config-to-enable-scoreboard-command.patch} (100%) rename lophine-server/minecraft-patches/features/{0009-Add-config-to-enable-trigger-command.patch => 0091-Add-config-to-enable-trigger-command.patch} (100%) rename lophine-server/minecraft-patches/features/{0010-Add-config-to-enable-raytracing-tracker.patch => 0092-Add-config-to-enable-raytracing-tracker.patch} (96%) rename lophine-server/minecraft-patches/features/{0011-Add-tick-rate-support.patch => 0093-Add-tick-rate-support.patch} (100%) rename lophine-server/minecraft-patches/features/{0012-Modify-merge-ItemEntity-logic.patch => 0094-Modify-merge-ItemEntity-logic.patch} (100%) rename lophine-server/minecraft-patches/features/{0013-Old-zombie-reinforcement.patch => 0095-Old-zombie-reinforcement.patch} (100%) rename lophine-server/minecraft-patches/features/{0014-Old-leader-zombie-health-logic.patch => 0096-Old-leader-zombie-health-logic.patch} (100%) rename lophine-server/minecraft-patches/features/{0015-Spawn-invulnerable-time.patch => 0097-Spawn-invulnerable-time.patch} (100%) rename lophine-server/minecraft-patches/features/{0016-Leaves-Item-overstack-util.patch => 0098-Leaves-Item-overstack-util.patch} (98%) rename lophine-server/minecraft-patches/features/{0017-Leaves-Old-raid-behavior.patch => 0099-Leaves-Old-raid-behavior.patch} (100%) rename lophine-server/minecraft-patches/features/{0018-Compatibility-fix-for-Raid-Revert-and-Cross-Region-D.patch => 0100-Compatibility-fix-for-Raid-Revert-and-Cross-Region-D.patch} (100%) rename lophine-server/minecraft-patches/features/{0019-Leaves-Leaves-Base-Protocol-Core.patch => 0101-Leaves-Leaves-Base-Protocol-Core.patch} (96%) rename lophine-server/minecraft-patches/features/{0020-Leaves-Configurable-trading-with-the-void.patch => 0102-Leaves-Configurable-trading-with-the-void.patch} (95%) rename lophine-server/minecraft-patches/features/{0021-Leaves-Servux-Protocol.patch => 0103-Leaves-Servux-Protocol.patch} (91%) rename lophine-server/minecraft-patches/features/{0022-LeavesHooks.patch => 0104-LeavesHooks.patch} (100%) rename lophine-server/minecraft-patches/features/{0023-Leaves-Old-Explosion-Damage-Calculator.patch => 0105-Leaves-Old-Explosion-Damage-Calculator.patch} (100%) rename lophine-server/minecraft-patches/features/{0024-Leaves-Redstone-Shears-Wrench.patch => 0106-Leaves-Redstone-Shears-Wrench.patch} (100%) rename lophine-server/minecraft-patches/features/{0025-Leaves-Syncmatica-Protocol.patch => 0107-Leaves-Syncmatica-Protocol.patch} (94%) rename lophine-server/minecraft-patches/features/{0026-Leaves-BBOR-Protocol.patch => 0108-Leaves-BBOR-Protocol.patch} (100%) rename lophine-server/minecraft-patches/features/{0027-Leaves-Xaero-Map-Protocol.patch => 0109-Leaves-Xaero-Map-Protocol.patch} (100%) rename lophine-server/minecraft-patches/features/{0028-Leaves-Support-REI-protocol.patch => 0110-Leaves-Support-REI-protocol.patch} (100%) rename lophine-server/minecraft-patches/features/{0029-Leaves-Jade-Protocol.patch => 0111-Leaves-Jade-Protocol.patch} (98%) rename lophine-server/minecraft-patches/features/{0030-Leaves-Alternative-block-placement-Protocol.patch => 0112-Leaves-Alternative-block-placement-Protocol.patch} (97%) rename lophine-server/minecraft-patches/features/{0031-Leaves-Fakeplayer.patch => 0113-Leaves-Fakeplayer.patch} (97%) rename lophine-server/minecraft-patches/features/{0032-Leaves-Replay-Mod-API.patch => 0114-Leaves-Replay-Mod-API.patch} (98%) rename lophine-server/minecraft-patches/features/{0033-Leaves-Creative-fly-no-clip.patch => 0115-Leaves-Creative-fly-no-clip.patch} (100%) rename lophine-server/minecraft-patches/features/{0034-Leaves-Wool-Hopper-Counter.patch => 0116-Leaves-Wool-Hopper-Counter.patch} (100%) rename lophine-server/minecraft-patches/features/{0035-Global-Entities-Counter.patch => 0117-Global-Entities-Counter.patch} (97%) rename lophine-server/minecraft-patches/features/{0036-Leaves-Catch-update-suppression-crash.patch => 0118-Leaves-Catch-update-suppression-crash.patch} (97%) rename lophine-server/minecraft-patches/features/{0037-Leaves-CCE-update-suppression.patch => 0119-Leaves-CCE-update-suppression.patch} (100%) rename lophine-server/minecraft-patches/features/{0038-Leaves-Redstone-ignore-upwards-update.patch => 0120-Leaves-Redstone-ignore-upwards-update.patch} (100%) rename lophine-server/minecraft-patches/features/{0039-Instant-Block-Updater.patch => 0121-Instant-Block-Updater.patch} (96%) rename lophine-server/minecraft-patches/features/{0040-Revert-TrapDoorBlock-changes-form-Paper.patch => 0122-Revert-TrapDoorBlock-changes-form-Paper.patch} (100%) rename lophine-server/minecraft-patches/features/{0041-Leaves-Prevent-loss-of-item-drops-due-to-update-supp.patch => 0123-Leaves-Prevent-loss-of-item-drops-due-to-update-supp.patch} (100%) rename lophine-server/minecraft-patches/features/{0042-Leaves-Do-not-reset-placed-block-on-exception-Do-not.patch => 0124-Leaves-Do-not-reset-placed-block-on-exception-Do-not.patch} (100%) rename lophine-server/minecraft-patches/features/{0043-Leaves-Old-Block-remove-behaviour.patch => 0125-Leaves-Old-Block-remove-behaviour.patch} (100%) rename lophine-server/minecraft-patches/features/{0044-MiniTweaks-mob-fire-and-explosion-rules.patch => 0126-MiniTweaks-mob-fire-and-explosion-rules.patch} (100%) rename lophine-server/minecraft-patches/features/{0045-Carpet-features.patch => 0127-Carpet-features.patch} (99%) rename lophine-server/minecraft-patches/features/{0046-Carpet-Features-Compatible.patch => 0128-Carpet-Features-Compatible.patch} (100%) rename lophine-server/minecraft-patches/features/{0047-PCA-sync-protocol.patch => 0129-PCA-sync-protocol.patch} (100%) rename lophine-server/minecraft-patches/features/{0048-Add-Vanilla-like-experience-Config.patch => 0130-Add-Vanilla-like-experience-Config.patch} (100%) rename lophine-server/minecraft-patches/features/{0049-Restore-vanilla-ender-pearl-loading.patch => 0131-Restore-vanilla-ender-pearl-loading.patch} (100%) create mode 100644 lophine-server/paper-patches/features/0001-Rebrand-to-Luminol.patch create mode 100644 lophine-server/paper-patches/features/0002-Luminol-Config-System.patch create mode 100644 lophine-server/paper-patches/features/0003-Correct-thread-unsafe-random-sources.patch create mode 100644 lophine-server/paper-patches/features/0004-Force-disable-builtin-spark-plugin.patch create mode 100644 lophine-server/paper-patches/features/0005-World-load-unload-APIs.patch create mode 100644 lophine-server/paper-patches/features/0006-Add-config-for-server-mod-name.patch create mode 100644 lophine-server/paper-patches/features/0007-Add-config-to-disable-async-catchers.patch create mode 100644 lophine-server/paper-patches/features/0008-Add-config-to-avoid-of-folia-supported-check.patch create mode 100644 lophine-server/paper-patches/features/0009-Add-tpsbar-with-chunkhot-membar-and-region-bar.patch create mode 100644 lophine-server/paper-patches/features/0010-Add-tick-regions-api.patch create mode 100644 lophine-server/paper-patches/features/0011-Add-back-BlockPos-transform-to-Position-support.patch create mode 100644 lophine-server/paper-patches/features/0012-Add-config-for-cpu-affinity.patch create mode 100644 lophine-server/paper-patches/features/0013-Leaf-Skip-event-if-no-listeners.patch create mode 100644 lophine-server/paper-patches/features/0014-Leaf-Secure-seed-and-matter-seed-command.patch create mode 100644 lophine-server/paper-patches/features/0015-Leaf-Configurable-vanilla-username-check.patch create mode 100644 lophine-server/paper-patches/features/0016-Leaves-Leaves-plugin.patch create mode 100644 lophine-server/paper-patches/features/0017-Purpur-Lobotomize-stuck-villagers.patch rename lophine-server/paper-patches/features/{0001-Rebrand-to-Lophine.patch => 0018-Rebrand-to-Lophine.patch} (100%) rename lophine-server/paper-patches/features/{0002-Purpur-Barrels-and-enderchests-6-rows.patch => 0019-Purpur-Barrels-and-enderchests-6-rows.patch} (100%) rename lophine-server/paper-patches/features/{0003-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch => 0020-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch} (100%) rename lophine-server/paper-patches/features/{0004-Leaves-Leaves-Base-Protocol-Core.patch => 0021-Leaves-Leaves-Base-Protocol-Core.patch} (100%) rename lophine-server/paper-patches/features/{0005-LeavesHooks.patch => 0022-LeavesHooks.patch} (100%) rename lophine-server/paper-patches/features/{0006-Leaves-Leaves-Fakeplayer.patch => 0023-Leaves-Leaves-Fakeplayer.patch} (96%) rename lophine-server/paper-patches/features/{0007-Leaves-Replay-Mod-API.patch => 0024-Leaves-Replay-Mod-API.patch} (96%) create mode 100644 lophine-server/src/main/java/abomination/IRegionFile.java create mode 100644 lophine-server/src/main/java/abomination/LinearRegionFile.java create mode 100644 lophine-server/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulableTickHack.java create mode 100644 lophine-server/src/main/java/com/kiocg/ChunkHot.java create mode 100644 lophine-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityLimits.java create mode 100644 lophine-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityThrottler.java create mode 100644 lophine-server/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java create mode 100644 lophine-server/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/api/impl/RegionStatsImpl.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/api/impl/ThreadedRegionImpl.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/api/impl/ThreadedRegionizerImpl.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/api/impl/TickRegionDataImpl.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/CommandRegister.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/bar/BarCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/bar/BarSubcommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/bar/sub/ConfigEditCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/bar/sub/ToggleCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/ConfigCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/ConfigSubcommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/CleanCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/OpenGuiCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ReloadCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ResetCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ResetCommentsCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/SetCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/SubmitCommand.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/ConfigManager.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/ConfigsInstance.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/ConfigsInstanceBuilder.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/DefaultTransformLogic.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/IConfigModule.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/IllegalFormatConversionExceptionWithOrigin.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/flags/CommandSuggestions.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/flags/ConfigClassInfo.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/flags/ConfigInfo.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/flags/DoNotLoad.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/flags/HotReloadUnsupported.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/flags/TransformedConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/DisableAsyncCatcherConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/DisableEntityCatchConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/CollisionBehaviorConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/FoliaEntityMovingFixConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/ForceCleanupEntityBrainMemoryConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/ItemMultitaskConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/LongCommandSupportConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/POIRangeFixes.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/PathfindingFixesConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/PreventIncorrectTeleportAsyncConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/UnsafeTeleportationConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/VanillaRandomSourceConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/function/MembarConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/function/PortalRateLimiterConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionBarConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionFormatConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/function/SecureSeedConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/function/TpsBarConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/function/TripwireBehaviorConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/AutoUpdateConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/DisableWarningConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/FoliaWatchogConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/InorderChatConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/PaperPacketLimiterConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/PublickeyVerifyConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/SavePortalTicketsConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/SentryConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/UsernameCheckConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/AsyncProtocolChangeConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/CpuAffinityConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/EntityGoalSelectorInactiveTickConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/GaleVariableEntityWakeupConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/KaiijuEntityLimiterConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/LeavesSleepingBlockEntityConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/LobotomizeVillageConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/PetalReduceSensorWorkConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/ProjectileChunkReduceConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/SIMDConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/removed/RemovedConfig.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/config/modules/unsupported/DisableCheckForFoliaSupported.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/data/BufferedLinearRegionFile.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/enums/EnumBarType.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/enums/EnumCollisionBehaviorMode.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/enums/EnumConfigCategory.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/enums/EnumRegionFormat.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/enums/EnumStatusBarDisplay.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/enums/EnumTripwireBehavior.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/functions/bars/TickableStatusBar.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/functions/bars/TickableStatusBarList.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/Membar.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/RegionBar.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/TpsBar.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/AffinityRunnableWrapper.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/AutoUpdateHelper.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/BufferedLinearRegionFileFlusher.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/ClassLoadUtil.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/EntityMoveOutOfRegionException.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/FoliaServerWaypointManager.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/IRegionCreateFunction.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/NullPlugin.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/RateThrottler.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/RegionCreatorInfo.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/TeleportRecord.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/dialog/ConfigCommandDialog.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/dialog/ConfigDialogUtil.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/dialog/DialogUtil.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/thread/LevelAwareRegionScheduler.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/thread/LevelSubRegionShutdownThread.java create mode 100644 lophine-server/src/main/java/me/earthme/luminol/utils/thread/SimpleReferenceRWLock.java create mode 100644 lophine-server/src/main/java/me/titaniumtown/ArrayConstants.java create mode 100644 lophine-server/src/main/java/org/dreeam/leaf/util/FastBitRadixSort.java create mode 100644 lophine-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/command/ArgumentNode.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/command/CommandContext.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/command/CommandNode.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/command/CommandUtils.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/command/LiteralNode.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/command/RootNode.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/command/WrappedArgument.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/FeaturesConfiguration.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/MixinConfiguration.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java create mode 100644 lophine-server/src/main/java/su/plo/matter/Globals.java create mode 100644 lophine-server/src/main/java/su/plo/matter/Hashing.java create mode 100644 lophine-server/src/main/java/su/plo/matter/HashingV2.java create mode 100644 lophine-server/src/main/java/su/plo/matter/WorldgenCryptoRandom.java diff --git a/build.gradle.kts b/build.gradle.kts index a76139d..571bffc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,19 +8,19 @@ plugins { paperweight { filterPatches = false - upstreams.register("luminol") { - repo = github("LuminolCustomArchive", "Luminol") - ref = providers.gradleProperty("luminolRef") + upstreams.register("folia") { + repo = github("PaperMC", "Folia") + ref = providers.gradleProperty("foliaRef") println("Upstream commit ref: " + ref.get()) patchFile { - path = "luminol-server/build.gradle.kts" + path = "folia-server/build.gradle.kts" outputFile = file("lophine-server/build.gradle.kts") patchFile = file("lophine-server/build.gradle.kts.patch") } patchFile { - path = "luminol-api/build.gradle.kts" + path = "folia-api/build.gradle.kts" outputFile = file("lophine-api/build.gradle.kts") patchFile = file("lophine-api/build.gradle.kts.patch") } @@ -29,17 +29,12 @@ paperweight { patchesDir = file("lophine-api/paper-patches") outputDir = file("paper-api") } - patchRepo("foliaApi") { + patchDir("foliaApi") { upstreamPath = "folia-api" + excludes = listOf("build.gradle.kts", "build.gradle.kts.patch", "paper-patches") patchesDir = file("lophine-api/folia-patches") outputDir = file("folia-api") } - patchDir("luminolApi") { - upstreamPath = "luminol-api" - excludes = listOf("build.gradle.kts", "build.gradle.kts.patch", "paper-patches") - patchesDir = file("lophine-api/luminol-patches") - outputDir = file("luminol-api") - } } } diff --git a/gradle.properties b/gradle.properties index e39365d..fd5d084 100644 --- a/gradle.properties +++ b/gradle.properties @@ -5,11 +5,11 @@ channel=STABLE clipVersion=3.0.18 weightVersion=2.0.15 # true for release, false for skip release, pre for pre-release -release=pre +release=false # true for push to repo, false for skip push repo, auto for detect by release value pushRepo=auto -luminolRef=bd964477d5dfea9a2349541c1a75c5afbebe48db +foliaRef=602048cb815db2ded68cca8cd43f480b983185a1 org.gradle.configuration-cache=true org.gradle.caching=true diff --git a/lophine-api/build.gradle.kts.patch b/lophine-api/build.gradle.kts.patch index e27e7dc..72805e4 100644 --- a/lophine-api/build.gradle.kts.patch +++ b/lophine-api/build.gradle.kts.patch @@ -1,27 +1,52 @@ ---- a/luminol-api/build.gradle.kts -+++ b/luminol-api/build.gradle.kts -@@ -107,20 +_,24 @@ +--- a/folia-api/build.gradle.kts ++++ b/folia-api/build.gradle.kts +@@ -53,6 +_,7 @@ + api("org.apache.logging.log4j:log4j-api:$log4jVersion") + api("org.slf4j:slf4j-api:$slf4jVersion") + api("com.mojang:brigadier:1.3.10") ++ api("io.sentry:sentry:8.0.0-rc.2") // Pufferfish + + // Deprecate bungeecord-chat in favor of adventure + api("net.md-5:bungeecord-chat:$bungeeCordChatVersion") { +@@ -105,17 +_,21 @@ + java { srcDir(generatedDir) srcDir(file("../paper-api/src/main/java")) - srcDir(file("../folia-api/src/main/java")) -+ srcDir(file("../luminol-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")) -+ srcDir(file("../luminol-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")) -+ srcDir(file("../luminol-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")) -+ srcDir(file("../luminol-api/src/test/resources")) ++ srcDir(file("../folia-api/src/test/resources")) } } } +@@ -259,3 +_,19 @@ + tasks.check { + dependsOn(scanJarForBadCalls) + } ++ ++// Pufferfish Start ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ compilerArgs.add("--add-modules=jdk.incubator.vector") ++} ++// Pufferfish End ++ ++// Lophine start - Hide unnecessary warnings ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ compilerArgs.add("-Xlint:-module") ++ compilerArgs.add("-Xlint:-removal") ++ compilerArgs.add("-Xlint:-dep-ann") ++} ++// Lophine end diff --git a/lophine-api/paper-patches/features/0001-Rebrand-to-Luminol.patch b/lophine-api/paper-patches/features/0001-Rebrand-to-Luminol.patch new file mode 100644 index 0000000..7a81093 --- /dev/null +++ b/lophine-api/paper-patches/features/0001-Rebrand-to-Luminol.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sun, 12 Jan 2025 10:18:38 +0800 +Subject: [PATCH] Rebrand to Luminol + + +diff --git a/src/main/java/io/papermc/paper/ServerBuildInfo.java b/src/main/java/io/papermc/paper/ServerBuildInfo.java +index 652ff54e7c50412503725d628bfe72ed03059790..db5e2f10278c20286779362cfdfadb2f42c724e0 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"); + ++ // Luminol start ++ /** ++ * The brand id for folia ++ */ ++ Key BRAND_FOLIA_ID = Key.key("papermc", "folia"); ++ /** ++ * The brand id for Luminol. ++ */ ++ Key BRAND_LUMINOL_ID = Key.key("luminolmc", "luminol"); ++ // Luminol end ++ + /** + * Gets the {@code ServerBuildInfo}. + * diff --git a/lophine-api/paper-patches/features/0002-Luminol-Config-System.patch b/lophine-api/paper-patches/features/0002-Luminol-Config-System.patch new file mode 100644 index 0000000..f637999 --- /dev/null +++ b/lophine-api/paper-patches/features/0002-Luminol-Config-System.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 14 Jun 2026 18:10:20 +0800 +Subject: [PATCH] Luminol Config System + + +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index fb5ca3ab0ddba433a39e65572beeb955b50d6f72..42335e351e0cb4f82f46501503a15349f8a82e1b 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -2776,6 +2776,11 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + */ + void allowPausing(@NotNull org.bukkit.plugin.Plugin plugin, boolean value); + // Paper end - API to check if the server is sleeping ++ ++ // Luminol start - Luminol Config API ++ me.earthme.luminol.api.config.LuminolConfigsInstance getLuminolConfigs(String name); ++ // Luminol end - Luminol Config API ++ + // Folia start - region TPS API + /** + * Gets the TPS from the region which owns the specified location, or {@code null} if no region owns diff --git a/lophine-api/paper-patches/features/0003-KioCG-Chunk-API.patch b/lophine-api/paper-patches/features/0003-KioCG-Chunk-API.patch new file mode 100644 index 0000000..f758d36 --- /dev/null +++ b/lophine-api/paper-patches/features/0003-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 7a719d31fbc7633a9bb87ea97338190ee3a6047a..b79b9a2ad108c0488d3cc3862f72985059bb4988 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/lophine-api/paper-patches/features/0004-Disable-timings-warn-msg-and-commands.patch b/lophine-api/paper-patches/features/0004-Disable-timings-warn-msg-and-commands.patch new file mode 100644 index 0000000..f8c0927 --- /dev/null +++ b/lophine-api/paper-patches/features/0004-Disable-timings-warn-msg-and-commands.patch @@ -0,0 +1,45 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sun, 12 Jan 2025 12:22:53 +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..b862779b48176dd7e67a2f1a3e7f24bc8c84e993 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())); // Luminol - 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..22edf161ba1ca8c31140e25e8c4fe1503c44dba8 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")); // Luminol - 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 457e75bbecb70ac3da4c21382f97d68de5a00547..cf22ea227f6dcc13702fb5f6f94bba5b6e326b97 100644 +--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java ++++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +@@ -960,7 +960,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 // Luminol - Disable timings warn msg and commands + } + + // Paper start diff --git a/lophine-api/paper-patches/features/0005-Force-disable-reload-command.patch b/lophine-api/paper-patches/features/0005-Force-disable-reload-command.patch new file mode 100644 index 0000000..8c76786 --- /dev/null +++ b/lophine-api/paper-patches/features/0005-Force-disable-reload-command.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 23 Jan 2025 12:04:04 +0800 +Subject: [PATCH] Force disable reload command + + +diff --git a/src/main/java/org/bukkit/command/SimpleCommandMap.java b/src/main/java/org/bukkit/command/SimpleCommandMap.java +index 22edf161ba1ca8c31140e25e8c4fe1503c44dba8..50384eb221ad96e766b91335ff57b082410885e9 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"); // Luminol - Force disable reload command ++ //this.knownCommands.put("bukkit:reload", reload); // Luminol - Force disable reload command ++ //this.knownCommands.put("bukkit:rl", reload); // Luminol - Force disable reload command + //register("bukkit", new co.aikar.timings.TimingsCommand("timings")); // Luminol - Disable timings warn msg and commands + } + diff --git a/lophine-api/paper-patches/features/0006-Pufferfish-Sentry.patch b/lophine-api/paper-patches/features/0006-Pufferfish-Sentry.patch new file mode 100644 index 0000000..b0cf13f --- /dev/null +++ b/lophine-api/paper-patches/features/0006-Pufferfish-Sentry.patch @@ -0,0 +1,62 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sun, 12 Jan 2025 13:27:38 +0800 +Subject: [PATCH] Pufferfish: Sentry + +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/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +index cf22ea227f6dcc13702fb5f6f94bba5b6e326b97..71ef6055df30506bb0d6648a2300d1cab7b0d026 100644 +--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java ++++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +@@ -596,7 +596,9 @@ public final class SimplePluginManager implements PluginManager { + + // Paper start + private void handlePluginException(String msg, Throwable ex, Plugin plugin) { ++ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish + server.getLogger().log(Level.SEVERE, msg, ex); ++ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish + callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerPluginEnableDisableException(msg, ex, plugin))); + } + // Paper end +@@ -666,9 +668,11 @@ public final class SimplePluginManager implements PluginManager { + )); + } + } catch (Throwable ex) { ++ gg.pufferfish.pufferfish.sentry.SentryContext.setEventContext(event, registration); // Pufferfish + // Paper start - error reporting + String msg = "Could not pass event " + event.getEventName() + " to " + registration.getPlugin().getDescription().getFullName(); + server.getLogger().log(Level.SEVERE, msg, ex); ++ gg.pufferfish.pufferfish.sentry.SentryContext.removeEventContext(); // Pufferfish + if (!(event instanceof com.destroystokyo.paper.event.server.ServerExceptionEvent)) { // We don't want to cause an endless event loop + callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerEventException(msg, ex, registration.getPlugin(), registration.getListener(), event))); + } +diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +index 9023a39c6481710478747482f0b16c21274afd15..0755065f86aba3ef5ecdf045620878eb045f4999 100644 +--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +@@ -329,7 +329,13 @@ public final class JavaPluginLoader implements PluginLoader { + try { + jPlugin.setEnabled(true); + } catch (Throwable ex) { ++ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish + server.getLogger().log(Level.SEVERE, "Error occurred while enabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); ++ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish ++ // Paper start - Disable plugins that fail to load ++ this.server.getPluginManager().disablePlugin(jPlugin); ++ return; ++ // Paper end + } + + // Perhaps abort here, rather than continue going, but as it stands, +@@ -354,7 +360,9 @@ public final class JavaPluginLoader implements PluginLoader { + try { + jPlugin.setEnabled(false); + } catch (Throwable ex) { ++ gg.pufferfish.pufferfish.sentry.SentryContext.setPluginContext(plugin); // Pufferfish + server.getLogger().log(Level.SEVERE, "Error occurred while disabling " + plugin.getDescription().getFullName() + " (Is it up to date?)", ex); ++ gg.pufferfish.pufferfish.sentry.SentryContext.removePluginContext(); // Pufferfish + } + + if (cloader instanceof PluginClassLoader) { diff --git a/lophine-api/paper-patches/features/0007-Pufferfish-SIMD-Utilities.patch b/lophine-api/paper-patches/features/0007-Pufferfish-SIMD-Utilities.patch new file mode 100644 index 0000000..fc2129f --- /dev/null +++ b/lophine-api/paper-patches/features/0007-Pufferfish-SIMD-Utilities.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sun, 12 Jan 2025 14:00:28 +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..fc9cd106b1ed2e92aced783e9cb0bc8867828df3 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 = { // Luminol - 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) { // Luminol - 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); // Luminol - Pufferfish - vectorized map color conversion + return result; + } + diff --git a/lophine-api/paper-patches/features/0008-Tick-regions-api.patch b/lophine-api/paper-patches/features/0008-Tick-regions-api.patch new file mode 100644 index 0000000..3f80e3d --- /dev/null +++ b/lophine-api/paper-patches/features/0008-Tick-regions-api.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Mon, 27 Jan 2025 13:01:59 +0800 +Subject: [PATCH] Tick regions api + + +diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java +index c66cd17fd037b73d8ff04dd2ed328ca25e8b0f1a..b86cd3b37ec3fd63f34acd6bf17f6a91c24b5038 100644 +--- a/src/main/java/org/bukkit/World.java ++++ b/src/main/java/org/bukkit/World.java +@@ -4708,4 +4708,8 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient + } + } + } ++ ++ // Luminol start - Tick regions api ++ me.earthme.luminol.api.ThreadedRegionizer getThreadedRegionizer(); ++ // Luminol end - Tick regions api + } diff --git a/lophine-api/paper-patches/features/0009-Leaves-Leaves-plugin.patch b/lophine-api/paper-patches/features/0009-Leaves-Leaves-plugin.patch new file mode 100644 index 0000000..d628016 --- /dev/null +++ b/lophine-api/paper-patches/features/0009-Leaves-Leaves-plugin.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Wed, 22 Apr 2026 12:59:08 +0800 +Subject: [PATCH] Leaves: Leaves plugin + +Co-authored by: MC_XiaoHei +As part of: Leaves (https://github.com/HyacinthHaru/Leaves/blob/90080d238e2fcfc14a9293150dc351a4f0f696e6/leaves-api/paper-patches/features/0009-Leaves-Plugin.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java +index 8a24b84db656da4cec59e9bccac306467c8155b0..0ac1e049fad85de8365380b03e60d915ee643f16 100644 +--- a/src/main/java/org/bukkit/Bukkit.java ++++ b/src/main/java/org/bukkit/Bukkit.java +@@ -3046,4 +3046,10 @@ public final class Bukkit { + public static void restart() { + server.restart(); + } ++ ++ // Leaves start - Feature API ++ public static org.leavesmc.leaves.plugin.FeatureManager getFeatureManager() { ++ return server.getFeatureManager(); ++ } ++ // Leaves end - Feature API + } +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index 42335e351e0cb4f82f46501503a15349f8a82e1b..cd5f47f6d7fa15c1533724673b8bc2c75394795b 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -2781,6 +2781,10 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + me.earthme.luminol.api.config.LuminolConfigsInstance getLuminolConfigs(String name); + // Luminol end - Luminol Config API + ++ // Leaves start - Feature API ++ org.leavesmc.leaves.plugin.FeatureManager getFeatureManager(); ++ // Leaves End - Feature API ++ + // Folia start - region TPS API + /** + * Gets the TPS from the region which owns the specified location, or {@code null} if no region owns diff --git a/lophine-api/paper-patches/features/0010-Leaves-Fix-SculkCatalyst-exp-skip.patch b/lophine-api/paper-patches/features/0010-Leaves-Fix-SculkCatalyst-exp-skip.patch new file mode 100644 index 0000000..61c952b --- /dev/null +++ b/lophine-api/paper-patches/features/0010-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/lophine-api/paper-patches/features/0011-Purpur-Lobotomize-stuck-villagers.patch b/lophine-api/paper-patches/features/0011-Purpur-Lobotomize-stuck-villagers.patch new file mode 100644 index 0000000..1683e9e --- /dev/null +++ b/lophine-api/paper-patches/features/0011-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/lophine-api/paper-patches/features/0012-World-load-unload-APIs.patch b/lophine-api/paper-patches/features/0012-World-load-unload-APIs.patch new file mode 100644 index 0000000..89679b5 --- /dev/null +++ b/lophine-api/paper-patches/features/0012-World-load-unload-APIs.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 5 Jun 2026 09:12:20 +0800 +Subject: [PATCH] World load/unload APIs + + +diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java +index cd5f47f6d7fa15c1533724673b8bc2c75394795b..48e53223ba7edeca5256244480441a56a6657075 100644 +--- a/src/main/java/org/bukkit/Server.java ++++ b/src/main/java/org/bukkit/Server.java +@@ -775,6 +775,17 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi + * @throws IllegalStateException when {@link #isTickingWorlds() isTickingWorlds} is true + */ + public boolean unloadWorld(@NotNull World world, boolean save); ++ // Luminol start - Async world unloading api ++ ++ /** ++ * Unloads the given world asynchronously ++ * ++ * @param world the world to unload ++ * @param save whether to save the chunks before unloading ++ * @param callback the callback to accept the result, true if successful, false otherwise ++ */ ++ public void unloadWorld(World world, boolean save, java.util.concurrent.CompletableFuture callback); ++ // Luminol end + + /** + * Gets the world that players respawn in. diff --git a/lophine-api/paper-patches/features/0001-Rebrand-to-Lophine.patch b/lophine-api/paper-patches/features/0013-Rebrand-to-Lophine.patch similarity index 100% rename from lophine-api/paper-patches/features/0001-Rebrand-to-Lophine.patch rename to lophine-api/paper-patches/features/0013-Rebrand-to-Lophine.patch diff --git a/lophine-api/paper-patches/features/0002-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch b/lophine-api/paper-patches/features/0014-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch similarity index 100% rename from lophine-api/paper-patches/features/0002-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch rename to lophine-api/paper-patches/features/0014-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch diff --git a/lophine-api/paper-patches/features/0003-Leaves-Add-fakeplayer-api.patch b/lophine-api/paper-patches/features/0015-Leaves-Add-fakeplayer-api.patch similarity index 100% rename from lophine-api/paper-patches/features/0003-Leaves-Add-fakeplayer-api.patch rename to lophine-api/paper-patches/features/0015-Leaves-Add-fakeplayer-api.patch diff --git a/lophine-api/paper-patches/features/0004-Leaves-Replay-Mod-API.patch b/lophine-api/paper-patches/features/0016-Leaves-Replay-Mod-API.patch similarity index 100% rename from lophine-api/paper-patches/features/0004-Leaves-Replay-Mod-API.patch rename to lophine-api/paper-patches/features/0016-Leaves-Replay-Mod-API.patch diff --git a/lophine-api/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java b/lophine-api/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java new file mode 100644 index 0000000..5216827 --- /dev/null +++ b/lophine-api/src/main/java/gg/pufferfish/pufferfish/sentry/SentryContext.java @@ -0,0 +1,181 @@ +/* + * 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.sentry; + +import com.google.gson.Gson; +import org.apache.logging.log4j.ThreadContext; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.player.PlayerEvent; +import org.bukkit.plugin.Plugin; +import org.bukkit.plugin.RegisteredListener; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.TreeMap; + +public class SentryContext { + + private static final Gson GSON = new Gson(); + + public static void setPluginContext(@Nullable Plugin plugin) { + if (plugin != null) { + ThreadContext.put("pufferfishsentry_pluginname", plugin.getName()); + ThreadContext.put("pufferfishsentry_pluginversion", plugin.getPluginMeta().getVersion()); + } + } + + public static void removePluginContext() { + ThreadContext.remove("pufferfishsentry_pluginname"); + ThreadContext.remove("pufferfishsentry_pluginversion"); + } + + public static void setSenderContext(@Nullable CommandSender sender) { + if (sender != null) { + ThreadContext.put("pufferfishsentry_playername", sender.getName()); + if (sender instanceof Player player) { + ThreadContext.put("pufferfishsentry_playerid", player.getUniqueId().toString()); + } + } + } + + public static void removeSenderContext() { + ThreadContext.remove("pufferfishsentry_playername"); + ThreadContext.remove("pufferfishsentry_playerid"); + } + + public static void setEventContext(Event event, RegisteredListener registration) { + setPluginContext(registration.getPlugin()); + + try { + // Find the player that was involved with this event + Player player = null; + if (event instanceof PlayerEvent) { + player = ((PlayerEvent) event).getPlayer(); + } else { + Class eventClass = event.getClass(); + + Field playerField = null; + + for (Field field : eventClass.getDeclaredFields()) { + if (field.getType().equals(Player.class)) { + playerField = field; + break; + } + } + + if (playerField != null) { + playerField.setAccessible(true); + player = (Player) playerField.get(event); + } + } + + if (player != null) { + setSenderContext(player); + } + } catch (Exception ignored) { + } // We can't really safely log exceptions. + + ThreadContext.put("pufferfishsentry_eventdata", GSON.toJson(serializeFields(event))); + } + + public static void removeEventContext() { + removePluginContext(); + removeSenderContext(); + ThreadContext.remove("pufferfishsentry_eventdata"); + } + + private static Map serializeFields(Object object) { + Map fields = new TreeMap<>(); + fields.put("_class", object.getClass().getName()); + for (Field declaredField : object.getClass().getDeclaredFields()) { + try { + if (Modifier.isStatic(declaredField.getModifiers())) { + continue; + } + + String fieldName = declaredField.getName(); + if (fieldName.equals("handlers")) { + continue; + } + declaredField.setAccessible(true); + Object value = declaredField.get(object); + if (value != null) { + fields.put(fieldName, value.toString()); + } else { + fields.put(fieldName, ""); + } + } catch (Exception ignored) { + } // We can't really safely log exceptions. + } + return fields; + } + + public static class State { + + private Plugin plugin; + private Command command; + private String commandLine; + private Event event; + private RegisteredListener registeredListener; + + public Plugin getPlugin() { + return plugin; + } + + public void setPlugin(Plugin plugin) { + this.plugin = plugin; + } + + public Command getCommand() { + return command; + } + + public void setCommand(Command command) { + this.command = command; + } + + public String getCommandLine() { + return commandLine; + } + + public void setCommandLine(String commandLine) { + this.commandLine = commandLine; + } + + public Event getEvent() { + return event; + } + + public void setEvent(Event event) { + this.event = event; + } + + public RegisteredListener getRegisteredListener() { + return registeredListener; + } + + public void setRegisteredListener(RegisteredListener registeredListener) { + this.registeredListener = registeredListener; + } + } +} diff --git a/lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java b/lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDChecker.java new file mode 100644 index 0000000..6a5c6ef --- /dev/null +++ b/lophine-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/lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java b/lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/SIMDDetection.java new file mode 100644 index 0000000..aaa7799 --- /dev/null +++ b/lophine-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/lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java b/lophine-api/src/main/java/gg/pufferfish/pufferfish/simd/VectorMapPalette.java new file mode 100644 index 0000000..ebb51f0 --- /dev/null +++ b/lophine-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/lophine-api/src/main/java/me/earthme/luminol/api/RegionStats.java b/lophine-api/src/main/java/me/earthme/luminol/api/RegionStats.java new file mode 100644 index 0000000..bdd1be8 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/RegionStats.java @@ -0,0 +1,28 @@ +package me.earthme.luminol.api; + +/** + * A simple package of folia's tick region state.It linked to the RegionStats of the nms part so that
+ * You could call these methods to get the status of this tick region
+ */ +public interface RegionStats { + /** + * Get the entity count in this tick region + * + * @return the entity count + */ + int getEntityCount(); + + /** + * Get the player count in this tick region + * + * @return the player count + */ + int getPlayerCount(); + + /** + * Get the chunk count in this tick region + * + * @return the chunk count + */ + int getChunkCount(); +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/ThreadedRegion.java b/lophine-api/src/main/java/me/earthme/luminol/api/ThreadedRegion.java new file mode 100644 index 0000000..29320b4 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/ThreadedRegion.java @@ -0,0 +1,61 @@ +package me.earthme.luminol.api; + +import org.bukkit.Location; +import org.bukkit.World; + +import org.jetbrains.annotations.Nullable; + +/** + * A mirror of folia's ThreadedRegion
+ * Including some handy methods to get the information of the tick region
+ * Note: You should call these methods inside this tick region's thread context + */ +public interface ThreadedRegion { + /** + * Get the center chunk pos of this tick region
+ * Note:
+ * 1.Global region will return a null value(But we don't finish the global region yet()
+ * 2.You should call these methods inside this tick region's thread context + * + * @return The center chunk pos + */ + @Nullable + Location getCenterChunkPos(); + + /** + * Get the dead section percent of this tick region + * Note:
+ * 1.Dead percent is mean the percent of the unloaded chunk count of this tick region, which is also used for determine + * that the tick region should or not check for splitting
+ * 2.You should call these methods inside this tick region's thread context + * + * @return The dead section percent + */ + double getDeadSectionPercent(); + + /** + * Get the tick region data of this tick region
+ * Note:
+ * 1.You should call this method inside this tick region's thread context
+ * 2.You should call these methods inside this tick region's thread context + * + * @return The tick region data + */ + TickRegionData getTickRegionData(); + + /** + * Get the world of this tick region
+ * Note: Global region will return a null value too + * + * @return The world of this tick region + */ + @Nullable + World getWorld(); + + /** + * Get the id of the tick region
+ * + * @return The id of the tick region + */ + long getId(); +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/ThreadedRegionizer.java b/lophine-api/src/main/java/me/earthme/luminol/api/ThreadedRegionizer.java new file mode 100644 index 0000000..4f06734 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/ThreadedRegionizer.java @@ -0,0 +1,61 @@ +package me.earthme.luminol.api; + +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; + +/** + * A mirror of folia's ThreadedRegionizer + */ +public interface ThreadedRegionizer { + /** + * Get all the tick regions + * + * @return Temporary copied collection of all tick regions + */ + Collection getAllRegions(); + + /** + * Get the tick region at the given chunk coordinates + * + * @param chunkX Chunk X + * @param chunkZ Chunk Z + * @return The tick region at the given chunk coordinates + */ + @Nullable + ThreadedRegion getAtSynchronized(int chunkX, int chunkZ); + + /** + * Get the tick region at the given chunk coordinates + * + * @param chunkX Chunk X + * @param chunkZ Chunk Z + * @return The tick region at the given chunk coordinates + */ + @Nullable + ThreadedRegion getAtUnSynchronized(int chunkX, int chunkZ); + + /** + * Get the tick region at the given location + * + * @param pos The location + * @return The tick region at the given location + */ + @Nullable + default ThreadedRegion getAtSynchronized(@NotNull Location pos) { + return this.getAtSynchronized(pos.getBlockX() >> 4, pos.getBlockZ() >> 4); + } + + /** + * Get the tick region at the given location + * + * @param pos The location + * @return The tick region at the given location + */ + @Nullable + default ThreadedRegion getAtUnSynchronized(@NotNull Location pos) { + return this.getAtUnSynchronized(pos.getBlockX() >> 4, pos.getBlockZ() >> 4); + } +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/TickRegionData.java b/lophine-api/src/main/java/me/earthme/luminol/api/TickRegionData.java new file mode 100644 index 0000000..cca3768 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/TickRegionData.java @@ -0,0 +1,29 @@ +package me.earthme.luminol.api; + +import org.bukkit.World; + +/** + * A mirror of folia's tick region data + */ +public interface TickRegionData { + /** + * Get the world it's currently holding + * + * @return the world + */ + World getWorld(); + + /** + * Get the current tick count + * + * @return the current tick count + */ + long getCurrentTickCount(); + + /** + * Get the region stats + * + * @return the region stats + */ + RegionStats getRegionStats(); +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/config/ConfigDataPair.java b/lophine-api/src/main/java/me/earthme/luminol/api/config/ConfigDataPair.java new file mode 100644 index 0000000..1dcab2e --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/config/ConfigDataPair.java @@ -0,0 +1,11 @@ +package me.earthme.luminol.api.config; + +import org.jspecify.annotations.Nullable; + +public record ConfigDataPair( + String key, + Object value, + @Nullable String comment, + @Nullable String[] suggestions +) { +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/config/LuminolConfigBuilder.java b/lophine-api/src/main/java/me/earthme/luminol/api/config/LuminolConfigBuilder.java new file mode 100644 index 0000000..c8fd1ae --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/config/LuminolConfigBuilder.java @@ -0,0 +1,82 @@ +package me.earthme.luminol.api.config; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +/** + * Builder interface for creating {@link LuminolConfigsInstance} 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 LuminolConfigBuilder { + /** + * 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 LuminolConfigsInstance} + */ + LuminolConfigsInstance 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 LuminolConfigsInstance} + */ + LuminolConfigsInstance 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 LuminolConfigsInstance} + */ + LuminolConfigsInstance 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 LuminolConfigsInstance} + */ + LuminolConfigsInstance of( + @NotNull ClassLoader loader, + @NotNull File base, + @NotNull String name, + @NotNull String file_name, + @NotNull String command_name, + @NotNull String pack + ); +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/config/LuminolConfigsInstance.java b/lophine-api/src/main/java/me/earthme/luminol/api/config/LuminolConfigsInstance.java new file mode 100644 index 0000000..3575715 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/config/LuminolConfigsInstance.java @@ -0,0 +1,203 @@ +package me.earthme.luminol.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 LuminolConfigsInstance { + /** + * 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/lophine-api/src/main/java/me/earthme/luminol/api/entity/EntityTeleportAsyncEvent.java b/lophine-api/src/main/java/me/earthme/luminol/api/entity/EntityTeleportAsyncEvent.java new file mode 100644 index 0000000..8df15a1 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/entity/EntityTeleportAsyncEvent.java @@ -0,0 +1,71 @@ +package me.earthme.luminol.api.entity; + +import org.apache.commons.lang3.Validate; +import org.bukkit.Location; +import org.bukkit.entity.Entity; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.jetbrains.annotations.NotNull; + +/** + * A simple event fired when a teleportAsync was called + * + * @see org.bukkit.entity.Entity#teleportAsync(org.bukkit.Location, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause) + * @see org.bukkit.entity.Entity#teleportAsync(org.bukkit.Location) + * (Also fired when teleportAsync called from nms) + */ +public class EntityTeleportAsyncEvent extends Event { + private static final HandlerList HANDLERS = new HandlerList(); + + private final Entity entity; + private final PlayerTeleportEvent.TeleportCause teleportCause; + private final Location destination; + + public EntityTeleportAsyncEvent(Entity entity, PlayerTeleportEvent.TeleportCause teleportCause, Location destination) { + Validate.notNull(entity, "entity cannot be a null value!"); + Validate.notNull(teleportCause, "teleportCause cannot be a null value!"); + Validate.notNull(destination, "destination cannot be a null value!"); + + this.entity = entity; + this.teleportCause = teleportCause; + this.destination = destination; + } + + /** + * Get the entity is about to be teleported + * + * @return that entity + */ + public @NotNull Entity getEntity() { + return this.entity; + } + + /** + * Get the cause of the teleport + * + * @return the cause + */ + public @NotNull PlayerTeleportEvent.TeleportCause getTeleportCause() { + return this.teleportCause; + } + + /** + * Get the destination of the teleport + * + * @return the destination + */ + public @NotNull Location getDestination() { + return this.destination; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + @NotNull + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/entity/PostEntityPortalEvent.java b/lophine-api/src/main/java/me/earthme/luminol/api/entity/PostEntityPortalEvent.java new file mode 100644 index 0000000..c754a26 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/entity/PostEntityPortalEvent.java @@ -0,0 +1,42 @@ +package me.earthme.luminol.api.entity; + +import org.apache.commons.lang3.Validate; +import org.bukkit.entity.Entity; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * A simple event created for missing teleport events api of folia + * This event is fired when the entity portal process has been done + */ +public class PostEntityPortalEvent extends Event { + private static final HandlerList HANDLER_LIST = new HandlerList(); + + private final Entity teleportedEntity; + + public PostEntityPortalEvent(Entity teleportedEntity) { + Validate.notNull(teleportedEntity, "teleportedEntity cannot be null!"); + + this.teleportedEntity = teleportedEntity; + } + + /** + * Get the entity which was teleported + * + * @return the entity which was teleported + */ + public Entity getTeleportedEntity() { + return this.teleportedEntity; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLER_LIST; + } + + @NotNull + public static HandlerList getHandlerList() { + return HANDLER_LIST; + } +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/entity/PreEntityPortalEvent.java b/lophine-api/src/main/java/me/earthme/luminol/api/entity/PreEntityPortalEvent.java new file mode 100644 index 0000000..64064d7 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/entity/PreEntityPortalEvent.java @@ -0,0 +1,81 @@ +package me.earthme.luminol.api.entity; + +import org.apache.commons.lang3.Validate; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.entity.Entity; +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * A simple event created for missing teleport events api of folia + * This event will be fired when a portal teleportation is about to happen + */ +public class PreEntityPortalEvent extends Event implements Cancellable { + private static final HandlerList HANDLERS = new HandlerList(); + + private final Entity entity; + private final Location portalPos; + private final World destination; + + private boolean cancelled = false; + + public PreEntityPortalEvent(Entity entity, Location portalPos, World destination) { + Validate.notNull(entity, "entity cannot be null!"); + Validate.notNull(portalPos, "portalPos cannot be null!"); + Validate.notNull(destination, "destination cannot be null!"); + + this.entity = entity; + this.portalPos = portalPos; + this.destination = destination; + } + + /** + * Get the entity that is about to teleport + * + * @return the entity + */ + public @NotNull Entity getEntity() { + return this.entity; + } + + /** + * Get the location of the portal + * + * @return the portal location + */ + public @NotNull Location getPortalPos() { + return this.portalPos; + } + + /** + * Get the destination world + * + * @return the destination world + */ + public @NotNull World getDestination() { + return this.destination; + } + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + @NotNull + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/entity/player/PostPlayerRespawnEvent.java b/lophine-api/src/main/java/me/earthme/luminol/api/entity/player/PostPlayerRespawnEvent.java new file mode 100644 index 0000000..356bd07 --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/entity/player/PostPlayerRespawnEvent.java @@ -0,0 +1,41 @@ +package me.earthme.luminol.api.entity.player; + +import org.apache.commons.lang3.Validate; +import org.bukkit.entity.Player; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * A simple event fired when the respawn process of player is done + */ +public class PostPlayerRespawnEvent extends Event { + private static final HandlerList HANDLERS = new HandlerList(); + + private final Player player; + + public PostPlayerRespawnEvent(Player player) { + Validate.notNull(player, "Player cannot be a null value!"); + + this.player = player; + } + + /** + * Get the respawned player + * + * @return the player + */ + public @NotNull Player getPlayer() { + return this.player; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + @NotNull + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/portal/EndPlatformCreateEvent.java b/lophine-api/src/main/java/me/earthme/luminol/api/portal/EndPlatformCreateEvent.java new file mode 100644 index 0000000..cf87a7c --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/portal/EndPlatformCreateEvent.java @@ -0,0 +1,35 @@ +package me.earthme.luminol.api.portal; + +import org.bukkit.event.Cancellable; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * A event fired when an end platform is created. + */ +public class EndPlatformCreateEvent extends Event implements Cancellable { + private static final HandlerList HANDLERS = new HandlerList(); + + private boolean cancelled = false; + + @Override + public boolean isCancelled() { + return this.cancelled; + } + + @Override + public void setCancelled(boolean cancel) { + this.cancelled = cancel; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + @NotNull + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/lophine-api/src/main/java/me/earthme/luminol/api/portal/PortalLocateEvent.java b/lophine-api/src/main/java/me/earthme/luminol/api/portal/PortalLocateEvent.java new file mode 100644 index 0000000..5681b9c --- /dev/null +++ b/lophine-api/src/main/java/me/earthme/luminol/api/portal/PortalLocateEvent.java @@ -0,0 +1,55 @@ +package me.earthme.luminol.api.portal; + +import org.apache.commons.lang3.Validate; +import org.bukkit.Location; +import org.bukkit.event.Event; +import org.bukkit.event.HandlerList; +import org.jetbrains.annotations.NotNull; + +/** + * A event fired when the portal process started locating the destination position + * Notice: If you changed the destination to an another position in end teleportation.The end platform won't create under the entity and won't create + * if the position is out of current tick region + */ +public class PortalLocateEvent extends Event { + private static final HandlerList HANDLERS = new HandlerList(); + + private final Location original; + private final Location destination; + + public PortalLocateEvent(Location original, Location destination) { + Validate.notNull(original, "original couldn't be null!"); + Validate.notNull(destination, "destination couldn't be null!"); + + this.original = original; + this.destination = destination; + } + + /** + * Get the destination position of this teleportation + * + * @return the destination position + */ + public Location getDestination() { + return this.destination; + } + + /** + * Get the original portal position of this teleportation + * + * @return the original portal position + */ + public Location getOriginal() { + return this.original; + } + + @Override + public @NotNull HandlerList getHandlers() { + return HANDLERS; + } + + @NotNull + public static HandlerList getHandlerList() { + return HANDLERS; + } +} diff --git a/lophine-api/src/main/java/org/leavesmc/leaves/plugin/FeatureManager.java b/lophine-api/src/main/java/org/leavesmc/leaves/plugin/FeatureManager.java new file mode 100644 index 0000000..891628c --- /dev/null +++ b/lophine-api/src/main/java/org/leavesmc/leaves/plugin/FeatureManager.java @@ -0,0 +1,14 @@ +/* + * This file is licensed under the MIT license. + * Origin : Leaves (https://github.com/LeavesMC/Leaves) + */ + +package org.leavesmc.leaves.plugin; + +import java.util.Set; + +public interface FeatureManager { + Set getAvailableFeatures(); + + boolean isFeatureAvailable(String feature); +} diff --git a/lophine-api/src/main/java/org/leavesmc/leaves/plugin/Features.java b/lophine-api/src/main/java/org/leavesmc/leaves/plugin/Features.java new file mode 100644 index 0000000..0c27c5f --- /dev/null +++ b/lophine-api/src/main/java/org/leavesmc/leaves/plugin/Features.java @@ -0,0 +1,15 @@ +/* + * This file is licensed under the MIT license. + * Origin : Leaves (https://github.com/LeavesMC/Leaves) + */ + +package org.leavesmc.leaves.plugin; + +public class Features { + public static final String MIXIN = "mixin"; + public static final String FAKEPLAYER = "fakeplayer"; + public static final String PHOTOGRAPHER = "photographer"; + public static final String RECORDER = "recorder"; + public static final String BYTEBUF = "bytebuf"; + public static final String UPDATE_SUPPRESSION_EVENT = "update_suppression_event"; +} diff --git a/lophine-server/build.gradle.kts.patch b/lophine-server/build.gradle.kts.patch index 27181a9..0321abd 100644 --- a/lophine-server/build.gradle.kts.patch +++ b/lophine-server/build.gradle.kts.patch @@ -1,72 +1,152 @@ ---- a/luminol-server/build.gradle.kts -+++ b/luminol-server/build.gradle.kts -@@ -50,7 +_,23 @@ +--- 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") // TODO Later - rebrand + 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()}") // TODO Later - rebrand + } + + paperweight { +@@ -23,7 +_,8 @@ + filterPatches = false + gitFilePatches = false + +- val fork = forks.register("folia") { ++ val folia = 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 = luminol +- activeFork = fork + val lophine = forks.register("lophine") { -+ forks = luminol ++ forks = folia + upstream.patchRepo("paperServer") { -+ upstreamRepo = luminol.patchedRepo("paperServer") ++ upstreamRepo = folia.patchedRepo("paperServer") + patchesDir = rootDirectory.dir("lophine-server/paper-patches") + outputDir = rootDirectory.dir("paper-server") + } + -+ upstream.patchDir("luminolServer") { -+ upstreamPath = "luminol-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("lophine-server/luminol-patches") -+ outputDir = rootDirectory.dir("luminol-server") ++ patchesDir = rootDirectory.dir("lophine-server/folia-patches") ++ outputDir = rootDirectory.dir("folia-server") + } + } + + activeFork = lophine + updatingMinecraft { +@@ -44,6 +_,7 @@ + libraryRepositories.addAll( + "https://repo.maven.apache.org/maven2/", + paperMavenPublicUrl, ++ bacteriawaMavenPublicUrl + ) + } -@@ -125,12 +_,16 @@ +@@ -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") } -+ java { srcDir("../luminol-server/src/main/java") } -+ resources { srcDir("../luminol-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") } -+ java { srcDir("../luminol-server/src/main/java") } -+ resources { srcDir("../luminol-server/src/main/resources") } ++ java { srcDir("../folia-server/src/test/java") } ++ resources { srcDir("../folia-server/src/test/resources") } } } val log4jPlugins = sourceSets.create("log4jPlugins") { -@@ -158,7 +_,7 @@ +@@ -134,7 +_,21 @@ } dependencies { -- implementation(project(":luminol-api")) -+ implementation(project(":lophine-api")) - // Luminol start - Dependenices insert - implementation("net.objecthunter:exp4j:0.4.8") - implementation("com.electronwill.night-config:toml:3.9.0") // Night config -@@ -234,14 +_,14 @@ +- implementation(project(":folia-api")) ++ implementation(project(":lophine-api")) // Lophine - api update ++ // Lophine start - Dependenices insert ++ implementation("net.objecthunter:exp4j:0.4.8") ++ implementation("com.electronwill.night-config:toml:3.9.0") // Night config ++ implementation("com.github.luben:zstd-jni:1.5.4-1") ++ implementation("net.openhft:zero-allocation-hashing:0.16") ++ implementation("io.github.classgraph:classgraph:4.8.158") ++ implementation("net.openhft:affinity:3.23.3") ++ // Leaves start - leaves plugin ++ implementation("org.spongepowered:configurate-gson:4.2.0-SNAPSHOT") { ++ exclude(group = "com.google.code.gson", module = "gson") ++ exclude(group = "com.google.guava", module = "guava") ++ } ++ // Leaves end - leaves plugin ++ // Lophine 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 +_,22 @@ + 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() // Always use current as build time + 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 "Luminol", // Luminol - Rebrand -+ "Implementation-Title" to "Lophine", // Luminol - Rebrand +- "Implementation-Title" to "Folia", ++ "Implementation-Title" to "Lophine", // Lophine - Rebrand "Implementation-Version" to implementationVersion, "Implementation-Vendor" to date, -- "Specification-Title" to "Luminol", // Luminol - Rebrand -+ "Specification-Title" to "Lophine", // Luminol - Rebrand +- "Specification-Title" to "Folia", ++ "Specification-Title" to "Lophine", // Lophine - Rebrand "Specification-Version" to project.version, - "Specification-Vendor" to "LuminoLMC", // Luminol - Rebrand -- "Brand-Id" to "luminolmc:luminol", // Luminol - Rebrand -- "Brand-Name" to "Luminol", // Luminol - Rebrand -+ "Brand-Id" to "luminolmc:lophine", // Luminol - Rebrand -+ "Brand-Name" to "Lophine", // Luminol - Rebrand +- "Specification-Vendor" to "Paper Team", +- "Brand-Id" to "papermc:folia", +- "Brand-Name" to "Folia", ++ // TODO Later - rebrand org name ++ "Specification-Vendor" to "LuminoLMC", // Lophine - Rebrand ++ "Brand-Id" to "luminolmc:lophine", // Lophine - Rebrand ++ "Brand-Name" to "Lophine", // Lophine - Rebrand "Build-Number" to (build ?: ""), "Build-Time" to buildTime.toString(), "Git-Branch" to gitBranch, +@@ -214,6 +_,20 @@ + } + } + } ++// Pufferfish Start ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ compilerArgs.add("--add-modules=jdk.incubator.vector") ++} ++// Pufferfish End ++// Lophine start - Hide unnecessary warnings ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ compilerArgs.add("-Xlint:-module") ++ compilerArgs.add("-Xlint:-removal") ++ compilerArgs.add("-Xlint:-dep-ann") ++} ++// Lophine end + + // Compile tests with -parameters for better junit parameterized test names + tasks.compileTestJava { diff --git a/lophine-server/luminol-patches/features/0001-Rebrand-to-Lophine.patch b/lophine-server/luminol-patches/features/0001-Rebrand-to-Lophine.patch deleted file mode 100644 index 679a28b..0000000 --- a/lophine-server/luminol-patches/features/0001-Rebrand-to-Lophine.patch +++ /dev/null @@ -1,31 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Helvetica Volubi -Date: Sun, 8 Jun 2025 00:58:13 +0800 -Subject: [PATCH] Rebrand to Lophine - - -diff --git a/src/main/java/me/earthme/luminol/config/ConfigManager.java b/src/main/java/me/earthme/luminol/config/ConfigManager.java -index 3b700b214ac6818cd75c34b207aa1436f52231e7..315c137cb9dbac51a8ac0d63f0ae0127e57c6c9f 100644 ---- a/src/main/java/me/earthme/luminol/config/ConfigManager.java -+++ b/src/main/java/me/earthme/luminol/config/ConfigManager.java -@@ -25,6 +25,7 @@ public class ConfigManager { - - public static void initConfigs() { - registerConfig("luminol", builder.of("luminol", "me.earthme.luminol.config.modules")); -+ registerConfig("lophine", builder.of("lophine", "fun.bm.lophine.config.modules")); // add lophine global config - preLoad(); - } - -diff --git a/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java b/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java -index e4022494b050b68d54bac3cde87bdfc9e908859c..a4188ef831825764c0756b826651e2e7a9f99c5b 100644 ---- a/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java -+++ b/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java -@@ -8,7 +8,7 @@ import me.earthme.luminol.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 = "Luminol"; -+ public static String serverModName = "Lophine"; - - @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; diff --git a/lophine-server/luminol-patches/features/0002-Diff-in-auto-update-patch.patch b/lophine-server/luminol-patches/features/0002-Diff-in-auto-update-patch.patch deleted file mode 100644 index 13984f7..0000000 --- a/lophine-server/luminol-patches/features/0002-Diff-in-auto-update-patch.patch +++ /dev/null @@ -1,141 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Helvetica Volubi -Date: Mon, 16 Mar 2026 15:51:40 +0800 -Subject: [PATCH] Diff in auto-update patch - - -diff --git a/src/main/java/me/earthme/luminol/config/modules/misc/AutoUpdateConfig.java b/src/main/java/me/earthme/luminol/config/modules/misc/AutoUpdateConfig.java -index ce01352b637547c1cf8308d56d4b3f9c48fcca70..9b5623ce1de3d68c949c3c00b966cedab3a4c986 100644 ---- a/src/main/java/me/earthme/luminol/config/modules/misc/AutoUpdateConfig.java -+++ b/src/main/java/me/earthme/luminol/config/modules/misc/AutoUpdateConfig.java -@@ -16,13 +16,13 @@ import java.util.Set; - category = EnumConfigCategory.MISC, - name = "auto_update", - comments = """ -- Checks GitHub Releases for newer Luminol jars on a schedule. -- Downloads are staged under auto_update/luminol and written to auto_update/core.path, -+ Checks GitHub Releases for newer version's jars on a schedule. -+ Downloads are staged under auto_update/lophine and written to auto_update/core.path, - which Hyacinthusclip can consume on the next restart. -- If target_jar_path is set, Luminol will also try to replace that launcher jar directly.""" -+ If target_jar_path is set, server will also try to replace that launcher jar directly.""" - ) - public class AutoUpdateConfig implements IConfigModule { -- @ConfigInfo(name = "enabled", comments = "Whether Luminol should check for updates automatically.") -+ @ConfigInfo(name = "enabled", comments = "Whether the server should check for updates automatically.") - public static boolean enabled = false; - - @ConfigInfo(name = "check_times", comments = "List of daily check times in HH:mm, based on the server's local time zone.") -@@ -33,7 +33,7 @@ public class AutoUpdateConfig implements IConfigModule { - - @ConfigInfo(name = "target_jar_path", comments = """ - Optional launcher jar path to replace after a successful download. -- Leave this blank to keep the downloaded jar staged in auto_update/luminol -+ Leave this blank to keep the downloaded jar staged in auto_update/lophine - and let Hyacinthusclip switch to it through auto_update/core.path on restart.""") - public static String targetJarPath = ""; - -diff --git a/src/main/java/me/earthme/luminol/utils/AutoUpdateHelper.java b/src/main/java/me/earthme/luminol/utils/AutoUpdateHelper.java -index 94a25cd681c9352d6931edafd9ec67fd7ba8a777..d0d0cf5e5ff8d4b8efa4d9778c27267b41cf8cf8 100644 ---- a/src/main/java/me/earthme/luminol/utils/AutoUpdateHelper.java -+++ b/src/main/java/me/earthme/luminol/utils/AutoUpdateHelper.java -@@ -35,10 +35,10 @@ public final class AutoUpdateHelper { - private final Logger LOGGER = LogUtils.getClassLogger(); - private final Gson GSON = new Gson(); - -- private final String GITHUB_API_BASE = "https://api.github.com/repos/LuminolMC/Luminol"; -+ private final String GITHUB_API_BASE = "https://api.github.com/repos/lophineMC/lophine"; - private final Path AUTO_UPDATE_DIR = Path.of("auto_update"); - private final Path CORE_PATH_FILE = AUTO_UPDATE_DIR.resolve("core.path"); -- private final Path LUMINOL_UPDATE_DIR = AUTO_UPDATE_DIR.resolve("luminol"); -+ private final Path LUMINOL_UPDATE_DIR = AUTO_UPDATE_DIR.resolve("lophine"); - private final Path LATEST_PATH_FILE = LUMINOL_UPDATE_DIR.resolve("latest.path"); - private final long DAILY_TASK_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1); - -@@ -51,13 +51,13 @@ public final class AutoUpdateHelper { - ensureWorkingDirectories(); - - scheduler = Executors.newSingleThreadScheduledExecutor(task -> { -- final Thread thread = new Thread(task, "Luminol Auto Update Scheduler"); -+ final Thread thread = new Thread(task, "Lophine Auto Update Scheduler"); - thread.setDaemon(true); - return thread; - }); - - if (AutoUpdateConfig.checkTimes.isEmpty()) { -- LOGGER.warn("Luminol auto-update is enabled, but misc.auto_update.check_times is empty."); -+ LOGGER.warn("Lophine auto-update is enabled, but misc.auto_update.check_times is empty."); - return; - } - -@@ -95,14 +95,14 @@ public final class AutoUpdateHelper { - - private void checkForUpdatesSafely() { - if (!UPDATE_RUNNING.compareAndSet(false, true)) { -- LOGGER.info("A Luminol auto-update check is already running, skipping this schedule."); -+ LOGGER.info("A Lophine auto-update check is already running, skipping this schedule."); - return; - } - - try { - checkForUpdates(); - } catch (Exception e) { -- LOGGER.warn("Luminol auto-update failed.", e); -+ LOGGER.warn("Lophine auto-update failed.", e); - } finally { - UPDATE_RUNNING.set(false); - } -@@ -123,7 +123,7 @@ public final class AutoUpdateHelper { - - final ReleaseAssetInfo latestRelease = getLatestCompatibleRelease(mcVersion, gitBranch); - if (latestRelease == null) { -- LOGGER.warn("No compatible Luminol release was found for Minecraft {}{}.", -+ LOGGER.warn("No compatible Lophine release was found for Minecraft {}{}.", - mcVersion, - gitBranch == null ? "" : " on branch " + gitBranch - ); -@@ -132,7 +132,7 @@ public final class AutoUpdateHelper { - - final UpdateStatus updateStatus = compareReleaseWithCurrentBuild(latestRelease.releaseCommitHash(), currentGitHash); - if (updateStatus == UpdateStatus.UP_TO_DATE) { -- LOGGER.info("You are already running the latest compatible Luminol release: {}", latestRelease.tagName()); -+ LOGGER.info("You are already running the latest compatible Lophine release: {}", latestRelease.tagName()); - return; - } - -@@ -154,7 +154,7 @@ public final class AutoUpdateHelper { - return; - } - -- LOGGER.info("Found a newer Luminol release: {}", latestRelease.tagName()); -+ LOGGER.info("Found a newer Lophine release: {}", latestRelease.tagName()); - - final Path stagedJar = downloadAndStageRelease(latestRelease); - final Path finalJarPath = tryApplyTargetJar(stagedJar); -@@ -163,7 +163,7 @@ public final class AutoUpdateHelper { - - if (finalJarPath.equals(stagedJar)) { - LOGGER.info( -- "Downloaded the latest Luminol jar to {} and refreshed auto_update/core.path for Hyacinthusclip. Please restart your server.", -+ "Downloaded the latest Lophine jar to {} and refreshed auto_update/core.path for Hyacinthusclip. Please restart your server.", - finalJarPath.toAbsolutePath() - ); - } else { -@@ -179,7 +179,7 @@ public final class AutoUpdateHelper { - try { - Files.createDirectories(LUMINOL_UPDATE_DIR); - } catch (IOException e) { -- throw new RuntimeException("Failed to create Luminol auto update directory", e); -+ throw new RuntimeException("Failed to create Lophine auto update directory", e); - } - } - -@@ -440,7 +440,7 @@ public final class AutoUpdateHelper { - private @NotNull HttpURLConnection openConnection(String url) throws IOException, URISyntaxException { - final HttpURLConnection connection = (HttpURLConnection) new URI(url).toURL().openConnection(); - connection.setRequestProperty("Accept", "application/vnd.github+json"); -- connection.setRequestProperty("User-Agent", "Luminol Auto Update"); -+ connection.setRequestProperty("User-Agent", "Lophine Auto Update"); - connection.setConnectTimeout(10000); - connection.setReadTimeout(10000); - return connection; diff --git a/lophine-server/luminol-patches/features/0003-Leaves-Item-overstack-util.patch b/lophine-server/luminol-patches/features/0003-Leaves-Item-overstack-util.patch deleted file mode 100644 index 377a4b8..0000000 --- a/lophine-server/luminol-patches/features/0003-Leaves-Item-overstack-util.patch +++ /dev/null @@ -1,190 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Helvetica Volubi -Date: Wed, 13 Aug 2025 22:23:46 +0800 -Subject: [PATCH] Leaves: Item overstack util - - -diff --git a/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java b/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java -index a5df7e40369cf2c309f4d37fede6615f34a467e2..84d0e1a7cd687ac4ed9f1bf824ca28940c2fab1e 100644 ---- a/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java -+++ b/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java -@@ -17,10 +17,98 @@ - - package org.leavesmc.leaves.util; - -+import fun.bm.lophine.config.modules.function.ContainerExpansionConfig; -+import net.minecraft.core.component.DataComponents; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.world.entity.item.ItemEntity; -+import net.minecraft.world.item.BlockItem; - import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.component.CustomData; -+import net.minecraft.world.item.component.ItemContainerContents; -+import net.minecraft.world.level.block.ShulkerBoxBlock; -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.List; -+import java.util.Objects; -+import java.util.Optional; - - // Only used part of the original code was copied - public class ItemOverstackUtils { -+ -+ private static final List overstackUtils = List.of( -+ new ShulkerBox() -+ ); -+ -+ public static int getItemStackMaxCount(ItemStack stack) { -+ int size; -+ for (ItemUtil util : overstackUtils) { -+ if (util.isEnabled() && (size = util.getMaxServerStackCount(stack)) != -1) { -+ return size; -+ } -+ } -+ return stack.getMaxStackSize(); -+ } -+ -+ public static int getNetworkMaxCount(ItemStack stack) { -+ int size; -+ for (ItemUtil util : overstackUtils) { -+ if (util.isEnabled() && (size = util.getMaxClientStackCount(stack)) != -1) { -+ return size; -+ } -+ } -+ return stack.getMaxStackSize(); -+ } -+ -+ public static boolean tryStackItems(ItemEntity self, ItemEntity other) { -+ for (ItemUtil util : overstackUtils) { -+ if (util.isEnabled() && util.tryStackItems(self, other)) { -+ return true; -+ } -+ } -+ return false; -+ } -+ -+ public static boolean hasOverstackingItem() { -+ for (ItemUtil util : overstackUtils) { -+ if (util.isEnabled()) { -+ return true; -+ } -+ } -+ return false; -+ } -+ -+ public static int getItemStackMaxCountReal(ItemStack stack) { -+ CompoundTag nbt = Optional.ofNullable(stack.get(DataComponents.CUSTOM_DATA)).orElse(CustomData.EMPTY).copyTag(); -+ return nbt.getInt("Leaves.RealStackSize").orElse(stack.getMaxStackSize()); -+ } -+ -+ public static ItemStack encodeMaxStackSize(ItemStack itemStack) { -+ int realMaxStackSize = getItemStackMaxCountReal(itemStack); -+ int modifiedMaxStackSize = getNetworkMaxCount(itemStack); -+ if (itemStack.getMaxStackSize() != modifiedMaxStackSize) { -+ itemStack.set(DataComponents.MAX_STACK_SIZE, modifiedMaxStackSize); -+ CompoundTag nbt = itemStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); -+ nbt.putInt("Leaves.RealStackSize", realMaxStackSize); -+ itemStack.set(DataComponents.CUSTOM_DATA, CustomData.of(nbt)); -+ } -+ return itemStack; -+ } -+ -+ public static ItemStack decodeMaxStackSize(ItemStack itemStack) { -+ int realMaxStackSize = getItemStackMaxCountReal(itemStack); -+ if (itemStack.getMaxStackSize() != realMaxStackSize) { -+ itemStack.set(DataComponents.MAX_STACK_SIZE, realMaxStackSize); -+ CompoundTag nbt = itemStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); -+ nbt.remove("Leaves.RealStackSize"); -+ if (nbt.isEmpty()) { -+ itemStack.remove(DataComponents.CUSTOM_DATA); -+ } else { -+ itemStack.set(DataComponents.CUSTOM_DATA, CustomData.of(nbt)); -+ } -+ } -+ return itemStack; -+ } -+ - 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 -@@ -28,4 +116,80 @@ public class ItemOverstackUtils { - }*/ // Luminol - return Math.clamp(result, 0f, 1f); - } -+ -+ public static boolean isStackable(ItemStack itemStack) { -+ return getItemStackMaxCount(itemStack) > 1 && (!itemStack.isDamageableItem() || !itemStack.isDamaged()); -+ } -+ -+ private interface ItemUtil { -+ boolean isEnabled(); -+ -+ boolean tryStackItems(ItemEntity self, ItemEntity other); -+ -+ // number -> modified count, -1 -> I don't care -+ int getMaxServerStackCount(ItemStack stack); -+ -+ // number -> modified count, -1 -> I don't care -+ default int getMaxClientStackCount(ItemStack stack) { -+ return getMaxServerStackCount(stack); -+ } -+ } -+ -+ private static class ShulkerBox implements ItemUtil { -+ public static boolean shulkerBoxCheck(@NotNull ItemStack stack1, @NotNull ItemStack stack2) { -+ if (ContainerExpansionConfig.nbtShulkerStackable) { -+ return Objects.equals(stack1.getComponents(), stack2.getComponents()); -+ } -+ return shulkerBoxNoItem(stack1) && shulkerBoxNoItem(stack2) && Objects.equals(stack1.getComponents(), stack2.getComponents()); -+ } -+ -+ public static boolean shulkerBoxNoItem(@NotNull ItemStack stack) { -+ // Leaves - Paper 26.1: ItemContainerContents#stream() removed, use nonEmptyItemCopyStream() -+ return stack.getComponents().getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY).nonEmptyItemCopyStream().findAny().isEmpty(); -+ } -+ -+ @Override -+ public boolean isEnabled() { -+ return ContainerExpansionConfig.shulkerCount > 1; -+ } -+ -+ @Override -+ public boolean tryStackItems(ItemEntity self, ItemEntity other) { -+ ItemStack selfStack = self.getItem(); -+ if (!(selfStack.getItem() instanceof BlockItem blockItem) || !(blockItem.getBlock() instanceof ShulkerBoxBlock)) { -+ return false; -+ } -+ -+ ItemStack otherStack = other.getItem(); -+ if (selfStack.getItem() == otherStack.getItem() -+ && shulkerBoxCheck(selfStack, otherStack) -+ && selfStack.getCount() != ContainerExpansionConfig.shulkerCount) { -+ int amount = Math.min(otherStack.getCount(), ContainerExpansionConfig.shulkerCount - selfStack.getCount()); -+ -+ selfStack.grow(amount); -+ self.setItem(selfStack); -+ -+ self.pickupDelay = Math.max(other.pickupDelay, self.pickupDelay); -+ self.age = Math.min(other.getAge(), self.age); -+ -+ otherStack.shrink(amount); -+ if (otherStack.isEmpty()) { -+ other.discard(); -+ } else { -+ other.setItem(otherStack); -+ } -+ return true; -+ } -+ return false; -+ } -+ -+ @Override -+ public int getMaxServerStackCount(ItemStack stack) { -+ if (stack.getItem() instanceof BlockItem bi && -+ bi.getBlock() instanceof ShulkerBoxBlock && (ContainerExpansionConfig.nbtShulkerStackable || shulkerBoxNoItem(stack))) { -+ return ContainerExpansionConfig.shulkerCount; -+ } -+ return -1; -+ } -+ } - } diff --git a/lophine-server/luminol-patches/features/0004-Leaves-Fakeplayer.patch b/lophine-server/luminol-patches/features/0004-Leaves-Fakeplayer.patch deleted file mode 100644 index 2da6128..0000000 --- a/lophine-server/luminol-patches/features/0004-Leaves-Fakeplayer.patch +++ /dev/null @@ -1,55 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Helvetica Volubi -Date: Wed, 18 Feb 2026 00:15:08 +0800 -Subject: [PATCH] Leaves: Fakeplayer - - -diff --git a/src/main/java/me/earthme/luminol/utils/FoliaServerWaypointManager.java b/src/main/java/me/earthme/luminol/utils/FoliaServerWaypointManager.java -index a4c188f71564fc77a9fa22b4cae032e1919d06f2..019266803f1452cb1e76350da8edae10a77020ac 100644 ---- a/src/main/java/me/earthme/luminol/utils/FoliaServerWaypointManager.java -+++ b/src/main/java/me/earthme/luminol/utils/FoliaServerWaypointManager.java -@@ -54,6 +54,12 @@ public class FoliaServerWaypointManager extends ServerWaypointManager { - return; - } - -+ // Leaves start - fakeplayer -+ if (waypoint instanceof org.leavesmc.leaves.bot.ServerBot bot && !bot.getConfigValue(org.leavesmc.leaves.bot.agent.Configs.ENABLE_LOCATOR_BAR)) { -+ return; -+ } -+ // Leaves end - fakeplayer -+ - this.waypoints.add(waypoint); - - for (ServerPlayer toCreateFor : this.trackingPlayers) { -@@ -110,6 +116,11 @@ public class FoliaServerWaypointManager extends ServerWaypointManager { - this.trackingPlayers.add(player); - - scheduleIfOffTarget(player, () -> { -+ // Leaves start - fakeplayer -+ if (player instanceof org.leavesmc.leaves.bot.ServerBot) { -+ return; -+ } -+ // Leaves end - fakeplayer - for (WaypointTransmitter transmitter : this.waypoints) { - this.createConnection(player, transmitter); - } -diff --git a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -index 063cc27df9c8f3ad5c8db5cedcaa3d2bccf6dc24..5ad6aab5e72e27bb402494873770ab5822601939 100644 ---- a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -+++ b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -@@ -11,14 +11,7 @@ public class ServerFeatureManager implements FeatureManager { - private final Set availableFeatures = new HashSet<>(); - - private ServerFeatureManager() { -- if (false) { // Luminol not provide this -- availableFeatures.addAll(Set.of( -- FAKEPLAYER, -- PHOTOGRAPHER, -- BYTEBUF, -- UPDATE_SUPPRESSION_EVENT -- )); -- } -+ availableFeatures.add(FAKEPLAYER); // Lophine - fakeplayer support - if (Boolean.getBoolean("leavesclip.enable.mixin") || Boolean.getBoolean("hyacinthusclip.enable.mixin")) { - availableFeatures.add(MIXIN); - } diff --git a/lophine-server/luminol-patches/features/0005-Leaves-Replay-Mod-API.patch b/lophine-server/luminol-patches/features/0005-Leaves-Replay-Mod-API.patch deleted file mode 100644 index 1eb3fee..0000000 --- a/lophine-server/luminol-patches/features/0005-Leaves-Replay-Mod-API.patch +++ /dev/null @@ -1,18 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Helvetica Volubi -Date: Sun, 17 May 2026 05:33:21 +0800 -Subject: [PATCH] Leaves: Replay Mod API - - -diff --git a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -index 5ad6aab5e72e27bb402494873770ab5822601939..0ac9edfa36833a7ebe0353c4aff40955ab3ddf8c 100644 ---- a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -+++ b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -@@ -12,6 +12,7 @@ public class ServerFeatureManager implements FeatureManager { - - private ServerFeatureManager() { - availableFeatures.add(FAKEPLAYER); // Lophine - fakeplayer support -+ availableFeatures.add(PHOTOGRAPHER); // Lophine - Replay Mod API support - if (Boolean.getBoolean("leavesclip.enable.mixin") || Boolean.getBoolean("hyacinthusclip.enable.mixin")) { - availableFeatures.add(MIXIN); - } diff --git a/lophine-server/luminol-patches/features/0006-Update-suppression-event.patch b/lophine-server/luminol-patches/features/0006-Update-suppression-event.patch deleted file mode 100644 index a100e4b..0000000 --- a/lophine-server/luminol-patches/features/0006-Update-suppression-event.patch +++ /dev/null @@ -1,18 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Helvetica Volubi -Date: Wed, 3 Jun 2026 13:49:39 +0800 -Subject: [PATCH] Update suppression event - - -diff --git a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -index 0ac9edfa36833a7ebe0353c4aff40955ab3ddf8c..c3db6c5281728fa1a2b4d84f9ebff9fcfd696698 100644 ---- a/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -+++ b/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java -@@ -13,6 +13,7 @@ public class ServerFeatureManager implements FeatureManager { - private ServerFeatureManager() { - availableFeatures.add(FAKEPLAYER); // Lophine - fakeplayer support - availableFeatures.add(PHOTOGRAPHER); // Lophine - Replay Mod API support -+ availableFeatures.add(UPDATE_SUPPRESSION_EVENT); // Lophine - update suppression event support - if (Boolean.getBoolean("leavesclip.enable.mixin") || Boolean.getBoolean("hyacinthusclip.enable.mixin")) { - availableFeatures.add(MIXIN); - } diff --git a/lophine-server/luminol-patches/features/0007-Carpet-features.patch b/lophine-server/luminol-patches/features/0007-Carpet-features.patch deleted file mode 100644 index fa402f4..0000000 --- a/lophine-server/luminol-patches/features/0007-Carpet-features.patch +++ /dev/null @@ -1,26 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Bacteriawa -Date: Sun, 22 Mar 2026 01:39:17 +0800 -Subject: [PATCH] Carpet features - - -diff --git a/src/main/java/me/earthme/luminol/config/ConfigManager.java b/src/main/java/me/earthme/luminol/config/ConfigManager.java -index 315c137cb9dbac51a8ac0d63f0ae0127e57c6c9f..2a2d6aff636ad960638b357d224a631c89d23d33 100644 ---- a/src/main/java/me/earthme/luminol/config/ConfigManager.java -+++ b/src/main/java/me/earthme/luminol/config/ConfigManager.java -@@ -26,6 +26,15 @@ public class ConfigManager { - public static void initConfigs() { - registerConfig("luminol", builder.of("luminol", "me.earthme.luminol.config.modules")); - registerConfig("lophine", builder.of("lophine", "fun.bm.lophine.config.modules")); // add lophine global config -+ // Lophine start - add carpet config -+ registerConfig("lophine_carpet", builder.of( -+ new java.io.File("lophine_config"), -+ "lophine_carpet", -+ "lophine_carpet_config.toml", -+ "lophinecarpetconfig", -+ "fun.bm.lophine.carpet.config.modules" -+ )); -+ // Lophine end - add carpet config - preLoad(); - } - diff --git a/lophine-server/luminol-patches/features/0008-Carpet-Features-Compatible.patch b/lophine-server/luminol-patches/features/0008-Carpet-Features-Compatible.patch deleted file mode 100644 index afc1c93..0000000 --- a/lophine-server/luminol-patches/features/0008-Carpet-Features-Compatible.patch +++ /dev/null @@ -1,38 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Helvetica Volubi -Date: Fri, 5 Jun 2026 16:10:25 +0800 -Subject: [PATCH] Carpet Features Compatible - - -diff --git a/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java b/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java -index f079a1cd99d648dc7ca97fb65406b7a42ad5a74e..30f78cbae7aea3e9fcee6d97f229ddf8e8344299 100644 ---- a/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java -+++ b/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java -@@ -23,9 +23,4 @@ public class CommandConfig implements IConfigModule { - """) - @HotReloadUnsupported - public static boolean waypointsAndWaypointCommand = 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/src/main/java/me/earthme/luminol/config/modules/optimizations/OptimizedDragonRespawnConfig.java b/src/main/java/me/earthme/luminol/config/modules/optimizations/OptimizedDragonRespawnConfig.java -deleted file mode 100644 -index f791feed92abd750ac10775c05150f122ecbe886..0000000000000000000000000000000000000000 ---- a/src/main/java/me/earthme/luminol/config/modules/optimizations/OptimizedDragonRespawnConfig.java -+++ /dev/null -@@ -1,12 +0,0 @@ --package me.earthme.luminol.config.modules.optimizations; -- --import me.earthme.luminol.config.IConfigModule; --import me.earthme.luminol.config.flags.ConfigClassInfo; --import me.earthme.luminol.config.flags.ConfigInfo; --import me.earthme.luminol.enums.EnumConfigCategory; -- --@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "end_dragon") --public class OptimizedDragonRespawnConfig implements IConfigModule { -- @ConfigInfo(name = "optimized_dragon_respawn") -- public static boolean optimizedRespawn = false; --} diff --git a/lophine-server/minecraft-patches/features/0001-Luminol-Config-System.patch b/lophine-server/minecraft-patches/features/0001-Luminol-Config-System.patch new file mode 100644 index 0000000..8859a16 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0001-Luminol-Config-System.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 14 Jun 2026 18:10:20 +0800 +Subject: [PATCH] Luminol Config System + + +diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java +index a4d608d64b7d3477c9144d93547fd3b4f39a1b02..2a75f8fbd2e5f41ca138740319d34683915e3b5b 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); + } + ++ me.earthme.luminol.config.ConfigManager.initConfigs(); // Luminol - Luminol config + 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 6f13870a1b463049dd37cf1c65d0bdecfc35b5c4..a49c46eb7df50fc3c5587a2c3668ab6edc887107 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1169,6 +1169,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Sun, 28 Jun 2026 17:56:30 +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..44b3dfbf529630d8cca9a4270ae2a7c94c5a077c 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); ++ // Luminol 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); ++ // Luminol 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) { ++ // Luminol 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; ++ } ++ } ++ // Luminol 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); // Luminol - 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 // Luminol - Correct player respawn place + this.connection.resetPosition(); + transition.postTeleportTransition().onTransition(this); + return this; diff --git a/lophine-server/minecraft-patches/features/0003-Correct-volatile-reference-get-in-RegionizedTaskQueu.patch b/lophine-server/minecraft-patches/features/0003-Correct-volatile-reference-get-in-RegionizedTaskQueu.patch new file mode 100644 index 0000000..1af8d86 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0003-Correct-volatile-reference-get-in-RegionizedTaskQueu.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 17 Apr 2026 23:01:59 +0800 +Subject: [PATCH] Correct volatile reference get in RegionizedTaskQueue + + +diff --git a/io/papermc/paper/threadedregions/RegionizedTaskQueue.java b/io/papermc/paper/threadedregions/RegionizedTaskQueue.java +index d1a045d47dcd12f246d95ecdfc202da357897cbf..b264dbd13a392cde3f24ff75c0034985fda68ccc 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); // Luminol - Correct volatile reference get in RegionizedTaskQueue + } + + private ReferenceCountData compareAndExchangeReferenceCounter(final ReferenceCountData expect, final ReferenceCountData update) { diff --git a/lophine-server/minecraft-patches/features/0004-Correct-thread-unsafe-random-sources.patch b/lophine-server/minecraft-patches/features/0004-Correct-thread-unsafe-random-sources.patch new file mode 100644 index 0000000..f9801a4 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0004-Correct-thread-unsafe-random-sources.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Tue, 30 Jun 2026 22:34:00 +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..dd35e72757b1be1a73e67b272c138bb4e3b6336a 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; // Luminol - 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..818598ed4e498bf3f177faac03336097b41915ee 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; // Luminol - Correct thread unsafe random sources + private @Nullable Component name; + + public EnchantingTableBlockEntity(final BlockPos worldPosition, final BlockState blockState) { diff --git a/lophine-server/minecraft-patches/features/0005-Correct-portal-logic-of-some-projectile-entities.patch b/lophine-server/minecraft-patches/features/0005-Correct-portal-logic-of-some-projectile-entities.patch new file mode 100644 index 0000000..0312487 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0005-Correct-portal-logic-of-some-projectile-entities.patch @@ -0,0 +1,58 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Thu, 2 Jul 2026 22:31:15 +0800 +Subject: [PATCH] Correct portal logic of some projectile entities + + +diff --git a/net/minecraft/world/entity/projectile/ShulkerBullet.java b/net/minecraft/world/entity/projectile/ShulkerBullet.java +index d5506bfc84912ec50cd5391225b9b5291435d3aa..6339a770526324bdc1bcc0c0e3bacc0a10f359ab 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; // Luminol - 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..d66a4826400eca835645e1115c4fefc61715f336 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; // Luminol - 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) { // Luminol - 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; // Luminol - 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; // Luminol - Correct portal logic of some projectile entities + } + + private ProjectileDeflection hitTargetsOrDeflectSelf(final Collection entityHitResults) { diff --git a/lophine-server/minecraft-patches/features/0006-Configurable-region-format-framework.patch b/lophine-server/minecraft-patches/features/0006-Configurable-region-format-framework.patch new file mode 100644 index 0000000..a9779ca --- /dev/null +++ b/lophine-server/minecraft-patches/features/0006-Configurable-region-format-framework.patch @@ -0,0 +1,388 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 18:54:50 +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..2e084a5b28cbe4737f48c25e10af589213525362 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 abomination.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); // Luminol - Configurable region file format + +- public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; ++ public abomination.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; // Luminol - 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..fc264727141c3897e6c06819b9713d4f6395baf0 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 abomination.IRegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); // Luminol - Add configurable region file + 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 abomination.IRegionFile regionFile) throws IOException; // Luminol - 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..c7d4d944eb198ac53a3eeae717a25c7d5815c8c1 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 abomination.IRegionFile regionFile) throws IOException; // Luminol - Configurable region file format + } +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index a49c46eb7df50fc3c5587a2c3668ab6edc887107..83b9aab16124ddcc416f7b4cd95084e1c64fe5b7 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(abomination.IRegionFile file, List chunksToUpgrade) { // Luminol - Configurable region file format + } +diff --git a/net/minecraft/util/worldupdate/RegionStorageUpgrader.java b/net/minecraft/util/worldupdate/RegionStorageUpgrader.java +index b228cd6c54e8c8998923ab332882d99092769c67..140ef4959fa2fe5b9cd251955c93683e8e69d5ba 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]+)\\." + me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat.getArgument() + "$"); // Luminol - 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 me.earthme.luminol.utils.RegionCreatorInfo(info, regionFile.toPath(), regionFolder, true); // Luminol - Configurable region file format ++ try (abomination.IRegionFile regionSource = me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat.getCreator().create(regionFileInfo)) { // Luminol - 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 abomination.IRegionFile regionFile) { // Luminol - 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..d3350dee1d6d5bc15dcdfba3611f93addb767392 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 , abomination.IRegionFile{ // Paper - rewrite chunk system // Luminol - 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 { // Luminol - 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 { // Luminol - 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 abomination.IRegionFile regionFile) throws IOException { // Luminol - 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) { // Luminol - 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 { // Luminol - 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 { // Luminol - 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..2e67e69862cbf5d804de50193442977ebfb18517 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<>(); // Luminol - 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())) { // Luminol - 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(); // Luminol - Configurable region file format + } ++ // Luminol start - Configurable region file format ++ public static abomination.IRegionFile createNew(RegionStorageInfo info, Path filePath, Path folder, boolean sync) throws IOException{ ++ final me.earthme.luminol.enums.EnumRegionFormat regionFormat = me.earthme.luminol.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)) { ++ // delayed crash ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { ++ new RuntimeException("Invalid region file format: " + extensionName + " expected " + regionFormat.getArgument()); ++ }); ++ ++ ++ throw new IOException("Invalid region file format: " + extensionName + " expected " + regionFormat.getArgument()); ++ } ++ ++ return regionFormat.getCreator().create(new me.earthme.luminol.utils.RegionCreatorInfo(info, filePath, folder, sync)); ++ } ++ ++ public static String getExtensionName() { ++ return "." + me.earthme.luminol.config.modules.function.RegionFormatConfig.regionFormat.getArgument(); ++ } ++ // Luminol 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 abomination.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { // Luminol - 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 abomination.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { // Luminol - Configurable region file format + final long key = ChunkPos.pack(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + +- RegionFile ret = this.regionCache.getAndMoveToFirst(key); ++ abomination.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // Luminol - 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); // Luminol - 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 abomination.IRegionFile regionFile = this.getRegionFile(pos); // Luminol - 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 abomination.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Luminol - 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 abomination.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Luminol - 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 abomination.IRegionFile regionFile = this.getRegionFile(pos); // Luminol - 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 abomination.IRegionFile getRegionFile(ChunkPos pos) throws IOException { // Luminol - 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 abomination.@Nullable IRegionFile getRegionFile(final ChunkPos pos, boolean existingOnly) throws IOException { // CraftBukkit // Luminol - 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); ++ abomination.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // Luminol - 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); // Luminol - 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(abomination.IRegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // Luminol - 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); ++ abomination.IRegionFile region = this.getRegionFile(pos, true); // Luminol - 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); ++ abomination.IRegionFile region = this.getRegionFile(pos, true); // Luminol - 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 ++ abomination.IRegionFile region = this.getRegionFile(pos, value == null); // CraftBukkit // Paper - rewrite chunk system // Luminol - 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 abomination.IRegionFile regionFile : this.regionCache.values()) { // Luminol - 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 abomination.IRegionFile regionFile : this.regionCache.values()) { // Luminol - Configurable region file format + try { + regionFile.flush(); + } catch (final IOException ex) { diff --git a/lophine-server/minecraft-patches/features/0007-Do-not-enable-any-debug-subscriptions.patch b/lophine-server/minecraft-patches/features/0007-Do-not-enable-any-debug-subscriptions.patch new file mode 100644 index 0000000..223a1ad --- /dev/null +++ b/lophine-server/minecraft-patches/features/0007-Do-not-enable-any-debug-subscriptions.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 17 Apr 2026 23:00:48 +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 44b3dfbf529630d8cca9a4270ae2a7c94c5a077c..34cc36f6bdf435593d16f2e7af280a27be048280 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -3504,7 +3504,8 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + + public Set> debugSubscriptions() { +- return !this.server.debugSubscribers().hasRequiredPermissions(this) ? Set.of() : this.requestedDebugSubscriptions; ++ // return !this.server.debugSubscribers().hasRequiredPermissions(this) ? Set.of() : this.requestedDebugSubscriptions; // Luminol - Do not enable any debug subscriptions ++ return Set.of(); // Luminol - Do not enable any debug subscriptions + } + + public record RespawnConfig(LevelData.RespawnData respawnData, boolean forced) { diff --git a/lophine-server/minecraft-patches/features/0008-Do-not-load-too-far-poi-chunk-in-poi-compete-scan.patch b/lophine-server/minecraft-patches/features/0008-Do-not-load-too-far-poi-chunk-in-poi-compete-scan.patch new file mode 100644 index 0000000..22170a3 --- /dev/null +++ b/lophine-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..0e9c19a820aefa802d6df134b21f5128a5e8d656 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()) ++ // Luminol start - POI fixes ++ 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 = me.earthme.luminol.config.modules.fixes.POIRangeFixes.doNotCompetePOIIfUnloaded ? poiManager.get(sectionPosOfJobSite) : poiManager.getOrLoad(sectionPosOfJobSite); ++ poiChunk.flatMap(poiSection -> poiSection.getType(blockPosOfJobSite)) ++ // Luminol end - POI fixes ++ /*level.getPoiManager() // Luminol - POI fixes ++ .getType(pos.pos())*/ // Luminol - POI fixes + .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/lophine-server/minecraft-patches/features/0009-Fixes-around-entity-brain-and-memories.patch b/lophine-server/minecraft-patches/features/0009-Fixes-around-entity-brain-and-memories.patch new file mode 100644 index 0000000..7832d75 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0009-Fixes-around-entity-brain-and-memories.patch @@ -0,0 +1,287 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 19:02:01 +0800 +Subject: [PATCH] Fixes around entity brain and memories + + +diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java +index 3b1c3e84ab24b6e3a0e4d129b3617b393b6d6651..9c8e8de53cbbd87ac7cb8483bd3c1af3fcf66b6e 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; + } ++ // Luminol start - Fix off-region targeting ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(target)) { ++ return false; ++ } ++ // Luminol 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..27894e2935e2afee8190708def4be3b00f922f64 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); // Luminol - Add config to force clean entity memory that don't belong to current tick region + 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) { // Luminol - Add config to force clean entity memory that don't belong to current tick region ++ this.memories.values().forEach(slot -> slot.tick(body)); // Luminol - Add config to force clean entity memory that don't belong to current tick region + } + + 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..aa60fbfbe7283f1ed9cf5343d59d1222380857dd 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 + ) { ++ // Luminol - Do not set walk target if target position is out of current tick region ++ if (!target.checkThread(walker.level())) { ++ return; ++ } ++ // Luminol 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..d83b353395bad7c6c41dfaa24018fd207904d995 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 + "}"; + } ++ ++ // Luminol start - Fix a series issue around entity memory typed GlobalPos and WalkTarget ++ @Override ++ public boolean checkThread(net.minecraft.world.level.Level currOwnedByLevel) { ++ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(currOwnedByLevel, this.blockPos); ++ } ++ // Luminol end + } +diff --git a/net/minecraft/world/entity/ai/behavior/EntityTracker.java b/net/minecraft/world/entity/ai/behavior/EntityTracker.java +index 3ac52b025ac3e1a3f9135b8e593a385a847afe0f..29c13f18cff52e9950e8932c75cb12fb02f952cb 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; + } ++ ++ // Luminol start - Fix a series issue around entity memory typed GlobalPos and WalkTarget ++ @Override ++ public boolean checkThread(net.minecraft.world.level.Level currOwnedByLevel) { ++ return ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.entity); ++ } ++ // Luminol end + } +diff --git a/net/minecraft/world/entity/ai/behavior/PositionTracker.java b/net/minecraft/world/entity/ai/behavior/PositionTracker.java +index ce6cf5ecfb190428e3ef9b7dd39c98e3d27a7b9d..3334da2236fbe90cbdc3e67884f9c6ed80ac0f56 100644 +--- a/net/minecraft/world/entity/ai/behavior/PositionTracker.java ++++ b/net/minecraft/world/entity/ai/behavior/PositionTracker.java +@@ -10,4 +10,7 @@ public interface PositionTracker { + BlockPos currentBlockPosition(); + + boolean isVisibleBy(final LivingEntity body); ++ ++ ++ boolean checkThread(net.minecraft.world.level.Level currOwnedByLevel); // Luminol - Fix a series issue around entity memory typed GlobalPos and WalkTarget + } +diff --git a/net/minecraft/world/entity/ai/behavior/SleepInBed.java b/net/minecraft/world/entity/ai/behavior/SleepInBed.java +index 16017d819c28b077f732e2ef571eac179d24e323..7a598aa1a0963fda3302946a30d0330c341da74f 100644 +--- a/net/minecraft/world/entity/ai/behavior/SleepInBed.java ++++ b/net/minecraft/world/entity/ai/behavior/SleepInBed.java +@@ -57,6 +57,11 @@ public class SleepInBed extends Behavior { + } + } + ++ // Luminol Start - Prevent off-tick-region chunk operations ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(level, target.pos())) { ++ return false; ++ } ++ // Luminol End + 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..3b51a44ab700740047c9e725547ceec65c0c80b1 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) { // Luminol - Add config to force clean entity memory that don't belong to current tick region + if (this.hasValue() && this.canExpire()) { + if (this.hasExpired()) { + this.clear(); +@@ -21,6 +21,41 @@ public class MemorySlot { + this.timeToLive--; + } + } ++ // Luminol start - Add config to force clean entity memory that don't belong to current tick region ++ final net.minecraft.world.level.Level ownerLevel = owner.level(); ++ ++ // type: entity ++ if (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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(); ++ } ++ } ++ // Luminol 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..c18e2071c699d9c5f4d46b7a58191df4f4d8ab5d 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); ++ // Luminol - Recompute path when path finding out of current tick region ++ if (me.earthme.luminol.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; ++ } ++ } ++ // Luminol 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..4c727e4f224f6a31a4cd9d024bdcfaf0fa1bc0e5 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 ++ // Luminol start - Do not path find for targets out of current region ++ if (me.earthme.luminol.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; ++ } ++ } ++ // Luminol end + ProfilerFiller profiler = Profiler.get(); + profiler.push("pathfind"); + BlockPos fromPos = above ? this.mob.blockPosition().above() : this.mob.blockPosition(); +@@ -286,6 +298,17 @@ public abstract class PathNavigation { + + if (!this.isDone()) { + Vec3 target = this.path.getNextEntityPos(this.mob); ++ // Luminol - Recompute path when path finding out of current tick region ++ if (me.earthme.luminol.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; ++ } ++ } ++ // Luminol end + this.mob.getMoveControl().setWantedPosition(target.x, this.getGroundY(target), target.z, this.speedModifier); + } + } +diff --git a/net/minecraft/world/entity/animal/allay/AllayAi.java b/net/minecraft/world/entity/animal/allay/AllayAi.java +index c3667e7997551e0f5ce63bc6ee890082d1431400..2e2458535a8ec542d6371ceba2fff1260773c7f5 100644 +--- a/net/minecraft/world/entity/animal/allay/AllayAi.java ++++ b/net/minecraft/world/entity/animal/allay/AllayAi.java +@@ -112,6 +112,17 @@ public class AllayAi { + Optional likedNoteblockPos = brain.getMemory(MemoryModuleType.LIKED_NOTEBLOCK_POSITION); + if (likedNoteblockPos.isPresent()) { + GlobalPos position = likedNoteblockPos.get(); ++ // Luminol start - Do not like item if they were out of current tickregion ++ 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(); ++ } ++ // Luminol end ++ + 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..a2873d8bbfa3e04678bc31637222b3a10cd6fedd 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 -> { // Luminol start - Do not pathfind out of tickregion ++ // 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 ++ }) // Luminol end ++ && Optional.ofNullable(this.getNavigation().createPath(position, 1)).map(Path::canReach).orElse(false); // Luminol - Do not pathfind out of tickregion - diff on change + } + + private void dropSeed() { diff --git a/lophine-server/minecraft-patches/features/0010-Fix-off-region-leashing.patch b/lophine-server/minecraft-patches/features/0010-Fix-off-region-leashing.patch new file mode 100644 index 0000000..192d13e --- /dev/null +++ b/lophine-server/minecraft-patches/features/0010-Fix-off-region-leashing.patch @@ -0,0 +1,38 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 17 Apr 2026 23:16:45 +0800 +Subject: [PATCH] Fix off region leashing + +PS: My friend reported me this bug and I still need to check the details of how the bug was caused + +XD + +diff --git a/net/minecraft/world/entity/Leashable.java b/net/minecraft/world/entity/Leashable.java +index 98034a4c96bf2972316078d3b3a49140921aab50..284ba0c3742d5d5ab0b344a84a9c8e2c2a98a74a 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) { ++ // Luminol start - Fix off region leashing ++ if (!ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(leasher)) { ++ entity.spawnAtLocation(serverLevel, Items.LEAD); ++ entity.setLeashData(null); ++ return; ++ } ++ // Luminol end + setLeashedTo(entity, leasher, true); + return; + } + } else if (pos.isPresent()) { ++ // Luminol 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; ++ } ++ // Luminol end + setLeashedTo(entity, LeashFenceKnotEntity.getOrCreateKnot(serverLevel, pos.get()), true); + return; + } diff --git a/lophine-server/minecraft-patches/features/0011-Fix-unpatched-task-returning-of-ServerConfigurationP.patch b/lophine-server/minecraft-patches/features/0011-Fix-unpatched-task-returning-of-ServerConfigurationP.patch new file mode 100644 index 0000000..bd3519d --- /dev/null +++ b/lophine-server/minecraft-patches/features/0011-Fix-unpatched-task-returning-of-ServerConfigurationP.patch @@ -0,0 +1,30 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 17 Apr 2026 23:21:30 +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..97f4c546663fb19558246366a3d8d77f2b9327ef 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()) { // Luminol - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync + this.disconnect(disconnectionInfo); + return; + } + + this.connection.setReadOnly(); +- this.server.scheduleOnMain(() -> { ++ // this.server.scheduleOnMain(() -> { // Luminol - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync ++ io.papermc.paper.threadedregions.RegionizedServer.getInstance().addTask(() -> { // Luminol - Fix unpatched task returning of ServerConfigurationPacketListenerImpl#disconnectAsync + this.disconnect(disconnectionInfo); // Currently you cannot cancel disconnect during the config stage + }); + } diff --git a/lophine-server/minecraft-patches/features/0012-Fix-riding-statistics-desync.patch b/lophine-server/minecraft-patches/features/0012-Fix-riding-statistics-desync.patch new file mode 100644 index 0000000..c26bcc2 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0012-Fix-riding-statistics-desync.patch @@ -0,0 +1,64 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 17 Apr 2026 23:24:07 +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 34cc36f6bdf435593d16f2e7af280a27be048280..98d1ab3aa8dfbe571371d01544da2750339ce988 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) { // Luminol - 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 a2b57f62b745dd21e215f919d335f14f5cb1706f..2cf8bdf6c4ab04ab42829b7af5eb89e6bd509d7a 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4198,7 +4198,13 @@ public abstract class Entity + } + } + ++ // Luminol start - Fix riding statics desync + public void adjustRiders(boolean teleport) { ++ this.adjustRiders(teleport, false); ++ } ++ ++ public void adjustRiders(boolean teleport, boolean syncStatics) { ++ // Luminol end - Fix riding statics desync + java.util.ArrayDeque queue = new java.util.ArrayDeque<>(); + queue.add(this); + +@@ -4211,14 +4217,24 @@ public abstract class Entity + + for (EntityTreeNode passenger : passengers) { + queue.add(passenger); ++ // Luminol start - Fix riding statics desync ++ final double oldX = passenger.root.getX(); ++ final double oldY = passenger.root.getY(); ++ final double oldZ = passenger.root.getZ(); ++ // Luminol end - Fix riding statics desync + curr.root.positionRider(passenger.root, teleport ? Entity::snapTo : Entity::setPos); ++ // Luminol 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); ++ } ++ // Luminol end - Fix riding statics desync + } + } + } + } + + public void repositionAllPassengers(boolean teleport) { +- this.makePassengerTree().adjustRiders(teleport); ++ this.makePassengerTree().adjustRiders(teleport, true); // Luminol - Fix riding statics desync + } + + protected EntityTreeNode makePassengerTree() { diff --git a/lophine-server/minecraft-patches/features/0013-Fix-misbehaved-ender-pearls-when-player-switched-dim.patch b/lophine-server/minecraft-patches/features/0013-Fix-misbehaved-ender-pearls-when-player-switched-dim.patch new file mode 100644 index 0000000..240c251 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0013-Fix-misbehaved-ender-pearls-when-player-switched-dim.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 10:45:19 +0800 +Subject: [PATCH] Fix misbehaved ender pearls when player switched dimension + +A fix of https://github.com/PaperMC/Folia/issues/421 + +Almost the same bug as: +https://github.com/PaperMC/Folia/pull/418 and https://github.com/PaperMC/Folia/issues/393 + +diff --git a/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java b/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java +index 501a0e89cc7bcfce6793f059b080b38666e0fb64..ad35eafa457f706879ba8a9c92b3a34e620ee2d6 100644 +--- a/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java ++++ b/net/minecraft/world/entity/projectile/throwableitemprojectile/ThrownEnderpearl.java +@@ -264,7 +264,8 @@ 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.isAlive() // Luminol - Fix misbehaved ender pearls when player switched dimension ++ && (owner.getBukkitEntity().taskScheduler.isRetired() || serverPlayer.getHealth() <= 0.0D) // Luminol - 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/lophine-server/minecraft-patches/features/0014-Fix-long-commands-support.patch b/lophine-server/minecraft-patches/features/0014-Fix-long-commands-support.patch new file mode 100644 index 0000000..7038394 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0014-Fix-long-commands-support.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Thu, 2 Apr 2026 12:26:24 +0800 +Subject: [PATCH] Fix long commands support + +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..042cb4792b0084f143608f05dd08da133a31e435 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: Mon, 20 Apr 2026 01:02:43 +0800 +Subject: [PATCH] Fix creative player item pick + + +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index 223793bea54a483fefc66caa7e07b29ab351c339..303b9befb30d7156a9a71900ac1a6ea8f7e8d876 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); // Luminol - Fix creative item picking + int remaining = orgCount - canHold; + boolean flyAtPlayer = false; // Paper + diff --git a/lophine-server/minecraft-patches/features/0016-Fix-entity-portal-teleport-speed.patch b/lophine-server/minecraft-patches/features/0016-Fix-entity-portal-teleport-speed.patch new file mode 100644 index 0000000..69ac73a --- /dev/null +++ b/lophine-server/minecraft-patches/features/0016-Fix-entity-portal-teleport-speed.patch @@ -0,0 +1,67 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Mon, 20 Apr 2026 01:10:30 +0800 +Subject: [PATCH] Fix entity portal-teleport speed + + +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index c6434ff2e5c211887c2e98c8462a49e0e1cb5b21..534f103234eb92571c664cbcfea4ce4e990d3ea8 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -1492,7 +1492,27 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + foliaProfiler.startTimer(timerId); + try { + // Folia end - profiler ++ // Luminol start - Entity portal-teleport speed fix + if (isActive) { // Paper - EAR 2 ++ if (!(entity instanceof Player) && entity.teleportTickType == 2) { // Luminol - after portal compensate tick ++ entity.tick(); ++ 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) { // Luminol - 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)) { +@@ -1503,6 +1523,8 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + // portalled + return; + } ++ } ++ // Luminol 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 2cf8bdf6c4ab04ab42829b7af5eb89e6bd509d7a..a52d5c0ba970d666fd57e075d14350ff07401bac 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;// Luminol - Entity portal-teleport speed fix + + public void inactiveTick() { + } +@@ -3595,6 +3596,7 @@ public abstract class Entity + } else { + if (this.portalProcess == null || !this.portalProcess.isSamePortal(portal)) { + this.portalProcess = new PortalProcessor(portal, pos.immutable()); ++ this.teleportTickType = 1; // Luminol - Entity portal-teleport speed fix + } else if (!this.portalProcess.isInsidePortalThisTick()) { + this.portalProcess.updateEntryPosition(pos.immutable()); + this.portalProcess.setAsInsidePortalThisTick(true); diff --git a/lophine-server/minecraft-patches/features/0017-Fix-player-auto-saving-ignores-interval.patch b/lophine-server/minecraft-patches/features/0017-Fix-player-auto-saving-ignores-interval.patch new file mode 100644 index 0000000..29fc78a --- /dev/null +++ b/lophine-server/minecraft-patches/features/0017-Fix-player-auto-saving-ignores-interval.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Wed, 24 Jun 2026 22:52:13 +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 98d1ab3aa8dfbe571371d01544da2750339ce988..264de9099b19779f20ad23c41d0967670759ae12 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 // Luminol - 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/lophine-server/minecraft-patches/features/0018-Force-disable-builtin-spark-plugin.patch b/lophine-server/minecraft-patches/features/0018-Force-disable-builtin-spark-plugin.patch new file mode 100644 index 0000000..220ce28 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0018-Force-disable-builtin-spark-plugin.patch @@ -0,0 +1,98 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Tue, 21 Apr 2026 01:15:09 +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 83b9aab16124ddcc416f7b4cd95084e1c64fe5b7..7683c12d48d23dd43bce3d3b5633029c596930a5 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 // Luminol - Force disable builtin spark + if (this.emptyTicks == emptyTickThreshold) { + LOGGER.info("Server empty for {} seconds, pausing", this.pauseWhenEmptySeconds()); + this.autoSave(); +@@ -1685,7 +1685,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Sun, 28 Jun 2026 19:10:30 +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..b0a01673224522552672b7c8c9aa219108e8010f 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()); // Luminol - Prevent tamable animals check can teleport in an unloaded chunk ++ if (blockStateBelow == null) return false; // Luminol - Prevent tamable animals check can teleport in an unloaded chunk + if (!this.canFlyToOwner() && blockStateBelow.getBlock() instanceof LeavesBlock) { + return false; + } diff --git a/lophine-server/minecraft-patches/features/0020-Prevent-teleprotAsync-calls-in-move-events.patch b/lophine-server/minecraft-patches/features/0020-Prevent-teleprotAsync-calls-in-move-events.patch new file mode 100644 index 0000000..40c0c0f --- /dev/null +++ b/lophine-server/minecraft-patches/features/0020-Prevent-teleprotAsync-calls-in-move-events.patch @@ -0,0 +1,88 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 1 May 2026 21:52:07 +0800 +Subject: [PATCH] Prevent teleprotAsync calls in move events + + +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index c1faaf7d514c23d756aa626fa93aabd8bb503463..0c5fb17e7cce18eac68183cc3b5de6fc6451dc50 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -730,7 +730,9 @@ public class ServerGamePacketListenerImpl + + Location oldTo = to.clone(); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); ++ this.player.blockTeleportAsync = true; // Luminol - Prevent teleprotAsync calls during move events + this.cserver.getPluginManager().callEvent(event); ++ this.player.blockTeleportAsync = false; // Luminol - Prevent teleprotAsync calls during move events + + // If the event is cancelled we move the player back to their old location. + if (event.isCancelled()) { +@@ -1740,7 +1742,9 @@ public class ServerGamePacketListenerImpl + + Location oldTo = to.clone(); + PlayerMoveEvent event = new PlayerMoveEvent(player, from, to); ++ this.player.blockTeleportAsync = true; // Luminol - Prevent teleprotAsync calls during move events + this.cserver.getPluginManager().callEvent(event); ++ this.player.blockTeleportAsync = false; // Luminol - Prevent teleprotAsync calls during move events + + // If the event is cancelled we move the player back to their old location. + if (event.isCancelled()) { +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index a52d5c0ba970d666fd57e075d14350ff07401bac..72f89eaba2ca8b66fa07a6378d93917dec83c688 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4476,12 +4476,25 @@ public abstract class Entity + teleportTarget.cause(), teleportFlags, teleportComplete + ); + } ++ // Luminol start - Prevent teleprotAsync calls in move events ++ public boolean blockTeleportAsync = false; ++ // Luminol end + + public final boolean teleportAsync(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity, + org.bukkit.event.player.PlayerTeleportEvent.TeleportCause cause, long teleportFlags, + java.util.function.Consumer teleportComplete) { + ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread(this, "Cannot teleport entity async"); + ++ // Luminol start - Prevent teleprotAsync calls in move events ++ if (this.blockTeleportAsync && me.earthme.luminol.config.modules.fixes.PreventIncorrectTeleportAsyncConfig.enabled) { ++ if (me.earthme.luminol.config.modules.fixes.PreventIncorrectTeleportAsyncConfig.throwWhenCaught) { ++ throw new IllegalStateException("Call teleportAsync during move events!"); ++ } ++ ++ LOGGER.error("Calling teleportAsync during move events!", new Throwable()); ++ return false; ++ } ++ // Luminol end + if (!ServerLevel.isInSpawnableBounds(new BlockPos(ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockX(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockY(pos), ca.spottedleaf.moonrise.common.util.CoordinateUtils.getBlockZ(pos)))) { + return false; + } +diff --git a/net/minecraft/world/entity/vehicle/boat/AbstractBoat.java b/net/minecraft/world/entity/vehicle/boat/AbstractBoat.java +index 3fcd534cee1f605a710ffc43eff4b5106c7dd40c..9b887b40236a212de513e3ceece1abfec5372ae2 100644 +--- a/net/minecraft/world/entity/vehicle/boat/AbstractBoat.java ++++ b/net/minecraft/world/entity/vehicle/boat/AbstractBoat.java +@@ -282,7 +282,9 @@ public abstract class AbstractBoat extends VehicleEntity implements Leashable { + + if (this.lastLocation != null && !this.lastLocation.equals(to)) { + org.bukkit.event.vehicle.VehicleMoveEvent event = new org.bukkit.event.vehicle.VehicleMoveEvent(vehicle, this.lastLocation, to); ++ this.blockTeleportAsync = true; // Luminol - Prevent teleprotAsync calls during move events + event.callEvent(); ++ this.blockTeleportAsync = false; // Luminol - Prevent teleprotAsync calls during move events + } + this.lastLocation = vehicle.getLocation(); + // CraftBukkit end +diff --git a/net/minecraft/world/entity/vehicle/minecart/AbstractMinecart.java b/net/minecraft/world/entity/vehicle/minecart/AbstractMinecart.java +index 5ad77636d0681d1f0341cff4482ba6ad52f3583b..0ae8b5222c5f2f608af352cbaa866c493dd065aa 100644 +--- a/net/minecraft/world/entity/vehicle/minecart/AbstractMinecart.java ++++ b/net/minecraft/world/entity/vehicle/minecart/AbstractMinecart.java +@@ -318,7 +318,9 @@ public abstract class AbstractMinecart extends VehicleEntity { + new org.bukkit.event.vehicle.VehicleUpdateEvent(vehicle).callEvent(); + + if (!from.equals(to)) { ++ this.blockTeleportAsync = true; // Luminol - Prevent teleprotAsync calls during move events + new org.bukkit.event.vehicle.VehicleMoveEvent(vehicle, from, to).callEvent(); ++ this.blockTeleportAsync = false; // Luminol - Prevent teleprotAsync calls during move events + } + // CraftBukkit end + this.updateFluidInteraction(); diff --git a/lophine-server/minecraft-patches/features/0021-Sync-dragon-part-when-teleportation-or-firstly-creat.patch b/lophine-server/minecraft-patches/features/0021-Sync-dragon-part-when-teleportation-or-firstly-creat.patch new file mode 100644 index 0000000..34524ca --- /dev/null +++ b/lophine-server/minecraft-patches/features/0021-Sync-dragon-part-when-teleportation-or-firstly-creat.patch @@ -0,0 +1,47 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 19:19:31 +0800 +Subject: [PATCH] Sync dragon part when teleportation or firstly created + + +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..75c36cbf86e56d4993169688a13f22ccef555d92 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(); // Luminol - Sync dragon part when teleportation or firstly created + entity.registerScheduler(); // Paper - optimise Folia entity scheduler + } + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 72f89eaba2ca8b66fa07a6378d93917dec83c688..6b7382f06a8c76b216b081e66789fe51466b1e29 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4460,6 +4460,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(); // Luminol - Sync dragon part when teleportation or firstly created + // 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..fd85816ec78c99f45c740ebf8052ff41f4859ed6 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 ++ ++ // Luminol start - Sync dragon part when teleportation or firstly created ++ public void syncDragonPartsAfterTeleportTransform() { ++ for (EnderDragonPart part : this.subEntities) { ++ this.tickPart(part, 0.0, 0.0, 0.0); // offset -> 0.0 ++ } ++ } ++ // Luminol end + } diff --git a/lophine-server/minecraft-patches/features/0022-Teleport-async-if-entity-was-moving-to-another-regio.patch b/lophine-server/minecraft-patches/features/0022-Teleport-async-if-entity-was-moving-to-another-regio.patch new file mode 100644 index 0000000..ba478fc --- /dev/null +++ b/lophine-server/minecraft-patches/features/0022-Teleport-async-if-entity-was-moving-to-another-regio.patch @@ -0,0 +1,63 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 19:32:34 +0800 +Subject: [PATCH] Teleport async if entity was moving to another region at once + +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 6b7382f06a8c76b216b081e66789fe51466b1e29..8c909108a0c468f353837787188c12f907369b9b 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; + } ++ //Luminol start - Fix high position moving ++ // Filter the threads as it may be called by the chunk system worker thread ++ if (me.earthme.luminol.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 me.earthme.luminol.utils.EntityMoveOutOfRegionException(this, delta, moverType); ++ } ++ } ++ } ++ //Luminol 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..dc28223684091cd0d8dbf7ff01626b20e4f1b662 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 (me.earthme.luminol.utils.EntityMoveOutOfRegionException moveOutOfRegionException) { // Luminol - Teleport async if entity was moving to another region at once ++ // Luminol start - Teleport async if entity was moving to another region at once ++ final Entity ent = moveOutOfRegionException.getEntity(); ++ var currPosition = ent.position(); ++ var toPosition = moveOutOfRegionException.getMovement().add(currPosition); ++ ++ if (me.earthme.luminol.config.modules.fixes.FoliaEntityMovingFixConfig.warnOnDetected) { ++ MinecraftServer.LOGGER.warn("Entity {} with entityId {} has tried moving to another region!",ent, ent.getId()); ++ } ++ ++ ent.getBukkitEntity().taskScheduler.schedule(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 ++ ), null, 1L); ++ // Luminol 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/lophine-server/minecraft-patches/features/0023-Temporarily-fix-teleport-yam-and-pitch.patch b/lophine-server/minecraft-patches/features/0023-Temporarily-fix-teleport-yam-and-pitch.patch new file mode 100644 index 0000000..47af9a0 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0023-Temporarily-fix-teleport-yam-and-pitch.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Tue, 21 Apr 2026 01:28:05 +0800 +Subject: [PATCH] Temporarily fix teleport yam and pitch + + +diff --git a/net/minecraft/server/commands/TeleportCommand.java b/net/minecraft/server/commands/TeleportCommand.java +index 13d7965fd4f99f0848b080349df41f8b1c31a19b..37268fe5d1f21d5b08ed4a1c0e5777bcb062c8e6 100644 +--- a/net/minecraft/server/commands/TeleportCommand.java ++++ b/net/minecraft/server/commands/TeleportCommand.java +@@ -248,8 +248,10 @@ 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); // Luminol - fix teleport yaw issue ++ //Float pitchFinal = Float.valueOf(newXRot); // Luminol - fix teleport yaw issue ++ Float yawFinal = Float.valueOf(newYRot + victim.getYRot()); // Luminol - fix teleport yaw issue ++ Float pitchFinal = Float.valueOf(newXRot + victim.getXRot()); // Luminol - fix teleport pitch issue + victim.getBukkitEntity().taskScheduler.schedule((Entity nmsEntity) -> { + nmsEntity.stopRiding(); + nmsEntity.teleportAsync( diff --git a/lophine-server/minecraft-patches/features/0024-Improved-Server-Health-Report.patch b/lophine-server/minecraft-patches/features/0024-Improved-Server-Health-Report.patch new file mode 100644 index 0000000..d1f8dce --- /dev/null +++ b/lophine-server/minecraft-patches/features/0024-Improved-Server-Health-Report.patch @@ -0,0 +1,288 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 19:47:24 +0800 +Subject: [PATCH] Improved Server Health Report + + +diff --git a/io/papermc/paper/threadedregions/commands/CommandServerHealth.java b/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +index aa860cfe9de616f185163fe74e0f4f49bd13a805..05d4f73340412f737224cb1443ecf1e33586ed38 100644 +--- a/io/papermc/paper/threadedregions/commands/CommandServerHealth.java ++++ b/io/papermc/paper/threadedregions/commands/CommandServerHealth.java +@@ -29,6 +29,14 @@ import java.util.ArrayList; + import java.util.Arrays; + import java.util.List; + import java.util.Locale; ++// Luminol start - Improved Server Health Report ++import java.util.concurrent.TimeUnit; ++import java.lang.management.ManagementFactory; ++import java.lang.management.MemoryMXBean; ++import java.lang.management.MemoryUsage; ++import java.lang.management.RuntimeMXBean; ++import org.jetbrains.annotations.NotNull; ++// Luminol end + + public final class CommandServerHealth extends Command { + +@@ -45,7 +53,7 @@ public final class CommandServerHealth extends Command { + private static final TextColor HEADER = TextColor.color(79, 164, 240); + private static final TextColor PRIMARY = TextColor.color(48, 145, 237); + private static final TextColor SECONDARY = TextColor.color(104, 177, 240); +- private static final TextColor INFORMATION = TextColor.color(145, 198, 243); ++ private static final TextColor INFORMATION = TextColor.color(180, 220, 255); // Luminol start - Improved Server Health Report + private static final TextColor LIST = TextColor.color(33, 97, 188); + + public CommandServerHealth() { +@@ -60,9 +68,11 @@ public final class CommandServerHealth extends Command { + return Component.text() + .append(Component.text(prefix, PRIMARY, TextDecoration.BOLD)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) +- .append(Component.text("% util at ", PRIMARY)) ++ .append(Component.text("% util", PRIMARY)) ++ .append(Component.text(" | ", SECONDARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) +- .append(Component.text(" MSPT at ", PRIMARY)) ++ .append(Component.text(" mspt", PRIMARY)) ++ .append(Component.text(" | ", SECONDARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) + .append(Component.text(" TPS" + (newline ? "\n" : ""), PRIMARY)) + .build(); +@@ -81,7 +91,7 @@ public final class CommandServerHealth extends Command { + + private static boolean executeRegion(final CommandSender sender, final String commandLabel, final String[] args) { + final ThreadedRegionizer.ThreadedRegion region = +- TickRegionScheduler.getCurrentRegion(); ++ TickRegionScheduler.getCurrentRegion(); + if (region == null) { + sender.sendMessage(Component.text("You are not in a region currently", NamedTextColor.RED)); + return true; +@@ -105,8 +115,7 @@ public final class CommandServerHealth extends Command { + final double tps1m = report1m.tpsData().segmentAll().average(); + final double mspt1m = report1m.timePerTickData().segmentAll().average() / 1.0E6; + +- final int yLoc = 80; +- final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; ++ final String location = world.getWorld().getName() + " (" + centerBlockX + ", " + centerBlockZ + ")"; + + final Component line = Component.text() + .append(Component.text("Region around block ", PRIMARY)) +@@ -144,7 +153,7 @@ public final class CommandServerHealth extends Command { + } + + final List> regions = +- new ArrayList<>(); ++ new ArrayList<>(); + + for (final World bukkitWorld : Bukkit.getWorlds()) { + final ServerLevel world = ((CraftWorld)bukkitWorld).getHandle(); +@@ -163,6 +172,18 @@ public final class CommandServerHealth extends Command { + final long currTime = System.nanoTime(); + final TickData.TickReportData globalTickReport = RegionizedServer.getGlobalTickData().getTickReport15s(currTime); + ++ final MemoryMXBean memoryBean = ManagementFactory.getMemoryMXBean(); ++ final MemoryUsage heapUsage = memoryBean.getHeapMemoryUsage(); ++ final long usedMemory = heapUsage.getUsed() / (1024 * 1024); ++ final long maxMemory = heapUsage.getMax() / (1024 * 1024); ++ ++ final double memPercent = (double) usedMemory / maxMemory * 100.0; ++ final TextColor memColor = memPercent < 60 ? CommandUtil.getUtilisationColourRegion(0.0) : (memPercent < 85 ? NamedTextColor.YELLOW : NamedTextColor.RED); ++ ++ final RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean(); ++ final long uptime = runtimeBean.getUptime(); ++ final String uptimeStr = formatUptime(uptime); ++ + for (final ThreadedRegionizer.ThreadedRegion region : regions) { + final TickData.TickReportData report = region.getData().getRegionSchedulingHandle().getTickReport15s(currTime); + tpsByRegion.add(report == null ? 20.0 : report.tpsData().segmentAll().average()); +@@ -174,31 +195,27 @@ public final class CommandServerHealth extends Command { + final double loadRate = ca.spottedleaf.moonrise.patches.chunk_system.scheduling.task.ChunkFullTask.loadRate(currTime); + + totalUtil += globalTickReport.utilisation(); ++ final TextColor utilisationColor = CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount); + + tpsByRegion.sort(null); + if (!tpsByRegion.isEmpty()) { + minTps = tpsByRegion.getDouble(0); + maxTps = tpsByRegion.getDouble(tpsByRegion.size() - 1); +- + final int middle = tpsByRegion.size() >> 1; + if ((tpsByRegion.size() & 1) == 0) { +- // even, average the two middle points + medianTps = (tpsByRegion.getDouble(middle - 1) + tpsByRegion.getDouble(middle)) / 2.0; + } else { +- // odd, can just grab middle + medianTps = tpsByRegion.getDouble(middle); + } + } else { +- // no regions = green + minTps = medianTps = maxTps = 20.0; + } + + final List, TickData.TickReportData>> +- regionsBelowThreshold = new ArrayList<>(); ++ regionsBelowThreshold = new ArrayList<>(); + + for (int i = 0, len = regions.size(); i < len; ++i) { + final TickData.TickReportData report = reportsByRegion.get(i); +- + regionsBelowThreshold.add(new ObjectObjectImmutablePair<>(regions.get(i), report)); + } + +@@ -207,11 +224,18 @@ public final class CommandServerHealth extends Command { + final TickData.TickReportData report2 = p2.right(); + final double util1 = report1 == null ? 0.0 : report1.utilisation(); + final double util2 = report2 == null ? 0.0 : report2.utilisation(); +- +- // we want the largest first + return Double.compare(util2, util1); + }); + ++ long totalChunks = 0; ++ long totalEntities = 0; ++ ++ for (final ThreadedRegionizer.ThreadedRegion region : regions) { ++ final TickRegions.RegionStats stats = region.getData().getRegionStats(); ++ totalChunks += stats.getChunkCount(); ++ totalEntities += stats.getEntityCount(); ++ } ++ + final TextComponent.Builder lowestRegionsBuilder = Component.text(); + + if (sender instanceof Player) { +@@ -219,23 +243,18 @@ public final class CommandServerHealth extends Command { + } + for (int i = 0, len = Math.min(lowestRegionsCount, regionsBelowThreshold.size()); i < len; ++i) { + final ObjectObjectImmutablePair, TickData.TickReportData> +- pair = regionsBelowThreshold.get(i); ++ pair = regionsBelowThreshold.get(i); + + final TickData.TickReportData report = pair.right(); + final ThreadedRegionizer.ThreadedRegion region = +- pair.left(); ++ pair.left(); + +- if (report == null) { +- // skip regions with no data +- continue; +- } ++ if (report == null) continue; + + final ServerLevel world = region.regioniser.world; + final ChunkPos chunkCenter = region.getCenterChunk(); +- if (chunkCenter == null) { +- // region does not exist anymore +- continue; +- } ++ if (chunkCenter == null) continue; ++ + final int centerBlockX = ((chunkCenter.x() << 4) | 7); + final int centerBlockZ = ((chunkCenter.z() << 4) | 7); + final double util = report.utilisation(); +@@ -243,18 +262,21 @@ public final class CommandServerHealth extends Command { + final double mspt = report.timePerTickData().segmentAll().average() / 1.0E6; + + final int yLoc = 80; +- final String location = "[w:'" + world.getWorld().getName() + "'," + centerBlockX + "," + yLoc + "," + centerBlockZ + "]"; ++ final String location = world.getWorld().getName() + " (" + centerBlockX + ", " + centerBlockZ + ")"; ++ + final Component line = Component.text() + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) +- .append(Component.text("Region around block ", PRIMARY)) ++ .append(Component.text("Region at ", PRIMARY)) + .append(Component.text(location, INFORMATION)) + .append(Component.text(":\n", PRIMARY)) + + .append(Component.text(" ", PRIMARY)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(util * 100.0), CommandUtil.getUtilisationColourRegion(util))) +- .append(Component.text("% util at ", PRIMARY)) ++ .append(Component.text("% util", PRIMARY)) ++ .append(Component.text(" | ", SECONDARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(mspt), CommandUtil.getColourForMSPT(mspt))) +- .append(Component.text(" MSPT at ", PRIMARY)) ++ .append(Component.text(" mspt", PRIMARY)) ++ .append(Component.text(" | ", SECONDARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(tps), CommandUtil.getColourForTPS(tps))) + .append(Component.text(" TPS\n", PRIMARY)) + +@@ -270,29 +292,53 @@ public final class CommandServerHealth extends Command { + + sender.sendMessage( + Component.text() +- .append(Component.text("Server Health Report\n", HEADER, TextDecoration.BOLD)) ++ .append(Component.text("Server Health Report", HEADER, TextDecoration.BOLD)) ++ .append(Component.text(" (Uptime: ", SECONDARY)) ++ .append(Component.text(uptimeStr, utilisationColor)) ++ .append(Component.text(")\n", SECONDARY)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Online Players: ", PRIMARY)) +- .append(Component.text(Bukkit.getOnlinePlayers().size() + "\n", INFORMATION)) ++ .append(Component.text(Bukkit.getOnlinePlayers().size(), INFORMATION)) ++ .append(Component.newline()) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Total regions: ", PRIMARY)) +- .append(Component.text(regions.size() + "\n", INFORMATION)) ++ .append(Component.text(regions.size(), INFORMATION)) ++ .append(Component.text(", ", PRIMARY)) ++ .append(Component.text("Total Chunks: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format(totalChunks), INFORMATION)) ++ .append(Component.text(", ", PRIMARY)) ++ .append(Component.text("Total Entities: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format(totalEntities) + "\n", INFORMATION)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Utilisation: ", PRIMARY)) +- .append(Component.text(ONE_DECIMAL_PLACES.get().format(totalUtil * 100.0), CommandUtil.getUtilisationColourRegion(totalUtil / (double)maxThreadCount))) +- .append(Component.text("% / ", PRIMARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(totalUtil * 100.0), utilisationColor)) ++ .append(Component.text("%", PRIMARY)) ++ .append(Component.text(" / ", SECONDARY)) + .append(Component.text(ONE_DECIMAL_PLACES.get().format(maxThreadCount * 100.0), INFORMATION)) + .append(Component.text("%\n", PRIMARY)) + + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Load rate: ", PRIMARY)) +- .append(Component.text(TWO_DECIMAL_PLACES.get().format(loadRate) + ", ", INFORMATION)) ++ .append(Component.text(TWO_DECIMAL_PLACES.get().format(loadRate), INFORMATION)) ++ .append(Component.text(", ", PRIMARY)) + .append(Component.text("Gen rate: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(genRate) + "\n", INFORMATION)) + ++ .append(Component.text(" - ", LIST, TextDecoration.BOLD)) ++ .append(Component.text("Memory: ", PRIMARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format(usedMemory), memColor)) ++ .append(Component.text(" MB", memColor)) ++ .append(Component.text(" / ", SECONDARY)) ++ .append(Component.text(NO_DECIMAL_PLACES.get().format(maxMemory), INFORMATION)) ++ .append(Component.text(" MB", INFORMATION)) ++ .append(Component.text(" (", SECONDARY)) ++ .append(Component.text(ONE_DECIMAL_PLACES.get().format(memPercent) + "%", memColor)) ++ .append(Component.text(")", SECONDARY)) ++ .append(Component.newline()) ++ + .append(Component.text(" - ", LIST, TextDecoration.BOLD)) + .append(Component.text("Lowest Region TPS: ", PRIMARY)) + .append(Component.text(TWO_DECIMAL_PLACES.get().format(minTps) + "\n", CommandUtil.getColourForTPS(minTps))) +@@ -361,4 +407,21 @@ public final class CommandServerHealth extends Command { + } + return new ArrayList<>(); + } ++ ++ // Luminol start - Improved Server Health Report ++ private static @NotNull String formatUptime(long uptimeMillis) { ++ long days = TimeUnit.MILLISECONDS.toDays(uptimeMillis); ++ long hours = TimeUnit.MILLISECONDS.toHours(uptimeMillis) % 24; ++ long minutes = TimeUnit.MILLISECONDS.toMinutes(uptimeMillis) % 60; ++ long seconds = TimeUnit.MILLISECONDS.toSeconds(uptimeMillis) % 60; ++ ++ StringBuilder sb = new StringBuilder(); ++ if (days > 0) sb.append(days).append("d "); ++ if (hours > 0) sb.append(hours).append("h "); ++ if (minutes > 0) sb.append(minutes).append("m "); ++ sb.append(seconds).append("s"); ++ ++ return sb.toString(); ++ } ++ // Luminol end + } +\ No newline at end of file diff --git a/lophine-server/minecraft-patches/features/0025-World-load-unload-APIs.patch b/lophine-server/minecraft-patches/features/0025-World-load-unload-APIs.patch new file mode 100644 index 0000000..cbd4202 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0025-World-load-unload-APIs.patch @@ -0,0 +1,522 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 1 May 2026 18:37:50 +0800 +Subject: [PATCH] World load/unload APIs +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +好吧这里这一坨需要说明的太多了而且我也不是很会翻译所以就用中文注释了) + +这一整个实现使用了一种安全点的思路,相对于folia pr里和canvas的让每个region自己卸载, 我更偏向于将调度器拆分到每个level中然后实现类似于单个level停服的逻辑,这样会更加符合直觉而且实现要更加快速而且易于排查问题 + +由于世界卸载可能发生在玩家登录或者传送的过程中,所以我使用了引用计数器来避免这个竟态条件的发生 + +diff --git a/io/papermc/paper/threadedregions/RegionShutdownThread.java b/io/papermc/paper/threadedregions/RegionShutdownThread.java +index f2208bcd5ccf3bd5a428e8ea3bf3ff26b3c9483b..d5c4e9b82f2ebc135b6747d19678b1a15e808ec8 100644 +--- a/io/papermc/paper/threadedregions/RegionShutdownThread.java ++++ b/io/papermc/paper/threadedregions/RegionShutdownThread.java +@@ -14,11 +14,11 @@ import java.util.ArrayList; + import java.util.List; + import java.util.concurrent.TimeUnit; + +-public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread { ++public class RegionShutdownThread extends ca.spottedleaf.moonrise.common.util.TickThread { // Luminol - Level schedulers - Make public + +- private static final Logger LOGGER = LogUtils.getClassLogger(); ++ public static final Logger LOGGER = LogUtils.getClassLogger(); // Luminol - Level schedulers - Make public + +- ThreadedRegionizer.ThreadedRegion shuttingDown; ++ public ThreadedRegionizer.ThreadedRegion shuttingDown; // Luminol - Level schedulers - Make public + + public RegionShutdownThread(final String name) { + super(name); +@@ -52,7 +52,7 @@ public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.u + // the data required to do that is regionised, and we can only access it when we OWN the region, and we do not. + // Thus, the only operation that the shutdown thread will perform + +- private void saveLevelData(final ServerLevel world) { ++ public void saveLevelData(final ServerLevel world) { // Luminol - Level hot unload apis - Make public + try { + world.saveLevelData(true); + } catch (final Throwable thr) { +@@ -60,7 +60,7 @@ public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.u + } + } + +- private void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region, ++ public void finishTeleportations(final ThreadedRegionizer.ThreadedRegion region, // Luminol - Level hot unload apis - Make public + final ServerLevel world) { + try { + this.shuttingDown = region; +@@ -94,7 +94,7 @@ public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.u + } + } + +- private void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region, ++ public void saveRegionChunks(final ThreadedRegionizer.ThreadedRegion region, // Luminol - Level hot unload apis - Make public + final boolean last) { + ChunkPos center = null; + try { +@@ -109,7 +109,7 @@ public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.u + } + } + +- private void haltChunkSystem(final ServerLevel world) { ++ public void haltChunkSystem(final ServerLevel world) { // Luminol - Level hot unload apis - Make public + try { + world.moonrise$getChunkTaskScheduler().chunkHolderManager.close(false, true, true, false, false); + } catch (final Throwable thr) { +@@ -117,7 +117,7 @@ public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.u + } + } + +- private void closePlayerInventories(final ThreadedRegionizer.ThreadedRegion region) { ++ public void closePlayerInventories(final ThreadedRegionizer.ThreadedRegion region) { // Luminol - Level hot unload apis - Make public + ChunkPos center = null; + try { + this.shuttingDown = region; +@@ -150,7 +150,7 @@ public final class RegionShutdownThread extends ca.spottedleaf.moonrise.common.u + } + + @Override +- public final void run() { ++ public void run() { // Luminol - Level hot unload apis - Make public + // await scheduler termination + LOGGER.info("Awaiting scheduler termination for 60s..."); + if (TickRegions.getScheduler().halt(true, TimeUnit.SECONDS.toNanos(60L))) { +diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java +index aa575f3b76ef70ffb9f0410e7e5cfe7af384bfbd..ff90abb47203c03636d3175f2b3efcf43517b3b0 100644 +--- a/io/papermc/paper/threadedregions/RegionizedServer.java ++++ b/io/papermc/paper/threadedregions/RegionizedServer.java +@@ -63,6 +63,12 @@ public final class RegionizedServer { + this.worlds.add(world); + } + ++ // Luminol start - Level hot unload apis ++ public void removeWorld(final ServerLevel world) { ++ this.worlds.remove(world); ++ } ++ // Luminol end ++ + public void init() { + // call init event _before_ scheduling anything + new RegionizedServerInitEvent().callEvent(); +diff --git a/io/papermc/paper/threadedregions/TickRegionScheduler.java b/io/papermc/paper/threadedregions/TickRegionScheduler.java +index 5a72fa0d72bfdb927293abe921c57f6ae964a6ec..f8344f640f119e3865f2718d1abd0ede9bdb8aa8 100644 +--- a/io/papermc/paper/threadedregions/TickRegionScheduler.java ++++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java +@@ -49,7 +49,7 @@ public final class TickRegionScheduler { + } + // Folia end - watchdog + +- private final Scheduler scheduler; ++ public final Scheduler scheduler; // Luminol - Level schedulers - Make public + + public static enum SchedulerType { + EDF, +@@ -220,7 +220,15 @@ public final class TickRegionScheduler { + */ + public void scheduleRegion(final RegionScheduleHandle region) { + region.scheduler = this; ++ // this.scheduler.schedule(region); // Luminol - Level schedulers ++ // Luminol start - Level schedulers ++ if(region.region != null) { ++ region.region.world.levelScheduler.schedule(region); ++ return; ++ } ++ + this.scheduler.schedule(region); ++ // Luminol end + } + + /** +@@ -233,13 +241,77 @@ public final class TickRegionScheduler { + region.markNonSchedulable(); + } + ++ // Luminol start - Level schedulers ++ private void handleParentSchedulerDead() { ++ for (ServerLevel level : MinecraftServer.getServer().getAllLevels()) { ++ level.levelScheduler.onParentHalted(); ++ } ++ } ++ ++ public boolean haltAndWaitLevelSchedulerHalt(final boolean sync, final long maxWaitNS) { ++ final long deadline = System.nanoTime() + maxWaitNS; ++ ++ boolean hasUnhaltedLevelScheduler = false; ++ for (net.minecraft.server.level.ServerLevel serverLevel : MinecraftServer.getServer().getAllLevels()) { ++ serverLevel.levelScheduler.halt(); // ensure it's halted so that we could skip running any other new tasks ++ ++ // block any new reading reference acquiring going into this lock ++ serverLevel.levelUnloadStateLock.blockReadingReferencing(); ++ ++ // sync not required, directly go next ++ if (!sync) { ++ hasUnhaltedLevelScheduler |= !serverLevel.levelScheduler.isFullyExited(); ++ continue; ++ } ++ ++ final boolean writeLockHeld = serverLevel.levelUnloadStateLock.acquireWrite(deadline); ++ // we got the write lock, abort the others unloading logics ++ if (writeLockHeld) { ++ serverLevel.levelUnloadStateLock.acquireUnreachable(); ++ ++ // since there is no unloading tasks, so the callback would be always empty, just directly go next, ++ // and we'll handle the final saving in global shutdown logics ++ continue; ++ } ++ ++ // cannot get write lock, might be marked as unreachable ++ final boolean isUnreachable = serverLevel.levelUnloadStateLock.isUnreachable(); ++ // already fully terminated (we mark it as unreachable after all separate unload saving logics), check the scheduler state ++ if (isUnreachable) { ++ continue; ++ } ++ ++ // try linearly block wait for halt ++ long failures = 0L; ++ while (!serverLevel.levelScheduler.isFullyExited()) { ++ // check deadline ++ if (deadline - System.nanoTime() <= 0) { ++ return false; ++ } ++ ++ failures = ca.spottedleaf.concurrentutil.util.ConcurrentUtil.linearLongBackoffDeadline(failures, 100, 1000L, deadline); ++ } ++ } ++ ++ return !hasUnhaltedLevelScheduler; ++ } ++ // Luminol end - Level schedulers ++ + public boolean halt(final boolean sync, final long maxWaitNS) { +- this.scheduler.halt(); ++ final boolean allLevelSchedulersExited = this.haltAndWaitLevelSchedulerHalt(sync, maxWaitNS); // Luminol - Level schedulers ++ if (allLevelSchedulersExited) this.scheduler.halt(); // Luminol - Level schedulers + if (!sync) { + return this.scheduler.getAliveThreads().length == 0; + } + +- return this.scheduler.join(maxWaitNS == 0L ? 0L : Math.max(1L, TimeUnit.NANOSECONDS.toMillis(maxWaitNS))); ++ // return this.scheduler.join(maxWaitNS == 0L ? 0L : Math.max(1L, TimeUnit.NANOSECONDS.toMillis(maxWaitNS))); // Luminol - Level schedulers ++ // Luminol start - Level schedulers ++ final boolean waitResult = allLevelSchedulersExited && this.scheduler.join(maxWaitNS == 0L ? 0L : Math.max(1L, TimeUnit.NANOSECONDS.toMillis(maxWaitNS))); ++ ++ this.handleParentSchedulerDead(); ++ ++ return waitResult; ++ // Luminol - end + } + + void dumpAliveThreadTraces(final String reason) { +@@ -251,7 +323,15 @@ public final class TickRegionScheduler { + } + + public void setHasTasks(final RegionScheduleHandle region) { ++ //this.scheduler.notifyTasks(region); // Luminol - Level schedulers ++ // Luminol start - Level schedulers ++ if (region.region != null) { ++ region.region.world.levelScheduler.notifyTasks(region); ++ return; ++ } ++ + this.scheduler.notifyTasks(region); ++ // Luminol end + } + + private void uncaughtException(final Thread thread, final Throwable thr) { +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 7683c12d48d23dd43bce3d3b5633029c596930a5..ab61d18845827ac835edf92651d0b510358106ff 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -236,7 +236,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop registries; +- private Map, ServerLevel> levels = Maps.newLinkedHashMap(); ++ private volatile Map, ServerLevel> levels = Maps.newLinkedHashMap(); // Luminol - World hot load/unload apis + private PlayerList playerList; + private volatile boolean running = true; + private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart +@@ -2034,14 +2034,14 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop, ServerLevel> oldLevels = this.levels; + Map, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); + newLevels.put(level.dimension(), level); + this.levels = Collections.unmodifiableMap(newLevels); + } + +- public void removeLevel(ServerLevel level) { ++ public synchronized void removeLevel(ServerLevel level) { // Luminol - World hot load/unload apis + Map, ServerLevel> oldLevels = this.levels; + Map, ServerLevel> newLevels = Maps.newLinkedHashMap(oldLevels); + newLevels.remove(level.dimension()); +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 534f103234eb92571c664cbcfea4ce4e990d3ea8..c4d9d890db784d376b876d70f0468df6edb75168 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -221,6 +221,12 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + private final StructureCheck structureCheck; + public final boolean tickTime; // Folia - region threading + private final LevelDebugSynchronizers debugSynchronizers = new LevelDebugSynchronizers(this); ++ // Luminol start - Level schedulers ++ private static final java.util.concurrent.atomic.AtomicInteger UNLOADER_ID_GEN = new java.util.concurrent.atomic.AtomicInteger(0); ++ public final me.earthme.luminol.utils.thread.LevelAwareRegionScheduler levelScheduler = new me.earthme.luminol.utils.thread.LevelAwareRegionScheduler(io.papermc.paper.threadedregions.TickRegions.getScheduler().scheduler); ++ public final me.earthme.luminol.utils.thread.SimpleReferenceRWLock levelUnloadStateLock = new me.earthme.luminol.utils.thread.SimpleReferenceRWLock(); ++ public final me.earthme.luminol.utils.thread.LevelSubRegionShutdownThread levelUnloader = new me.earthme.luminol.utils.thread.LevelSubRegionShutdownThread("Level Unload Thread - " + UNLOADER_ID_GEN.getAndIncrement(), this); ++ // Luminol end + + // CraftBukkit start + private final ResourceKey typeKey; +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 264de9099b19779f20ad23c41d0967670759ae12..ab6229ee412cf2aa5affc87510230c587765080b 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -1705,7 +1705,30 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + + ServerLevel origin = this.level(); + ServerPlayer.RespawnConfig respawnConfig = this.getRespawnConfig(); +- ServerLevel respawnWorld = this.server.getLevel(ServerPlayer.RespawnConfig.getDimensionOrDefault(respawnConfig)); ++ ServerLevel respawnWorldLookup = this.server.getLevel(ServerPlayer.RespawnConfig.getDimensionOrDefault(respawnConfig)); // Luminol - Level hot unload apis ++ // Luminol start - Level hot unload apis ++ var toLockRelease = (ServerLevel)null; ++ if (respawnWorldLookup != null) { ++ if (respawnWorldLookup.levelUnloadStateLock.acquireRead()) { ++ toLockRelease = respawnWorldLookup; ++ } else { ++ respawnWorldLookup = null; ++ } ++ } ++ ++ final ServerLevel respawnWorld = respawnWorldLookup; ++ final ServerLevel finalToLockRelease = toLockRelease; ++ ++ var finalRespawnCompleteCallback = (java.util.function.Consumer) player -> { ++ if (finalToLockRelease != null) { ++ finalToLockRelease.levelUnloadStateLock.releaseRead(); ++ } ++ ++ if (respawnComplete != null) { ++ respawnComplete.accept(player); ++ } ++ }; ++ // Luminol end + + // modified based off PlayerList#respawn + +@@ -1756,8 +1779,8 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + // now the respawn logic is complete + + // last, call the function callback +- if (respawnComplete != null) { +- respawnComplete.accept(ServerPlayer.this); ++ if (finalRespawnCompleteCallback != null) { // Luminol - Level hot unload apis ++ finalRespawnCompleteCallback.accept(ServerPlayer.this); // Luminol - Level hot unload apis + } + } + ); +diff --git a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +index 97f4c546663fb19558246366a3d8d77f2b9327ef..e5304e9d1ea57c01a1147621a42a9b584878f765 100644 +--- a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +@@ -210,8 +210,23 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + + this.switchToMain = serverPlayer; + // now the connection responsibility is transferred to the region +- final net.minecraft.server.level.ServerLevel world = this.prepareSpawnTask.getSpawnWorld(); ++ net.minecraft.server.level.ServerLevel worldLookup = this.prepareSpawnTask.getSpawnWorld(); // Luminol - Level hot unload apis - world -> worldLookup, make mutable + final net.minecraft.world.level.ChunkPos chunkPos = net.minecraft.world.level.ChunkPos.containing(net.minecraft.core.BlockPos.containing(this.prepareSpawnTask.getSpawnPosition())); ++ // Luminol start - Level hot unload apis ++ if (!worldLookup.levelUnloadStateLock.acquireRead()) { ++ LOGGER.warn("Failed to acquire read reference for level {} for player placement, sending back to overworld!", worldLookup.dimension()); ++ ++ worldLookup = net.minecraft.server.MinecraftServer.getServer().overworld(); ++ if (!worldLookup.levelUnloadStateLock.acquireRead()) { ++ throw new java.lang.IllegalStateException("Reference read for overworld should be always acquirable!"); ++ } ++ ++ serverPlayer.setServerLevel(worldLookup); ++ } ++ var world = worldLookup; ++ // note: the following logics would always be successfully executed, otherwise the server go crashed ++ // so do not care the exception handling of the callbacks following ++ // Luminol end + world.moonrise$getChunkTaskScheduler().scheduleTickingState( + chunkPos.x(), chunkPos.z(), net.minecraft.server.level.FullChunkStatus.ENTITY_TICKING, true, + ca.spottedleaf.concurrentutil.util.Priority.HIGHER, +@@ -223,9 +238,15 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + ); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + world, chunkPos.x(), chunkPos.z(), () -> { ++ try { // Luminol - Level hot unload apis + net.minecraft.server.network.ServerConfigurationPacketListenerImpl.this.prepareSpawnTask.spawnPlayer( + this.connection, commonListenerCookie, serverPlayer + ); ++ // Luminol start - Level hot unload apis ++ } finally { ++ world.levelUnloadStateLock.releaseRead(); ++ } ++ // Luminol end + } + ); + } +diff --git a/net/minecraft/server/network/config/PrepareSpawnTask.java b/net/minecraft/server/network/config/PrepareSpawnTask.java +index 48f51b0f94df3e59c9410129489c63a4287bc0bc..2682f8702785b1e501748a308c5e394b8b664428 100644 +--- a/net/minecraft/server/network/config/PrepareSpawnTask.java ++++ b/net/minecraft/server/network/config/PrepareSpawnTask.java +@@ -104,6 +104,16 @@ public class PrepareSpawnTask implements ConfigurationTask { + spawnLevel = spawnDataLevel; + } + } ++ // Luminol start - Level hot unload apis ++ if (!spawnLevel.levelUnloadStateLock.acquireRead()) { ++ spawnLevel = spawnDataLevel; ++ ++ // the apis don't allow devs to unload overworld, so just simply do a check withoud fail-processing logics on it ++ if (!spawnLevel.levelUnloadStateLock.acquireRead()) { ++ throw new java.lang.IllegalStateException("Default level should be acquirable!"); ++ } ++ } ++ // Luminol end + // Folia start - region threading + CompletableFuture spawnPosition = new java.util.concurrent.CompletableFuture<>(); + if (loadedPosition.position().isPresent()) { +@@ -123,6 +133,10 @@ public class PrepareSpawnTask implements ConfigurationTask { + } + // Folia end - region threading + // Paper end - move logic in Entity to here, to use bukkit supplied world UUID & reset to main world spawn if no valid world is found ++ // Luminol start - Level hot unload apis ++ final ServerLevel finalSpawnLevel = spawnLevel; ++ spawnPosition.whenComplete((_, _) -> finalSpawnLevel.levelUnloadStateLock.releaseRead()); ++ // Luminol end + Vec2 spawnAngle = loadedPosition.rotation().orElse(new Vec2(respawnData.yaw(), respawnData.pitch())); + this.state = new PrepareSpawnTask.Preparing(spawnLevel, spawnPosition, spawnAngle); + } +@@ -272,9 +286,20 @@ public class PrepareSpawnTask implements ConfigurationTask { + this.spawnPosition = CompletableFuture.completedFuture(spawnPosition); + this.spawnAngle = new Vec2(location.getYaw(), location.getPitch()); + } ++ // Luminol start - Level hot unload apis ++ if (!this.spawnLevel.levelUnloadStateLock.acquireRead()) { ++ LOGGER.warn("Could not fetch read reference lock for level {}! Falling back to overworld!", this.spawnLevel); ++ ++ this.spawnLevel = net.minecraft.server.MinecraftServer.getServer().overworld(); ++ if (!this.spawnLevel.levelUnloadStateLock.acquireRead()) { ++ throw new java.lang.IllegalStateException(); ++ } ++ } ++ // Luminol end + // Paper end - PlayerSpawnLocationEvent + ChunkPos spawnChunk = ChunkPos.containing(BlockPos.containing(spawnPosition)); + this.chunkLoadFuture = ((ca.spottedleaf.moonrise.patches.chunk_system.MoonriseChunkLoadCounter)this.chunkLoadCounter).trackLoadWithRadius(this.spawnLevel, spawnChunk, 3, net.minecraft.world.level.chunk.status.ChunkStatus.FULL, ca.spottedleaf.concurrentutil.util.Priority.HIGH, () -> { Preparing.this.spawnLevel.getChunkSource().addTicketWithRadius(TicketType.PLAYER_SPAWN, spawnChunk, 3); }); // Paper - rewrite chunk system ++ this.chunkLoadFuture.whenComplete((_,_) -> this.spawnLevel.levelUnloadStateLock.releaseRead()); // Luminol - Level hot unload apis + PrepareSpawnTask.this.loadListener.start(LevelLoadListener.Stage.LOAD_PLAYER_CHUNKS, this.chunkLoadCounter.totalChunks()); + PrepareSpawnTask.this.loadListener.updateFocus(this.spawnLevel.dimension(), spawnChunk); + } +@@ -291,7 +316,7 @@ public class PrepareSpawnTask implements ConfigurationTask { + } + + private final class Ready implements PrepareSpawnTask.State { +- private final ServerLevel spawnLevel; ++ private ServerLevel spawnLevel; // Luminol - Level hot unloaad apis - Make public + private final Vec3 spawnPosition; + private final Vec2 spawnAngle; + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 8c909108a0c468f353837787188c12f907369b9b..1dd0ce3f8af52f5ba0da1d6617489a0de2c971b2 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -392,6 +392,9 @@ public abstract class Entity + public void inactiveTick() { + } + // Paper end - EAR 2 ++ // Luminol start - Level hot unload apis ++ public me.earthme.luminol.utils.TeleportRecord lastTeleportRecord; ++ // Luminol end + // CraftBukkit end + + // Paper start +@@ -4538,6 +4541,17 @@ public abstract class Entity + } + } + ++ // Luminol start - Level hot unload apis ++ if (!destination.levelUnloadStateLock.acquireRead()) { ++ return false; ++ } ++ final java.util.function.Consumer finalTeleportComplete = entity -> { ++ destination.levelUnloadStateLock.releaseRead(); ++ ++ if (teleportComplete != null) teleportComplete.accept(entity); ++ }; ++ // Luminol end ++ + // TODO any events that can modify go HERE + + // check for same region +@@ -4556,6 +4570,14 @@ public abstract class Entity + } + + for (EntityTreeNode entity : passengerTree.getFullTree()) { ++ // Luminol start - Level hot unload apis ++ entity.root.lastTeleportRecord = me.earthme.luminol.utils.TeleportRecord.create( ++ (ServerLevel) entity.root.level, ++ entity.root.position, ++ entity.root.yRot, ++ entity.root.xRot ++ ); ++ // Luminol end + entity.root.teleportSyncSameRegion(pos, yaw, pitch, velocity); + } + +@@ -4571,8 +4593,8 @@ public abstract class Entity + // performs add/remove from world logic which will also perform add/remove tracker logic + } + +- if (teleportComplete != null) { +- teleportComplete.accept(this); ++ if (finalTeleportComplete != null) { // Luminol - Level hot unload apis ++ finalTeleportComplete.accept(this); // Luminol - Level hot unload apis + } + return true; + } +@@ -4589,7 +4611,7 @@ public abstract class Entity + node.root = node.root.transformForAsyncTeleport(destination, pos, yaw, pitch, velocity); + } + +- passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, teleportComplete); ++ passengerTree.root.placeInAsync(originWorld, destination, teleportFlags, passengerTree, finalTeleportComplete); // Luminol - Level hot unload apis + + return true; + } +@@ -4864,6 +4886,13 @@ public abstract class Entity + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(initialPosition), + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(initialPosition) + ); ++ // Luminol start - Level hot load/unload apis ++ // let's acquire read reference first ++ if (!destination.levelUnloadStateLock.acquireRead()) { ++ // failed, destination is under unloading process ++ return false; ++ } ++ // Luminol end + + // first, remove entity/passengers from world + EntityTreeNode passengerTree = this.detachPassengers(); +@@ -4921,6 +4950,10 @@ public abstract class Entity + if (info.postTeleportTransition() != null) { + info.postTeleportTransition().onTransition(teleported); + } ++ // Luminol start - Level hot load/unload apis ++ // all done, release the read reference ++ destination.levelUnloadStateLock.releaseRead(); ++ // Luminol end + + if (teleportComplete != null) { + teleportComplete.accept(teleported); diff --git a/lophine-server/minecraft-patches/features/0026-Kaiiju-Do-not-pathfind-outside-region.patch b/lophine-server/minecraft-patches/features/0026-Kaiiju-Do-not-pathfind-outside-region.patch new file mode 100644 index 0000000..1be4d5c --- /dev/null +++ b/lophine-server/minecraft-patches/features/0026-Kaiiju-Do-not-pathfind-outside-region.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Tue, 21 Apr 2026 01:32:38 +0800 +Subject: [PATCH] Kaiiju: Do not pathfind outside region + +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/0028-Don-t-pathfind-outside-region.patch) +Licensed under: GPL-3.0 (https://github.com/KaiijuMC/Kaiiju/blob/c2b7aec8f7b418a39a2ec408e6411e6f752379da/LICENSE) + +diff --git a/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java b/net/minecraft/world/entity/ai/behavior/MoveToTargetSink.java +index b40da004b5281a041ee896ae176bfb9c4660f353..ffb8a725c41d49a0f0e023ab37bb77062d67eccc 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)) // Kaiiju - Don't pathfind outside region + this.path = body.getNavigation().createPath(targetPos, 0); ++ else this.path = null; // Kaiiju - Don't pathfind outside region + this.speedModifier = walkTarget.getSpeedModifier(); + Brain brain = body.getBrain(); + if (this.reachedTarget(body, walkTarget)) { diff --git a/lophine-server/minecraft-patches/features/0027-Kaiiju-Entity-tick-and-removal-limiter.patch b/lophine-server/minecraft-patches/features/0027-Kaiiju-Entity-tick-and-removal-limiter.patch new file mode 100644 index 0000000..0cb5ee0 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0027-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 c4d9d890db784d376b876d70f0468df6edb75168..0641b98ef3298eb682ceae83f60b730f3bf90103 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -917,6 +917,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 +@@ -938,6 +939,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); +@@ -947,6 +955,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/lophine-server/minecraft-patches/features/0028-Kaiiju-Vanilla-end-portal-teleportation.patch b/lophine-server/minecraft-patches/features/0028-Kaiiju-Vanilla-end-portal-teleportation.patch new file mode 100644 index 0000000..6cd24c0 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0028-Kaiiju-Vanilla-end-portal-teleportation.patch @@ -0,0 +1,81 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:21:31 +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 1dd0ce3f8af52f5ba0da1d6617489a0de2c971b2..cf70a8e2fe2fe70bcdf71f8112479031d80d8b75 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4676,14 +4676,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 + ) +@@ -4698,11 +4702,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 +@@ -4881,6 +4889,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), +@@ -4955,9 +4967,14 @@ public abstract class Entity + destination.levelUnloadStateLock.releaseRead(); + // Luminol end + +- 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/lophine-server/minecraft-patches/features/0029-Async-protocol-switching-optimization.patch b/lophine-server/minecraft-patches/features/0029-Async-protocol-switching-optimization.patch new file mode 100644 index 0000000..10b9f58 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0029-Async-protocol-switching-optimization.patch @@ -0,0 +1,288 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 19:58:46 +0800 +Subject: [PATCH] Async protocol switching optimization + + +diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java +index 40f54536422fa38bd84017fc634808016b4de58b..ecaa296f576c5c90fda89e0406d6552628ef5de3 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 ++ // Luminol 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(); ++ } ++ } ++ // Luminol end ++ + } +diff --git a/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java b/net/minecraft/server/network/ServerConfigurationPacketListenerImpl.java +index e5304e9d1ea57c01a1147621a42a9b584878f765..af337f77545df94760b12a2b2f5eaf43d18e3774 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()))); // Luminol - Async protocol switch - move down + ++ Runnable afterSwitch = () -> { // Luminol - Async protocol switch + try { + PlayerList playerList = this.server.getPlayerList(); + if (playerList.getPlayer(this.gameProfile.id()) != null) { +@@ -256,6 +257,18 @@ public class ServerConfigurationPacketListenerImpl extends ServerCommonPacketLis + LOGGER.error("Couldn't place player in world", e); + this.disconnect(DISCONNECT_REASON_INVALID_DATA); + } ++ // Luminol start - Async protocol switch ++ }; ++ if (!me.earthme.luminol.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 ++ } ++ // Luminol end + } + + @Override +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 0c5fb17e7cce18eac68183cc3b5de6fc6451dc50..1ea8e03a4dc39486505d21bff1d15cb2339cafc2 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -2901,7 +2901,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 (!me.earthme.luminol.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { // Luminol - Async protocol switch + this.connection.setupOutboundProtocol(ConfigurationProtocols.CLIENTBOUND); ++ // Luminol start - Async protcol switch ++ } else { ++ this.connection.setupOutboundProtocolAsync(ConfigurationProtocols.CLIENTBOUND, null, true); ++ } ++ // Luminol end + } + + @Override +@@ -3786,12 +3792,26 @@ public class ServerGamePacketListenerImpl + } + + final ServerConfigurationPacketListenerImpl listener = new ServerConfigurationPacketListenerImpl(this.server, this.connection, this.createCookie(this.player.clientInformation())); // Paper ++ if (!me.earthme.luminol.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { // Luminol - Async protocol switch + this.connection + .setupInboundProtocol( + ConfigurationProtocols.SERVERBOUND, + listener // Paper + ); + new io.papermc.paper.event.connection.configuration.PlayerConnectionReconfigureEvent(listener.paperConnection).callEvent(); // Paper ++ } // Luminol - Async protocol switch - add "{" ++ // Luminol 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 ++ ); ++ // Luminol end ++ // new io.papermc.paper.event.connection.configuration.PlayerConnectionReconfigureEvent(listener.paperConnection).callEvent(); // Paper // Luminol - Async protocol switch - move up + } + + @Override +diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index f2329d1a9d9d4bf4c9d771e25e54f2f9ef65a76c..deee051c103f9797f4612e06c12f293ec54724ac 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); // Luminol - 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;*/ // Luminol - Async protocol switch - Rewrite ++ ++ // Luminol 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 (!me.earthme.luminol.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 ++ // Luminol end + } + + @Override +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index e7579a9873362317dd82b63306cf650af9472574..53449af103c1774f68cb0c296d47e7032e4e3843 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 (!me.earthme.luminol.config.modules.optimizations.AsyncProtocolChangeConfig.enabled) { // Luminol - 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 + ); ++ } // Luminol - 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 ++ // Luminol start - Async protocol switch ++ if (me.earthme.luminol.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 ++ ); ++ } ++ // Luminol end + } + + public void updateEntireScoreboard(final ServerScoreboard scoreboard, final ServerPlayer player) { diff --git a/lophine-server/minecraft-patches/features/0030-Add-config-for-server-mod-name.patch b/lophine-server/minecraft-patches/features/0030-Add-config-for-server-mod-name.patch new file mode 100644 index 0000000..47e5086 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0030-Add-config-for-server-mod-name.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +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 ab61d18845827ac835edf92651d0b510358106ff..d4e31bb829451a687bf567fd0357f46fd7d46902 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -2072,7 +2072,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Sat, 18 Apr 2026 09:09:33 +0800 +Subject: [PATCH] Add config for offline mode warning + + +diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java +index 1145d5839e621a02b7972c2ff1142f51db2237de..81934fb56744dbbb3fdd100e8ec83cb634034d04 100644 +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -304,7 +304,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + String proxyFlavor = (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) ? "Velocity" : "BungeeCord"; + String proxyLink = (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) ? "https://docs.papermc.io/velocity/security" : "http://www.spigotmc.org/wiki/firewall-guide/"; + // Paper end - Add Velocity IP Forwarding Support +- if (!this.usesAuthentication()) { ++ if (!this.usesAuthentication() && !me.earthme.luminol.config.modules.misc.DisableWarningConfig.disableOfflineModeWarning) { //Luminol - Add config for offline mod warning + LOGGER.warn("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!"); + LOGGER.warn("The server will make no attempt to authenticate usernames. Beware."); + // Spigot start diff --git a/lophine-server/minecraft-patches/features/0032-Add-config-for-cpu-affinity.patch b/lophine-server/minecraft-patches/features/0032-Add-config-for-cpu-affinity.patch new file mode 100644 index 0000000..9ce4a27 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0032-Add-config-for-cpu-affinity.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +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 f8344f640f119e3865f2718d1abd0ede9bdb8aa8..58e557923dfe6cee39ec45e7f5aa43a5ded9f107 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, me.earthme.luminol.config.modules.optimizations.CpuAffinityConfig.wrapForTickRegion(run), "Folia Region Scheduler Thread #" + this.idGenerator.getAndIncrement()); // Luminol - cpu affinity + ret.setUncaughtExceptionHandler(TickRegionScheduler.this::uncaughtException); + return ret; + } diff --git a/lophine-server/minecraft-patches/features/0033-Add-config-for-unsafe-teleportation.patch b/lophine-server/minecraft-patches/features/0033-Add-config-for-unsafe-teleportation.patch new file mode 100644 index 0000000..789079b --- /dev/null +++ b/lophine-server/minecraft-patches/features/0033-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..137389f7ee97fc3a15c8f5f87c058065af91264e 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 = me.earthme.luminol.config.modules.fixes.UnsafeTeleportationConfig.enabled; // Luminol - 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 || me.earthme.luminol.config.modules.fixes.UnsafeTeleportationConfig.enabled); // Paper // Luminol - Unsafe teleportation + return newEntity; + } + } +diff --git a/net/minecraft/world/level/block/EndPortalBlock.java b/net/minecraft/world/level/block/EndPortalBlock.java +index d42bb2a1721e4ab8c9956e18c3ca418b8d648c59..4f06daf619596d4116140b0710ea92c2c8552fc0 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 { ++ // Luminol start - unsafe teleportation ++ if (me.earthme.luminol.config.modules.fixes.UnsafeTeleportationConfig.enabled && !(entity instanceof net.minecraft.world.entity.player.Player)) { ++ entity.endPortalLogicAsync(pos); ++ return; ++ } ++ // Luminol end + entity.setAsInsidePortal(this, pos); + } + } diff --git a/lophine-server/minecraft-patches/features/0034-Add-config-for-vanilla-random.patch b/lophine-server/minecraft-patches/features/0034-Add-config-for-vanilla-random.patch new file mode 100644 index 0000000..8b43f3a --- /dev/null +++ b/lophine-server/minecraft-patches/features/0034-Add-config-for-vanilla-random.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 20:04:06 +0800 +Subject: [PATCH] Add config for vanilla random + + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index cf70a8e2fe2fe70bcdf71f8112479031d80d8b75..17650e196f1e5b79765651bf302291873a4eb509 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -298,7 +298,7 @@ public abstract class Entity + public double yOld; + public double zOld; + public boolean noPhysics; +- protected final RandomSource random = SHARED_RANDOM; // Paper - Share random for entities to make them more random ++ protected final RandomSource random = me.earthme.luminol.config.modules.fixes.VanillaRandomSourceConfig.useLegacyRandomSourceForPlayers ? RandomSource.create() : SHARED_RANDOM; // Paper - Share random for entities to make them more random // Luminol - Add config for vanilla random SHARED_RANDOM + public int tickCount; + private int remainingFireTicks; + private final EntityFluidInteraction fluidInteraction = new EntityFluidInteraction(Set.of(FluidTags.WATER, FluidTags.LAVA)); diff --git a/lophine-server/minecraft-patches/features/0035-Add-config-for-watchdog-timeout.patch b/lophine-server/minecraft-patches/features/0035-Add-config-for-watchdog-timeout.patch new file mode 100644 index 0000000..1619dfb --- /dev/null +++ b/lophine-server/minecraft-patches/features/0035-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..fe1560579f9c9572082d52cf910f5916957f5601 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(me.earthme.luminol.config.modules.misc.FoliaWatchogConfig.tickRegionTimeOutMs)) { // Luminol - Add config for watchdog timeout + continue; + } + tick.lastPrint = now; diff --git a/lophine-server/minecraft-patches/features/0036-Add-config-to-disable-entity-tick-catchers.patch b/lophine-server/minecraft-patches/features/0036-Add-config-to-disable-entity-tick-catchers.patch new file mode 100644 index 0000000..7eb8d1a --- /dev/null +++ b/lophine-server/minecraft-patches/features/0036-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 dc28223684091cd0d8dbf7ff01626b20e4f1b662..6fd7bfa561f71cfd043d7a1092d77b01bb320640 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 + ), null, 1L); + // Luminol end + } catch (Throwable t) { ++ if (me.earthme.luminol.config.modules.experiment.DisableEntityCatchConfig.enabled) throw t; // Luminol + // 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/lophine-server/minecraft-patches/features/0037-Add-config-for-heightmap-warning.patch b/lophine-server/minecraft-patches/features/0037-Add-config-for-heightmap-warning.patch new file mode 100644 index 0000000..ae074be --- /dev/null +++ b/lophine-server/minecraft-patches/features/0037-Add-config-for-heightmap-warning.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:14:31 +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..21f0214992b82554c9532797cdc6fc593af1efe5 100644 +--- a/net/minecraft/world/level/levelgen/Heightmap.java ++++ b/net/minecraft/world/level/levelgen/Heightmap.java +@@ -128,7 +128,9 @@ public class Heightmap { + if (rawData.length == data.length) { + System.arraycopy(data, 0, rawData, 0, data.length); + } else { +- LOGGER.warn("Ignoring heightmap data for chunk {}, size does not match; expected: {}, got: {}", chunk.getPos(), rawData.length, data.length); ++ // Luminol - Add config for heightmap warning ++ if (!me.earthme.luminol.config.modules.misc.DisableWarningConfig.disableHeightmapWarning) ++ LOGGER.warn("Ignoring heightmap data for chunk {}, size does not match; expected: {}, got: {}", chunk.getPos(), rawData.length, data.length); // Luminol - Add config for heightmap warning + primeHeightmaps(chunk, EnumSet.of(type)); + } + } diff --git a/lophine-server/minecraft-patches/features/0038-Add-config-gui-support.patch b/lophine-server/minecraft-patches/features/0038-Add-config-gui-support.patch new file mode 100644 index 0000000..1e4cead --- /dev/null +++ b/lophine-server/minecraft-patches/features/0038-Add-config-gui-support.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:15:04 +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..f1807523e1e6dbd13093f195c368b43a92b82af9 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) { // Luminol - make public + this.raw = raw; + this.parsed = parsed; + } diff --git a/lophine-server/minecraft-patches/features/0039-Add-force-the-data-command-to-be-enabled-config.patch b/lophine-server/minecraft-patches/features/0039-Add-force-the-data-command-to-be-enabled-config.patch new file mode 100644 index 0000000..81bc4d2 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0039-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: Sat, 18 Apr 2026 09:15:22 +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..afbaf2116930d72ebb0c77745d02115abab14dc4 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(me.earthme.luminol.config.modules.experiment.CommandConfig.data) { // Luminol - Config for data command ++ DataCommands.register(this.dispatcher); // Folia - region threading - TODO // Luminol - Config for data command ++ } // Luminol - Config for data command + //DataPackCommand.register(this.dispatcher, context); // Folia - region threading - TODO + //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO + DefaultGameModeCommands.register(this.dispatcher); diff --git a/lophine-server/minecraft-patches/features/0040-Add-experimental-config-for-command-block-command-ex.patch b/lophine-server/minecraft-patches/features/0040-Add-experimental-config-for-command-block-command-ex.patch new file mode 100644 index 0000000..b6ac0ac --- /dev/null +++ b/lophine-server/minecraft-patches/features/0040-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: Sat, 18 Apr 2026 09:15:32 +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..47fe63e49a7cfb77f8fe5cc95ab9faf46d244ba9 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 (!me.earthme.luminol.config.modules.experiment.CommandConfig.commandBlock) return false; // Folia - region threading // Luminol - Add experimental config for command block command execution + if (level.getGameTime() == this.lastExecution) { + return false; + } diff --git a/lophine-server/minecraft-patches/features/0041-Add-config-to-verify-signature-only-in-online-mode.patch b/lophine-server/minecraft-patches/features/0041-Add-config-to-verify-signature-only-in-online-mode.patch new file mode 100644 index 0000000..023c5ea --- /dev/null +++ b/lophine-server/minecraft-patches/features/0041-Add-config-to-verify-signature-only-in-online-mode.patch @@ -0,0 +1,20 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:16:40 +0800 +Subject: [PATCH] Add config to verify signature only in online-mode + +Co-authored-by: Stabrinai + +diff --git a/net/minecraft/world/entity/player/ProfilePublicKey.java b/net/minecraft/world/entity/player/ProfilePublicKey.java +index 5e29a9de070e9922513e972254fc7ec43e8774f2..2de1886f9d9f06f8b92755e8421c176679e8b5ce 100644 +--- a/net/minecraft/world/entity/player/ProfilePublicKey.java ++++ b/net/minecraft/world/entity/player/ProfilePublicKey.java +@@ -23,7 +23,7 @@ public record ProfilePublicKey(ProfilePublicKey.Data data) { + public static final Codec TRUSTED_CODEC = ProfilePublicKey.Data.CODEC.xmap(ProfilePublicKey::new, ProfilePublicKey::data); + + public static ProfilePublicKey createValidated(final SignatureValidator validator, final UUID profileId, final ProfilePublicKey.Data data) throws ProfilePublicKey.ValidationException { +- if (!data.validateSignature(validator, profileId)) { ++ if (!data.validateSignature(validator, profileId) && (org.bukkit.Bukkit.getServer().getOnlineMode() && me.earthme.luminol.config.modules.misc.PublickeyVerifyConfig.enabled)) { // Luminol - Verify signature only in online-mode + throw new ProfilePublicKey.ValidationException(INVALID_SIGNATURE, org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PUBLIC_KEY_SIGNATURE); // Paper - kick event causes + } else { + return new ProfilePublicKey(data); diff --git a/lophine-server/minecraft-patches/features/0042-Add-config-for-whether-to-save-portal-tickets.patch b/lophine-server/minecraft-patches/features/0042-Add-config-for-whether-to-save-portal-tickets.patch new file mode 100644 index 0000000..a5802ac --- /dev/null +++ b/lophine-server/minecraft-patches/features/0042-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: Sat, 18 Apr 2026 09:17:02 +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..083a82b5200a251e85f155d1df016cd8b4f18db5 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, me.earthme.luminol.config.modules.misc.SavePortalTicketsConfig.doSave ? FLAG_PERSIST | FLAG_LOADING | FLAG_SIMULATION | FLAG_KEEP_DIMENSION_ACTIVE : FLAG_LOADING | FLAG_SIMULATION | FLAG_KEEP_DIMENSION_ACTIVE); + 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/lophine-server/minecraft-patches/features/0043-Add-back-read-only-datapack-command.patch b/lophine-server/minecraft-patches/features/0043-Add-back-read-only-datapack-command.patch new file mode 100644 index 0000000..ea75d57 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0043-Add-back-read-only-datapack-command.patch @@ -0,0 +1,61 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:18:31 +0800 +Subject: [PATCH] Add back read-only datapack command + + +diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java +index afbaf2116930d72ebb0c77745d02115abab14dc4..6823c41c08ca1a1baf9257fc861ae97bbcbe3a50 100644 +--- a/net/minecraft/commands/Commands.java ++++ b/net/minecraft/commands/Commands.java +@@ -205,7 +205,7 @@ public class Commands { + if(me.earthme.luminol.config.modules.experiment.CommandConfig.data) { // Luminol - Config for data command + DataCommands.register(this.dispatcher); // Folia - region threading - TODO // Luminol - Config for data command + } // Luminol - Config for data command +- //DataPackCommand.register(this.dispatcher, context); // Folia - region threading - TODO ++ DataPackCommand.register(this.dispatcher, context); // Folia - region threading - TODO // Luminol - Add back read-only datapack command + //DebugCommand.register(this.dispatcher); // Folia - region threading - TODO + DefaultGameModeCommands.register(this.dispatcher); + //DialogCommand.register(this.dispatcher, context); // Folia - region threading - TODO +diff --git a/net/minecraft/server/commands/DataPackCommand.java b/net/minecraft/server/commands/DataPackCommand.java +index 0da1e4ada8616af825ddd4678e61246c672f9c4f..80d9bc3d680c30dae15064e0e8c3d1379ccd52e6 100644 +--- a/net/minecraft/server/commands/DataPackCommand.java ++++ b/net/minecraft/server/commands/DataPackCommand.java +@@ -99,7 +99,7 @@ public class DataPackCommand { + dispatcher.register( + Commands.literal("datapack") + .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS)) +- .then( ++ /*.then( // Luminol - Add back read-only datapack command + Commands.literal("enable") + .then( + Commands.argument("name", StringArgumentType.string()) +@@ -146,11 +146,11 @@ public class DataPackCommand { + .suggests(SELECTED_PACKS) + .executes(c -> disablePack(c.getSource(), getPack(c, "name", false))) + ) +- ) ++ )*/ // Luminol - Add back read-only datapack command + .then( + Commands.literal("list") + .executes(c -> listPacks(c.getSource())) +- .then(Commands.literal("available").executes(c -> listAvailablePacks(c.getSource()))) ++ //.then(Commands.literal("available").executes(c -> listAvailablePacks(c.getSource()))) // Luminol - Add back read-only datapack command + .then(Commands.literal("enabled").executes(c -> listEnabledPacks(c.getSource()))) + ) + .then( +@@ -238,12 +238,12 @@ public class DataPackCommand { + } + + private static int listPacks(final CommandSourceStack source) { +- return listEnabledPacks(source) + listAvailablePacks(source); ++ return listEnabledPacks(source) ;// + listAvailablePacks(source); // Luminol - Add back read-only datapack command + } + + private static int listAvailablePacks(final CommandSourceStack source) { + PackRepository repository = source.getServer().getPackRepository(); +- repository.reload(); ++ //repository.reload(); // Luminol - Add back read-only datapack command + Collection selectedPacks = repository.getSelectedPacks(); + Collection availablePacks = repository.getAvailablePacks(); + FeatureFlagSet enabledFeatures = source.enabledFeatures(); diff --git a/lophine-server/minecraft-patches/features/0044-Add-tpsbar-with-chunkhot-membar-and-regionbar.patch b/lophine-server/minecraft-patches/features/0044-Add-tpsbar-with-chunkhot-membar-and-regionbar.patch new file mode 100644 index 0000000..71b0e28 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0044-Add-tpsbar-with-chunkhot-membar-and-regionbar.patch @@ -0,0 +1,360 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 20:16:33 +0800 +Subject: [PATCH] Add tpsbar with chunkhot, membar and regionbar + + +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 d4e31bb829451a687bf567fd0357f46fd7d46902..350b788e17dc49118498541ad8ac54f6e0a59c9d 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1743,7 +1743,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 0641b98ef3298eb682ceae83f60b730f3bf90103..cea4bc7036dfd8777c151a0dd811fc13579d6501 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -1491,6 +1491,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 +@@ -1552,6 +1554,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 + } + +@@ -1563,6 +1566,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(); +@@ -1597,6 +1602,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 ab6229ee412cf2aa5affc87510230c587765080b..f0c574e039f80ff0227239356de8d61ef06a0936 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -441,7 +441,9 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + public boolean isRealPlayer; // Paper + public com.destroystokyo.paper.event.entity.@Nullable PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent + public org.bukkit.event.player.PlayerQuitEvent.@Nullable QuitReason quitReason = null; // Paper - Add API for quit reason; there are a lot of changes to do if we change all methods leading to the event +- ++ public volatile boolean isTpsBarVisible = false; //Luminol - Tps bar ++ public volatile boolean isMemBarVisible = false; //Luminol - Memory bar ++ public volatile boolean isRegionBarVisible = false; //Luminol - Region bar + // Paper start - rewrite chunk system + private ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; + private final ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder viewDistanceHolder = new ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.ViewDistanceHolder(); +@@ -889,6 +891,9 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + + @Override + public void tick() { ++ // Luminol start - Status bars ++ this.statusBarList.tick(); ++ // Luminol end - Status bars + // CraftBukkit start + if (this.joining) { + this.joining = false; +@@ -949,8 +954,35 @@ 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); + if (blockInteractionRange != null) { +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 17650e196f1e5b79765651bf302291873a4eb509..f218b7f6ce32d7ac959255e59c620d6fd420bf8e 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -6468,4 +6468,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 9c8e8de53cbbd87ac7cb8483bd3c1af3fcf66b6e..301e75393d1074eca0c8533219f516b6d625a38a 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..a4bc767b799c050ce07a3685f655b4ba947d2466 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 me.earthme.luminol.functions.bars.TickableStatusBarList statusBarList = new me.earthme.luminol.functions.bars.TickableStatusBarList(this); // Luminol 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); // Luminol - 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); // Luminol - 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/lophine-server/minecraft-patches/features/0045-Add-config-to-modify-tripwire-behavior.patch b/lophine-server/minecraft-patches/features/0045-Add-config-to-modify-tripwire-behavior.patch new file mode 100644 index 0000000..5f45680 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0045-Add-config-to-modify-tripwire-behavior.patch @@ -0,0 +1,110 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 28 Jun 2026 20:30: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..49b3f9e78ae9db257b653a6094619880784c6370 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 ++ // Luminol start - tripwire and tripwireHook dupe ++ if (me.earthme.luminol.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 ++ } + } ++ // Luminol 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..a8e69343f47ba2383f9dcc2cf6328463892aaeec 100644 +--- a/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/EndPlatformFeature.java +@@ -28,14 +28,39 @@ public class EndPlatformFeature extends Feature { + // CraftBukkit end + BlockPos.MutableBlockPos pos = origin.mutable(); + +- for (int dz = -2; dz <= 2; dz++) { ++ // Luminol start - tripwire behavior modifier ++ java.util.List blockList1 = new java.util.ArrayList<>(); ++ java.util.List blockList2 = new java.util.ArrayList<>(); ++ boolean flag21 = me.earthme.luminol.config.modules.function.TripwireBehaviorConfig.behaviorMode == me.earthme.luminol.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 (me.earthme.luminol.config.modules.function.TripwireBehaviorConfig.enabled) { ++ switch (me.earthme.luminol.config.modules.function.TripwireBehaviorConfig.behaviorMode) { ++ case me.earthme.luminol.enums.EnumTripwireBehavior.VANILLA20: { ++ flag = true; ++ } ++ case me.earthme.luminol.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: {} // Luminol - 1.21 & default Logic - default empty ++ } ++ } ++ if (flag) blockList1.add(blockPos.immutable()); ++ else blockList.destroyBlock(blockPos, true, null); // CraftBukkit + } + + blockList.setBlock(blockPos, block.defaultBlockState(), Block.UPDATE_ALL); // CraftBukkit +@@ -53,11 +78,30 @@ public class EndPlatformFeature extends Feature { + if (portalEvent.isCancelled()) return; + } + +- if (dropResources) { +- blockList.placeBlocks(state -> newLevel.destroyBlock(state.getPosition(), true, null)); ++ if (flag21 || !me.earthme.luminol.config.modules.function.TripwireBehaviorConfig.enabled) { ++ if (dropResources) { ++ blockList.placeBlocks(state -> newLevel.destroyBlock(state.getPosition(), true, null)); ++ } else { ++ blockList.placeBlocks(); ++ } + } else { ++ if (dropResources) { ++ blockList.getSnapshotBlocks().forEach((state) -> { ++ newLevel.destroyBlock(state.getPosition(), !blockList1.contains(state.getPosition()), null); ++ }); ++ // Luminol - prevent tripwire dupe in end platform generate ++ } + blockList.placeBlocks(); + } + // CraftBukkit end + } ++ ++ 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; ++ } ++ // Luminol end - tripwire behavior modifier + } diff --git a/lophine-server/minecraft-patches/features/0046-Add-config-to-enable-tick-command.patch b/lophine-server/minecraft-patches/features/0046-Add-config-to-enable-tick-command.patch new file mode 100644 index 0000000..0b660e2 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0046-Add-config-to-enable-tick-command.patch @@ -0,0 +1,104 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Fri, 5 Jun 2026 15:09:29 +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 ff90abb47203c03636d3175f2b3efcf43517b3b0..276349d778c2e6f0b1081eec4851bad5d25db237 100644 +--- a/io/papermc/paper/threadedregions/RegionizedServer.java ++++ b/io/papermc/paper/threadedregions/RegionizedServer.java +@@ -239,6 +239,11 @@ public final class RegionizedServer { + + private void globalTick(final long tickCount) { + ++this.tickCount; ++ // Luminol start - Add a config to enable tick command ++ if (me.earthme.luminol.config.modules.experiment.CommandConfig.tick) { ++ MinecraftServer.getServer().tickRateManager().tick(); ++ } ++ // Luminol 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 +@@ -415,7 +420,7 @@ public final class RegionizedServer { + } + + private void tickTime(final ServerLevel world, final long tickCount) { +- if (world.tickTime) { ++ if ((!me.earthme.luminol.config.modules.experiment.CommandConfig.tick || world.tickRateManager().runsNormally()) && world.tickTime) { // Luminol - 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 58e557923dfe6cee39ec45e7f5aa43a5ded9f107..5f4f80c1d4003254fd840f71d86908603b57bfa2 100644 +--- a/io/papermc/paper/threadedregions/TickRegionScheduler.java ++++ b/io/papermc/paper/threadedregions/TickRegionScheduler.java +@@ -559,6 +559,11 @@ public final class TickRegionScheduler { + try { + // next start isn't updated until the end of this tick + this.tickRegion(tickCount, tickStart, scheduledEnd); ++ // Luminol start - Add a config to enable tick command ++ if (me.earthme.luminol.config.modules.experiment.CommandConfig.tick) { ++ MinecraftServer.getServer().tickRateManager().endTickWork(); ++ } ++ // Luminol 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 6823c41c08ca1a1baf9257fc861ae97bbcbe3a50..0fa14f60310b063f419620539fd40898627b46b9 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 ++ // Luminol start - Add a config to enable tick command ++ if (me.earthme.luminol.config.modules.experiment.CommandConfig.tick) { ++ TickCommand.register(this.dispatcher); // Folia - region threading - TODO later ++ } ++ // Luminol 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..64b685b219b930a1bb7f85db9947f4d1ca9df093 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( + 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"))) + ) +- ) ++ )*/ + .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( + 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"))) + ) +- ) ++ )*/ + .then(Commands.literal("unfreeze").executes(c -> setFreeze(c.getSource(), false))) + .then(Commands.literal("freeze").executes(c -> setFreeze(c.getSource(), true))) + ); diff --git a/lophine-server/minecraft-patches/features/0047-Add-config-for-item-multitask.patch b/lophine-server/minecraft-patches/features/0047-Add-config-for-item-multitask.patch new file mode 100644 index 0000000..5c6c716 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0047-Add-config-for-item-multitask.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Libalpm64 +Date: Wed, 4 Mar 2026 00:00:00 +0000 +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 1ea8e03a4dc39486505d21bff1d15cb2339cafc2..a2146662c8ec83f62547bcd813c0e4dcb10efc07 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -2186,7 +2186,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 (!me.earthme.luminol.config.modules.fixes.ItemMultitaskConfig.enabled) { // Luminol - Add item multitask config + this.player.stopUsingItem(); // CraftBukkit - SPIGOT-4706 ++ } // Luminol - Add item multitask config + 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); +@@ -2385,9 +2387,13 @@ public class ServerGamePacketListenerImpl + return; + } + // CraftBukkit end +- if (this.player.getInventory().getSelectedSlot() != packet.getSlot() && this.player.getUsedItemHand() == InteractionHand.MAIN_HAND) { +- this.player.stopUsingItem(); ++ // Luminol start - Add item multitask config ++ if (!me.earthme.luminol.config.modules.fixes.ItemMultitaskConfig.enabled) { ++ if (this.player.getInventory().getSelectedSlot() != packet.getSlot() && this.player.getUsedItemHand() == InteractionHand.MAIN_HAND) { ++ this.player.stopUsingItem(); ++ } + } ++ // Luminol end + + this.player.getInventory().setSelectedSlot(packet.getSlot()); + this.player.resetLastActionTime(); diff --git a/lophine-server/minecraft-patches/features/0048-Add-tick-regions-api.patch b/lophine-server/minecraft-patches/features/0048-Add-tick-regions-api.patch new file mode 100644 index 0000000..45f6a02 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0048-Add-tick-regions-api.patch @@ -0,0 +1,63 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 10:49:29 +0800 +Subject: [PATCH] Add tick regions api + + +diff --git a/io/papermc/paper/threadedregions/ThreadedRegionizer.java b/io/papermc/paper/threadedregions/ThreadedRegionizer.java +index 3449918b5001bb39c541ce1168eac4acf9144e1f..a346b96500ae859e9469c80e2ff6b39c4d3d0b52 100644 +--- a/io/papermc/paper/threadedregions/ThreadedRegionizer.java ++++ b/io/papermc/paper/threadedregions/ThreadedRegionizer.java +@@ -43,6 +43,7 @@ public final class ThreadedRegionizer callbacks; + private final StampedLock regionLock = new StampedLock(); + private Thread writeLockOwner; ++ public final me.earthme.luminol.api.ThreadedRegionizer threadedRegionizerAPI; // Luminol - Tick region API + + /* + static final record Operation(String type, int chunkX, int chunkZ) {} +@@ -72,6 +73,7 @@ public final class ThreadedRegionizer> mergeIntoLater = new ReferenceOpenHashSet<>(); + private final ReferenceOpenHashSet> expectingMergeFrom = new ReferenceOpenHashSet<>(); ++ public final me.earthme.luminol.api.ThreadedRegion threadedRegionAPI = new me.earthme.luminol.api.impl.ThreadedRegionImpl((ThreadedRegion) this); // Luminol - Tickregion API + + public ThreadedRegion(final ThreadedRegionizer regioniser) { + this.regioniser = regioniser; +@@ -819,7 +822,7 @@ public final class ThreadedRegionizer region) { + this.region = region; diff --git a/lophine-server/minecraft-patches/features/0049-Add-missing-teleportation-event-APIs-for-folia.patch b/lophine-server/minecraft-patches/features/0049-Add-missing-teleportation-event-APIs-for-folia.patch new file mode 100644 index 0000000..472673e --- /dev/null +++ b/lophine-server/minecraft-patches/features/0049-Add-missing-teleportation-event-APIs-for-folia.patch @@ -0,0 +1,149 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 10:50:12 +0800 +Subject: [PATCH] Add missing teleportation event APIs for folia + + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index f0c574e039f80ff0227239356de8d61ef06a0936..093c48e2379c01f45277b88f6c1b19b107a46326 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -1814,6 +1814,9 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + if (finalRespawnCompleteCallback != null) { // Luminol - Level hot unload apis + finalRespawnCompleteCallback.accept(ServerPlayer.this); // Luminol - Level hot unload apis + } ++ // Luminol - Add missing teleportation apis ++ new me.earthme.luminol.api.entity.player.PostPlayerRespawnEvent(ServerPlayer.this.getBukkitEntity()).callEvent(); ++ // Luminol end + } + ); + }); +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index f218b7f6ce32d7ac959255e59c620d6fd420bf8e..2ae0cf7c9a4febfa79a9eab2f0e9309a61a78f56 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4553,6 +4553,31 @@ public abstract class Entity + // Luminol end + + // TODO any events that can modify go HERE ++ // Luminol start - Add missing teleportation apis ++ org.bukkit.Location destinationLoc; ++ ++ if (pitch == null) { ++ if (yaw == null) { ++ destinationLoc = io.papermc.paper.util.MCUtil.toLocation(destination, pos, Float.NaN, Float.NaN); ++ } else { ++ destinationLoc = io.papermc.paper.util.MCUtil.toLocation(destination, pos, yaw, Float.NaN); ++ } ++ }else { ++ if (yaw == null) { ++ destinationLoc = io.papermc.paper.util.MCUtil.toLocation(destination, pos, Float.NaN, pitch); ++ }else { ++ destinationLoc = io.papermc.paper.util.MCUtil.toLocation(destination, pos, yaw, pitch); ++ } ++ } ++ ++ final me.earthme.luminol.api.entity.EntityTeleportAsyncEvent wrapped = new me.earthme.luminol.api.entity.EntityTeleportAsyncEvent( ++ this.getBukkitEntity(), ++ cause, ++ destinationLoc ++ ); ++ ++ wrapped.callEvent(); ++ // Luminol end + + // check for same region + if (destination == this.level() +@@ -4671,6 +4696,17 @@ public abstract class Entity + case END: { + if (destination.getTypeKey() == net.minecraft.world.level.dimension.LevelStem.END) { + BlockPos targetPos = ServerLevel.END_SPAWN_POINT; ++ // Luminol start - Add missing teleportation apis ++ final org.bukkit.Location orginalPortalLocation = io.papermc.paper.util.MCUtil.toLocation(origin, originPortal); ++ final org.bukkit.Location targetPortalLocation = io.papermc.paper.util.MCUtil.toLocation(destination, targetPos); ++ ++ final me.earthme.luminol.api.portal.PortalLocateEvent portalLocateEvent = new me.earthme.luminol.api.portal.PortalLocateEvent( ++ orginalPortalLocation, ++ targetPortalLocation ++ ); ++ ++ portalLocateEvent.callEvent(); ++ // Luminol end + // need to load chunks so we can create the platform + destination.moonrise$loadChunksAsync( + targetPos, 16, // load 16 blocks to be safe from block physics +@@ -4696,6 +4732,17 @@ public abstract class Entity + ); + } else { + BlockPos spawnPos = destination.getRespawnData().pos(); ++ // Luminol start - Add missing teleportation apis ++ final org.bukkit.Location orginalPortalLocation = io.papermc.paper.util.MCUtil.toLocation(origin, originPortal); ++ final org.bukkit.Location targetPortalLocation = io.papermc.paper.util.MCUtil.toLocation(destination, spawnPos); ++ ++ final me.earthme.luminol.api.portal.PortalLocateEvent portalLocateEvent = new me.earthme.luminol.api.portal.PortalLocateEvent( ++ orginalPortalLocation, ++ targetPortalLocation ++ ); ++ ++ portalLocateEvent.callEvent(); ++ // Luminol end + // need to load chunk for heightmap + destination.moonrise$loadChunksAsync( + spawnPos, 0, +@@ -4751,7 +4798,17 @@ public abstract class Entity + WorldBorder destinationBorder = destination.getWorldBorder(); + double dimensionScale = net.minecraft.world.level.dimension.DimensionType.getTeleportationScale(origin.dimensionType(), destination.dimensionType()); + BlockPos targetPos = destination.getWorldBorder().clampToBounds(this.getX() * dimensionScale, this.getY(), this.getZ() * dimensionScale); ++ // Luminol start - Add missing teleportation apis ++ final org.bukkit.Location orginalPortalLocation = io.papermc.paper.util.MCUtil.toLocation(origin, originPortal); ++ final org.bukkit.Location targetPortalLocation = io.papermc.paper.util.MCUtil.toLocation(destination, targetPos); + ++ final me.earthme.luminol.api.portal.PortalLocateEvent portalLocateEvent = new me.earthme.luminol.api.portal.PortalLocateEvent( ++ orginalPortalLocation, ++ targetPortalLocation ++ ); ++ ++ portalLocateEvent.callEvent(); ++ // Luminol end + ca.spottedleaf.concurrentutil.completable.CallbackCompletable portalFound + = new ca.spottedleaf.concurrentutil.completable.CallbackCompletable<>(); + +@@ -4888,6 +4945,15 @@ public abstract class Entity + if (!this.canPortalAsync(destination, takePassengers)) { + return false; + } ++ // Luminol start - Add missing teleportation events ++ if (!new me.earthme.luminol.api.entity.PreEntityPortalEvent( ++ this.getBukkitEntity(), ++ io.papermc.paper.util.MCUtil.toLocation(this.level, portalPos), ++ destination.getWorld() ++ ).callEvent()) { ++ return false; ++ } ++ // Luminol end + + // 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 ? +diff --git a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +index e29e379f9b2238eafe27545681bb19c710003bcc..fdf220424f7693a7ea3ae385ddd771720825b0f7 100644 +--- a/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/TheEndGatewayBlockEntity.java +@@ -192,6 +192,18 @@ public class TheEndGatewayBlockEntity extends TheEndPortalBlockEntity { + portalTile.trySearchForExit(portalWorld, portalPos); + return false; + } ++ // Luminol start - Add missing teleportation apis ++ final org.bukkit.Location orginalPortalLocation = io.papermc.paper.util.MCUtil.toLocation(toTeleport.level(), portalPos); ++ final org.bukkit.Location targetPortalLocation = io.papermc.paper.util.MCUtil.toLocation(portalWorld, teleportPos); ++ ++ final me.earthme.luminol.api.portal.PortalLocateEvent portalLocateEvent = new me.earthme.luminol.api.portal.PortalLocateEvent( ++ orginalPortalLocation, ++ targetPortalLocation ++ ); ++ ++ portalLocateEvent.callEvent(); ++ // Luminol end ++ + + // note: we handle the position from the TeleportTransition + net.minecraft.world.level.portal.TeleportTransition teleport = net.minecraft.world.level.block.EndGatewayBlock.getTeleportTransition( diff --git a/lophine-server/minecraft-patches/features/0050-Add-Portal-rate-limiter.patch b/lophine-server/minecraft-patches/features/0050-Add-Portal-rate-limiter.patch new file mode 100644 index 0000000..3eba1b0 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0050-Add-Portal-rate-limiter.patch @@ -0,0 +1,130 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bacteriawa +Date: Tue, 21 Apr 2026 02:27:38 +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..2d5d4170e1f45ae9113c37623b4066861dc79d64 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); + } ++ // Luminol start - Portal rate limiter ++ into.portalRateThrottler.mergeWith(from.portalRateThrottler); ++ from.portalRateThrottler.destroy(); ++ // Luminol end + } + + @Override +@@ -322,6 +326,12 @@ public final class RegionizedWorldData { + into.chunkHoldersToBroadcast.add(chunkHolder); + } + } ++ // Luminol start - Portal rate limiter ++ for (var worldData : dataSet) { ++ from.portalRateThrottler.splitInto(worldData.portalRateThrottler); ++ } ++ from.portalRateThrottler.destroy(); ++ // Luminol end + } + }; + +@@ -343,6 +353,35 @@ public final class RegionizedWorldData { + return this.isHandlingTick; + } + ++ // Luminol start - Portal rate limiter ++ public final me.earthme.luminol.utils.RateThrottler portalRateThrottler = new me.earthme.luminol.utils.RateThrottler(); ++ public final net.objecthunter.exp4j.Expression portalRateCapExpression = me.earthme.luminol.config.modules.function.PortalRateLimiterConfig.getExpressionIfConfigured(); ++ ++ private int computePortalRateCap() { ++ // expression mode is disabled ++ if (this.portalRateCapExpression == null) { ++ return me.earthme.luminol.config.modules.function.PortalRateLimiterConfig.maxPortalTeleportsPerTick; ++ } ++ ++ final int tickingEntityCount = this.entityTickList.size(); ++ final int tickingChunkCount = this.tickingChunks.size(); ++ final int playerCount = this.localPlayers.size(); ++ ++ return me.earthme.luminol.config.modules.function.PortalRateLimiterConfig.computeExpression( ++ this.portalRateCapExpression, ++ tickingEntityCount, ++ tickingChunkCount, ++ playerCount ++ ); ++ } ++ public boolean isPortalTeleportationOutOfRate() { ++ if (!me.earthme.luminol.config.modules.function.PortalRateLimiterConfig.enabled) { ++ return false; ++ } ++ ++ return this.portalRateThrottler.isOutOfRate(this.computePortalRateCap()); ++ } ++ // Luminol 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 350b788e17dc49118498541ad8ac54f6e0a59c9d..dc83fea1f4b8a1d2a82a52b88ee5c3157653f6f8 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1973,6 +1973,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop level + " " + level.dimension().identifier()); + profiler.push("tick"); ++ // Luminol start - Portal rate limiter ++ regionizedWorldData.portalRateThrottler.begin(); ++ // Luminol end + + try { + foliaProfiler.startTimer(level.tickTimerId); try { // Folia - profiler +@@ -1987,6 +1990,9 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Fri, 1 May 2026 22:05:23 +0800 +Subject: [PATCH] Add config for waypoint restoration + + +diff --git a/net/minecraft/commands/Commands.java b/net/minecraft/commands/Commands.java +index 0fa14f60310b063f419620539fd40898627b46b9..9800ae1d3c49800b0c7342d43d696109b5b72cf4 100644 +--- a/net/minecraft/commands/Commands.java ++++ b/net/minecraft/commands/Commands.java +@@ -267,7 +267,7 @@ public class Commands { + TimeCommand.register(this.dispatcher, context); + TitleCommand.register(this.dispatcher, context); + //TriggerCommand.register(this.dispatcher); // Folia - region threading - TODO later +- //WaypointCommand.register(this.dispatcher, context); // Folia - region threading - TODO later ++ if (me.earthme.luminol.config.modules.experiment.CommandConfig.waypointsAndWaypointCommand) WaypointCommand.register(this.dispatcher, context); // Folia - region threading - TODO later // Luminol - Restore waypoints + WeatherCommand.register(this.dispatcher); + WorldBorderCommand.register(this.dispatcher); + if (JvmProfiler.INSTANCE.isAvailable()) { +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index dc83fea1f4b8a1d2a82a52b88ee5c3157653f6f8..7840b0e00ab84eab016e971ac0c2693056fb0721 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -3117,8 +3117,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop playerx.connection.send(packet)); // Paper - per-world game rules + } else if (rule == GameRules.LOCATOR_BAR) { + // this.getAllLevels().forEach(level -> { // Paper - per-world game rules +- ServerWaypointManager waypointManager = level.getWaypointManager(); +- waypointManager.locatorBarEnabled = (Boolean) value; // Paper - optimize ServerWaypointManager with locator bar disabled ++ ServerWaypointManager waypointManager = level.getWaypointManager(); // Luminol - Restore waypoints ++ //waypointManager.locatorBarEnabled = (Boolean) value; // Paper - optimize ServerWaypointManager with locator bar disabled // Luminol - Restore waypoints + if ((Boolean)value) { + level.players().forEach(waypointManager::updatePlayer); + } else { +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 402e2f01f2b3a8e9ce5c818f07581b4dab38fc26..a1c3f3e5c7c3f32e0bdead33ebd72c7601295291 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -690,7 +690,8 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet + + this.sleepStatus = new SleepStatus(); + this.gameEventDispatcher = new GameEventDispatcher(this); +- this.waypointManager = new ServerWaypointManager(this); // Paper - optimize ServerWaypointManager with locator bar disabled ++ //this.waypointManager = new ServerWaypointManager(this); // Paper - optimize ServerWaypointManager with locator bar disabled // Luminol - Restore waypoints ++ this.waypointManager = new me.earthme.luminol.utils.FoliaServerWaypointManager(this); // Luminol - Restore waypoints + this.environmentAttributes = EnvironmentAttributeSystem.builder().addDefaultLayers(this).build(); + //this.updateSkyBrightness(); // Folia - region threading - delay until first tick + // Paper start - rewrite chunk system +diff --git a/net/minecraft/world/waypoints/WaypointTransmitter.java b/net/minecraft/world/waypoints/WaypointTransmitter.java +index 2866b750a28cc32c2913d3c1afee95f05956982c..f0b62f49382eb808dcd6b6b654b99b9f7763c342 100644 +--- a/net/minecraft/world/waypoints/WaypointTransmitter.java ++++ b/net/minecraft/world/waypoints/WaypointTransmitter.java +@@ -76,7 +76,8 @@ public interface WaypointTransmitter extends Waypoint { + private final LivingEntity source; + private final Waypoint.Icon icon; + private final ServerPlayer receiver; +- private float lastAngle; ++ private volatile float lastAngle; // Luminol - Restore waypoints ++ private final java.util.UUID sourceUUID; // Luminol - Restore waypoints (prevent UUID mutation) + + public EntityAzimuthConnection(final LivingEntity source, final Waypoint.Icon icon, final ServerPlayer receiver) { + this.source = source; +@@ -84,6 +85,7 @@ public interface WaypointTransmitter extends Waypoint { + this.receiver = receiver; + Vec3 direction = receiver.position().subtract(source.position()).rotateClockwise90(); + this.lastAngle = (float)Mth.atan2(direction.z(), direction.x()); ++ this.sourceUUID = this.source.getUUID(); // Luminol - Restore waypoints (prevent UUID mutation) + } + + @Override +@@ -95,16 +97,16 @@ public interface WaypointTransmitter extends Waypoint { + + @Override + public void connect() { +- this.receiver.connection.send(ClientboundTrackedWaypointPacket.addWaypointAzimuth(this.source.getUUID(), this.icon, this.lastAngle)); ++ this.receiver.connection.send(ClientboundTrackedWaypointPacket.addWaypointAzimuth(this.sourceUUID, this.icon, this.lastAngle)); // Luminol - Restore waypoints (prevent UUID mutation) + } + + @Override + public void disconnect() { +- this.receiver.connection.send(ClientboundTrackedWaypointPacket.removeWaypoint(this.source.getUUID())); ++ this.receiver.connection.send(ClientboundTrackedWaypointPacket.removeWaypoint(this.sourceUUID)); // Luminol - Restore waypoints (prevent UUID mutation) + } + + @Override +- public void update() { ++ public synchronized void update() { // Luminol - Restore waypoints + Vec3 direction = this.receiver.position().subtract(this.source.position()).rotateClockwise90(); + float currentAngle = (float)Mth.atan2(direction.z(), direction.x()); + if (Mth.abs(currentAngle - this.lastAngle) > 0.008726646F) { +@@ -118,27 +120,29 @@ public interface WaypointTransmitter extends Waypoint { + private final LivingEntity source; + private final Waypoint.Icon icon; + private final ServerPlayer receiver; +- private BlockPos lastPosition; ++ private volatile BlockPos lastPosition; // Luminol - Restore waypoints ++ private final java.util.UUID sourceUUID; // Luminol - Restore waypoints (prevent UUID mutation) + + public EntityBlockConnection(final LivingEntity source, final Waypoint.Icon icon, final ServerPlayer receiver) { + this.source = source; + this.receiver = receiver; + this.icon = icon; + this.lastPosition = source.blockPosition(); ++ this.sourceUUID = this.source.getUUID(); // Luminol - Restore waypoints (prevent UUID mutation) + } + + @Override + public void connect() { +- this.receiver.connection.send(ClientboundTrackedWaypointPacket.addWaypointPosition(this.source.getUUID(), this.icon, this.lastPosition)); ++ this.receiver.connection.send(ClientboundTrackedWaypointPacket.addWaypointPosition(this.sourceUUID, this.icon, this.lastPosition)); // Luminol - Restore waypoints (prevent UUID mutation) + } + + @Override + public void disconnect() { +- this.receiver.connection.send(ClientboundTrackedWaypointPacket.removeWaypoint(this.source.getUUID())); ++ this.receiver.connection.send(ClientboundTrackedWaypointPacket.removeWaypoint(this.sourceUUID)); // Luminol - Restore waypoints (prevent UUID mutation) + } + + @Override +- public void update() { ++ public synchronized void update() { // Luminol - Restore waypoints + BlockPos currentPosition = this.source.blockPosition(); + if (currentPosition.distManhattan(this.lastPosition) > 0) { + this.receiver.connection.send(ClientboundTrackedWaypointPacket.updateWaypointPosition(this.source.getUUID(), this.icon, currentPosition)); +@@ -161,13 +165,15 @@ public interface WaypointTransmitter extends Waypoint { + private final LivingEntity source; + private final Waypoint.Icon icon; + private final ServerPlayer receiver; +- private ChunkPos lastPosition; ++ private volatile ChunkPos lastPosition; // Luminol - Restore waypoints ++ private final java.util.UUID sourceUUID; // Luminol - Restore waypoints (prevent UUID mutation) + + public EntityChunkConnection(final LivingEntity source, final Waypoint.Icon icon, final ServerPlayer receiver) { + this.source = source; + this.icon = icon; + this.receiver = receiver; + this.lastPosition = source.chunkPosition(); ++ this.sourceUUID = this.source.getUUID(); // Luminol - Restore waypoints (prevent UUID mutation) + } + + @Override +@@ -177,16 +183,16 @@ public interface WaypointTransmitter extends Waypoint { + + @Override + public void connect() { +- this.receiver.connection.send(ClientboundTrackedWaypointPacket.addWaypointChunk(this.source.getUUID(), this.icon, this.lastPosition)); ++ this.receiver.connection.send(ClientboundTrackedWaypointPacket.addWaypointChunk(this.sourceUUID, this.icon, this.lastPosition)); // Luminol - Restore waypoints (prevent UUID mutable) + } + + @Override + public void disconnect() { +- this.receiver.connection.send(ClientboundTrackedWaypointPacket.removeWaypoint(this.source.getUUID())); ++ this.receiver.connection.send(ClientboundTrackedWaypointPacket.removeWaypoint(this.sourceUUID)); // Luminol - Restore waypoints (prevent UUID mutable) + } + + @Override +- public void update() { ++ public synchronized void update() { // Luminol - Restore waypoints + ChunkPos currentPosition = this.source.chunkPosition(); + if (currentPosition.getChessboardDistance(this.lastPosition) > 0) { + this.receiver.connection.send(ClientboundTrackedWaypointPacket.updateWaypointChunk(this.source.getUUID(), this.icon, currentPosition)); diff --git a/lophine-server/minecraft-patches/features/0052-Leaf-Secure-seed-and-matter-seed-command.patch b/lophine-server/minecraft-patches/features/0052-Leaf-Secure-seed-and-matter-seed-command.patch new file mode 100644 index 0000000..bfbf771 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0052-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..2042dc0c7046dec25ff46ddd1e5bd45c3358040b 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 (me.earthme.luminol.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..8c0bee7856c8ac62880038770c2f430ecd6c44d9 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 7927769cf9bccb892745f4d1390eb83b2861d1bb..414c21345879f589fb61130180d0ca606e37a454 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 a1c3f3e5c7c3f32e0bdead33ebd72c7601295291..85eed61919b3c2ef93943e1c1b50fbcbca6fb319 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -639,6 +639,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..da3bbf395ef5c173124fc564d0d7d9c96346436e 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 = me.earthme.luminol.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..bac2047025bf133d14438b5f667bb12401b5cdca 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 = me.earthme.luminol.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 (me.earthme.luminol.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..9d91679a2696c065cd99cd6ce395c0d00c9dd933 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 = me.earthme.luminol.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 (!me.earthme.luminol.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..38495536001442b1591251d7625ccdad4b73caf3 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 = me.earthme.luminol.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..55abc8e32d9dabbb5699d6eff109f4067c452098 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 = me.earthme.luminol.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..a3a1ab1b610fd0f75c4d5cc96ab921e47d0cfea7 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 (me.earthme.luminol.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..8fcb08d7ffc90d2d46375faecd17774483a4560a 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 (me.earthme.luminol.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..1a5690b793ad6c30690fc9659c7f1746c55e5997 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 (me.earthme.luminol.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..484977ebf2cd7b75b228ebd7246fe602c2a271d1 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 = me.earthme.luminol.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/lophine-server/minecraft-patches/features/0053-Add-secure-seed-V2-with-Blake3.patch b/lophine-server/minecraft-patches/features/0053-Add-secure-seed-V2-with-Blake3.patch new file mode 100644 index 0000000..f02989e --- /dev/null +++ b/lophine-server/minecraft-patches/features/0053-Add-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] Add 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..208d4b93ee47083bd9069aec710c6db27044203c 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(); // Luminol - Add secure seed V2 with Blake3 ++ // Luminol start - Add secure seed V2 with Blake3 ++ final long[] secureWorldSeed = (me.earthme.luminol.config.modules.function.SecureSeedConfig.enabled && me.earthme.luminol.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.expandLevelSeedTo1024Bits(seed) ++ : null; ++ ++ long terrainSeed = (me.earthme.luminol.config.modules.function.SecureSeedConfig.enabled && me.earthme.luminol.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.BASE_TERRAIN) ++ : seed; ++ long aquiferSeed = (me.earthme.luminol.config.modules.function.SecureSeedConfig.enabled && me.earthme.luminol.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.AQUIFER) ++ : seed; ++ long oreSeed = (me.earthme.luminol.config.modules.function.SecureSeedConfig.enabled && me.earthme.luminol.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.ORE) ++ : seed; ++ // Luminol end ++ this.random = settings.getRandomSource().newInstance(terrainSeed).forkPositional(); // Luminol - 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(); // Luminol - Add secure seed V2 with Blake3 ++ //this.oreRandom = this.random.fromHashOf(Identifier.withDefaultNamespace("ore")).forkPositional(); // Luminol - Add secure seed V2 with Blake3 ++ this.aquiferRandom = settings.getRandomSource().newInstance(aquiferSeed).forkPositional(); // Luminol - Add secure seed V2 with Blake3 ++ this.oreRandom = settings.getRandomSource().newInstance(oreSeed).forkPositional(); // Luminol - 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) { ++ // Luminol start - Add secure seed V2 with Blake3 ++ if (me.earthme.luminol.config.modules.function.SecureSeedConfig.enabled && me.earthme.luminol.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); ++ } ++ // Luminol 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)// Luminol - Add secure seed V2 with Blake3 ++ // Luminol start - Add secure seed V2 with Blake3 ++ ? new DensityFunctions.EndIslandDensityFunction((me.earthme.luminol.config.modules.function.SecureSeedConfig.enabled && me.earthme.luminol.config.modules.function.SecureSeedConfig.version == 2) ++ ? su.plo.matter.HashingV2.getTerrainSeed(secureWorldSeed, su.plo.matter.HashingV2.TerrainType.SURFACE) ++ : seed) ++ // Luminol end ++ : function; + } + } + diff --git a/lophine-server/minecraft-patches/features/0054-Leaf-Replace-brain-maps-with-optimized-collection.patch b/lophine-server/minecraft-patches/features/0054-Leaf-Replace-brain-maps-with-optimized-collection.patch new file mode 100644 index 0000000..cc10695 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0054-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 27894e2935e2afee8190708def4be3b00f922f64..71a11238835b1090c283d03cc30e09b8a65a9762 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/lophine-server/minecraft-patches/features/0055-Leaf-Remove-useless-creating-stats-json-bases-on-pla.patch b/lophine-server/minecraft-patches/features/0055-Leaf-Remove-useless-creating-stats-json-bases-on-pla.patch new file mode 100644 index 0000000..98e7996 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0055-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 53449af103c1774f68cb0c296d47e7032e4e3843..a8f051e6aff31a723a3ba59842fac3a43f4911fe 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/lophine-server/minecraft-patches/features/0056-Leaf-Optimize-PatchedDataComponentMap-equals.patch b/lophine-server/minecraft-patches/features/0056-Leaf-Optimize-PatchedDataComponentMap-equals.patch new file mode 100644 index 0000000..e55826c --- /dev/null +++ b/lophine-server/minecraft-patches/features/0056-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 08ea8107fc8cbec3742278ab5cf03ef9048720f9..217e4fb33d2707c5e50c7ec2dd2aaee410154254 100644 +--- a/net/minecraft/core/component/PatchedDataComponentMap.java ++++ b/net/minecraft/core/component/PatchedDataComponentMap.java +@@ -221,7 +221,19 @@ public final class PatchedDataComponentMap implements DataComponentMap { + + @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/lophine-server/minecraft-patches/features/0057-Leaf-Better-checking-for-useless-move-packets.patch b/lophine-server/minecraft-patches/features/0057-Leaf-Better-checking-for-useless-move-packets.patch new file mode 100644 index 0000000..71d3d00 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0057-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/lophine-server/minecraft-patches/features/0058-Leaf-fast-bit-radix-sort.patch b/lophine-server/minecraft-patches/features/0058-Leaf-fast-bit-radix-sort.patch new file mode 100644 index 0000000..bacceb4 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0058-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 2d5d4170e1f45ae9113c37623b4066861dc79d64..48fea9051efe038a823d7c2c9ae3bf28078f2fcc 100644 +--- a/io/papermc/paper/threadedregions/RegionizedWorldData.java ++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -457,6 +457,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(); + + // 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/lophine-server/minecraft-patches/features/0059-Leaf-Configurable-vanilla-username-check.patch b/lophine-server/minecraft-patches/features/0059-Leaf-Configurable-vanilla-username-check.patch new file mode 100644 index 0000000..d84bf4c --- /dev/null +++ b/lophine-server/minecraft-patches/features/0059-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 7840b0e00ab84eab016e971ac0c2693056fb0721..d2303ad91c9634cdbf599552a6bce7279a1f0e0a 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 deee051c103f9797f4612e06c12f293ec54724ac..911fb0912c7bc6ab0e29c03a21c6bddb248bbc0a 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 (me.earthme.luminol.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 (!me.earthme.luminol.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 (me.earthme.luminol.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 a8f051e6aff31a723a3ba59842fac3a43f4911fe..2939aa1ecd68504424198dd7c90c71c6c1aa4c80 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 (me.earthme.luminol.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..3de226b84f5c72ae277384b3fda4580a7fa2b43d 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, me.earthme.luminol.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 (me.earthme.luminol.config.modules.misc.UsernameCheckConfig.allowOldPlayersJoin && net.minecraft.server.MinecraftServer.getServer().getPlayerList().playedPlayers.contains(name)) return true; ++ if (me.earthme.luminol.config.modules.misc.UsernameCheckConfig.useCustomUsernameRegex()) { ++ return me.earthme.luminol.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..3312a751bdd71080bf429990490061bfbf86b758 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 me.earthme.luminol.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 me.earthme.luminol.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 me.earthme.luminol.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/lophine-server/minecraft-patches/features/0060-Purpur-Lobotomize-stuck-villagers.patch b/lophine-server/minecraft-patches/features/0060-Purpur-Lobotomize-stuck-villagers.patch new file mode 100644 index 0000000..cc7cb63 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0060-Purpur-Lobotomize-stuck-villagers.patch @@ -0,0 +1,89 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 10:48:14 +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..10715e891205eadd3774dc349c52963606ce1ab7 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 = me.earthme.luminol.config.modules.optimizations.LobotomizeVillageConfig.villagerLobotomizeCheckInterval; ++ boolean shouldCheckForTradeLocked = me.earthme.luminol.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 (me.earthme.luminol.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/lophine-server/minecraft-patches/features/0061-Pufferfish-Reduce-projectile-chunk-loading.patch b/lophine-server/minecraft-patches/features/0061-Pufferfish-Reduce-projectile-chunk-loading.patch new file mode 100644 index 0000000..dc3eb42 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0061-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 48fea9051efe038a823d7c2c9ae3bf28078f2fcc..c9f2d54be6f0699c2bf922c66ba0bafc7fb83ccc 100644 +--- a/io/papermc/paper/threadedregions/RegionizedWorldData.java ++++ b/io/papermc/paper/threadedregions/RegionizedWorldData.java +@@ -339,6 +339,10 @@ public final class RegionizedWorldData { + + private RegionizedServer.WorldLevelData tickData; + ++ // Luminol start - Pufferfish projectile limiter ++ public long pufferfish$loadedThisTick = 0L; ++ public long pufferfish$loadedTick = 0L; ++ // Luminol 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..81aafd64cc80038eaafba059373db3682afb8c2c 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 > me.earthme.luminol.config.modules.optimizations.ProjectileChunkReduceConfig.maxProjectileLoadsPerTick) { ++ if (++this.loadedLifetime > me.earthme.luminol.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/lophine-server/minecraft-patches/features/0062-Pufferfish-Throttle-goal-selector-during-inactive-ti.patch b/lophine-server/minecraft-patches/features/0062-Pufferfish-Throttle-goal-selector-during-inactive-ti.patch new file mode 100644 index 0000000..aa45cc4 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0062-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 301e75393d1074eca0c8533219f516b6d625a38a..24171563af7c629bf2bf492fdc575e2bc67c361e 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 = me.earthme.luminol.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/lophine-server/minecraft-patches/features/0063-Petal-Reduce-sensor-work.patch b/lophine-server/minecraft-patches/features/0063-Petal-Reduce-sensor-work.patch new file mode 100644 index 0000000..85da282 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0063-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 24171563af7c629bf2bf492fdc575e2bc67c361e..e7ebdbb8d77f841551c5ebb0f8e7b4402f6a4751 100644 +--- a/net/minecraft/world/entity/Mob.java ++++ b/net/minecraft/world/entity/Mob.java +@@ -842,11 +842,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(); // Luminol - Petal - Move up + ProfilerFiller profiler = Profiler.get(); + profiler.push("sensing"); +- this.sensing.tick(); ++ if (idBasedTickCount % me.earthme.luminol.config.modules.optimizations.PetalReduceSensorWorkConfig.delayTicks == 0 || !me.earthme.luminol.config.modules.optimizations.PetalReduceSensorWorkConfig.enabled) this.sensing.tick(); // Luminol - Petal - Reduce sensor work + profiler.pop(); +- int idBasedTickCount = this.tickCount + this.getId(); ++ //int idBasedTickCount = this.tickCount + this.getId(); // Luminol - Petal - Move up + if (idBasedTickCount % 2 != 0 && this.tickCount > 1) { + profiler.push("targetSelector"); + this.targetSelector.tickRunningGoals(false); diff --git a/lophine-server/minecraft-patches/features/0064-Leaves-Disable-packet-limit.patch b/lophine-server/minecraft-patches/features/0064-Leaves-Disable-packet-limit.patch new file mode 100644 index 0000000..7cfade5 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0064-Leaves-Disable-packet-limit.patch @@ -0,0 +1,60 @@ +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 + + +diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java +index ecaa296f576c5c90fda89e0406d6552628ef5de3..d2e9d46bbd9ee4ce45419b11499d7a7bc50f0219 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 (!me.earthme.luminol.config.modules.misc.PaperPacketLimiterConfig.forceDisable && (this.allPacketCounts != null || // Luminol - Add config to force disable the packet limiter of Paper ++ io.papermc.paper.configuration.GlobalConfiguration.get().packetLimiter.overrides.containsKey(packet.getClass()))) { // Luminol - 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 a2146662c8ec83f62547bcd813c0e4dcb10efc07..31c6defd8941a2c9e55eca3c8ca5623111e98e51 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -892,7 +892,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 (!me.earthme.luminol.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; + } +@@ -2128,6 +2128,7 @@ public class ServerGamePacketListenerImpl + private long lastLimitedPacket = -1; + + private boolean checkLimit(long timestamp) { ++ if (me.earthme.luminol.config.modules.misc.PaperPacketLimiterConfig.forceDisable) return true; // Leaves - disable + if (!io.papermc.paper.configuration.GlobalConfiguration.get().spamLimiter.incomingPacketThreshold.enabled()) { + return true; + } +@@ -2694,6 +2695,8 @@ public class ServerGamePacketListenerImpl + + // Spigot start - spam exclusions + private void detectRateSpam(final TickThrottler throttler, final String message) { ++ if (me.earthme.luminol.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)) { +@@ -3492,7 +3495,7 @@ public class ServerGamePacketListenerImpl + @Override + public void handlePlaceRecipe(final ServerboundPlaceRecipePacket packet) { + // Paper start - auto recipe limit +- if (!org.bukkit.Bukkit.isPrimaryThread()) { ++ if (!me.earthme.luminol.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/lophine-server/minecraft-patches/features/0065-Leaves-Vanilla-Hopper.patch b/lophine-server/minecraft-patches/features/0065-Leaves-Vanilla-Hopper.patch new file mode 100644 index 0000000..e48320a --- /dev/null +++ b/lophine-server/minecraft-patches/features/0065-Leaves-Vanilla-Hopper.patch @@ -0,0 +1,94 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 12:04:49 +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/lophine-server/minecraft-patches/features/0066-Leaves-Lithium-Sleeping-Block-Entity.patch b/lophine-server/minecraft-patches/features/0066-Leaves-Lithium-Sleeping-Block-Entity.patch new file mode 100644 index 0000000..356e2bd --- /dev/null +++ b/lophine-server/minecraft-patches/features/0066-Leaves-Lithium-Sleeping-Block-Entity.patch @@ -0,0 +1,2739 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sun, 5 Jul 2026 23:31:44 +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 c9f2d54be6f0699c2bf922c66ba0bafc7fb83ccc..24e186d05aa0333885178f444fade3b9c930ac97 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; ++// Luminol start - imports for lithium sleeping block entity ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionInventoryEntityTracker; // Luminol - Lithium Sleeping Block Entity ++import org.leavesmc.leaves.lithium.common.tracking.entity.ChunkSectionItemEntityMovementTracker; ++// Luminol 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) { ++ // Luminol start - 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); ++ } ++ // Luminol 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) { // Luminol - region threading for lithium sleeping block entity + into.pendingBlockEntityTickers.add(tileEntityWrapped); +- final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); ++ BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); // Luminol - region threading for lithium sleeping block entity ++ // Luminol start - region threading for lithium sleeping block entity ++ tileEntityWrapped.updateTicksForLithium(fromRedstoneTimeOffset); ++ // Luminol 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) { // Luminol - region threading for lithium sleeping block entity + into.blockEntityTickers.add(tileEntityWrapped); +- final BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); ++ BlockEntity tileEntity = tileEntityWrapped.getTileEntity(); // Luminol - region threading for lithium sleeping block entity ++ // Luminol start - region threading for lithium sleeping block entity ++ tileEntityWrapped.updateTicksForLithium(fromRedstoneTimeOffset); ++ // Luminol 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) { ++ // Luminol start - 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); ++ } ++ // Luminol 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) { // Luminol - region threading for lithium sleeping block entity ++ BlockPos pos = tileEntity.getPos(); // Luminol - region threading for lithium sleeping block entity ++ if (pos == null) pos = tileEntity.getTileEntity().getBlockPos(); // Luminol - 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) { // Luminol - region threading for lithium sleeping block entity ++ BlockPos pos = tileEntity.getPos(); // Luminol - region threading for lithium sleeping block entity ++ if (pos == null) pos = tileEntity.getTileEntity().getBlockPos(); // Luminol - region threading for lithium sleeping block entity + final int chunkX = pos.getX() >> 4; + final int chunkZ = pos.getZ() >> 4; + +@@ -488,6 +557,11 @@ public final class RegionizedWorldData { + public final io.papermc.paper.redstone.RedstoneWireTurbo turbo; + // Environment attribute system + public Reference2ObjectOpenHashMap, EnvironmentAttributeSystem.ValueSampler> attributeSamplers; ++ // Luminol start - 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<>(); ++ // Luminol 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 217e4fb33d2707c5e50c7ec2dd2aaee410154254..e904dedd1444044fdafdb2a42b4a9e4cd58ae678 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 (me.earthme.luminol.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; +@@ -245,4 +246,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..1c7fc913434fd69c0baec72e8816ca2b41d774b9 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 (me.earthme.luminol.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 2ae0cf7c9a4febfa79a9eab2f0e9309a61a78f56..6b3c691dcc9c9e4a8b2b43110aedd5d5131a9520 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; +@@ -4471,11 +4471,13 @@ public abstract class Entity + } + + protected Entity transformForAsyncTeleport(ServerLevel destination, Vec3 pos, Float yaw, Float pitch, Vec3 velocity) { ++ final boolean toSameWorld = this.level != destination; // Luminol - region threading for 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();// Luminol - region threading for sleeping block entity + if (copy instanceof net.minecraft.world.entity.boss.enderdragon.EnderDragon dragon) dragon.syncDragonPartsAfterTeleportTransform(); // Luminol - Sync dragon part when teleportation or firstly created + // 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 +@@ -6140,8 +6142,29 @@ public abstract class Entity + this.setBoundingBox(this.makeBoundingBox()); + } + // Paper end - Block invalid positions and bounding box ++ this.notifyLithiumTrackerIfNeeded(); // Luminol - Leaves lithium sleeping block entity + } + ++ // Luminol start - split out sleeping block entity notify method from leaves's implementation ++ private void notifyLithiumTrackerIfNeeded() { ++ // Leaves start - Lithium Sleeping Block Entity ++ if (!me.earthme.luminol.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()); // Luminol - region threading for 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()); // Luminol - region threading for sleeping block entity ++ } ++ // Leaves end - Lithium Sleeping Block Entity ++ } ++ // Luminol end ++ + public void checkDespawn() { + } + +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index 303b9befb30d7156a9a71900ac1a6ea8f7e8d876..6f5d3ebb7adc567e5106a3c76fccdbee93f51c27 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 (me.earthme.luminol.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..c063694237c0c98ad370ac7fc170172f9ed3fb37 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 (me.earthme.luminol.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..5cbfc9e178b5823f92c0afedb1b0248d34c96292 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 (me.earthme.luminol.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 (me.earthme.luminol.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 6fd7bfa561f71cfd043d7a1092d77b01bb320640..87fb17917f4cecfef2d5d03bf8fd6e61d075212e 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..0c8cc60efc8999e76632c605fa7d2b0cfa347e17 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 (me.earthme.luminol.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..5e0bd45aad738b0bc15404494e5e004b375f8a71 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 (me.earthme.luminol.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..1095c94d7adb8e6baa51bf3cb8a31000592aa990 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 (me.earthme.luminol.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 (me.earthme.luminol.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..9006438c36771794c9375babffa8c5944a38983d 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 (me.earthme.luminol.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 (me.earthme.luminol.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..00aa2d0269f636f6e4c12c1be98cf901e9be8558 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 (me.earthme.luminol.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..b961ec1532937be513e12e5306a34e6cbff24aee 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 (me.earthme.luminol.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..f8a6f3d4d6593ec416ce25a72871a5d40352480a 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..345ccc36cee017c21547138f7dee625f8c5a3fd5 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 (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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..04cfd5d63081d398619b7ebf9f5d6dd3f7e956b5 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 (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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..d315c97d4ee289fd77385cb988a8b7c47eade0f1 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 (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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..1e6ff94fdbf6459bceb4f438ce1e615a5ce72085 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 (me.earthme.luminol.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 (me.earthme.luminol.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled && craftingTicksRemaining < 0) entity.checkSleep(); // Leaves - Lithium Sleeping Block Entity + } + + public void setCraftingTicksRemaining(final int maxCraftingTicks) { + this.craftingTicksRemaining = maxCraftingTicks; ++ if (me.earthme.luminol.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..3604ffc9e85b0b34da7446ae13b7bfbbab374b9e 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 (me.earthme.luminol.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..1109013d0d321f96a11fc4eb437aecaf55e6104f 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 (me.earthme.luminol.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 (me.earthme.luminol.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..18e6dd47b0525531bae56e0ab3c3846185470ce4 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) { // Luminol - Lithium sleeping block entity for folia + this.tickedGameTime += fromRedstoneTimeOffset; + } ++ // Luminol start - Lithium sleeping block entity for folia ++ 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; ++ // Luminol end + } + // Folia end - region threading + +@@ -129,6 +160,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + @Override + public void setBlockState(final BlockState blockState) { ++ if (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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 = me.earthme.luminol.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 (me.earthme.luminol.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 = me.earthme.luminol.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 (me.earthme.luminol.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 : me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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..8025fec49e6b9550d6154d6e735c6a39a2352f24 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 (me.earthme.luminol.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 (me.earthme.luminol.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 (me.earthme.luminol.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..4bc8481049805cd2ec3b3f3844915cae5dc8ff59 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 ++ ++ // Luminol start - region threading for sleeping block entity ++ default void updateTicksForLithium(long redstoneGameTimeOffset) {} ++ // Luminol end + } +diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java +index c78d77089e34c4bb559f173b449bfb7d91b05d5c..7358c75802a37e84e30b1b94a046251d2547a388 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 (me.earthme.luminol.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..a74eb2079863e4aaa8e982497c0fcd890d7de44b 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; // Luminol - 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); // Luminol - 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 (me.earthme.luminol.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 (me.earthme.luminol.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) existingTicker.createdBy = blockEntity; // Luminol - 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 (me.earthme.luminol.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 (me.earthme.luminol.config.modules.optimizations.LeavesSleepingBlockEntityConfig.enabled) result.createdBy = blockEntity; // Luminol - 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; // Luminol - region threading for 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() { ++ // Luminol start - Region threading for sleeping block entity ++ if (true) { ++ if (this.ticker != null) { ++ var ret = this.ticker.getTileEntity(); ++ ++ if (ret == null) { ++ return this.createdBy; ++ } ++ ++ return ret; ++ } ++ ++ return null; ++ } ++ // Luminol 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 + " "; + } ++ ++ // Luminol start - region threading for sleeping block entity ++ @Override ++ public void updateTicksForLithium(long redstoneGameTimeOffset) { ++ if (this.ticker != null) { ++ this.ticker.updateTicksForLithium(redstoneGameTimeOffset); ++ } ++ } ++ ++ // Luminol end + } + + @FunctionalInterface diff --git a/lophine-server/minecraft-patches/features/0067-Leaves-Configurable-collision-behavior.patch b/lophine-server/minecraft-patches/features/0067-Leaves-Configurable-collision-behavior.patch new file mode 100644 index 0000000..69b3631 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0067-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..09e1d701038042052fcbde4b4686da5d6a15f695 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 (me.earthme.luminol.config.modules.fixes.CollisionBehaviorConfig.behaviorMode) { ++ case me.earthme.luminol.enums.EnumCollisionBehaviorMode.VANILLA -> !voxelShapeIntersectVanilla(aabb, singleAABB); ++ case me.earthme.luminol.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/lophine-server/minecraft-patches/features/0068-Leaves-Disable-moved-wrongly-threshold.patch b/lophine-server/minecraft-patches/features/0068-Leaves-Disable-moved-wrongly-threshold.patch new file mode 100644 index 0000000..b4f2c91 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0068-Leaves-Disable-moved-wrongly-threshold.patch @@ -0,0 +1,50 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Tue, 21 Apr 2026 23:35:48 +0800 +Subject: [PATCH] Leaves: Disable moved wrongly threshold + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As part of: Leaves (https://github.com/LeavesMC/Leaves/blob/f553c53e4230aa032e54a69b6479f1959ed24a60/patches/removed/server/0099-Disable-moved-wrongly-threshold.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 31c6defd8941a2c9e55eca3c8ca5623111e98e51..164e2101688a9f29ecfa83f62141d8cb9df22ed1 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -644,7 +644,7 @@ public class ServerGamePacketListenerImpl + return; + } + // Paper end - Prevent moving into unloaded chunks +- if (movedDist - expectedDist > Math.max(100.0, Mth.square(org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed)) && !this.isSingleplayerOwner()) { ++ if (!me.earthme.luminol.config.modules.misc.DisableWarningConfig.disableMovedWronglyThresholdWarning && movedDist - expectedDist > Math.max(100.0, Mth.square(org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) i * speed)) && !this.isSingleplayerOwner()) { // Leaves - disable can + // CraftBukkit end + LOGGER.warn( + "{} (vehicle of {}) moved too quickly! {},{},{}", vehicle.getPlainTextName(), this.player.getPlainTextName(), xDist, yDist, zDist +@@ -674,7 +674,7 @@ public class ServerGamePacketListenerImpl + zDist = targetZ - vehicle.getZ(); + movedDist = xDist * xDist + yDist * yDist + zDist * zDist; + boolean fail = false; +- if (movedDist > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot ++ if (!me.earthme.luminol.config.modules.misc.DisableWarningConfig.disableMovedWronglyThresholdWarning && movedDist > org.spigotmc.SpigotConfig.movedWronglyThreshold) { // Spigot // Leaves - disable can + fail = true; + LOGGER.warn("{} (vehicle of {}) moved wrongly! {}", vehicle.getPlainTextName(), this.player.getPlainTextName(), Math.sqrt(movedDist)); + } +@@ -1614,7 +1614,7 @@ public class ServerGamePacketListenerImpl + + if (this.shouldCheckPlayerMovement(isFallFlying)) { + float metersPerTick = isFallFlying ? 300.0F : 100.0F; +- if (movedDist - expectedDist > Math.max(metersPerTick, Mth.square(org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) deltaPackets * speed))) { ++ if (!me.earthme.luminol.config.modules.misc.DisableWarningConfig.disableMovedWronglyThresholdWarning && movedDist - expectedDist > Math.max(metersPerTick, Mth.square(org.spigotmc.SpigotConfig.movedTooQuicklyMultiplier * (float) deltaPackets * speed))) { // Leaves - disable can + // CraftBukkit end + // Paper start - Add fail move event + io.papermc.paper.event.player.PlayerFailMoveEvent event = fireFailMove(io.papermc.paper.event.player.PlayerFailMoveEvent.FailReason.MOVED_TOO_QUICKLY, +@@ -1675,7 +1675,8 @@ public class ServerGamePacketListenerImpl + zDist = targetZ - this.player.getZ(); + movedDist = xDist * xDist + yDist * yDist + zDist * zDist; + boolean movedWrongly = false; // Paper - Add fail move event; rename +- if (!this.player.isChangingDimension() ++ if (!me.earthme.luminol.config.modules.misc.DisableWarningConfig.disableMovedWronglyThresholdWarning // Leaves - disable can ++ && !this.player.isChangingDimension() + && movedDist > org.spigotmc.SpigotConfig.movedWronglyThreshold // Spigot + && !this.player.isSleeping() + && !this.player.isCreative() diff --git a/lophine-server/minecraft-patches/features/0069-Leaves-Optimized-dragon-respawn.patch b/lophine-server/minecraft-patches/features/0069-Leaves-Optimized-dragon-respawn.patch new file mode 100644 index 0000000..2a27100 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0069-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..3388bf5d2cff00b397af1dd7b9ab93669985b35c 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 (me.earthme.luminol.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/lophine-server/minecraft-patches/features/0070-Gale-Variable-entity-wake-up-duration.patch b/lophine-server/minecraft-patches/features/0070-Gale-Variable-entity-wake-up-duration.patch new file mode 100644 index 0000000..ace6030 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0070-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..6e1c2dcb1ef0b496e3d3bca9353d070488d16f44 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 = me.earthme.luminol.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/lophine-server/minecraft-patches/features/0071-Gale-Replace-AI-attributes-with-optimized-collection.patch b/lophine-server/minecraft-patches/features/0071-Gale-Replace-AI-attributes-with-optimized-collection.patch new file mode 100644 index 0000000..63f1987 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0071-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/lophine-server/minecraft-patches/features/0072-Gale-Skip-entity-move-if-movement-is-zero.patch b/lophine-server/minecraft-patches/features/0072-Gale-Skip-entity-move-if-movement-is-zero.patch new file mode 100644 index 0000000..c75d8ff --- /dev/null +++ b/lophine-server/minecraft-patches/features/0072-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 6b3c691dcc9c9e4a8b2b43110aedd5d5131a9520..d1298c499f6551d7a084f2d1ccd2a2b8b9a2475b 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -1155,8 +1155,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"); +@@ -5634,6 +5640,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/lophine-server/minecraft-patches/features/0073-Gale-Store-mob-counts-in-an-array.patch b/lophine-server/minecraft-patches/features/0073-Gale-Store-mob-counts-in-an-array.patch new file mode 100644 index 0000000..272d37c --- /dev/null +++ b/lophine-server/minecraft-patches/features/0073-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/lophine-server/minecraft-patches/features/0074-Gale-Optimize-random-calls-in-chunk-ticking.patch b/lophine-server/minecraft-patches/features/0074-Gale-Optimize-random-calls-in-chunk-ticking.patch new file mode 100644 index 0000000..89477d0 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0074-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 85eed61919b3c2ef93943e1c1b50fbcbca6fb319..3fda5043937af3d66ab44c41b7e76822a3ddf53b 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -1059,7 +1059,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 // Luminol - Make public for : Gale: Optimize random calls in chunk ticking + + private void optimiseRandomTick(final LevelChunk chunk, final int tickSpeed) { + final LevelChunkSection[] sections = chunk.getSections(); +@@ -1141,7 +1141,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 a74eb2079863e4aaa8e982497c0fcd890d7de44b..3a727763cfee696c9fc87dda90260558936043bf 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/lophine-server/minecraft-patches/features/0075-Gale-Reduce-enderman-teleport-chunk-lookups.patch b/lophine-server/minecraft-patches/features/0075-Gale-Reduce-enderman-teleport-chunk-lookups.patch new file mode 100644 index 0000000..cc780f6 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0075-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/lophine-server/minecraft-patches/features/0076-Gale-Cache-ShapePairKey-hash.patch b/lophine-server/minecraft-patches/features/0076-Gale-Cache-ShapePairKey-hash.patch new file mode 100644 index 0000000..1169ebc --- /dev/null +++ b/lophine-server/minecraft-patches/features/0076-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/lophine-server/minecraft-patches/features/0077-Gale-For-collision-check-has-physics-before-same-veh.patch b/lophine-server/minecraft-patches/features/0077-Gale-For-collision-check-has-physics-before-same-veh.patch new file mode 100644 index 0000000..16fdaf3 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0077-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 d1298c499f6551d7a084f2d1ccd2a2b8b9a2475b..1888210d014f9c00316059a7ccce086dfb0e3a0f 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -2441,8 +2441,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/lophine-server/minecraft-patches/features/0078-Gale-Skip-negligible-planar-movement-multiplication.patch b/lophine-server/minecraft-patches/features/0078-Gale-Skip-negligible-planar-movement-multiplication.patch new file mode 100644 index 0000000..6d847c3 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0078-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 1888210d014f9c00316059a7ccce086dfb0e3a0f..00bc928a19cb877698b34b7c039bee5fe8b5cda5 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -1302,8 +1302,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/lophine-server/minecraft-patches/features/0079-Gale-Optimize-matching-item-checks.patch b/lophine-server/minecraft-patches/features/0079-Gale-Optimize-matching-item-checks.patch new file mode 100644 index 0000000..25b62d2 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0079-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 5cbfc9e178b5823f92c0afedb1b0248d34c96292..88a5453ac9e3415b5b5c5e81899b9b13627ca817 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/lophine-server/minecraft-patches/features/0080-Gale-Reduce-lambda-and-Optional-allocation-in-Entity.patch b/lophine-server/minecraft-patches/features/0080-Gale-Reduce-lambda-and-Optional-allocation-in-Entity.patch new file mode 100644 index 0000000..19ddafc --- /dev/null +++ b/lophine-server/minecraft-patches/features/0080-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/lophine-server/minecraft-patches/features/0081-Gale-Reduce-array-allocations.patch b/lophine-server/minecraft-patches/features/0081-Gale-Reduce-array-allocations.patch new file mode 100644 index 0000000..457e838 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0081-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 093c48e2379c01f45277b88f6c1b19b107a46326..b4d8cc8b520266abe1921b5e990c34fce3f1d982 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -1424,7 +1424,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 164e2101688a9f29ecfa83f62141d8cb9df22ed1..2a383eee714b7da08b552c5bfb3988fad3cadb93 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -3027,7 +3027,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 a4bc767b799c050ce07a3685f655b4ba947d2466..a447b9fbb579591c62b3d8f8e2527796feefc4a0 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 88a5453ac9e3415b5b5c5e81899b9b13627ca817..a6805d436fd3675e385cbf8a3e5b262ae694ed90 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 87fb17917f4cecfef2d5d03bf8fd6e61d075212e..000e4924e8e338728715a3946002903a3118a98a 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 0c8cc60efc8999e76632c605fa7d2b0cfa347e17..4e0c19970595dd2899a2e08ee122266c2338d84d 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 9006438c36771794c9375babffa8c5944a38983d..ac93d2992fe780133439302ce5b851f9ac0c7353 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/lophine-server/minecraft-patches/features/0082-Some-optimizations-from-krypton.patch b/lophine-server/minecraft-patches/features/0082-Some-optimizations-from-krypton.patch new file mode 100644 index 0000000..44d970f --- /dev/null +++ b/lophine-server/minecraft-patches/features/0082-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..c46bf9940d4275ac09e8c94c8ae1019312fa90b7 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) { ++ // Luminol 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; ++ } ++ // Luminol 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..73b01c4ac8c69f6a984c8f3947cdd2688cd70c14 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); // Luminol - Krypton optimizations ++ writeVarIntFull(output, value); // Luminol - Krypton optimizations + } + return output; + } +@@ -74,4 +75,27 @@ public class VarInt { + output.writeByte(value); + return output; + } ++ // Luminol 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); ++ } ++ } ++ // Luminol end + } diff --git a/lophine-server/minecraft-patches/features/0001-Server-I18n.patch b/lophine-server/minecraft-patches/features/0083-Server-I18n.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0001-Server-I18n.patch rename to lophine-server/minecraft-patches/features/0083-Server-I18n.patch diff --git a/lophine-server/minecraft-patches/features/0002-Fix-datapack-command-save-function.patch b/lophine-server/minecraft-patches/features/0084-Fix-datapack-command-save-function.patch similarity index 96% rename from lophine-server/minecraft-patches/features/0002-Fix-datapack-command-save-function.patch rename to lophine-server/minecraft-patches/features/0084-Fix-datapack-command-save-function.patch index 1d4710f..c6003e5 100644 --- a/lophine-server/minecraft-patches/features/0002-Fix-datapack-command-save-function.patch +++ b/lophine-server/minecraft-patches/features/0084-Fix-datapack-command-save-function.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Fix datapack command save function diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index fb5f9176916931b23ebf4a5b7ecdfc5285ffbc2b..ecdcf0db97341d2cb460f86f451cecd5c40dbfee 100644 +index d2303ad91c9634cdbf599552a6bce7279a1f0e0a..4b6f961e695155db1d6cc86496491017bf3a6383 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java -@@ -2450,6 +2450,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 20 * 10 && this.isAlive(); } // KioCG diff --git a/lophine-server/minecraft-patches/features/0011-Add-tick-rate-support.patch b/lophine-server/minecraft-patches/features/0093-Add-tick-rate-support.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0011-Add-tick-rate-support.patch rename to lophine-server/minecraft-patches/features/0093-Add-tick-rate-support.patch diff --git a/lophine-server/minecraft-patches/features/0012-Modify-merge-ItemEntity-logic.patch b/lophine-server/minecraft-patches/features/0094-Modify-merge-ItemEntity-logic.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0012-Modify-merge-ItemEntity-logic.patch rename to lophine-server/minecraft-patches/features/0094-Modify-merge-ItemEntity-logic.patch diff --git a/lophine-server/minecraft-patches/features/0013-Old-zombie-reinforcement.patch b/lophine-server/minecraft-patches/features/0095-Old-zombie-reinforcement.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0013-Old-zombie-reinforcement.patch rename to lophine-server/minecraft-patches/features/0095-Old-zombie-reinforcement.patch diff --git a/lophine-server/minecraft-patches/features/0014-Old-leader-zombie-health-logic.patch b/lophine-server/minecraft-patches/features/0096-Old-leader-zombie-health-logic.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0014-Old-leader-zombie-health-logic.patch rename to lophine-server/minecraft-patches/features/0096-Old-leader-zombie-health-logic.patch diff --git a/lophine-server/minecraft-patches/features/0015-Spawn-invulnerable-time.patch b/lophine-server/minecraft-patches/features/0097-Spawn-invulnerable-time.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0015-Spawn-invulnerable-time.patch rename to lophine-server/minecraft-patches/features/0097-Spawn-invulnerable-time.patch diff --git a/lophine-server/minecraft-patches/features/0016-Leaves-Item-overstack-util.patch b/lophine-server/minecraft-patches/features/0098-Leaves-Item-overstack-util.patch similarity index 98% rename from lophine-server/minecraft-patches/features/0016-Leaves-Item-overstack-util.patch rename to lophine-server/minecraft-patches/features/0098-Leaves-Item-overstack-util.patch index c5f78a5..bd8b0c0 100644 --- a/lophine-server/minecraft-patches/features/0016-Leaves-Item-overstack-util.patch +++ b/lophine-server/minecraft-patches/features/0098-Leaves-Item-overstack-util.patch @@ -38,10 +38,10 @@ index be357d315f5c83349a7f7287ff45e92137950a8c..a542e38ea6a1e8fc81466acbca071d89 if (count > maxAllowedCount) { source.sendFailure(Component.translatable("commands.give.failed.toomanyitems", maxAllowedCount, prototypeItemStack.getDisplayName())); diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index fe6e1aea364620f82de44b942da0d3846c7d17b5..fd1ceb8c6f20b515e3a054050feeb443de33038c 100644 +index 6f5d5b123f69b7bdc3960e0ad216ec291aa100c3..f036ef938bdd66ab87610e76c8c878bf3d3c86b1 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -3219,7 +3219,7 @@ public class ServerGamePacketListenerImpl +@@ -3232,7 +3232,7 @@ public class ServerGamePacketListenerImpl } else if (slot.mayPlace(cursor)) { if (ItemStack.isSameItemSameComponents(clickedItem, cursor)) { int toPlace = packet.buttonNum() == 0 ? cursor.getCount() : 1; @@ -50,7 +50,7 @@ index fe6e1aea364620f82de44b942da0d3846c7d17b5..fd1ceb8c6f20b515e3a054050feeb443 toPlace = Math.min(toPlace, slot.container.getMaxStackSize() - clickedItem.getCount()); if (toPlace == 1) { action = InventoryAction.PLACE_ONE; -@@ -3255,7 +3255,7 @@ public class ServerGamePacketListenerImpl +@@ -3268,7 +3268,7 @@ public class ServerGamePacketListenerImpl } } else if (ItemStack.isSameItemSameComponents(cursor, clickedItem)) { if (clickedItem.getCount() >= 0) { @@ -59,7 +59,7 @@ index fe6e1aea364620f82de44b942da0d3846c7d17b5..fd1ceb8c6f20b515e3a054050feeb443 // As of 1.5, this is result slots only action = InventoryAction.PICKUP_ALL; } -@@ -3472,6 +3472,7 @@ public class ServerGamePacketListenerImpl +@@ -3485,6 +3485,7 @@ public class ServerGamePacketListenerImpl this.player.containerMenu.broadcastFullState(); } else { this.player.containerMenu.broadcastChanges(); @@ -67,7 +67,7 @@ index fe6e1aea364620f82de44b942da0d3846c7d17b5..fd1ceb8c6f20b515e3a054050feeb443 } if (packet.buttonNum() == Inventory.SLOT_OFFHAND && this.player.containerMenu != this.player.inventoryMenu) this.player.containerSynchronizer.sendOffHandSlotChange(); // Paper - update offhand data when the player is clicking in an inventory not their own as the sychronizer does not include offhand slots if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes. -@@ -3583,7 +3584,7 @@ public class ServerGamePacketListenerImpl +@@ -3596,7 +3597,7 @@ public class ServerGamePacketListenerImpl } boolean validSlot = packet.slotNum() >= 1 && packet.slotNum() <= 45; @@ -76,7 +76,7 @@ index fe6e1aea364620f82de44b942da0d3846c7d17b5..fd1ceb8c6f20b515e3a054050feeb443 if (drop || (validSlot && !ItemStack.matches(this.player.inventoryMenu.getSlot(packet.slotNum()).getItem(), packet.itemStack()))) { // Insist on valid slot // CraftBukkit start - Call click event org.bukkit.inventory.InventoryView inventory = this.player.inventoryMenu.getBukkitView(); -@@ -3625,6 +3626,7 @@ public class ServerGamePacketListenerImpl +@@ -3638,6 +3639,7 @@ public class ServerGamePacketListenerImpl this.player.inventoryMenu.getSlot(packet.slotNum()).setByPlayer(itemStack); this.player.inventoryMenu.setRemoteSlot(packet.slotNum(), itemStack); this.player.inventoryMenu.broadcastChanges(); diff --git a/lophine-server/minecraft-patches/features/0017-Leaves-Old-raid-behavior.patch b/lophine-server/minecraft-patches/features/0099-Leaves-Old-raid-behavior.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0017-Leaves-Old-raid-behavior.patch rename to lophine-server/minecraft-patches/features/0099-Leaves-Old-raid-behavior.patch diff --git a/lophine-server/minecraft-patches/features/0018-Compatibility-fix-for-Raid-Revert-and-Cross-Region-D.patch b/lophine-server/minecraft-patches/features/0100-Compatibility-fix-for-Raid-Revert-and-Cross-Region-D.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0018-Compatibility-fix-for-Raid-Revert-and-Cross-Region-D.patch rename to lophine-server/minecraft-patches/features/0100-Compatibility-fix-for-Raid-Revert-and-Cross-Region-D.patch diff --git a/lophine-server/minecraft-patches/features/0019-Leaves-Leaves-Base-Protocol-Core.patch b/lophine-server/minecraft-patches/features/0101-Leaves-Leaves-Base-Protocol-Core.patch similarity index 96% rename from lophine-server/minecraft-patches/features/0019-Leaves-Leaves-Base-Protocol-Core.patch rename to lophine-server/minecraft-patches/features/0101-Leaves-Leaves-Base-Protocol-Core.patch index 8e9d3f2..d6f6cc0 100644 --- a/lophine-server/minecraft-patches/features/0019-Leaves-Leaves-Base-Protocol-Core.patch +++ b/lophine-server/minecraft-patches/features/0101-Leaves-Leaves-Base-Protocol-Core.patch @@ -36,10 +36,10 @@ index 08e253662a2c183c0ab53a51cc07485cff33bf2c..1bd678d668a1febc129f4cb4d0d95ea2 }; } diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index 437abeb7b2d021b38b438d0e046f05809e196930..2ccf454454582d53aee210917218c0867556181e 100644 +index e7f7f7927fc1e2ce45ea5dbf7cc9ec19197c599f..1f13f7b016b4256e92989d83003a983d6ba1c750 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java -@@ -2006,6 +2006,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginMessagerChannels; // Paper end - retain certain values + public final GameProfile profile; // Leaves - protocol core diff --git a/lophine-server/minecraft-patches/features/0020-Leaves-Configurable-trading-with-the-void.patch b/lophine-server/minecraft-patches/features/0102-Leaves-Configurable-trading-with-the-void.patch similarity index 95% rename from lophine-server/minecraft-patches/features/0020-Leaves-Configurable-trading-with-the-void.patch rename to lophine-server/minecraft-patches/features/0102-Leaves-Configurable-trading-with-the-void.patch index c9459a9..63565ef 100644 --- a/lophine-server/minecraft-patches/features/0020-Leaves-Configurable-trading-with-the-void.patch +++ b/lophine-server/minecraft-patches/features/0102-Leaves-Configurable-trading-with-the-void.patch @@ -8,10 +8,10 @@ As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/9d2bd3f7b0a48f00d Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 10b5783ec1220393e6f9c6233c6111654f5fd461..f001830e178d3c5d7ffb1486eae4b7c1d950e969 100644 +index 3fda5043937af3d66ab44c41b7e76822a3ddf53b..7e362b71c629c75475016973a0181fce75a5156a 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -3135,7 +3135,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -3136,7 +3136,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet // Spigot start if (entity.getBukkitEntity() instanceof org.bukkit.inventory.InventoryHolder && (!(entity instanceof ServerPlayer) || entity.getRemovalReason() != Entity.RemovalReason.KILLED)) { // SPIGOT-6876: closeInventory clears death message // Paper start - Fix merchant inventory not closing on entity removal diff --git a/lophine-server/minecraft-patches/features/0021-Leaves-Servux-Protocol.patch b/lophine-server/minecraft-patches/features/0103-Leaves-Servux-Protocol.patch similarity index 91% rename from lophine-server/minecraft-patches/features/0021-Leaves-Servux-Protocol.patch rename to lophine-server/minecraft-patches/features/0103-Leaves-Servux-Protocol.patch index 32696e3..142d609 100644 --- a/lophine-server/minecraft-patches/features/0021-Leaves-Servux-Protocol.patch +++ b/lophine-server/minecraft-patches/features/0103-Leaves-Servux-Protocol.patch @@ -23,10 +23,10 @@ index 8432d0ce22ddc9c9bfa98901e403697f40e53c99..9ec141359c4422c1574dac71a88b7f50 + // Leaves end - servux } diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index f001830e178d3c5d7ffb1486eae4b7c1d950e969..b03491230ed9b68e0e8f29719e34ea4b9947f408 100644 +index 7e362b71c629c75475016973a0181fce75a5156a..cb11830c2f399c9706b20f8104686dc0d9b686f9 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java -@@ -2568,6 +2568,8 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -2569,6 +2569,8 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet this.server.updateEffectiveRespawnData(); } // Paper end diff --git a/lophine-server/minecraft-patches/features/0022-LeavesHooks.patch b/lophine-server/minecraft-patches/features/0104-LeavesHooks.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0022-LeavesHooks.patch rename to lophine-server/minecraft-patches/features/0104-LeavesHooks.patch diff --git a/lophine-server/minecraft-patches/features/0023-Leaves-Old-Explosion-Damage-Calculator.patch b/lophine-server/minecraft-patches/features/0105-Leaves-Old-Explosion-Damage-Calculator.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0023-Leaves-Old-Explosion-Damage-Calculator.patch rename to lophine-server/minecraft-patches/features/0105-Leaves-Old-Explosion-Damage-Calculator.patch diff --git a/lophine-server/minecraft-patches/features/0024-Leaves-Redstone-Shears-Wrench.patch b/lophine-server/minecraft-patches/features/0106-Leaves-Redstone-Shears-Wrench.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0024-Leaves-Redstone-Shears-Wrench.patch rename to lophine-server/minecraft-patches/features/0106-Leaves-Redstone-Shears-Wrench.patch diff --git a/lophine-server/minecraft-patches/features/0025-Leaves-Syncmatica-Protocol.patch b/lophine-server/minecraft-patches/features/0107-Leaves-Syncmatica-Protocol.patch similarity index 94% rename from lophine-server/minecraft-patches/features/0025-Leaves-Syncmatica-Protocol.patch rename to lophine-server/minecraft-patches/features/0107-Leaves-Syncmatica-Protocol.patch index d65ca3d..4024df0 100644 --- a/lophine-server/minecraft-patches/features/0025-Leaves-Syncmatica-Protocol.patch +++ b/lophine-server/minecraft-patches/features/0107-Leaves-Syncmatica-Protocol.patch @@ -9,7 +9,7 @@ As a part of : Leaves (https://github.com/LeavesMC/Leaves) Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index fd1ceb8c6f20b515e3a054050feeb443de33038c..9ff62124dad00c4d9ba6de87f0cc21c25019acd4 100644 +index f036ef938bdd66ab87610e76c8c878bf3d3c86b1..170ce02f1e93f3cb493e01d5464768e872bfb491 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -348,9 +348,12 @@ public class ServerGamePacketListenerImpl diff --git a/lophine-server/minecraft-patches/features/0026-Leaves-BBOR-Protocol.patch b/lophine-server/minecraft-patches/features/0108-Leaves-BBOR-Protocol.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0026-Leaves-BBOR-Protocol.patch rename to lophine-server/minecraft-patches/features/0108-Leaves-BBOR-Protocol.patch diff --git a/lophine-server/minecraft-patches/features/0027-Leaves-Xaero-Map-Protocol.patch b/lophine-server/minecraft-patches/features/0109-Leaves-Xaero-Map-Protocol.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0027-Leaves-Xaero-Map-Protocol.patch rename to lophine-server/minecraft-patches/features/0109-Leaves-Xaero-Map-Protocol.patch diff --git a/lophine-server/minecraft-patches/features/0028-Leaves-Support-REI-protocol.patch b/lophine-server/minecraft-patches/features/0110-Leaves-Support-REI-protocol.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0028-Leaves-Support-REI-protocol.patch rename to lophine-server/minecraft-patches/features/0110-Leaves-Support-REI-protocol.patch diff --git a/lophine-server/minecraft-patches/features/0029-Leaves-Jade-Protocol.patch b/lophine-server/minecraft-patches/features/0111-Leaves-Jade-Protocol.patch similarity index 98% rename from lophine-server/minecraft-patches/features/0029-Leaves-Jade-Protocol.patch rename to lophine-server/minecraft-patches/features/0111-Leaves-Jade-Protocol.patch index c0ca6c1..3fbaf9a 100644 --- a/lophine-server/minecraft-patches/features/0029-Leaves-Jade-Protocol.patch +++ b/lophine-server/minecraft-patches/features/0111-Leaves-Jade-Protocol.patch @@ -9,10 +9,10 @@ As a part of : Leaves (https://github.com/LeavesMC/Leaves) Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 9ff62124dad00c4d9ba6de87f0cc21c25019acd4..de3c09b26c28a1bc6012cff6f524f72332f7b1f0 100644 +index 170ce02f1e93f3cb493e01d5464768e872bfb491..6b3d1cdfdee2a815807ac32a6fc2f353215a13ab 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -3893,7 +3893,7 @@ public class ServerGamePacketListenerImpl +@@ -3906,7 +3906,7 @@ public class ServerGamePacketListenerImpl } @Deprecated @io.papermc.paper.annotation.DoNotUse // Paper - Add PlayerLoadedWorldEvent diff --git a/lophine-server/minecraft-patches/features/0030-Leaves-Alternative-block-placement-Protocol.patch b/lophine-server/minecraft-patches/features/0112-Leaves-Alternative-block-placement-Protocol.patch similarity index 97% rename from lophine-server/minecraft-patches/features/0030-Leaves-Alternative-block-placement-Protocol.patch rename to lophine-server/minecraft-patches/features/0112-Leaves-Alternative-block-placement-Protocol.patch index 885ec38..8d47f29 100644 --- a/lophine-server/minecraft-patches/features/0030-Leaves-Alternative-block-placement-Protocol.patch +++ b/lophine-server/minecraft-patches/features/0112-Leaves-Alternative-block-placement-Protocol.patch @@ -13,10 +13,10 @@ MasaGadget(https://github.com/plusls/MasaGadget) litematica(https://github.com/maruohon/litematica) diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index de3c09b26c28a1bc6012cff6f524f72332f7b1f0..aec23bc77fc78eea6a3137b71a6923c217ad40a7 100644 +index 6b3d1cdfdee2a815807ac32a6fc2f353215a13ab..77b9cd3d14eddc735d9131597916405fde4230ec 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -2162,7 +2162,7 @@ public class ServerGamePacketListenerImpl +@@ -2175,7 +2175,7 @@ public class ServerGamePacketListenerImpl if (ca.spottedleaf.moonrise.common.util.TickThread.isTickThreadFor(this.player.level(), pos.getX() >> 4, pos.getZ() >> 4, 8) && this.player.isWithinBlockInteractionRange(pos, 1.0)) { // Folia - do not allow players to interact with blocks outside the current region Vec3 distance = location.subtract(Vec3.atCenterOf(pos)); double limit = 1.0000001; diff --git a/lophine-server/minecraft-patches/features/0031-Leaves-Fakeplayer.patch b/lophine-server/minecraft-patches/features/0113-Leaves-Fakeplayer.patch similarity index 97% rename from lophine-server/minecraft-patches/features/0031-Leaves-Fakeplayer.patch rename to lophine-server/minecraft-patches/features/0113-Leaves-Fakeplayer.patch index 0207796..85e10dd 100644 --- a/lophine-server/minecraft-patches/features/0031-Leaves-Fakeplayer.patch +++ b/lophine-server/minecraft-patches/features/0113-Leaves-Fakeplayer.patch @@ -33,7 +33,7 @@ index d2e9d46bbd9ee4ce45419b11499d7a7bc50f0219..848967dbe41694411603ef369cfdc3a0 private final java.util.concurrent.atomic.AtomicBoolean disconnectionHandled = new java.util.concurrent.atomic.AtomicBoolean(false); // Folia - region threading - may be called concurrently during configuration stage private int receivedPackets; diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index 2ccf454454582d53aee210917218c0867556181e..dd2a108f31bcbc1beae8c820c22baaa515fec7ec 100644 +index 1f13f7b016b4256e92989d83003a983d6ba1c750..f76e40f8bbea076ad620f7525d4d8666603a5175 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java @@ -296,6 +296,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop 0) { this.playerList.saveAll(playerSaveInterval); @@ -79,7 +79,7 @@ index 2ccf454454582d53aee210917218c0867556181e..dd2a108f31bcbc1beae8c820c22baaa5 } if (region == null && fullSave) { // Folia - region threading - don't auto save level data this.saveGlobalData(false); -@@ -2034,6 +2047,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> oldType = PoiTypes.forState(oldState); -@@ -3068,6 +3076,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -3069,6 +3077,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet // ServerLevel.this.getChunkSource().addEntity(entity); // Paper - ignore and warn about illegal addEntity calls instead of crashing server; moved down below valid=true if (entity instanceof ServerPlayer player) { ServerLevel.this.players.add(player); @@ -331,7 +331,7 @@ index b03491230ed9b68e0e8f29719e34ea4b9947f408..558f465e9ec2536064716a196d52d78d if (player.isReceivingWaypoints()) { ServerLevel.this.getWaypointManager().addPlayer(player); } -@@ -3149,6 +3162,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet +@@ -3150,6 +3163,11 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet ServerLevel.this.getChunkSource().removeEntity(entity); if (entity instanceof ServerPlayer player) { ServerLevel.this.players.remove(player); @@ -542,7 +542,7 @@ index ca1e635b0fdf3d0d17447b43a558fe030536d297..ed50484ca24ef24b1e5bef9f6369dfa4 public @Nullable ServerPlayer getPlayer(final String playerName) { diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java -index b7cc122d45ecaf922ca1aca8ff5f1632965ad357..8631acfea913aceabefad3d418d282042bbaca1a 100644 +index db7281fcf1f4dc878a87ef9b8f24f2f9da12238a..b11637543c3ef8650ad5c65c2cab8a1cb6e95158 100644 --- a/net/minecraft/world/entity/Entity.java +++ b/net/minecraft/world/entity/Entity.java @@ -1263,7 +1263,7 @@ public abstract class Entity diff --git a/lophine-server/minecraft-patches/features/0032-Leaves-Replay-Mod-API.patch b/lophine-server/minecraft-patches/features/0114-Leaves-Replay-Mod-API.patch similarity index 98% rename from lophine-server/minecraft-patches/features/0032-Leaves-Replay-Mod-API.patch rename to lophine-server/minecraft-patches/features/0114-Leaves-Replay-Mod-API.patch index 3ed3387..289ef1e 100644 --- a/lophine-server/minecraft-patches/features/0032-Leaves-Replay-Mod-API.patch +++ b/lophine-server/minecraft-patches/features/0114-Leaves-Replay-Mod-API.patch @@ -88,10 +88,10 @@ index 6c05b9276bdc267a36376f6c64592a50e4b2b36e..21d2c97b3d1e1d29c9de02b6acd58985 result.add(player); if (result.size() >= limit) { diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java -index dd2a108f31bcbc1beae8c820c22baaa515fec7ec..67bdd2887afed082fd1bae03f030f2d25d55e282 100644 +index f76e40f8bbea076ad620f7525d4d8666603a5175..77bbc527b017c2ac42ebe4638457caf32868e3f6 100644 --- a/net/minecraft/server/MinecraftServer.java +++ b/net/minecraft/server/MinecraftServer.java -@@ -1899,7 +1899,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop { // Folia - region threading // Paper start - Expand PlayerGameModeChangeEvent org.bukkit.event.player.PlayerGameModeChangeEvent event = updatedPlayer.setGameMode(gameType, org.bukkit.event.player.PlayerGameModeChangeEvent.Cause.DEFAULT_GAMEMODE, null); // Folia - region threading -@@ -2528,7 +2528,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop ServerGamePacketListenerImpl.this.getCraftPlayer().acceptConversationInput(conversationInput)); @@ -244,7 +244,7 @@ index aec23bc77fc78eea6a3137b71a6923c217ad40a7..3f3125e0f198f5530057371d631d8e32 } else if (this.player.getChatVisibility() == ChatVisiblity.SYSTEM) { // Re-add "Command Only" flag check this.send(new ClientboundSystemChatPacket(Component.translatable("chat.cannotSend").withStyle(ChatFormatting.RED), false)); } else { -@@ -2686,6 +2713,9 @@ public class ServerGamePacketListenerImpl +@@ -2699,6 +2726,9 @@ public class ServerGamePacketListenerImpl // Spigot start - spam exclusions private void detectRateSpam(final TickThrottler throttler, final String message) { @@ -254,7 +254,7 @@ index aec23bc77fc78eea6a3137b71a6923c217ad40a7..3f3125e0f198f5530057371d631d8e32 if (me.earthme.luminol.config.modules.misc.PaperPacketLimiterConfig.forceDisable) return; // Leaves - disable // CraftBukkit start - replaced with thread safe throttle if (org.spigotmc.SpigotConfig.enableSpamExclusions) { -@@ -3632,8 +3662,10 @@ public class ServerGamePacketListenerImpl +@@ -3645,8 +3675,10 @@ public class ServerGamePacketListenerImpl if (org.leavesmc.leaves.util.ItemOverstackUtils.hasOverstackingItem()) this.player.containerMenu.sendSingleSlot(packet.slotNum(), itemStack); // Leaves - item over-stack util - force send carried item if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.updateEquipmentOnPlayerActions) this.player.detectEquipmentUpdates(); // Paper - Force update attributes. } else if (drop && validData) { diff --git a/lophine-server/minecraft-patches/features/0046-Carpet-Features-Compatible.patch b/lophine-server/minecraft-patches/features/0128-Carpet-Features-Compatible.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0046-Carpet-Features-Compatible.patch rename to lophine-server/minecraft-patches/features/0128-Carpet-Features-Compatible.patch diff --git a/lophine-server/minecraft-patches/features/0047-PCA-sync-protocol.patch b/lophine-server/minecraft-patches/features/0129-PCA-sync-protocol.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0047-PCA-sync-protocol.patch rename to lophine-server/minecraft-patches/features/0129-PCA-sync-protocol.patch diff --git a/lophine-server/minecraft-patches/features/0048-Add-Vanilla-like-experience-Config.patch b/lophine-server/minecraft-patches/features/0130-Add-Vanilla-like-experience-Config.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0048-Add-Vanilla-like-experience-Config.patch rename to lophine-server/minecraft-patches/features/0130-Add-Vanilla-like-experience-Config.patch diff --git a/lophine-server/minecraft-patches/features/0049-Restore-vanilla-ender-pearl-loading.patch b/lophine-server/minecraft-patches/features/0131-Restore-vanilla-ender-pearl-loading.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0049-Restore-vanilla-ender-pearl-loading.patch rename to lophine-server/minecraft-patches/features/0131-Restore-vanilla-ender-pearl-loading.patch diff --git a/lophine-server/paper-patches/features/0001-Rebrand-to-Luminol.patch b/lophine-server/paper-patches/features/0001-Rebrand-to-Luminol.patch new file mode 100644 index 0000000..375b86e --- /dev/null +++ b/lophine-server/paper-patches/features/0001-Rebrand-to-Luminol.patch @@ -0,0 +1,1346 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 17 Apr 2026 22:46:06 +0800 +Subject: [PATCH] Rebrand to Luminol + + +diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java +index 08745a35b1ce8cc56d970472088de3cd17b9e43a..fcdfd4f6de5e1c2d5e80c7f3d8cb0b33a62d257d 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("Luminol", serverUUID, logFailedRequests, Bukkit.getLogger()); // Folia - we have our own bstats page // Luminol + + 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-Luminol-%s-%s".formatted(Bukkit.getServer().getMinecraftVersion(), buildOrHash); // Folia - we have our own bstats page // Luminol - Rebrand to Luminol + } else { + paperVersion = "unknown"; + } +- metrics.addCustomChart(new Metrics.SimplePie("folia_version", () -> paperVersion)); // Folia - we have our own bstats page ++ metrics.addCustomChart(new Metrics.SimplePie("luminol_version", () -> paperVersion)); // Folia - we have our own bstats page // Luminol + + 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..fd7cd6f72390c71d036ebfc0e4c67dad51eb2e9c 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/LuminolMC/Luminol"; // Luminol ++ private static final String REPOSITORY = "LuminolMC/Luminol"; // Luminol + 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) { // Luminol + 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) { // Luminol + 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..4ada790812957657d580cb748077c4961987d0cb 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_LUMINOL_NAME = "Luminol"; + + 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_LUMINOL_ID), // Folia // Luminol + getManifestAttribute(manifest, ATTRIBUTE_BRAND_NAME) +- .orElse("Folia"), // Folia ++ .orElse(BRAND_LUMINOL_NAME), // Folia // Luminol + 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); // Luminol + } + + @Override +diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java +index 5607f4cc8ca2f620461eb5e97d01d9db0d6b2303..1f8eba1a93d6844bc2e1ddf8c1e261fbf7caa925 100644 +--- a/src/main/java/org/spigotmc/WatchdogThread.java ++++ b/src/main/java/org/spigotmc/WatchdogThread.java +@@ -80,9 +80,10 @@ 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, "DO NOT REPORT THIS TO PAPER!"); ++ logger.log(Level.SEVERE, "If you are unsure or still think this is a Luminol bug, please report this to https://github.com/LuminolMC/Luminol/issues"); + 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, "Server version: " + Bukkit.getServer().getVersion()); + + if (net.minecraft.world.level.Level.lastPhysicsProblem != null) { + logger.log(Level.SEVERE, "------------------------------"); +diff --git a/src/main/resources/logo.png b/src/main/resources/logo.png +index c06126b92e36566c93b901637926703bbfcc42ff..2b6037aab645e303df6edbbc8af34dc4b4a55bc5 100644 +GIT binary patch +literal 32775 +zcmeFZ`9IYA`#(O?zNsup8mH5tER&{?sX?W33OOZX7+d2+8tY8T7+am95G7korgBi2 +zL1B!&Y#D2bu@hz{dkn@{Kab~h&inQHer}&X;QPb-hjZL+T#xH8(W;QPy2M|V0IbV@PY3^p!DcY)pdT-pYS0f@i3RjW*mpW45bTtP1@r^vo3RG^ +z(TtOYegyUY@6!K;>3?0~eq#Q)zO97mqbbIQuPsVn`)E0pqf@tHLiP(eP$6$MnF+F?L=P%IsyC_)oN* +zs|!N=9IdnfH +zygcGSZ0l8iqEgP}cmU0-(OYsNYct@Q?UAFHw1UV?^_2>FZ|Ded4O)J*N`Ira;SE1{ +zYv9~1fh9xT)8OYH9&*FeYqDoOw#3=k_Xmi#5`+1c37hCR^!UZL3 +z@-3ip;uajWR@Frj_3t{UI^!wcx#4eG_aoQY{YXQtk8uaEr6z!`I0 +z<2WYK# +z{zjUg(%o?g>7ZD5=~a?z_Pax?-OnqDP8>@9nWMp^(zt(CZ^_ZRsh=zBX}ThMU%*{K +zL_Tn_nOd) +z+4a_UbcIj7Q2w7~7hMByw@&)iap0yJ+cK!fNkzQHt*&X1G5s*P2JeTB8?XKLt$enm +zkVo0}&+n_biZ4~Q#`**0YkanAl&xbRvfRp`U*Q6Yzwq+bB;4N@c3+itAI0G${U?+3R&wGk|D_z6|-uM0ZP!QFE5{)96W&eB%cPl&Qure9app3TN=@>4f- +z-%`xIWmOyFUo^yr{UhCr?|1qavPyZyzG0r6Ch6tO@XKNpy`Wb==#=fYdE)h?mRlH5gT#?YrJ>hybwpO92y +zw`C9-v(n@a`9u#JY +zwiFKQN(cT@j;wkY`X(X%&Q*-oa*SY%8(SxgQA9D){)b?yGQO+#FsjMUo#kg+8|hfY +zyG6i&I+qRj8CL&Pbmi#{kDsQ(N? +zPer6t#m&DhF!}4IPx6w?O~@_-(dE-BakDT?!f|{^ZK9JVotT%-@HGphDrhiZ5OJ&YMZuYmq1dFnTJz;EQEznNrZ1dynA_6 +z+PTkKB}qrPDIg^`t{R+1ldx|+S$06 +zO~M=O{HikI1pR>prnpDdz@Xe8${P+!_$9ZyRgR5CdD-kx;c)Mt50b^~mhcg7ch3o# +z*<$*pr6rgU$`wJZDzth?65vyX&cko*6C7LHEj~kTGHXZdk>%+xnFU<3?Q_@2@3_X2 +zJlys^%jtW4q6FIhpP4(m!1NEZ9M|_0mo>Hm+MWhPjevcYa#aeo8jE*18~ay3$u3*X +zz_8pOvWD(!Z2qH|U^6SSph&V_Nzk|VLVfFf(NlrPcx(kTrnvu3p}~7kEht_i`gJ6h +zcNw<~)HJlP*LpW7uTq~@+TZ{nt8D}0!hN9=T~^<2K!A+3x_uF8#~!cp&$aTZH6Y34 +zXniA=9#~Y5e3gRD58zSzwWUO;H4_<|0jNpQkDN5rpX9ge+^IDPltDIq8$Bmqj4K^o1A&DnEWL;Vwxao{8$nJZJ*VUIfK)1Yi%RK5%; +z6>p`n?Gc6k8pR!b{EL2RPflDfPI6y)BgMbAX{To(q>PhnU1%~QUt*1v4_%=o_#X0#vScLQ>)mRhBSIZcn)_l2AzuHz# +zY-%FgOzy0E-%|YT=^YaPl;)K(UTpi?6^ +zKF(nJ*5^rfLYe1C0#>Ety7ASdtNFJY$g{fuxi5S0hH9+yA-+~v4LYf5Ga&!I@CA0N +zWiu@3L(C(`@Xvpf>shlq>OOR#n0wH?oLm_}+c*whhrzqp=+SMS-A(xF`6q<-`JKke +z*X{>fc-4w#cL;R$G?FzCJ&e_YsFv@IWJO$9iYhhB_^(dMKr!TNKMR-AwbQ7MgdD{) +zG(kd@A>7OuY5#tbTH~pS>~*Fy(`*jBj8p@R^{U=ocd4{MN})JyuUA=iO0#$1#>UK1 +zgUlhVcO-a)URAM2a&eq7V`sAwr6dVHvGzT(z#NH(1V&WNZ_8ltk+Mk6T)wsx*#to! +zIu^BG0duaN;?#*b%u+dJ4Eb)4NI8$4FPS<+L3Qlx@ODhLt6J<0j7#YF5b1Xumva!dwL+vDn<-Y +z!Zv(_VWqu550J{g52(-v+}y?7u0q@>p^p?W#|?-p^s4!;gR4V0xVbUX?ts&$V2hn3 +z55whfW5&88!H_P(T_muU1UCeL2in%U=%k?;v6$&$m!aTRdvy`~gE^oSSJ%QF0zk>Y +z1-o-rbD~;Y;nM}yk8%`{6*`%r?M8pi94Jh(+EC2c46;TW9%bT}@8-@yN5tYYgX==L +zUU@Qu#VF73V1LJ?Wp59T%?1Yz2%~%%5?J5sjBF@4idcMJ(|RTRa>X76Owo-e0p-oY +zbZl4A(EN$K8lL%>qdw6}e +zI&)h_8agQ!Z=h~psI#6uYe~F$JvO+tg2X_r>QO=)`K=NjP&)5_&%XbI$7lcPC0CM& +zIN(a?`luX92k2+1qwt@e(IAO)slIM><7GwWYz-wBc1{lc-x-dR4mT1k%y!|##|T(KMZl~tEgbM +zd0W(7)E6GbRos-y*xxsbQqoO8wXe#H?CwTHlhLP{Fj>;sA$i**l`Mzc|xmm-FpfM_1K3Z^)Cg0G3J^ +zVkg^mTgE&8qR +z8W>}csfenTM#VxNGCx16#o45Fqbf_EPp$D<GyoiqJ26Y*BF&ZU6{HVnR})%nJ7Tp@}MUxQB60ldNkT +zy0LA5WU3DKjF$E8mDzamDsZ4*ih%tXCNjbyBRC1rkv<+KQ|U2^m}%cFfbs_RLxfm3 +zi|0G4R}NOjp(kSTw2bzDi58>*k%@O!kPu24KQh{#4V6!Fy_hnKX!WtEnhxvYzz6+T +zO`k-Kl0fD*I?`IQMn4t8FU)84??6mX{_Yq;*3kP6ap<WdrERW#`XZwV#rIPDa8TZW`2JYgXUa<3GEmV-8t_p^ +z=+rlosare6XRp1fUXhg_8tHMD+M&#Ol|o#Oq24|a$qd0_-n(EMUM+18_%sWPfXX^W +z(_9xtGiDPIrkY^TVY#X^?@))?M(J$LWqrIkO7pXC_4vDH=BVrPu#0b-Z;!UjEEV{8p=J+LAwVp +z_1w}lLt@{<yzUb|(*2+~!s_5{>p9rH;#%YD@P +zko|YwV)}jD_OzJph_M91*DH#n0XBkp;o-T@*=nZl8RPz_S3G-q3Bquyf88ak18l~ESelJtVOZBB*ff{p1* +z?i=j)@LtRN1EQ9ODi#vln7~woW2Vovq~{qkJ|tBZY|Dsh(T?TQGbc#;vPWXnfbF7= +z)`p_?V$KOS;epvsrrBfw2~0DTSnDaES4;11nI?};Esmt1EEua_Uiy9_uc?O;arm!1 +zcdQ<7Dp$^U`Mk|C@}4=v(%7pwwRH#XIxaNjsdog|ZuMcx+nE8El=Fmu<&)<`R#i-_ +zf_a*sZIB-1wFe#oM58RjpEq4c4KKI`m7d?XVvSK)+`5`o3(? +zkQ7*lIqID2)!=00>GGCEQKK+e79>SUrM?1&gJ!h3qtKZ{RhnRZn4}J!xXr;hC-d+! +ze70?Rx~z^rzDu(B7fBx)#L-a0qa$6VQ2GQvCr_@f62bquYEYZL(x@HO6q<6{KH;)Q +zfbq?hJ>s!27z@&XS6IUB2~!c9x-WAR^5kj=*0!W={mFQ;l@S93`g0SOugpfC#a*M?21aOioub}b_~hJ;_`xYda&es8!h+dx|062$Wj<=8+(pK(R{ +zd_qloCn7{io&0pDP`rWOkU}}7a=I@J5m5GGx@o<*@Uwn+eNB{9{R=DMG0I1YF^^aR +z$J)<5<&!I1F#k=@y{VbmKB1(0NyL{Ngc3KRQomPbKBETI9r)+%++yrgK;P(GbCJYY +zM0wP!>X*rSKBlK1c?y8>^tk>d`{>j7o{cz3W>WV!hG9!U(O#&jJC+XRRM0Bl-Mke> +z4*=C6;H94FZ=ooCpC75lzq5A@(WbXNSi5q^LWuM8cd!AD(Qk(@^Dud@ +z?Rk_H0OgFc6`^+?xk*_ChE0@Z-DN@@~l(0^Y8m`^u)Gji`U})wQa`o}?Jk +zj7vV{{Iy>J9|r;`Z8Gi9ys3P-{Pm?c#x+R5=Int;e8+k$1~YpDBWs2_=->*`f+H5t +z-s-!#Ygr|zg{|$1;2%S6Q@4|KYs&O(K=IdyLl;`EsuRM_>B~zoHQiU*73TM|B7lG@ +zY1gXKyWUTE3gnj)Evn2LrNm`&`$dheQ`42#%mRjxOdvqO?+iyEz@ry}_FrNV-;$UA +zs#4`|3Wq97*Zr}9*Ob25b0&wWH9gk<;q2WwI8g9azRnNLA+ee-V> +ztu)0@oxBktKQZllAe>sYE?oDB1GYBmq`8 +z7#qNhJ1;WX#`$t}bm86BeiGyg&XryYs%hvsP}~lTUt%R8s;H(JB>09!Ig(C#z^r!U +zCh_4l|K{jv>~pQL&wb=EdX?8rf6X+TmRNjgYAY)8u74=FgP25mB#(fRSkW4qB?u<{ +zUt^CO#n9!+1KCDB2Mu#f)JS$tcG+2+N(XogS8IInV(}3` +zbiQ6XRslnE8H!!JjM0pBYE)fava0&>@`OR8Td=eH7N`-mgM7M#{oA$c1KE~lkQ7HZ +zHIS$DZ7uPY^R(xnNWMBH3T~Z5R}pE4r@HGyPg^?a@#khyoOky|;Ir|ktx$7MMkVW^ +z(yG`(jF?>{{oP#=m)e%~n7*5K!u;*h@{GWa +zA$WK5=8*H7-5y)+zko95p#UB7-hNq4NrkYV^!`G7c^u2_71E0Is-zrmw2E>wi>PI|%*S7JkS6Z`7hG*YA2)f}zNh`FK=&NgzJ{0}k&4gz0%;m(#GCasc~ +zWZD=eEE?*}ht5cIO+K~n&6$c&YfhXROgyYw`7MIQPXDft+KhXhPpr*zdutP@oCa@1 +zF)u#|md%@3eFE!-a_jsv-HDsD3EUsRD;*EB^2J5f*G?2)JH0vk^_)y&&b=NelVm-T +zR9c%v_-w6_RgyJ5U|7g#ez*esx6$S5t83@zXsMpnoe~AmCEt$(c>&!SVM9WI)eVl`ZPh50iEIzPWOBdk&*C^?3=ST~}sYtY47e$(y&Sx9Rbm*R?}8so5$K +zi`C`3dz#FwY9B=w@fPxB#uG7oY6XOlH)kmmwg|;dTV;-=)$&13a^8);q4pvI!^`;d +zTtJdw)<{+XlexGDbb%pI7x?a|)k8;J#vrK-A`18JDyOQPfz#xNFjX0MWr$7~Wi#fJr-}?(f(n=S$gkxL)r#S<7rk5G3 +z8fvyvNTt1E`MA%&2gLr;EVhkBZdbL|J9CCH0R+@FL?FTt5aB7@y6rB(Y*s*Kl5_G2j_*Kg|w2WkHy(Ob(;#V=v +z`bLq$KT1ffLPeEbw~ADsrHZT(slnR?47aZpus4@DM(6(=41ybhF+fy<7xQ>96iN$)}+p7sMomkODBU0H{I)hk4n^O6P10 +zwfz)>M3`a$r&=u*J2WxCLfA(ypHBxbu5{-YSBE-t<$4u;%xtCnBO=Sb_frGgAb2|+ +zZ>@|;2%wCgGmdLWjU}{L->~j!QRNh+%wtLO)WAlFEb!gF9X^mRDdIGw8_*rP=>&!u +zVJfG3y$6Xs>#o`x=mGQ%2l~#ZjW!qtT!O9q4OFg?Q|fz{Ys9gkwGAvk-qt%2J=$}n +zc(ON1(5D1!Hf6v?&BxMq1~Gw4m*?o29i6Tmd~=wkPp{%{ALV(c*-XX`jDw(BDT0t* +z;TvIX*ZExm2t|zQe3&mhI;H#0m~ol#%Ej9ZqJ`#Z>_*Y#V+eQjIewE|wh~Aax(saZ +zNc6#9jN1Hhbr&zW@*yc$D;F=7o#2;2A@~tAVP)#a7VdDk1&r)8v-AKRXtf$nD6O`8;HFdIJS$u!*6oTA?YXQig`$-_*| +zupJf-io^2X@h;e&#q!xcwrH%@&96`3BMKoftc*YZ7|WL|8Y+~84T21GBvjtvOA@K` +z+4uK7L=}$B`&c|#+N$xtMLw;O1#gnBZ)2dc^sM%1uHI_UCa7xBtqcC;(TvjyFzi<^ +z<#*5hN{yQVh%;kBEBwHiaS|rJFVL&f-Ig)1pU>QltiG84*_PDv|J7*5GZDQayWG2@ +zfQRH49K7zQ?#QDtSnh}isTSa&%RkUqhk;9WuG<4CRFmGLoDn^c4sph>a@s#CbN0dH +z!F3RL_wlS+6KN;O#Wj*VFx4jG38QV|qys$ZA6QPQgNlZoo{D}tHk=^nPW0Tkh_fY) +zTSH^t|5EzKesK35ST_n0C7% +zj{p@o2lKsay*I$B!Dy#H3Ev$IYK}t7S9_*09GfC>y8JJ5PjGckFDW?cWFAIw^l*C& +zM*m+MRk#s-?4BQq=tuGkotabmnO}pvIW}egH?Mp-1x&?8kL`Yu1ctphh33$+{&gBx +z)oM5Fa=2u%>XD+yqIXxewCQmd@?;V)2UB1|4k#e$k1=Iq7vfxi;>uG*_>f0Jbe10%PV*b@I;|3<#(+Ux;7MeAF` +z-N{%&JJ`Ob>l$;DWVr_LeZRBMVbx%|lIGb@WEu5gZdRxhx8Xo|P<+|5>tWU4E~UQ& +z-*Fq$`zh{VaB}wR0fW^Nv$&YfAE7oCz$r(PO8LG`Je}YWY;Uu*J|{8o?I(zD4R~n;qLhLTeA-~|Zsavj$_$Wo?915C1uPRV!s&Q|ULjUNCMkp*U45Bl +zkJ~eRN7&M!YLE;QUIALt@f|0glr={TOlEK6Dq(I`DnYW;{h7ypWTslS8&0Gf$AubonPDHL6!aB<2%nRzc92H2nA|EK7}cL +zQa%6dZ5s9Xst0Y&_vadi>xWjip9agBBPLbW=~N6fq~ei(}vh2Pz2bC!=YEAUfp9t>bmynK7DglBe>^4X|(bfT}CcRauL!pRN^@ +z+Rl)vL)}$i*Mp}tZen`y#;8m+`w-vN6nT0)<*RdwdvSv*=Y33b?!v4eOGSVA+|eXqs0L0$A%IYP+2#oH+3nA3>@;@6Dctr0l&|v3WuCltvPhL?C!B +zo;JjZO!YkLw@Og`=&Ib!WHx8tD{T~6ZGfQeFTg*)%GLb)fSPofs!IBSg4U76i`&SF +z<_u@`S1z*$6)??xGPKiBhaBXnW{-AEvdp59R+jAAo#ytW*)%~YY6Bt~{cd>=kSgM> +zpU6gZ=EQ3n>WCdGPwK5$v;+DoFyFp^> +zm&bRs#g;vc7~3EDDs5hXa^Ni8@h#wbS=T;h77Zoj@0zehnpcbzM;VMgw!oY+2OBJ_ +z+w0QGe0ZA4$-ecD-&K3_6Yvk2iow-2&d?^CgrK{sCN(x0lm+#B{C7CfH{&%SlJ58 +z1#MY-)^rMq8}|JZTp;hu;!f|Fbe-qYJ{#V!_Wvn_omhz3Ck7i}2oOGVs=O{aEV$71 +zeRMp@aW-S=nD0YTqvqZQNMfu)3MgH^WZ(0$o#w+{f>y44rIOmUCX1{HZb3w)QGGb- +zbkj}xS1)Bv@P3QX`Q@TM#L6Tx5H9{0s2;)7^8=mMl=xB8X!2nZpD817-?^_=o0BK*tDGQV7WYU}4{mw_ +zaOcU9;H7p;FCgw+zZDG8`o4@Wq{UEuXaoj9Ty!)*Xr^6Tw-sQ2BrBeB-Bc*q?zp(H +z{E2e|l#Tg6jsec`MJ_3TZz7MhSrL-kj;Waxsc!$f^XFw_l55$;@r10;P@?}8(1PbL +zd_NUr!&zOeOkIYHewx?;crv8phYA@^##-ForS#7TyO*?e)4ux<=wP4{m&jknP+~NBtpR^ +zeo1Bbd77#3<*yU^!Ns#a!afZY=ACB&JV-Nfj0levEANOm0cs!iqRE~?c=st_Gl%jX +z0eQf+R!uWd0la9zH9DUH0a!}7ft8R3y?VVe&+fVfdI*^(ccRF&_5azRCV~ +zLEP8p;qSD6xF|E}w-UdGq_$#wdn+!BQ$RiqS?eqVT>}N7z$MNuDY6ZiioR+fyA4NY +zbzB@9+_UF{DkndNI3DBY3kFO%{!)BPiIbrGmA4liRS=j}p6^hYcT;8}UQBD_HkoG7 +z!IVXuL(>j|T*NWIwdB9fQwp%3s?tx`X{fk$Z{6M)qC4&cmyo|}(dW{6Pj;Rg_x5-Q +z;143(%gT1$7&x#T{Bj~|vBFvp4fQsn9quH5G=CQ;Pf*-W5T<5QQ1YUE7Tu8%r_bu! +zi$q^L(U%j6Wr6~bn5Vpfrnb}GVmU&DCx0#?fJI5uRGPpU=HLNxfn(TW=`bUv2F-w9 +zgsZdM#-p;jF0$q>>KFF`jX~~A`Mb>n`{awji=P=PZ$rCx%jx7_D-18%~a{ArMY=ycC3=8ZBT +zE3m*+SjH^@93jn(P1pbfv#IBj-@&NjNNiVJXBnpSN8^ZUWLC(H?)qAehq6K>sRk0K +zYB>=DUOU-E`#F*mM;yoa>tx3)$>;0dTZ8vx>5|1z#?|;dQT{^?+@kqDTs$tZvfsW8 +zlvtf?WnhnjNr5%S4?RR^Oby>38+@w6$)Y;CW)mXj{EtX%QfwII-us2 +zH_~{Pxy1Ex>S6wJu1k=)qRwvyg_#>SfzWlOo3B4ncrEV%HvgB{zTM~CePY&E&F*HE +zj#*lrvzQ>0x3fv#NNYWffGA#-%NXpfrvT~hG>IPvZ4dT)RvqF@F{3!tC)|~OP-fQ~=@|1`A(k14N$q(B5 +zj(x4u22i$7=-CrR@kux_SH5ce~esCxR+*Q1gi!~MBZ@B +zqw5nYG6&o*d!w@CE}`P&@I_8Fpw9Mnb3TluQQ!ppZ7Rw0WS8Xb=(5S-oP&484h*WVU59~{r-Sxp +zVWD!Z_n&+~SU=OsRuz6Zvym?MtR_EYzJ2jf_8J%l49lA>O%gkKo4$9xnD)w-Q%aoI +z;bZJ7v)H +ztP#D_wgkv$vsf@xs!#|~GDG$)orWmgeo|5arBY6hk!0YICR)zGLSYcK%4QZ+e+IsN_m5Bh=l-B +ze<0C}KBPaNZ3yqeunA2w=B3zE4(nj9))ugrCG8W*cdPa*c@Q}8OUL}RV<T_19#Z4)L70Bv&IME(aKl7nR5zrI;$2F1b +zvM?A4SP?i~Ze*C8byoS5_!PXFA}GamTW$#Hw%l^e{|7Pzoeu-|R$zDf=%4dmPWOuU +zh`@8H;hgkzX7zSfmYmZ{q*{i{?or^>83@GSC&&x7A0Z%$pD*b?{~9< +zKYKyi8gJZB=R=5{1w(383+@#)4=m;-dC3U{|SYLnmPp+p6ghhCUm +zJpg4v=>+GwolluDysPBV^4gbXQzvE?29qPmAk40V%e4v3xg1*SDtB;Vf9eJ2R-6=z +zDThzEYFYBb9i##ZQEx?1AOC}JU3nZaqju3}<>(AMqhxGy9Sn**p%8ty_8GOIwx-8$ +zBmF+6u3#vqCu|3H_J>TCVb(M}I(w8S5BFN@u;C~1rrKxGV&bh@ZD5}>H}c}a9y``# +zWn@x7>?Z{?X@638v +z*JOS7w3T!)fY!|VJ)<4mh`Uu=(`z{Fn98)<&MJd^QF6M#-NM9P<}zmFsw2$fi6wdY +z=npm@4qnzt1TFfOx-=aZ{l8aT-k5n~QZ?1Wzb1RX9_)eza0I;X*I)Z`Jw7WdQKUHf +zD=&uWafTCF51#AK2ca1_={4%rf2=%~agg`{9t)TISIywKe$PtJ#rcgv8L`C?kjkqU +z;OKyM7ofe0aqwojuQ?J7h?gt1koCv8IPcn;J}6FJ5CajF*vQ`*7&wQ%@viKk`iDhk +zhNfs~odfh~l~8)I14?y+lh;zSk}d&N0hSK|-yLhz+tM;IE3r-2Vfi3Oc^V-7DOXA* +z`qGW{SKv+yBg4^)g#Gt1VV?#IZ|~2>mqdvC);lErKUv~^GgX8i7s2O+g--uNTJ%4p +zy4;_;9|pVQ?bVkixKTCU^fL}szWW5Ct>w_SVH3{0^?r4iMDvvR;fyq6pg*wVHwv7i +zz4@-}=v5s&bJNN4?GWLCxx7sjFScVdq`WOq-o{=yYNA=(q#8NedA~f=dlyz>3=3?a +zj}QFtK{fKU)G`0rTeZu1I~KQ?OnyT~jwV1|E^Hx)@dSGLIkyQO|fx +z&JozJ;wR-tci~Y8ZWHjfOwU|sZ(Vgjdh!!Na?BaNpNDRB +zi6E}62ixq}@lhOjecf!mk-lwQVDThz>9 +zZQzr>?BeHp=WRdz6f~`P!bG!PU3%=BVJxV%IwuP)A +zh7-7mSL$w_f9>G$gdJF9>8@xgrIj^D@&Tsr@b&$FKSIj^pd42b0H|=3+yR9Ei{Ce9 +z%6FkK@01i_gTU|_qrQR_Z(eG^?rChUoa1`!xNheyNFFwvN3C(w0#cfAk>w2!ERg=a +z>^y;d3pbOz_(*Wr?JCnb+$L}w(L}AFk5{xI_$2_;2AC9UaoJH#KL|QEd +zGk<(cpZ4fZg-eD<`Zuy^N-Ll7L3yAT&`?k9tLG(l*?w~T3@l{#eE7iyD^_m$g*zq` +zZj3B^@&&eH*I~;VKRfr+eiMCTy9VeZ)@zOwinS|bH670{dQ$TmSghn|%NF&mneXD) +zWSX0B6e711HI;usoE|pR-@yDOmNGL(4b*`tf!7RJj0Q=%7z4f?0grqQGgj;T^LL%x +z{zu?9YR!!>{DNY4bWPI5a&Pl94~cf&V +zh1i*C_BsY+t-XJJi5)*N(KW$F)+}4Tu$vxT^8*R_b>+)f7YswtDu@^kZP;b4txD+r +z+!K7{wM2a9)gr$Yd$xKB`+%o%v5$+fdMP_6+EJ~>H~bRk#pTLdm#EIw!|=nRS&4b@ +zqNtpR?{5A(#t{B2mOa`uqjk@4BJV-G3WF6_qRX2C50p;8)Zv5P8|SW&yC^a0w?9f9EAzj`Uub7w}&>gbF9m+b{ +zQU_NQU}0UmTeK%#cnO{!aot;BN?@9Vz2u`f`lRM>F|3dKpHSRhK_P{^Q$GlE#hFlE +z$F#bOe4E+AZs~vMH_|VWO{_z`3=+@9?3^vEVAV$Lc!1GB56u_B_bOGPYDo#XG+?1=ak37w4{OZef&MkVZTawPm +zgvntbjebg)2#nJQe62FSsn${7&VhorKDX$|Q)o^s`Ij(!c5go%+7zM%!K}{Bb6&72 +znnqFTYc{*q&5~9u!;373%qMmSH9_(3%uBuP@!#~wiTjhu7aeAml@4YjkiE%4&-)d@ +zON}+Ky63F7J&BY5#Tx0q&u_w+z7~`1g`7wZ(zGS^{Y(nf=X<+KSioRzscGP&V<7Nt +zsGoAhh70LIWmg#Iq*#HlSSY7A);Emz+5ivMX=&L32yN^a<0)4A0qs?jMNEvR|B#V{ +z{RyrkfC@P3^~#yw4NF-)iN^4_HYZ<&Ga%bwSIMf`_a29;UJXnTbw`dE>92>yLTUYc +zrhy_Be8Oc+IHtD^92FNc7mH=EAJ$jhvb8qHdJp=p^>h~VEUdVjVl!RvuLza7W+tso +zn>S`UIN}%XOdl1H?f){@hr?o_1af{OJ@lW=tAKBVTzCFp_PuS7yhAJ=)U1(EfYRE5 +z`{0%L-o@uh=e<6OJ>(NWXlKq!cKQ#uyRdB*z>jvG$ygooazO}>J%PAjo0!B+nGTZp +zz1LbBkV;D6`{JK_tvstq$&)|+5(QlTk+%ta9VVNYQ|ckP^o*o`UfermhmSUlbDr<* +z?WrC7VjaG>S&^7UuPksR9>~5I6FOW6FVzc-5I9pB0&JFJB6o{B1&u*p5+#8(-17o` +zm|j&N(V!~qWY6Z6HO(9YRY_+3?6E~llZ^E9CA|T${+X~7|0uO8-r?k<6I?r-YL;G0 +z(?+1zuBl^h6i{pJ+543YU~Z7-?;Kn&Y03r4-N%Ugx*3)1dmpNDEge|_;l8#dAz;W4 +zy8*7Yvcu(%$$x(}3jYN;F*E|aKc#ri#6XXJI6KrO=EC!GgKDiLtG98hfi1Hnn9`Kr +zM67`Pb@Y453qd^Sm_YFCLE0T@uG=4JYK)xtbCd#blpjq*SW087WUj_Ed3QWPDb-M- +z9S`%l4b1sHxBVaQzGgWgF~RjM^?x+%RYuegm{aV#r$i2i-2i=gcK8nK9?%Hp$_xf$ +z;<^dy$V)f!Qp?jbscOqS#~Z_zr#W>NwwPN}MooP7lnAXbtI%P-Y1}UswjO-7X{~nr +zfhFHZZ;xN-uu7pxH3s?Wg`hjIbZ2j}Av`e_-@U0rS=f0oe5W;CHCyf3V)v>-G-)-} +zCeod~XrGB+5*19tYnqI(Wg)UxJ#rEV(&g~0>w++B({F-?Yo*uflkf{}KMYs)8~{~R +z1Eli)+yw0wGvkcsGS$A48A7$m3!+RJ_>*f|T>jaS_=`hAMLz10S&nu{MZ{zvVAeH6~}6{iK4>)Ovg#xsty0#!5(8Lw?7 +z-QkAKc@IbD^@`=|y5@VUmkz83Pg5dAda#$E!}au-nkpYjAr9)#1V}gO@k)i?pVXwL +z$j|!&?~kUqoOP&*ocC_GP>iymYI?*((C|Cl+$+3OKe-#WY_Tqg62^7^ +zRM)iU_8Do(5aq|-%MIRJm-K<(hjR5pJe0HX7nnf@)hBI5^~|dnlARO!T+eaG6Fl&J +zfg_le2xVFV_KKXQjs?HiSflm4c6s8nDnN-l>kMTHaN(fRYGHQg=oo({CB9VtHMan^ +z>|e)O172y!!X}hcYt{%F26hEgchr<$yWp6H*7L783Xca2;_ma9NR9Mz5UCfb%zI23>w3$OD6IU`xtF +zmzEB{ih^A8fS#m_2FLXNOTw!C!mf+Cd*U1edOqJT-#qtSya_0_yA4hDw(=6IDL$XF +zdW>^7t!wL#2G~ON!sCG_i*Cvj28U~w5|TIFl^y$xvfl!e2XhH*=j{5aYrNDcyGGG0 +z<4@>9_ +zf4>*QZjprn$IQc*^%Y4#t7UkH9*?77YI4`2!$V#8?=tX-GuE1QnaUy;r)9B1-~$1# +z$~$eKSaK|rO6&Ge_o8`$-S(J*q#D^MvWAtP0EncVTQu?v7=nXA?)^1{TeUz$un!eN +zPo{S$Kz}ga-Vv0b_w>L;fCOw5SSnV=P59n-c)t*2x_z;^|Fs0sFeA8Jv-I8ww4m+D +zj-_(2?86ok4i~t=UObg|4<;fvR<~cfU>pZ5Th%^Up(^b9Vq&1=Rc~O*v=GiL~sMnM^V6Na<}z9@Q4&Lq8ZzHIX+7g=4uS+*@A7?OKV++ +zRfS(WP0Wg-!I9P7^gA&NQPpLb0U8Cd7PaQLdYQYxKd7*#d>8(S(L16&U^}MMuAxBSB$Q)y4pLjkx)eIb~dCBE1_!lqFMLj)MC3moXN +zo+T`FQ}?Yo&rcx1zQ1btFInV#v&2Yy1~OQkn%cz9y(*q>_JTrFc4`keD~|z6N5$YG{6Ep(Hb@7 +zbDk%DwgXcWahaZL6e;#|!wmC@IL;Y|bvVlvdT!z7ATV#A&*UdI`5AWBsR}vb3^E5k +z&P+|&VNJ&XKd2kCU03b_f}p9eHz=$XGzYD=`Hbv&1zWnS40!tQttt{*OME +zj&2)mNBeuNFXmN|a2?KMqfNE6Q&tcY8}jmnq-0PRnZG=RL_orMyFfu2!N?>~%Y%8ZJjCA~~ +zF;9QD`tABlUd?W^*d%Vy#4Iv6sUAo~c0E%=e9Na*rrmmbvzgHNLeC#=U@yUZSuIv9 +zj-ZPM`^j8_uO*Hsfpu!IO=R6_cSdTNMMqLgH5jQTxOOp)xjUN%xLAwe1sUWz=sN>2 +zB_k!RWTJmP^$#F!AC?(hO%0IXzgv%(fMJ#}?z<|^^)iiCLC^V%zBmdasztj~&f0Pc +zo|$^Wg6Mb1zD^ApNdpRxh2^LGygz?gRpdOq>HWXjoq%@XRgN<;hIPnQQ=&kp*5Qjp +z(dWc*Sjn#j2mNFFoqm_d?@L8(!&)8PWj!$tWWop3t$9xDnVaLr#$WrjGT+0-XALO6 +z#oc6P`u7b75na$&2{?}~y_;qu?_S9+e9j_)`iYvCt5N0^5}T>?V5dF1iXebmoKlsN +zqQUoAixk5esf)=M#k^PL{c{#z8`J=9tD}hLci`6uYzliOL}c(MMUic==R?<8`YT<> +zU_z?!!>7P0a5ucA1Cy?jgbd0y2+d6pVuY))*pw3Pl=tMU5@Pg=A`ktWuK9JSdV8v` +z0tm@`>NnWlYVNAG>HePFtzdX7{qSsAJN~u|5n-K%H4qjn-?QS!lugAAw>7u_wHWRP +z6Z_Pi-Ddvxdvk9>rjV_+{Y~wT^MQz2|zI4b76(o8^HRr>IbN@ +zSO@HW)p9?HQ~BibP1B+2+zEq{F?H6=i&Xdc$jMKa)R6t4_mV)@7v5PwZ2N_1TvB#15=k+6+T%LJ}n7OK-tvS +z(UkP3|9)Ocuitmmw7%MCFGXL)E*`U3jG>lZXGR#c@isNdS@P-3Y6`~tJV~zM>%f}{jKajcFhWCBP)Zx6t}ReP +z83sy-`%a<{*C?Z6eeL4({rTJ3p+3SbC-cRl>Mrch{cY$~@@qpa=d*Kc#cCZt4DWeG +z*J@+|LInl`d=dQ)7URWW?vAn+YD3jZ7gsJ$N6>`ZHT)E#=*PSjMyGZyv&r5S#R4nt +zJKW=dH?1&st60ogF~Ev-!23{5s&pK266$NiLJfs1!A;WC)mlMquIP%dUGd*X`2B^I3lZAx&+#SEnoaLK0ZpmZ(zaOHN!aR_$- +z_Rb7)&DVw_S=s2cp^Wy=?`X0c8+Yn_$bFgp1%TvKyv<@H4W42Pg0CcVD`?OE)`DJ1 +zhq@KNy71fCK=-T$Cb(C2eO?jQsC)S^N;K&Ym}Hox+Z}HWRs;~JZ&f&RHNKxIGjAb) +zaG+F00Hfek)$-%m*NpD_v@64{9~rSFP;{dbg?&6twfscd<8(A}BmL`%zNy2d`JuYv9ja$1jO +z9-wyO3OmE~F{wwt(wiaDh>VG%>dQf8WE($z-UW#0B9IFoaN&5>W~)Wo^nEh$)2nc0 +z6PP*Hg>wA!?!9tnF^r}cwX@Nj0TVwLpp%Z$S=u5WZrI~zeRrIFVZ2=&W~88GDLOVj +z>URj_q8JaF%#31)i!=9Wt+7VXhl7t*D8yQkO{gS;>h_nW8Xoor!vK?7JC!8_0cB(t +zI>uvR%fntlZ4iy~%DC?M>++N(gg=qITPSU?0u!AjIzUwJqTQz9^=qf5&oISUcCBWq +zzPIf8Pol=M^1_WR1-zIteSL=4J;I!}&Brw=fG`ZA&+hVekJ~Y`O{X^=$mjly$4q*` +ziM7Dnm&;&;68{!QN7K#b;wOq{?U;3)+2WfzDhFW({C-@e@xmZYNm<{U-`7=KF +zUbi*eD(fFM(jNi5pY^?%ox@S%u|4@XV`edekW$cz_7t4$DeH!#Stuihu@UpFcgQ$-1u0Kxv{Am3N$8rDf&F^^87iyZ)jK;sXqb_VeJ)AsNK#(qp4`Xl4T9{FY +zH>27NT^9N`7(d^PaOBZF>V#C@_ijI0aGl|c98xmC;i$;WN!^f1T?2%~qfLC`FZP7A +zXu7lUrJdBjjQdc`d~bgpeeoVAbl(Oa0VEFHi3h!3MC>@bt>57IT}JB6KQ~PLnj|cb*&fzFD6+u7cXkWwpTUmVbogT9$s&A~l^JE4n&kiUyDDI@eJ~(J +zbU@|?$pXj~RMhaMgoz^#zjJ{R<1?LqI`0N}|88zLUG#F=}SBD2E51fLHNSkHn{=L +zvvqrrF=zC)A1+u+pSdX72y93Ayl%Ij@LwJfH&Z?TZ0y~CFhU$F`SI}-<1SJ0*XPon8d{H4C^*VZC}iV%3>E=Oc2K1 +z6bzPPpQ;2c^l*y+MHDw%s>}OoMCeE%PUm=fEBFLAGuyyEGwso(WXmAyU~%kHE+-%T +z8~-wRoNYh8d*Z;9q+y*Y7`u;PVUfVdg6k50iUV)c@CuG|tws9c_)=*r0MFRs{C6Tq +z7;2gSy9p{v!%z<6Zj+rD5uh_u?t0|0q_Nus97? +zb=$|>l}2ErMY%s!(61%P9>bNE+5@W&bh~wCOIp0gKbdq~kI16Fa+T-JF&Bp~nxQ5z +z@Go3~t2JakaL{4XEXpv~5DqY$qS`F_p{u6!g0vyXSYUnVX0Wh%TQdX$B0=?=s6Gme +zt^BwmezP5Ky(EJ4#n-SpPc4NDlq^6NI)LgKFJ)=l&+s~m$h2AAhUTWH699SN8QW5^^l(|n`izjb1P872t7G8&$2=VmE{(}(&VHapgn9aN+ey+Hk58~zT{ +zo(i>qTHeK|G<$~yFjSc|3BHF2!h#LQ!iqo(?G7nhb1J_X-Ls&>Tg3(U%Df$X19+-f +znd-zQuw`?u;ZjR|!<-u3Me|H{n)?>&UKx)O=zIksQLY*WtAqg%yuBLv1J7 +zAqXggcCiz?Qqo=5w5fFJ=d5-5hMta=llN93K;Wygytr_(dpf=}?|FOilS$d3lXACW +z-2|3c=QRoqTj?4&{GO%HN5U;_PW|5CKe!XJ@WE~}t38z&$y$J!$OW~@W +z-xjE@J+jn)1`qQsZ(@=C&(Ryh3JYG&2fjuvvMBVPlLV$jwV%Gxo&5SUma2c%B*dnw +zakudJZy%2Gvk^)-tZNtsGerCs|K39I8GqO5V^70c`?~l&Zfr|;VUe)cfw?_x{uK!f +zb(%r1{w5RMo@Swy&=^m$q4`>${J;j^S{T{9m77N;w{E^POE*2xMw<&3oJ@2#hN;t; +zc!ja7IKeMoUB`{*c4IIT&Z{_&fs?cloO#$2bd+4S`layQ{8=ZBBx7j_4F%9VL=1UX +zUyljDIFP4?6=m>!+wR8r-DsHT_~0=jfHb{a!M2vq1T*rw4QRnQZZBk}%vvVDHut56 +zH@|PrFY3_9dcxAWw|q0Dh2PtWqua(il=rn?!;c^=SQL5DF)G=7hk{(C{5p(X8=gJ# +z{UIZP9_rLssXkV)!8eucJ!N`&`op9}gj*%G@dI^aHPZ9~9%?pK=(PN*jVh6u?kZW- +z-4#?9>{DhgdV?%NF3W#HtGwErD1*K5Be$g=Lu60t@AfXT((Q>yDC#J$!P@2d8h!g} +zH1aMewj^iZAw%)l22X^fkSP8>)OvRM3x;$vG9z8VS?<&4w6K;Vl^AZP@LI1pgH7l#lFSF{?(_R^-;Duq_pEbeeu+RF3DSg +z3yCxg^Hm3fq1&_cZz5tC@9DK@4{3)BgUk%BJxhowIX+>MHi_%mqV4f*us?zZ6O7`O +zo9HF@{_A%v_)=Xb{~9Sv;&6IPrI*6PZ9O7*j{k{;*r +z(GJ(u3#nk(vLGcRFE{4O2vdIWUc5U4Upi$y*Y>ryyGz7^8MEJ7iT`=TV?^%Plv)$C +z`TX(%@J%&vKV1hlPi<4u;%q6)y0P@LdB|u)ex+5GWY_RKu2iKIM>jJcaWT2mVz>_M +z--Sk^c66H_!a51?v2s%S)jI7KydwHQudcUy<1$t;Wmb*a?4t&g%b~^u$loTrqLW?OHvqp2S{Ay(CPLN)bGW)zbb4|J1`DGSO!DG<+pJnH(1k+m&Q2 +zV5-&p?m;AXbh(Nvh&Y3;hRT97LiycGKC%5SSp6+)SjX}mZ1A<`tB>_%9$ayjvfPU^ +zmvlYeeXC~Yvu)wg1JPp<3@OSHT&kB%0|cfNC&v-aW7zCTc16bD?|41=(aQBs%FWj6 +z?Glnb4uPgr{v7QSR|s(iNC*JGlH2|F=LeO`t#0H5PI%Qy-q?DOlxB(>w%+%JZ9F=? +zO=&Z7`4F%|wR|phrn!wQfos>s`ZeK{wjFfM49`f(7}rJOU=C8DD}59bHBH}S)X9)zR-<|{3syQE*^WzCa4#@#KZA#O_ti2k?Yce9x(!1N6I7ys?9^;c6I2#}L4StC_KashL +z69t^bRE5vh)OrKjYps!4QW?z=yQr2FSzz6=YD~Rqi9yN~%)r8vg9gKAAzC$s*^Lru +zBbC{>nuJTeqPK7}@r@A=8)3OA0t@ +zNyZ`faKg7twYXC|dPdLN?v(mD;rA_~QEB_ooT(qZlm5f;^z?(e!PeA~Eo)amlH$^> +zj~=v{Yh>Q5ZK-x$HrI*`Rb}bcDHxmB3;L`r4EQrhE;n(2~1&?b}HEuLs!kC9R37%I8;2&suNoXgF*lK)Cqgs3`t16Rj9l^gY +z*kB=$L4&bIyL%XTG_-#N=9z@)x#{)U8gFVi;x&Au(Q!?o>{$X+iCtbWGv?eA!(EHa +zBmu9bseyZwflgfSk&!3!3YLWD1Ot)wBwvi{A#=C21)&@FE@%ve%A^(~Cd}q1wDI{1 +zRecJNs2Dw@1^l9Vv#{~t&h5NL6T8J;*Sy0a?Z*Td_5m#O^B +z0;Yp>cGx?+i>5GnK#*e#(+BY-b>EU3uEVNudsw#IJdUUR_RDAuPO}6oaIeU%g+eyk +zJzC?2e0p+1AMd5mxxSd0qUup~|7$f}Vh9cFMW4F@Dth_JwsucyTNwGd4@7(=%*|q7EEG8yT`8!pmyf^6J`ZazO`A +zZ=~-C>~V|dwlzS}@5vxf?4cTyzSkeqxh)iAlcCX@AJHV97?bG*{F#10aEI8V#8BRu= +zx2?c8e8Q2dkpR%8vWKQ5lup4APH1Q?e*3y*6?3x24G9~HNw-Hh0E|(DKKmQZn2fxM +zjv*cIUK%-05x(lr^;KWB;$9~P`!UL7mV5ntLUQ6V*Rqh2QS`=xklz#bTWh*zmF=)x +zgIU%;VK)D{trs}|;7G^%n-cj6wm+7bpTLKOh*+=zIBdM>=M8l0ecIrRaGNZcH=VB)iOd=v!pBpoZqGl?n1&|!`(aQQggOAeArO!QeKi0a*lALkrCU4$Tp^= +zy+bqsAJR75b*KO=1NX(?C}qP+Da6c_GwcMd!f*Vq=mqshd6l2yFl-R +zu?TDITCu;IZW`%P<<*<@J0@{+5A-JN`_=3d%A-km*w3^_x4LQqzsKL<*LwS2)|#O% +z=_IOA6$P;rn|pjKk(Go5)}gF#5sUZ9>}aoLhav8Q)-lLHqu5lXbTs-xekuhXsqP<2|{s2c$Qu-TW^J!vuWlZ +z_4kV-*02bgy1ggv?~pc`e?ZQKbAZRDIwm^gh!6<$)ae_Pg=9n+{mC?`U=6KDp=CYI +z!>WGlgl3&({uw~EEuz!;x*{v7#TIc;-z)a~uHpOS-00b9>Vv)oM`M&`$VY23D!C_s +z@L;EE+UduhyD0i`;m%@fbr->ZrLx6JqwE1?G8b8iwrnYr4qR|bPp*@7NO=7kSlwr+ +zwL-Srr)EvaVTN&$nnlYmIDE;BF!3vnx1I9*wCoCKykOJw1;Or^^b5#A&}Nj{AIW>86MrY;!NM_^gUm>O304~TU0{G&o2l%~Jb&N4#xUXdF1=5Pi5io6IY%790+gAv +zGwEQDlf!S5w)?3G8{m28qnSaz-@q*frst?7xk~S1yVOZN=E3>5!t21Pax|Ba1qnSZ +zZ!NawRT+BJp+l@3P(xY*fJV>ekQ$)B4}ZCQ$sfA=gJdqixdUbrKJV0}ds>g40$UPt +zwb70y@hE{OVWHACC`qJUE0&|7ghy!n<5knFhEUM;6<^A|$uwEgp`oPh>PUl?2Uq0b +zN_*-2EvEluUCXUKxOF#VH1E9qT(BrCKOm@>Uv%mp3~xO>pQ=M0BhVjzHg1^a3y +zg%IeV@(24Zvtx|R;OT3LAnH#)BnBYphE1vaG5o$!dHEZP{?|vl3JV21g>9Jz)v40N +z`J*o2(FOSJ&V1yo4u}2Jkg=rBftck_O%TNX*`<2zYPj$Us;8;?C@Hq`?^@MmzB+`e +z>)YAp{H}cR2-3KE4%BEaG>|ez0NI+bPq(sc*G-SmWLyx6d{^p_?B{nt=DzA`#cub>` +zc&JivjtFPv?DuSGNHnmwyR>i9>4?RY6M2jG)8(MZ8`y=N>Q%q*W0l`5o*Ui3$$&b| +za27}aF;MeME7uKbw8%Kx^NwDTK!@VpAgLJFxe^1Q06_p?a$#KUM~7phD{6v+#+dYZ-hk_|XW6 +z+YsoA4J1`LvRHmo5Ejvd(;)~P{2rLiH;mf4)}SxPw9#mZC4hv1U&gUIqWGtGmZaZs +z&I`aBqM%3#NDXE91IF_7f_#G`C($3_s6kd!%ZtI6i`A3e#jL6Vebb+lzbiGR1!i@C +zoM=Hoto<;;ncr*;Z7&ixIiLGr&R7Iw4ee;u&MIBZy)bQJa9se&0WTDp +zge#u3(u_b$l7pLTZsfV1Cwko;8xcx1jC#yq+{d2eU6a8Ito**J=&0!7YMpZ*&S2jl +zD|Q=a&yT)%OAT{J(X2f0(9O)(AG85ZDT0X(n8yl~d7O4zn{@@7Rnmsd= +zivD3MVr@@;sgRTrwsA|zx}f8<4cih=3y`ML5^5cDN~0u+gOUXyBv*tN%J2KGC`s|SC=o_@K}<212E +zXDGG9{hDZ;Aw&cI0z@;XR|ibsz_YHz4_ItEbh8E4we^{sNlaHQ<-d&=Xw%!-=(`Jf +zXxptx^fxE!BR;||qLLTdKU*c1@fCAkf?o#>Sdi{)QD||+v++r?Z1F^8FLUwJV% +zSs!=Op~RIhK(-dljbCbnba-QU_B9FIaN~4xEDXcL53P$jFf +zof6`tk*x3`>uSy7)JC4@u#hFx;Q~OLqJHB3M*sGP{&>Icc%P8mVb|=yjemZ_sOxtd +zz_X)3CYrX+ttM%I^fe+AI%H$!oxk^BEMEm(D2E55=^B~^vO6-=;5-FjSNFi}jWFKN +zCO@@n-Lv5rISCRq)5)l7cf +zhEi$q9$RC3A`pHW7U{N7+$!de2%6&i>~!`IgN~goXGPnqQZhF>C?1Z1`9%*S9h2%3 +zA(c@_u1jZzcdB9g-Vbm1n0}!Px^7`X*AHd&&`LD%#%Jg5W^!Fsb2Y20cEPn^xE3mF +zc$KdW?~IW3R~StVwCw1DWf7!*42u7G0ZZ4|DITiU;X!3Yu2;@A>!;J|ercb!`1B}1 +zb!M=Lf2InpuDfwqT<(}hMKNPnlUCX`uTb53-EJIEHUj(ll=Pq|oFwJ#3!V-et$+pf +z2`6mD%?N_Q?Sg>w)W$)vbK?2S*}+av{A(1Xs`FC6R6UO}mJmQX6#)g1us^IcD9S47 +zTWaq0SqG)aQ<67clF8*t2*c5+`>;bcRUN@MNm)c10348UtmlB8*r^HJo;w@WqIi8Y +z!uzJ`tAqDDOku331ALZ+g<0auHtkO0woJIa-JKh0U8dfthUwDj7NcDeKr;3a1)fD( +zkSWvqls=M5vu^HC()fDKy3*5wc5b}ad0}1A(vAc@r40>g?1gvuEushq^;vK4ITY?G +zOzQJpXDwxRH6w`^n5kbjC^?iANafcp9p~%y#eDcX$Tv+8&h`V8)P9(%JMzu&>uoL-(zJcb=x4&KGC72-rsjzBI*4}6wD4ROz +z?(8G+4`af6y7m@=$b)W;I;Zj|GcCi2GQAe(^5sgS%r>>=7^;Dz0Ai5~OD00n!sUx+ +z*uBg&UgY9|-bqkt(`a`gp2G5i%dc{3;TsfLZ<4vVhGpvGX#5NZGMS{#P`pKMh0W*^x*LT_%1;5MBby> +z{5jD1e0m^5x7%a=ub$WdFK-F%*T>Sx^aO9y@A&u$zA(b^fhp2a&UkT4k}|F{^G*=4 +z$V@@*-{yo0lnx@*+XBHOOE3au2?abuxAe +zZ@M%Qcol~CG_?ObeVy{A28Shx4F;@-VH}N}Dw|VTX7CTRBum;2e>Tel6@3}fbb>ua +zNL*iqt5Szq>hrt-X|OPn%cqFQOe$f)j{azlY9K!Q)JxIM2CANxo2Qv^7urEw6^Q~Z +z>2Ml`igOvVB@L~~EGJ?Fc8$WTZ;b!w}*d +zhGNPjtUV)T)SN4MP5>E#)0i{cg-Bz{%B5Ck)#*1#M37vzD%tKAQnCp!+;2Rz~XB$&P)VyQAK*oKR6}WMUnZl&?v;o|MCB5^hS(x +zt+ZoVzjTT?r6aqg@n0>^LC?Yc*Z~jCy(!cYh(be2Q8CVbXzBY@^v*HQnnma^19or= +z_UwED+YZkV#(2OaAg5&lrth+sb!r2Rw(0h^10m0Vj)F2DlBJ@@C^WcgCKKR^y--g$ +zxED?@$ipwT&~rf<^qe3Xa?HI34?Q1*k$K*AgJZRHdgUVQ${aZ5%Q7d-TlQpJ6hb4< +zNIS$w24(!0Jz_+|d;k-GVEPpxCtX$}3r?QFH|3RY*nfSy0N-+B$6l{qVOIih!V}2k +z;hwQaFhS?q^6WgHpIVRI^gul%77;cum3V# +zA!8E}9k5e2QS_mE-d_^GelrI?2Lls62@y=NaV3DQz`hLbR?fDO`51~QYpE(-6n-)R +z&g7KR!8rn;0fOm(iCs3`C4U|&vaAMq$;WtinuKj$t4c@tAY?f!f62s6;MC~ +znNkH6{%4hvv=wF*(`jJlm}#XowOPBrp69ve$2S>_KnR2`;q?1DZ~)Hv_k9kBgUgr@ +zLI@#*5JCtcgb+dqAw+LqufauxkTAGcZ9Bes^|l{36fjN*fpzMrjc0F|J!FQJP!i{tJ-YvI< +z&Lsth22hx!gn0P%73;e{d*t^qH~QVNfuS2t4*iO2bFDbEdx$#1toP9X29t;o_itTR +z$WQNhD%Yu>9~mCO&tLuqW9o36piLPJeslt+$%Z&<-gMOjB1HEm#SYT?QxALRsUxRQ +z{waY2V@jLGXvK0qWjfk7#<{kXXf${Fz~F)7S9Bmkto^vysf=0!gRMzYqmkj$2pOTy +z49aQPBt>TG=ro=Nc9)=LCZtL^#J=zWP{kD)h!AV{bUmtL+Tw%1>CD;FNY1D5L~sb; +zSTQ(II+~(=s^XSPmA-zj#L*n4xE`TaOSxT7-wt4M2O>nz8$A)`m*+ag_fSALBP1Yj}J$vU#o1Dk+ynQ=?I!&@7NFlAPObyG8?HbE2=GEGcg +z?L-3+8c2wqU90jw-|`X^tpM?j1R`J}ihtsWiG2tqZR-)h4CjGJ5fYog`v7Jz;HXlq +z!Ni(twobLO!|gK+cD;BM8AUK1F=%`Mp_U@Jfb5K4dj4qgjRbPZMECBlJ9L(FZ$9w$ +z_h>W*t?Mt&4NU8`(i`wECek1THe5$ms?l}RS}|7f@BsV3qp +zfdZOHQI!L3^$r7lb!wic(Ke%vb)FF5K}I7N#R!S5UWnr?E`_{7MEBm}0w8<~roH{W +z{SN=p-|YQ&)HPdK=X-yzIjdH<^ONI_Ij}c52L%)ff+#^mKt#dTHX$l>8=7OAb53oJ +zC`K@obK1n6bAEET<65g~&g=TU=XLM#dfp!GIR@m2{d{WFxYrn~{=C0q&RKJ;s&9VZ +z%O8?b{sEz_IXw3Rb`*O)iBG(lXs2zj+b1={wFQ4vTFyj&X#v9VDlpm-*g=72PCr_fjAS$ +z|D!X$>Th1Kz3cqiJTgOkGye*&{=irMc4PUAa?XpA@Cbx3OBU4$L@7Oe^|Wm`WMR}` +zePYfsWD$UeTLMhUHcDadD6ydyg`;p~!)(ydq=B6{wMsLW>H2KSiA%>lKk%v#YxhGJ +zY-Ps|=C%zvb=x|t$2T!0S~6-Xi&}ikaZUs9A8d+s#_M1I`oyQnEB^cyi#@vU=;o$1 +zX{cZSyf@y!Khta9@~Y);`oPQI@Gb9uMYOs3@+LP6hLKVtn_&hKff*20DH&r6wk-{5 +zo0fqkQo8?o41{D^SPHT#v+7{N05dZYq!6K?Be6VcusUUNa$Fz&gBw^rZ-pHf?c~(a +zlid5sBY4f^`Ia;cTqrzf^7zLA{sWF6XY8u>O~6m^X;8--cOE`bM`K@LzRd=Re}>n6 +z@N0hCl3qNQhDm}%q8$uK?#O~E4iTi35J5lf?dWaCcmcu*s-WEjZ^ +zCz-J_Y}hC+OZTS%fPoY!U@#nT!>z}7@L5aP(V6|lHHO+P%XbeM%(bMshU5tg2#J)d +z_0yT({L%Fj{{aIz>>Osebujh*?&1P=^sCswDhA*#)C!8k5@AP4F8TVql_p!ZYBNK{jSCWWnu64)g3sUC25jSt4U36PAn! +zCubufWRhje+UmD#Ze>iGNGX|xi7+8c&^Uc(JbTXxzWPNs+0IKBnLD`1+;S#oLl~9= +zGlvu~5Q}rmoY*|XVj2B0_xq2qNIzrix>x^7lljTD(>H508Xx_ZZ~2KS|3Y8)6JLGV +z(LKlC&~%w$YPs;>cD#X{#sKltw|v8p|Aim=)VIENI2>L*YzB0+Y?ATXYuF~5oM;7Y +zGIC1F36e=|N?=B#zsKs@mM%KDk8GKYVP+T*5>k*5;ckIwBblob(L*wmganpN#Ve+k +z&)>VQA9~%b40q47aA1*z9V7A}%bEl!)U=toxdF*clao_U-*;51?l)g^@}~bFQ>-(( +z>%QgJW%lyf)^VR7E$3*)ckwUu+8=!F^2e61{`gq)ZJrymi^|F{fwc^VLvy(;G`oHp +z_!|C!{n&M{{o_&FJf}?!Yh()(i^EJGZmkl@g4AR(jud8QB+ST}C{w~@Nn(_-jZ(=N +zmJL%NM0FvFK*+!ZeJUgmbsh!)Qh<42u3%?-fJGY|Ob8(#Mt^Mm0Rx4Fq1X#i%VoER7k +zNwg`^rdBy6+MF>nl7X2CbEF_76?(4RcYwp26PX!EL?pz2@)xZq~RL@KFb3+ +zV|M*(-jz~%s;i61>WRFteH$q)&~FYt$iKj?=YQf2!=;6D+^Y4y!Sb-d8?i8)XS9%+ +zA2yI3(EQ}C8(t3Z^EZCS3&w-dOWUSlGe(#N!`v`TlH3`jOk+kumH{(JDUp(aWPJ%1 +zhjUaa-iwEwx1aUuW(f>PW_kx*31<{D4Y5&h_oo@V=l +z7ukOHPMV=vPMJYtFVZI$Qms*CoVHOFLgymoQg&SUf +z?qD!@LzA<08HP>LC?y(#xs)uYhD;(^QnrklVJ2ZlHUpEC5}_bUwXXJ@wQUj1h>!q; +zz#u#*i1|Dq6v$n@SK8#b#0+R^uj89rJb&9^zU~`O+up~|vHSd;v}03l5;+^mOp-9b +z60#&hj)A4dm~M9PM*OolkTY>~*MG~^DcPeWk6KPz)6QA5+|sn$EMr{#sh#WmGyJC? +zf7yYf+fMup8qkart-++exML@)TN_xD00A-~KuE$<$SkqZPf-K~w}{F(4d@c! +zg=3C=-Fb(3-WMHV_ro%~FWkpqzM&lqEagNpL&W``0X)oPM1}FFW&P9)BmDxvXK5g3 +zbl1P;BWCt6Axgs9c4V_7cQL>7F)%xZrOfr~>xF-YlY5VScz(FIotCKvtw<*wWC#ILA`~Ik +zrW4NHu~(K1fIx^K6a;}nh6ii4FgI)|wbFT^r=bp}tXtugyYJ;oo^}_z9-Ou7lKl*p +z8gk0W9LpI`pai8<`fz%Mw(nk2^fOK#E(``)DeRZ{H`MTN=*)urTi$EN!v&rp2UWT5g-@b$m^6~A(1;pG0*7UsqyTU=OT(oNa1w4g{vo3o8l +z%OEFeB${NHWh{XM;m)8LNY;Q=>%*DL*{UZJ2}1&zNd&?X5XcgtFw7vpLri>TwXa}c +zND>GvJTO6tAU7@dt*$c4K?EG1oMblJrB@xijW2!fO}2R6h+PlbZHp@d7I%zkQzM_b +zdFRk%Yz+NaY#!O*p8HPP%{NRKE;Qt(V101x!&{&HEDYp~>2A!6HL6e0ayljDLdH{rIadylLAfzJhK{Z3^e?*}<(Rj)_^=%Gq-^321V{ +z3{uV{GcpMS%(9ZgNSP|PhK2wJsS)nhHVtx-ESVGlB)~mLCc3DE0x1Mo@k)f4`b=4} +zz|7$0_c)=_G@} +z&@4M%!TW$IC@K+x;gI1CeIGq$hi;!Tzc8nj{qqdB{ryys;jm#eY-q-i +zS`#J`hGCX+g4P)I+h6@z7RVWs>%Qf8Q%cW6B0Sd?H#H-}3xUf<1EN;U3@M;aZP)*a +z{|DdjOE3M#lZz+!q-eD;Y&d%S7zdWNQ&Q#PZ7a+IS{lrZL2gMHNk+D$#>~thn+Y>A +z0z@{55QCI4WR{EsU|<3v43W(&NmK={P{?t>5!J!M2nU6b0w*Y5kpd|sueDRp!ilJndtRX3379darW&oH0AS@vP{njS;eEjg$pGAS3 +zv3~8hzt$|j7*K>JIjdDvOBzt3(9l|Br-q>jgyVy%NA5X(CI2{H@k6gzylZgxPbM3a +z8(EVQyS6Xu&SS@DX}Em%POQn~2kt_diGd|@f@Ee7j3}6}CK+ZTC1Zfodng1va18=U +zUpb04M#0)YougkGb|-Q07L}I3^PcUKr&0gSW7nne3k@q#>RE8``#v{AC$mLH_188 +zK}gD|xSF}0Y;7K=Ekm-;aGw#Q?R2Yaw)?>NW&Gn@D8OaPc149iULjyAcND(9q!WkvVDVbT2Oe6zIBtR0?E5cO5nbn>_ +z+v0MHDz31B4cPP!6nF>^Aq5cv43|^`HcxHpi}p?U=5IaB&PUAu{a(jlzM&npy2L1)38OOiF;QK}=e(&V{0LXZZACBQ6Z +z9dmmw#``H{cAC7jlXCo4Y}V?cbMj;ON4)6uPkP*Bv3~?1n%vN2(dK~$IUyNygAto` +z!XUR;3K~mfkO{Jx0a#!;Wy}DB1Y{G_(gBi@KtdQKBM?Hk9CNHFAi=aR;DtUy)q_^f +zw5vo4A{-1VK}G691Ysw(w)pl(hP~uDx3KRq^K3hB2ZM!{;dt=(!HveP+Cf9xCh{QD +zjuXu=VGT$XAxqf^1(7Q|cP_JeVj`^#KJ-}-$Qh@u`~D|3srh4(=q4MaX57lEkquG< +z20=i|^rsKAaPC7j7|vk*2+eSwc93n?Hl6bg-*DlO|Al+%iNBp~c3@7^8g1K>k}L@t +znfA^tb9{4+CL5WI8CXt+AY&MUWF!m;5=kJkK~Z4BK!A{h2!gNt-!qWa_mUj-dXZI3K6C6LX#(j5f +z+L1%+96P+p(Yx0;eETVm-+hWxM>d&F3TDA@)G}Jmqz+17@MgMo-;MX3{C6J68K*z~ +z`g7al@3Lfwq}&Wp(~@$Fr7%dwQJh8d&K5! +z3*StC3GnYYkTWD%Gt4&|VSpI)HlABh4EJG{Whs$DX5$4?2+6HqKSi@`KPelv%&hiP +zAW>$WRu=8g_y@e;1!TtQmrDsq!QFQsvrG0JBnjFkQB>KQO}KQ=c2btIK{iu1kRi;# +z%mNW60$CWKKnCDu7?My(m?41~gaTI(IIchvMQ&9=xLEHC3*!;HMu};4id==DhSUd4 +zYv>F1o#v-scZ2zkUBsTu(R)tY)@sB2-Ua4&j8Q7qT0?3wQA5cfS;BH6CBr;$ubg|) +z5_f%gl?xue*Nz<9!urgf`MIzDcO1wWy6*LZjT^t`2zZM`8YbGjNV~Xf4JQzo3tqgecJ3h%3`_E(8 +ziY5;@Iqet?U@&M51YiwDge(gr2S5TNFogsVE_tOfLly{`R524ZLQYT+9wHEdRI3ON +z@IVqr*C*^6&C?Nhgt-h4Y*NKl!(;Z_-*G*6oEkHC`H*w2+{?C$7RZY(lo+(Fnx@4J +zFe8CkN`Sya5l$C{i_hP|$KHB~%O1B~ci(Y}Rs$yM)3xi~d3^KVaUf@`nLe82yN!@F +zS=r`kngNmKIP{Ts(l}*OJ8HnpkdT2Cq;ZS9VuHR4+r2Qyer1l`I$`g{WA3@Nx5hG| +zj8Aw0z`xT#&e**EyMDXHA7lh6iJV)YT7+R19KC0Q(T;-vm}O8VtD_k&Ddrlqc^^iL +zSyC7nATw5vp5oCLY<~6rhwq=8wduLX4{dsrtD4kcO=ggcQQLC%fgN_>$`yX-SKr3- +zuY9!4X2?v2Adv_}LVy8aA%w`7iUKSM1uj>UGfqVak01ayG%Fn@QNtC2>P|>t!rhG! +z5wJGv&1+Sk!xV{~zQN}EZ{n@*Nu2YX1MEL%7lXM5ONKy70_lOh8U#?PqqQk!4fFFo +z55H)KZg~G;PDjhp$%ba@4agxf{d7`JePZh^|84_0vk~~+&o+xMHv|OHj4Y*O#{Ea~ +z%tcT6BDPMhVdh{NOsE>==v}tB?+WtfaUuyL8(#6+5&ev<)lRz)jxMTO>mN4a`2!md +zRz@R}HMF@=9t;>-Wnb04*D;&lKSww184ZRe1<8^y&{p|% +zfPaU9oN?^B*YC|qZz9~-|2Rlv4f0R|EH&`=XV$i}&*p=F_-5P^C@@umig#PIdmn{G +z$S@*jQclz;6c3a!^-~6;xWbelIb5=Q&()`4P)KdIwn+>JLo9-1fx#>?50B^Y1W_Gj#Q<+OfrrhEt~-c(=h|G;f$Jj#wq={gTE6!DW4z!+w{gXb_S=Cg4${n-+EGK!8ImBGNJf?9 +zmQ1r5CD0c?WQmWx@jh-m?#!P9qyc%5$cSzjR=vsx+lk1#3)R4XSYnQwcf^Q`CG$VE@v$9WGs +zm(gNN9+;VdCBPCOa|t6Xc}G8UzU;g?`=M{Sk@?GZVslB>X424=l3~d(Gn0v|mf6U&K&h=*>?pb +z!^@1}&WFR=&DeAU)M81PgcPI*Yubj5)lD-w7$74rz{mdTPF5CL8#EcS1SuI3MXqh* +zF%?5041oZQkOU$K0D=HH!Wg$ua^Zc)Z(+}PK(C%j=?xQ7SjZ7V60RUO4U>Lm$wZZy +z)@r3MtgX%TpPn$!E57QZ?7nmt=Rf8gMvDzOfsti7K{BjK$N*q-_XD +z9_GLoERsi=G{90eQc9L`B8&(2LlQLy?dJH&0RPScIpg?^-@e%9`kNpC(#m%EY{J5} +zMRMC139tkRQUWBxl)~+0-~VlV=zsh%HC4Qyl9!gH*^H26mOw_r2#E-1W&1YN_2i6l +zGGva-)GN^*0i$D(6SBY(D8iOUL#hcgBgp`hD-;Togd{*nK}3iUQ6&o?WhT9ts!+l~ +zA|wJSBIt3dSE)KBf<~2=U|L|k+v2MpvE6>>H}BwN9dOwfUdZB(F(i{UNo1J}ECO7V +zY0s4_Sub2}`O)wAknXT$=C2x%@(62#CN<Bqx)+FtD*iL@s_utRq +zkFRp}coD}iFHCETQNWUf5F!)^Arp?kpoRmILRc7*aKuJ8fpDO=2jTz&kDx?A0#{Wj +z9eu!59kf +zCC}f_?hAIKNHjUDuZ5La$5&szBKz=N{P?fj#ryj?u6W^j47U$Rxiw|T$!M|xC^ZHN +zl#De1Ajfb1nZL+c&p2_@&wY&lg}G+*V@9-w=5}|^SKnR%tBXD;Ds8LV^ +zk_bsA;pW!Oip-%;h2nt>P^hat;lV~V=JJrvgRN5e&yQH-)!+UphWod1;bYFF$wQ_w +zWjmXE;veKe?U3*%!2nmtMN#Ody{@N3Z$m3l`?qKYw9$!&v~$SKjJM9xOe +zW~ohJU@4mnLZT2X?U}<@WV*XD(H@m1G&dN`&IIxqw6JpeZ_E0f1MfDN-??nVLHjo+ +zulc#NctAThd^;wECGub~<+&wJ-1ISOPRJ5w1=J8_jp5v0PTqTjuuLR@3XsV@C5;xS +z(UaS4MA=YKXm%V_J6vLR;x?iVWhszBDV3&a>AJ%9@et)qGPCseakS*7AvcL88w3ne +z<}pvZgui?D&9o^IRtNAy)__Ng*mmW3AR^Tg;nK2#>%csH&7&>0LHM +z)GnCbbpuz88h-MZR(bX-F5;z^?dKb=+Q!GeU`$xS9ViIfc? +z+O{Djl#*d2$%G7)Oo35LVQJ4i^9wCo{fuqe&Gqj%MJx_YmZF`Xk28UMhHM`D;g|dR +zjTfYpY1?H>TkDLMhSDHzIsAzqzhd=<@B2lw^w1z%ySOi9OGFAz-E)`JERvEW5}3%5 +zb>R~q%isO%ABvUjpa38dgbcb3+QEqN1&<=lj#{4EO|0EzFIIqG-1e5dJ#%VnOFk4!WrdIY@{NkK +z4q!!C6ub8WA2>6Dd?rYLI7t2JUr*zGDjWCV?GADivNE-~m6hReWY>!HH8vXAaA6+? +z$Oc#+tekVH(JT@cMluW`GqbMnlAnF8z42RqkZ2Z>CFFp>6in~Co#D2NQJKKx1ZV^x +ziy&K~?|LDlFJTHGCC=GBFb{`pq--c9@*r8$W-KMkDUpD|ptX&%&RNaKdO4B^i4aJF +zWVnLrjv@#NDR6~?O30YhsujyjyjXj;Ua(MD6i9Bae4Me@u#s +zMGs&R(03D%tnX%+K}s1Fad6u_ky>byXd2R>!E%EDYfJ)?C7Nv7vdrSV=%ZI)1VRXf +zK*p9A8X0a?F@aSip +zr)_(d8RU6JO`|3`BTF=v1i}&^FoYz!>TH$Dw1&27V%9seS+S~0Y6c{C+kbFBH~f#A +zx#bfbzGsCrbvzqtvMlN~{M$Pd$mu(NVx>{{s64+5r!XEZGsp)JYbX{-Dbbd!K^_tW +z$y$=hFe6!JFc@?9N3Ld?8VfU!P;wAhR4)JgC)nS8-w#vA%Qy*zgZHGl9cX%-#MmSV +z1(IQAxQnhUm}QV*n2J|c77Q2AU`=CK7VW^u!-fD$$#`JKLff{KI;AVwLV>24>OY)z#I?tX8(VEuOeCvE%PQ#7Ay2E_&8QTyoKVcCF0Y +zTsB6H7??3=GATimu_hZyMoI<-LOk$Z5ezeMkZ1;(90^s}yR?V5eCvCe4)+j)M$Ihn +zwgGpNHMW@NKYS*T&w%-r^lZ&eiEaa%zXWDI>vzAIG+dCH3IU{{g@omd%p^0+GAc%Z7gjp6j6J9B^4dfLhDS~7M +zk`0nLe00WeXd-9S7=uBhoD(S-ZQIDWUryPU=jS=GIl<}_4=DgepfJJ&W^8(;!3aV^ +zcm(djEENWkHC0fpQYD+XvDJ4huTS;3q4V88y~=}LaIvm<@IK}jjW)TOrok|j%p`|w +z+Wyh*URewqMRDtr4g0vOt(+FvC&*lanX;Z;u<<^IrNX9{i;ja?wN1vW2EG5NTNw5SH|6Fnug(m+0N}__RnqOw%@vikN^HDV&{rk2S&mzkOFgvCK;QX+3f{+Md +zVKAFePambuUyd;&4YH~9M8Ao56O2RQ4OWqtIn``Dc_E(kd?5rT>ggA`)j9kP?6QVc4Ag&=3{UOi3InpCD7 +zJ#mbmd)hote%430=nK!|L6170ohuECqs)Ap7&VPjHl=JBfmBtKU;Re>p#l7zy50e`afOG-~RXOICW})m`mgyWCE8+8M6i@k%2`UeJ%g?&IB?! +z{L8l|FnNG_MF2*Ls(!h{I2!MX=qwJ_giO6`TA +zmdpq4+2HKCsU7Ji{P1Jot6q7vwm)tQGU>Hzoak|@!mvOJT!9c0NPtYU +zl(~I#15MsGfi%3AJ_>ugD_VR +zxL1mLf+VjFLu`38uxV!3(T^7X9ccIu?8qm7WZq`ey(UvA8SNbAGekymy3avKYS|}J!lV&GGQ|_hfpQg%zu9E7x126{av2&y2pcBBtS`6 +zGD{gnJa86L2}b+2+uy$R80V}ks(NpOBqBo^X4=sJvqU?{N-3Eo129VvGO{wG`8n%L +z#rq%zEuKY|5#~fd0O68CW`Y3&4n^YB(Gwh;o8!uJcG&bp&)l8s`n5N?eZjY!&AI#c +zF`6q3^8g4y3SmOHFqt7UEM??wcV2y*_x<9>S--Q8cCFxhcEJ1sd3ifASOl3Y!Wyrr +zii|)a_cP?3MRB0Y%q*SUSi9@^_20?Ay)z@o;;zwUgdiGAWRkm-=RrYAnepN_PG0{Z +zO4DMP1pxviB*$un+yF8&gDM{WoJZ+Tf9!Xe*)9qbEDTW~1kph^<*LVAgeHec48f8_ +zm=N-!P!P2evyPPuFXcCW>n3(h9LqBk73GRqOKuxdGTLEF?^qrfAcQ0YfD!E9v&5(F +zJVKj-KuAHw@euAoL_83O5!F>BngB-I_}JkYcK9A1%MrfjhmP5^zwJ^ky5M}4mx8%w +zNKVkC2Eam2#YqhsjOMuc!^iopuX!s!^#y;$-~9AFtSw#$PkJna&wCDY&-+izf6-Sn +ze8QJ7zUoPuyX?_6I{ylW2QJWH_W_!v6$axu(s&LVEnxHWFrK$2GV=?g&*3xUOdw^( +zkC`y20GMUNE2&u^l%RnVH(#qfw+C5}FfcF!6PN{rV`j)2P=VDAd(E$Y72o~&U!!jB +z9BOhD!c=ju_|}y90}nzQw}1jg7=Tm>uSTs9`sr00%X4MH9HMi!;P)c|*A}3^_ +z93sgGGY0MOseERfnPSbY44%lw7G_BpCNr{SL}1Mr)hScoW!8*{)RVnMmazzogb;uf +zVMdRdAxx*Fe#-a#$*b&Dk9-Bc@ZsNP)O~{RglflReT|(De+cFH`$%MAW&{d%BLX~_ +zJO3hn`Ro6X&wbcFW_@qrLXk+hWo~F#PNX)|whgJx%9hQV%(Pav4O%9hBg0H!so}<> +zTYSlU=Dw|-rPR`9nD&8Nqv|sYArH4cVCrIXYl9P~Du4aCJzw%Ihwa#87eD*cf$do! +zR+h-QW!4AuGa@?We$KM(#KxD?zIZ45msYUh(2|W2PN`0r^)TznHIW1XNJSu%g|cK` +zD;6M2U=3KsQe!1mC_zezlPW*f?%VTSpxQH=b=Y{Hh_G-okszvBI5^mjJ-qifKE&f5 +zd4O5;STfPeidTloIBV2$*C3Gxqx%==!^WCqv;xbS)xNN8G|=jFYLYQfW>Uq0F}!yz +zNI8(CaSqEFoA$xX!#qTfvC&sfZUn#fNaw%5_72{6;&B>0@yRy6=wkNl-bFKP5fGLr +zeTO{EqYy4?nGva>Y)w?7H>(}$Dz#K79X9K+TCoU`vXMq6>j+^~Cr1!w51auA(I{JM +zsdiM$kTSk={}r1lu2}Em+XqRc?G1G~$wsrk)2AnnGgGXy=T5x($aUAF(Ev+^B$9#Q +z0)ZuvFVm +zh`MS?4VE&dKv9U=(@{8e^O}~MN>DK+6tdK022Dex#J-_HZlE}4`Y9rM3}G@P5eiU%obaL~pmfNDnFzV< +z+#al~taIPt9hAvQM!WVBn}?AS3Mr!CeNXhE`CSj?*e9;H;V6-sBoH|_)LKCSIkB>s +znPJ3GWXt5#(4-88l0*^=nwDd;U>a`wmPaftHe7S(5f+)FxT9JrUYUh-bdvb+@hxuN +zm?^i;=SA2BH{H!EuX&hOp7D7cxbh;Ju}Dn@6OLmtkU(GnArcY>$w9SZf(BW*ld7OP +zNrt?j2xN$6jW%bLhTOfn#YN}s +z(tB?_%;k$ky)spjg+Z-WlcSuOsj=}rYrJyt7?1w(oh&@}rMBaui!m9?qk-l|2}76} +zG9d!YhzJab2r~~7V0}Oc%MG~W3Ms&&N)n2YNrW(q00Oz2Oqdt)WQ){SQ(DAT7}lbu +z^q7lrYN$(#=pm0|whZOeDy7Ru_ihcpl~0c|fjsBI2hXS5YE!^PpZhr0Z@iPhkx7QY +zBm%GmW~9dK#%r%9mUcrFNe*(P00oj{0g52Wun>A+PCn~r%(~uQ`s@q&)Q4`jv~3Uc +zs}|vC+JQHW&w2{C|IV+mN +zN;x43Ni=3O0m%|e;{}eK?(K$?Jr|B!JFP+=vMxIMfJuQ_H)Y3a$4^|is*AtgSa{jj +z>ELBoF&YmTBycs>cfnlSGRP)H5E2TR=2Rh$nyFLu$GN|SIW2Vf;&EpazjK7JALdc4;?x*ITOgI +zNwE{(qP`N!m`ynR(RWarIgpuDQMh4XfVr?R&bshY*t!>CW&|iO1O$p|qJkVH$xDG8 +zMBl5HidVE_m-yxH`$KZO7p9_Yo+R&jC=3pA;;p~W_Oo`Aax(5`0A|qSL@AY}g*n!` +z!pCo%*wYUVi9CnX5SFQ64G?CeWDnd#WlWisf +zM=cr*P$?*cKM<649lo$aQ=O2TY9UuyQ=O*ou<3Dn#^mRJ51e-)gB$^;M*)eY&kvi0GGP>Y-tbOublv_T^V0j)%O36qmff<7~ +z%iU1~^2#S~S!Lh$F;QoF!pZ{O$Vrx!t!3s7l|-A2WLP#MS;h>jN;1s0?;LaYkrU() +zQw5u0OlK3;*D`;6$sN4l)oa>(&a2q|kcTi{95Aw>8iKjj7&Z{KLKqQJS1OZP2cVFs +z?u3Lq@aiT(gd@p9AqA$5O=9&V&D=UxRb +z`2m7T{nKx!#&Nn~BWoML@QBsXI6a?FuQP!R2il7nNrqWwG&khdxBRj7quqcZ74TxF +zKsaV1934G$gtBshK|oX#Zt_YpQ>X$CQ6U1nAa~@lSzp-LFqY;Z%dlM!XX8D;%qTag +z;Q?3(K(vj~^@@^Cgg4k +zVFq1ate0Hs59;J+*Jj}p?=r^#fEyi7iJ`=|#W{-M0lPA8E +z_=2w`J?L|6;}h@1{_s1n<|Osp^VCi@(frtC2_s=AR$sUEkvor^3FOo0#&>Q$?j%Y8t{TA%Oz9 +z=qA0Il<^9y8k)pa +ze=d8U^jJpo^9*t*XBgy$oDwM+2F&_us4yC}Sc1OqkqcMQZETtf2m>#q+L5{~+FI01 +zCN#4dM1z{4%DxL=|K->to=bk@^YBX^$t(}i;ZHFA&F>|C>Lv1DdKLAKzohOM(Cyt# +z8jsOnXweRl3qvD5wZXv +zNJ=cP%<+3a{Rdp|^UtR_cBAZp?+uf>&Z0?}M7Y!U6TG->ZcG^t8aB2n7hJg4`t`#M +zhV!aXO#y^R)<{+jLtfo-lkjSaK5XCaG4H +z0f0eqr-oS`B%00%OB_2nwB3V4oIHM>er*0WYp*%Qi$DH&4t(j;w6blFjfT=NVTPy* +zrDUdB5ecYFBU8GLe%7(LFsJp+4fE9wuf0l4O?i&_-RICOuAsC+4Kunm`upyKL$}~J +zebTt^7S=Y7tL-P&4~L{tOKuw3^1#Z{P9@uGnje!+t*LDqQ +z7jE&-b|#Q({_dN;@5-nB)OYZY7%w)Dur(P9ltePaEEy35+2}ixhKOpykc7$QgsDi+ +zdHJ*Ko~z%=`3JXI#1O*>RSYNhJ)+2lX1+RQHYv=ePAQeGwVq3!`C!V?J4ncoA%bL? +zuwo!2SxSZ^Lb6c7Baz#Yl7%It`DSQ?cFg%DY{n4xz(}-8gbBkWg@r*i=sM`5a@O{F +z?z>>jO~<$Mn(l6U_~bPEe*bI^eB}$-v3svA3}KWT1II`-CYyQ1EaTzo9qW9mZgbUU +z(?rpnopbOK=C54h^y!n-+wbJ?M?c2u-+oHd+paU4o+iu~?^&S?2hc2FZ6da}v^(~h +zY5>qjAq5sSNG8iNw6=*xtuY>BeI5mx28H>t8Yb!W;-xm9?dp%39^xH2qSzrj^@(84$Mh|k>J_NC;Ly=6UHXuVu#zxw^7{M7unvHfwEGhAL`#0a&CiZPX|gt23JJC~n-0UK|74}bNH$Mea*`UTP# +zJ<1lI`gqzsd&rAp$VR)6=c=@(KZ| +z^s0jvLSP_))k>COo0}hb@OW|mo8SEA3IBX&0y%5P>^D+7v^txRvh$Bg!IRBQeatv{ +z%gwMjfN%^j$%w!(NQP?2Rf#A_L3l;(pg;w_{3{;K8-Ma=DZ|!G4i;ABgep}|R|~=r +z;@}~QE*{v^O_j4Yier_ARhPW9^ +z!F{yOEadM@LKJOa%vpf=;l2&+gzV9+|{?i;Kf +z?f`c~K}4Wh2`@lE1PZt8P)B3@#0dpXP)Ie9frW5OmZu#7NI{CKOu|lY{A&M+>mL0t +z{0;hqHBp0j_xEjd$ZcFt3_HhIf0y=s~Ng|_i$K{6qBUvfG1_-XDJD1r!wa9OFS +zNk+Irfk#EE2vKXV@XC1Jc<(!JqwKl_%)uQ~Mb(LZib=RcR|=EKjHOF1SK2&Hq-4o5 +zVLhi<3pQi$*4!VHyrwnhb+s`@oYzl2S^XyKA0ze&{q;wySg!xKZ5+ +zhc1j+B-S@tzGK_XjBdS)XMF5MI{31eaM7Lv_VV+#^WsZJTriw);!oejuYdI``Gw~{ +zmD9V&%)Q`CmB0Auq@DZCn>p3glQ66?ATYVi1m>6l12Mb%dZGZdq`qR|2=IP_d5_8Q +zBxSF31G6%lkvu4)hTI39f)t`AAY92SsZwY6-1hSBx*NWlf4MXJAQxP$U+JbD<>ZX{ +zv(F+Xm4`fR;hTZ~ga66gfYGcEavq{F-1m`>ap1{MW>^j6NFcc(h|)vC5=yOVU~m~$Wed360IllS~Co3qCDKjqnU +zNz~h~G0X`IC_RPKCuBO=d{`c)v1S13;Mu&BWpBl@1+jAmn< +zIvA1jn5rr5ObD58Z62>1n-Am8bnLkB;kxxt|J(+PbMhj~$x(!cO*SEN +zGL#H6hzNw(HcmF_G8aXmn+BZ?iMFy?dhYHLPdUB8%jR$6!N2(+PF?tR?)=}o +z`a1sg&P=hc`@BN7-Hi29J-JPi6$FyNB`jyj5~ofSJ@RuN +zMjX8zSt2At0q)41!C9Ab+uPn@GsVr4?A*_@7;Bq9`+M0_WY;ns+WE_S3Ko$?Af-Tg+ZpVgu+o08Ieq8 +z5#*FjIWszYAIlHE&^E7rGsoZftBiJ@uR$sF5#Uu(3$noaDfBbsQlvx^RjCxJCGzIF +z9HTl)QNROvhb$4Z9=e{EhSUtGwfFTfW6ru`TUhUA +zQHg2aY~QcX<^O7~c|_lLAOKO7FgMP9{F69!!~3WN2oxbA2!~J=3Vbp#H3ks?79O}e +zwF{p5)bn`jZ~d*6;gCc!kBUi{u-U9|@}8Mp{(lDItE+?=GJy*@_`(iWZ@G_cbMvIu +z08p4lO~dM1M=B^`G|9{WGe`*o(E#V~T;ii2znjP8E!*he@J6-s@4b(oIeU#KeP3_o +zSzpTTb1t`06ErE9Zy>xd%tkUnIEo;fVXDY0NWub1)P1|uUV09v-te>5Z$OhqsLU*t +z4thrsC<+!~X)+~Th72neLY5~}ntoO?pm +zg4$s_E?}}I2HhGAlPqVDF&wwH)fI;05w%pysUf9|VHoND!wkzw+ZQtLdG~#M{vKy- +z(y;B0`}8{(b-e0lj&SQ!zDNfi_As`uEHU3E5yIUWHJOM?V_*mrc(^1-(P31x5J4qX +zx3(G3UVJw7?vK#F@13~M5#t?1naFBEHK?Ux0ahjT6++;WyAxfd-B_2C$WdqlVr%t| +z*=_eO#3yfi86f^`oC&0*``k!QvkGZ(k!CzLuOJV~M!$1%-D{rD1NQFGorI9U%rG+$ +zm=fL>tCLM^*r4d}03j4kD5w#v9X+Y}Z95@4T!m8IY8BmN!X@YJX6?`_mEMwxlrttc +zcF#KJUc5xCZ(5s#iW32&2S0<2JFa7HdJLJ6hb0oZZ73xe497GnVFD>o1PUoBK;SMY +zvZgwR?{dx>H~1%3`OCB2e)J9Z@h9gzjq@M(cy?~v!EjKax@81{J}ON%kPsD_koTrq +zh$tv}EPAp6d55T&C1Pns?3hEhejLC0{m>1q%!hQcLPJe*0L +zQZ&O5)EN;KsmQ!Xr6=TsSB#LtkvXZw)Fd=F#J3IQYj-fX@l%wIEmaoqW{9dQCR1#( +zMV?Ho*_@Czrwq0_II&)5hdyyGw;lc^J~Pe)GE&`Ew`AEeNg|)FHn0ZC@n!(4rP({| +zPrvt9zkSiyj{l`(5RL@NOoo9OQ3C^0cD(CQ0+|SfxtyrrQ8<09V|m*)`q|7>oYL0^ +zMiS=}SFbW&INQvEupr!7JM0{M^y9I!A4Hj(v!j3Wn+#4KrJ0`xLS_j8)--@yN(nOn +zXa^19Zo}M=OTxnm59ER>>w9Ce+2gZh7Y~BF@7Ul=-Z!?3U-U&RuIwXQM&`(ZA!6K^ +zwMn!&$bz0;TV<8f2;tzpDI8Q$t>6VBOtoSzRGcO^I3UYrmgxrxO-@_B_hy=FZzSDu +zpQSsFv+MSCF1hz47al#$?&E6=jvYUBu(Uh5b$TzKIcEYH_J&1b^X_$gE)&yUE>lFJ +zX&1?u(&{P?y6p6=^K(0InHvYDj3h*uBn$w@qk;ehE+JJ+fua()ygH)=c>AAyoC6n~ +z4Q){RiUhU51^ec>{$n?)+Du8o)@otTzLA>6vpDg-Kc_u>H}lKO$bxKOW>&qT;@H5j +z5E4W<*^I^#!?__$Dz%0Ph+g2L7MN~zoIKL;ny>jhbSm@Y-#x;GU;j!i?L9}sK}*vF +zO-eGvEPA&38Czw-WV+6*TgAFf8v~lIQR*gjIK&B59cqPusDWHWA8JT=tyoSF0VQa9 +z$0n8jc*pXQP5#T=HvP>-7wI=IImi#5e=a|G?m7CYv(Mqjx1Ez-G&eWmGv`bouYbW6 +zgEWJ#^psU7H}u$2W37^<`CY`;F;Wao)fq9_9qsThj!l;(tC)nsF%wc?7-ohP2oa@9 +z3WWlN2rpLM@hbN1+o2!(-gj~9trIT1bT{smQiG-K1K#*6?=zcQGL*RU)-9g?yf3h= +z>;8s?@dDLCU;+>Z5-EgI!nb|z;1sB-DAlYbOw3bj{QH9)ykZN&xsYLI1=S;TNiEcvlmHx!NMCmC& +zM3L7J4)TDI^|KjPoiyo{Rwr=iG{c*Z^Q_|&{`|a)`K!l1l+W3|WC=G}CUQfyY$Y}1 +zC0jXvln#IIWtY6++aG_);+a4`1NQG;xu{MuY0{Dg&e}bz*xHyfT2$>eK|^FgVx&yV +zxeM!`KSGu%$O1F)s2Cs#Md%;y6k!Tk@rrwuSI4VUx`Nlv9{G@6+;Q_J-~GKG=B%@q +z$*IGqaQOBUEbTj}#xsX+IL?JndmLN0d`b~ufl0|E06_?pNGYKZ0ws)O8Ne{JlrksR +zW?GtWumJa<%1oO*+sBJM=^0n@{MY>@_LX1AzK31FXf%LUk~tM8!f|)G_e57L%FJq4 +zsC|b|rj)HM`n5IuHh*NBm2DQ4Nt*%Yf+;e)oy>sZJc}F4jUMN6DG*AzD#JNVLO%LnKVd9!eoR3 +zvm}s(M7WTgs7|&-uioTNtwp7G>pM|8W7~4#stb1UeLs9H-~LUP>6539+4gOV+;f}T +zuymBo3iB(Z>SoypgsEbdPy`Ba7daVNmYW*}5HvXx)k#ULEDo*7BVy)Kc5Al8>hYfE +ze(M+VU9bKqw$|5~mMOk#f#^hYdS)wYGafP6t#7*Y;daGCmzhmAnRY?(APR>nizf9U%^ +zbnQ|8Cr=sg_~~ZTJ`Vi)nLz%TmeSzS>Jxcj)H<`lY>{$1C>mMF!aB?bNCoRfB}mrN +z26+xLR6=EP44LOj+7{9dGeu_NJzA`^2Qq^Odavd=m~3{`2HJ6hm&&a6Y?RJw6|*X; +zLVf2pEkX5!$fOd)q_S0dJdA;$^PruZ-?0LJwt2X>uiU+FeKP4kRt7q8dTq8eZ`&WY +zJXlyb+;!*j&vzz}ZG&c+siT^KhB~h7y5wwfJ=bK{gJl5*20+Rgxu6EA@6o~#Q9;Qh +z5r9di5D|fUAO}dQeKB`uIxCc!F_}SMd%PF5Hy(813cv7spWsDLIiFwqlk4m&zVVqn +z&8z#a91ty08(lweGdY;p#}p3GoYH0Fbt^@Va)nYCs()Z%8M`J=4iZ8Bra1m`h~Kc*^F* +zi6ok2(t`(6lT?t9GT%3$V7yUWLTCYSzn#Wmb2+o7KWLA3ZBHY +z_C#ddtC14(!}+B)FMMuxWBZD@xok4uQr^CH$6aUEApZh8(jc2RWLi-Jt8>il8gcrj +z)3`fHG7ASmP7S066QoGAqj{R+1!kzHIJ%}1iwt9;sx=z$Cm`!_1@02QP +z1TL4!x$LYxyyXKYxbU1MhJyh+ue!p@ZTCYzwBPQ!ywJGRtITJ~i9qMCp0K-itXak3Pab-a{o +zidSj670jlEu5-FR=sHnq5FrpIheV~xiP8KVw|?+;4F`@FC*;WfVMp9B(d{@g%_U3xJjVaq=dpyh^Pi# +zk@rFEE45ZUoLUMsoG=JOj^oIxL>Lx;yYMKK>hvKZ8z+xcDUVQsC=lKwiCT>iNNy2E +zDNgT>c_K=vn~7Qj_e7lqr8q&**nr8_R77DmlbMSo?8t1&QJ?VAJ?Hb7o%^`GpHNEq +z@iT$^3k=R4j7lBN{lH(`NYjk5nut<}X~+8YtHh3rEy=K^Rq?>ACAmOg5_B>KBbt-z +z9KPWw!=)j8?@)SXrKr@P?<=*3zNhlknEmJ1zlJ;Cd@bMqr#JAu +z&-gqpJm&)D28o>JAhoE;1V)6RFlr4`0G1U261am$!d#goGF55;ZE} +zAcd{1RyIzF;oOMmGvP)pj%vj#RPUMf0vXi{lS#F~0J>??bTYd?j)0IjQ8&46?UXIg +zmUz&?3pk*?TrfVDXY9Iy2QBXAqZ_AGo5VIX{M4!I{`gEF{{sC&KifGO@A=**K8<(2 +z=`P02A}OSPyrKi@gAuW;-5g4EV^`coEq1CKQ&YR=AzWy)C!=!1cMAz8^ +zZ$(n6g}$%!rLwwOn3hW4Ii)!6;9&#_5j2KUhI4n$bM=SsruIYJdW&$7PzV#q4F#AR +zDQ88ggutsrB)Vy}D8|~UiLSf$5U=|0-@?6jT+4s^jrZ|0&wdKq_wHmKk!YYXQp!XX +zA_+@G4GbeBYIO@GN(}@c;s3v|1wtuj!VM#vc(7Ds_RoTv~kx+?Cx?KI7B9%@3p +z5fR7(uK?2KRu66vVzwSkXEU1?vDF*aFK}VGm`9Hu%`=yu#BE!r`0E=#q4yrYfvb<) +z%rzS)H1;9q%`e+VsoZ?x(A)W!ITOem-}uH}tMfaobDs9_tGMlhQ`)%|vq8h&^Y(Dv +zFTRtya~sWUiWH;>83|LCDPhql4~BRoEB83x^|cS?jk_*z>>kC9K_ZD}Vbl*Xmba^dfd2+{L!Jmee+2EeO0+z=$e~B4kDc)Z!2z +z3BwfOM7Sj3HRLWLWL}+|f{3t$As|9{bt9bc09MCql{#r^AzNul&%rFYg>JGxTk +zF5E>R46u}x%ot>2ag>>DPN5Th16Cc$Onn7mQlO9th%l06taC~YHnvA`$O9E8^ +zQY%7qvx-F^h2+9;d2xIeG+FesYSnupDxzvOforZk%(iCCY_o8B(uu9TT$P`}=PZ30 +z^SYRiA3Da@-t&?Hzt@VUmxp0tY@4mvW8Xe%mox5$J8895Q3=&MDGO0boQq<2* +zaocA1<9~hXU-P!8Gy5Rpl-gvpq-I9L$ae37wd+=qVoh1&^{Pe%=%K +z%^&+K_Mdw`3ww64`?5zSPn_U%}t>np3>l$1nGaB6MU +zK5=^e_P;8JU&FuLnLw^Te!PD1;@PIQSK|YVC^U>14+2|lZTI4CPJZw{=Fi$P&kG_j +zFffoA$Z#bLBLo$Q6IEB7wP+u_X`K&#;20O2zpT=Ql}?l%W}VX&QA?<}n*?SiOIX`v +z+MH;cM3WnvpPOU-#4#53?zic!w@5A{Kman5iRyw7vsGs}&Ky6orjK2_VGn-FRXp`M +zPv)gx@s{P)>@X*%>rA54{L;Mb-?xvw2lwc#v(C0%XPw33-Ua4{ +z!~gGTbUkIdPJimO#rkQ*WNOu?;7-}vA}JvgM8d1n12tTf#%Ll2YUgcXaZF5#!&X>t68s!9}~)x@Kxr +z3dl8`xoxdaSvt6ryY4y0wb$Rq=l|9dn5!pH?XeyrdgM+_E4I)Qs})71Ogl80vNdk_ +z&fmR;JCAJfgWq};Cr<^FiLQYGP7IN`1kR{D;vc5F^2obI+bvbD~k$(l~iHtgthjnj2PZDDad&sod+xN7%> +z?BB7So#!23|6niMhht{NS=SbxV0vPM)s0geo2_$v{RI8Slsl#;IJUXP#%zrf(=BFn +z6ha5JWS1}PqH7Yn>jJ}JOCv)_F07*68oqNm6}PKipk#4She +zW~O}KU-pL%@~?Mh1UWK3vg;BpK_jvUE^IK!^aCdkGkdo!^S+atjOQOG9s6@r7L*Ff +zWM-x?l7k^o2G2xI3^q0y)v6tH$xa-HY41#@&TJO+Gq=(OwTRprCXo#W*%;<*!!|K! +zjJC;~w||jO-gqxpebJM(IxXgH!EA`aq`<-J{Yp@y^iEL(33YLt9#KL&YAZK>XEpc(b+j@6qrKgrk7tS;)$I7NYddtV{ +z@R2<?8ym?Iy@$ +zOPYNg8?2+6sTTBd3v-essz9mINzD4tTq`?PdYV!g4+q*1mT+nmk{LG~xd&JC$-nIH +zdkCK%X97tjI;AX<3?Ru=%x%29!0E%MXjkUB_`>tK_q|8#taA!#ogm1OEig=y+|9xW +z1#-z|Tiawt +zZNtvxmbZWOPF}9w!l8+VC_y5dcLi!_GIcv~%(?M~(_H+}v-Bme`C|L_XT3q+`i!r& +z9F9dJ)nbtc;4>6{|0#zg9X!bjNt +z`JEg(dRUSSL0G2Ns?Lhxq0Zf0(5b4n19LBgfNV6i<<7~JJNy>DXy2u}bMv$zK|)fp +zP(=z{AOx75E@B@_fs^Co6fQ(2nOofvOR`s4j||ojwNF +zeeyI*`?m4o|MFb>#;3o5uYA_ajM7v4DPF31nISI#0tlo~APGfSgp+fF*S$T +z+U>ic8gd~wpyBlKUN?U7G|l`xFaN$5^G#3t1HS5MFD2HeqzDp5Bv48yoa)7@SEMk7 +zVHn90LbwNxtb{0G^d9CCj;TtJ0!b8AR@x;VJbx}XuN~F*U;ie4`R=!IBqr<{EKw+g +zR}_vD6r4^y9Z)cu(ojt+E~N9 +z62+x1q^2Cs%*u#Y{Mak_rsw`HuXy^`kv)`04Jln-6%#_87kW7b!+l0o&&-5-rG~2lvlQ%Zw(Ae>{S+UW+|4kv69Rh%!{^$^>f7F>ZVs221Q +zax<&pxM4*FkD#WoZE1xQ?)=)lAJi2Kd+qY&{q+5Wt<4Er{U+6WMEstY^6h`iiNkzG +zoY^R3cEje$e8tMn!RChYaA+`;J*8=Cy(ctPLWyIsbj{qS8wv_-+v`P`n7+;=bZIi=Efa;R7@^evZx~HpbD=Q +z)qZ0*^{>!6=PY +z)!Y9#{B9mUe*xn(z%hzDj;sI$Py|lkav;&=P%dF-jrMbQx5~%vxr?)#UF?gPWYf?5 +zWjXXZKJ(55a@XeRd%HKS@4VnS=h^(mCb?;ha0*UV!(ag0SC*Nj8Kd$ri}Zf_^(~q- +zk|QIG)-uzs5MiJ&Ik-cR@D7cAW)laJPzD5s(Ii6{qhZ6qvb9Z1n;UEM{Zp)_X^0ve +z*tWoQb&J`?%-9(7iMJi)n|}V4`sr_dmt8jhB<9CGstK7R$s&YDMIjVbA(=}=h^T=_ +z;8iFRT_n2VOx>+SrT1zI!zCuQFo~d#z`f8#u`a|+g-TIzS&2f20${oFlW`z_aTUC;kZ&ID4^C(cjh +zA%Fgd*Yd3AUe31J1WZsJ1SYXE9<#K42OsLDMAK)X?UZwz>aI*@(Vy&h`av_~?x+`3hHm@IHO* +z&;Doo*&q1}D;sCAyt`0q#&abDNK^t`ig2r?m^>6wEnL*v;~~1zt8=&er%*kmI=xqV +z1=TBkIOT!qRdr{o%4+m<-f326R--e?ktIS2PnjtlE|@SyS!!yvKFO}xxJ%p9f~m>K +zpdkS!fkg_2F{E1H88U-t8s2<*vwP8<+&Sa_U1tKhak%lb`|`reFPj_lCx3Gj&wJMS +zEUb4}BydME+5{IY?&Yn2``27?)pM+N?|~AS8EMckJ>8)u6CuKbQia#Bz6O1%tZ$lK +zzITZmPW6Z&EJ!q#%*db!Y|u2cgNB@-$wq5}NKB?dPR8&2*$4Qx=RKL1J@?u6`#=8{ +zj_vv?E`91lx&GKajIa9;yJi#S#t9N6Kys4@?!w*8i_^;~)hVSgi)y8>%xb4u6zjY& +z^PVc*ABYE?dS(hXYR7uF#d_(PF730}b_q?Mql*kaMYChr^j@srAjwHKVmiNKWnmAb +zAt>7kIi?v{p+*ntj5;CtR4YtvE-f-@+h;qI_k8s9?Q8tYw6MR0Lz6Rse45e&WchD^*@^Hj#y+@IWQiurE +z2rE?zH|wh?9-12s&E2SF#&9^mNI)W)(Kg9U#;9#cDbqHYoD2}*m7~Xd){bv+-?1s@ +zJ^5k$!4Lj9e|f_JF5i7Gx7~G&*m*APRnOxvQ^ueAkanEB)s_ca8syf(J3>j-t95-x +zUkkI+F{vFJlTD7zHaNM}*;XzTK(!({79yHTFK4X=b-0_f|1qSw0}N&k%&1WqCg}45 +z$~!3YS0Xcos)dT8y0uIIsYaNo5Nf~*0M&)HqBPYG`2WkE0eSF07{ajAtCjn1U(?6$n%N({;d}M&-}$h;=ibZs +zobxUNbNbR#R&QkT&X03S+oq+lEKnhponIr7?>CBr2!kSZBLa&q)UxZ#yB +zEX)Z4ms_$P8B`ET7ADA{5GD)@qZw|eX_uZb$~&JJ>Sa|nNJXwW_uAbO)_o{S`D176 +zZb&;5&9hTV&-$x3y=Q+jSgt*%j_=*M?ZJl*P5EqO{5$qL4|?g^V$YInL`jKp8uP1% +z-^TB}_?4W#?KbMPuvOuYZu&TvKKM~Q;o%*_xx1+W4k(G~w4=;=Hn(~3KO%%;XoA3wq$zw17J +z|9Aci*Szl}Kl`VvJo)^`Q?nDLP`#&%MD08Jx=Ft_VYScro-3K2eVO{QX63Gr)8BQy +z7>CVm+s`=d0^g)W!3PZ<1?z>U9(jk{@u*a)MsJ4j>Y;bgf+IfFi}yW+r~l@Uv$W@I&9V@Q-GdeV?fd_V`Afcl +zx+&_^aj!@a#qmIWFS=4Gy;&cbt_1F3)q~}^5g=rS0m-1rXfSFSv>DvUX82UPYl&n8L8K6t2?AqIwVc98gf^7Yl<0ff +z;db`yxrnYD@8{C+G5>~foC##B;jObeLrrD|xXccBGhTY|89et7ev&;4dr=gYh9f)N +zO=#Qm=})XtOOG5BiP{I +ze?*i+#QXrG5(*{n$nM}ynoi8OI-(ijUU7lR%rMLv2tkOY*$LXwf<+WWg{WXb5GWkO +zLDdqDg(Cusg2j|%2_+Cg$R&wn0D;NP5`+Q+lmKBeh^QDHp#nAmwYIqO2zKtjn9YrQ +zxbMg(%eJ}gUw-)HT{rM==u9A+!<$!yZY-y%x@eLM?OUwFQEvF2;3b4DIBy(**vY3hh#4xNdPjy3?R|6a{4d>P#P>kGbH5! +zjWslBh~**Hu26Y=Bi_Bcl|N{0T}8Zc!cJ5%-bEeoj?k_8tc|AEo$^zL$o(y@%yYAv)551Ttzw&W>-50-$$3NyjQ_?2kfn#_H +z6bX4yTc{gbN~Kumg$Sp5p_VCLgX$GU&{dGcmd$8a&LgI4cc9YRzj +z5eh<1RZqw$3MxI5C@|Vaoo0>?qzw~gQ#qB>g3x%TSU&JD%#xKvOc@-GQjx{BJ=Ou<90phMLC+YB_Wa= +zs2=7fN*B6jcFMMVi}MG&c*n7i@q(9rGn1R%#c12S-Fm}OtT7xY9(2{2d9d!4Ua;j* +zF*dh~wk-@TC!-y-ERRQQUm7tQkLkLG_q^ji&fc+yXMOz>^<%GlKYRAS)S97#yJVP( +z@CYn|aHn=1Wo~S>n?Z4^2QCre^nHhi=xU)eE6^Lw8N*K%7ZT&P@@2{sG=$$ +zB(IK#nMc6MQVKIChz6=$v+_F1|3uS8@8*$`Ag^ +zjORV>0u?$7f^cw`!ezCFML1Cd??j12cj^$H=SiN}>N|D)mfbf!&Scy9w($6;u>CPt +zvUf1lp_}fp)ob6v$$LJ?M#FSlXZ#n$Ww +zPIPa=NXQ1X?RGpisbyUt13>=MQ$P7?Aovfe;j_R8H*fgSCvJP_x3AVhvT9vqTn;9Z +zB}>Br8`Dz^w8$IZ{w}`lwJ+hmcmD=zoYtWiDj~}W6yOF%27)%5jfVqrn*?F4K{IUW +zH-caPlN9iEadiC%*S-0#_`qxb4@Qw}yt0#Kw4j`ZHs9>P7Zy;6aX>^xR3SI_ +zO=^GIPM&%baj2t~)8JDxLCQmNo+l6Y(+m&N47O7nqn+DH&T}%2OyT5|s2d-}+7aQK +zh%l#8eNC}+rKOE)Sm$1H8jyVlvI%IaS~g7v28KAL>Q_xUc_0758J`8NxZ;XIx%KeY +z*X@0JbI-Jsn#PL57D1^!8|pbcJI#6=XV4Yi_20jj-}=)ZVB7W)ZIjehH!~2f)3a)6 +z5ZtoSAroDQ%`SM-h1)qg>Dj$~z=7@C_@g)9%6ELtv$^nT=kc;<{SJ?L_zyA|I95@p +zH}{|vG3&M{wNlCykD!~bF7 +zp*bRgP{Bh)6kLJ4;wrctS0SQc;Z!Q7ko(M>N^&RE1BLJ&siNqRdLjynf|vaDP}_|4n>bFSQuh!dCM$c$mtVrlqc3N0`94js-5TAvDp@` +zz(Y(`nZ*=g>~&+4O_Hi)$%-UXvIy9WplO0rRhSG6^DsVn?C`eA(AO +zluv!+I6wRAJ9OpJWp?k@X^x*-+QxE45e)^=OH?9uQAk7oW^PiNcnJ|7->8K>_( +z%# +zXOTL#M=r0e++3{c4fnjw9<*{k^JP1mGBQR(!-P%Vd-z5@?!w)6eB+Q+3-Z7$s9@mL +zD2;)HfC#C$TL?y@dG5RWHh$?lpFkd6&ab@beZ2g&ud#mr`RqMdxpS~gRYU5kHjZ!E +z$-7T--#b6fv0FdPaBIq7+cM)F2id-Bue9TUg?CmxNS3UPLg_aM1u|h6NJz+#NkVem +zD`tj2a9u-^6E!Gl9JO-VfdKxP?n@S8C4Ro_BKF)^VvEvWG2S!J#QMF`#$j`qS(7cm6A# +z8rJ*W447jyoMSv#u+ico&3IXPIA?p#zMK%qg-f`*iJ;1H^_UV0P--z1MX44*2qamJ +zKnfz9PyiwnKoAiKAq5H{gd~$3sTU+eP9P-51eLH9l?*r{6d)&X3?!1F0D)OD{wETz +zoz`PN3G@JG0{JY`4fOL1v%!x}@+QfXl|kbDtJiSp=t3rKCe`3nKjYwVC&#v?YTL$$ +zzz11E*2}FH2%+j>og0%{Idpu3*L>w88J)e%FaGvN`RQwy7#(;TyXFrv7;j_$c(>-4 +z=1gAj&6BhS)ruiusw9F)6|V(w!VAchkVTzQN)?8M%xD^FKT$+Ak4nh#2o#QZU=%S? +z+!2A~pb&%w4g``AGQ||L1O$eV93(;@5bBL^!X(2ClgS_{3Bz~*(tQeka2m&dmIs^x +z#%G~NjW3L9txYI+1br$hz1#Vt^L0;u$oksrcfaxr?Fawl_c>?hZsv!g9T*ajgDF98 +z!OX?14?c9~N&WhlKiaN5Z<#yFfTw)lb)5I0uVUBAIpiGRiIAvOL~2;vc@Wf-*A5d1 +zsh8v?kiyMt5ef@;+zT~=a3@qL-s9B?fd~|Vdr^(vJPL|J$Z;wP$Gt-;xP-|O9j<~! +zMRFnni%yDSLJCzwRVYkRFmv3Ulni8G0ALsZ5qg6fy}s*C+{9iaD=oG@t8&A74?cF!8bA7+OYA`h +z=Se$v@QiQzpf0`Y8*JN-eKc|$gA_`_QsXF`)gw0{TV&V1i{Qk~2s0@NVT2RNBuAJzG!t5jh)UES)GbGg=y{CVLN&Hl`G6P +zS2;4-WUd`DXoYfO??UFT$rJ=1z56uZ^Q_Bw@`HAA@QO=0_Z5G|!K+@&;^KDNlz@uD +z2t;@g5rn(Cz#^Q|g_ibSYR&rXO7!MYQ8*!g;CjcS;&K1yh(h&>MkK{dpi8!M^PyG3pi1)SZE7&-D<8XeRp0zM7ux4t +zxs5$%@8_AXe+#G6LpgBCGc+)_Y=I$RG7yG{2rw+dK~73x)3w90xdRqapi~t9u-JA5 +zAqWXqRj8UGkb;OJskfjY8OU*WoGOp*{@5g;Y&nmx)M6=NU{WRAQB)HlLuRR9IA#^8 +zf*Dc}mH{#I*+I+gFGZ~0#y{B^#%Hlh$LDQKqAZAvQ*oTW_7tn^UKsGYCtkr%zUMmT +z2Z=TtOT)y}ox9dMzWA~|e8pq-vge##{PH{Q<`+JAmYTtSd(!j2lT&wmkb!4p9U+Kt +zxrZs7TAh#)UWlkTLHG=}U9`hxC~M%GP`6O@cy&Y&3OpS5LdbCy6op!Q!VLEi3JV4C +zzz8E%B!}n}R0}yoAQdDB6_60ZR0%m!!Q_OTP=yd+B!p~2@&0264_^G#JMa9&>OV50 +z5Dz#5GM~jt)gNqc7r&{efFP_&n0W+EBc65XIlSeTyRn?8CfcUq=vJ?tbJ@Q63Folw +zz!Eo}a=z*JPjl}1UuZjap3U0vyII+JDPB)w0U#pWkce1-!`+D^wj9tl^6!WCZ3q9>vN6}*6oi6G=4C**)YB9I{gLJG{l06<7)7KDV5 +zWHQ4L00~K0>WL88kVM_Pxq0e!9?k2AHhw~G79M1zhr}Qq!~GQhf6hpJ7F%QO_QQ4c +z8Ve)LL9GQ&WXY-#+38)(+?6eJYS!z-#-@GO7hKB1?xEVn9el}8T+0O)KA**vU9|0( +z>GUY4wvJ(=-7-KxG7}caKng^FB}kYdLOpVUvL$u2enP`?FKxMzT&^RR8>KizC{%1{ +z-fU;u>{b~ZpbpNVj?N;6dr)2k8$xa|%aRh3kV2sV3By2uK*GRK02oHHq(lgrGT}3F +z0%EAZQFI~=3`Pv{P_%rW<#>r@j?;Cw@gK{XKmaa(pbHzMC5~hR+GYb+81tH8F +zanGqu``%|>&eAAz{v`+PiLd)h&OYx;S=o6&jb(BUEsS<@>dud{J}Xw{_8<=wLV~0U +zav|Y}2;oX#EkXn$5Dik2s5qcDC$4Z-H0-;JeYv=drZo +zLXtHmgpn*|SX|u2(VO4K*2aW7e>Nqs2nZ!mGM5wKif}v}S;Cc(9EGSVDhfb^SA-)B +zw@mR$m=h65hRB4nVVO`E<^Yg{A&}!<&3#Jo84P02oz3}j1*4wpsQ7VH^p;e +z*sid!bQZgIoy+{<0k-eFjFq|b7%y&jx&FNNYhTDe>Wqf}0B*nM>R+21?)rvBOApR! +zXsEV4n4LjOxp?=yo_@s++rDQ`AHMmt{ly*IIOoELGa9sDAdK)p3K_DM +z)&YqSx>QrZRcYrZ$yHSLx>b^9swb$J1(b5W4X2ng-L;jfFPm}?n1@PhVtqr +ziI5W^03s?6Ocf=$2>=&_nJQ9{%?MRyUB{rw7Ct4Hj%@@F-`;cX0Ci2r`g+)%jJYd@ +zY|M;}%|o~sk<5mJv5GeO$%(a>!}JaO-#Wwi4`T0y58YMoIdTkw>dr&YU*U@`-OKmA +z<5O5I{Nf8A#Qt4NEbkcc;P3ntyS6`t0~b7+@n{}vkOig)syki^?oKV@$dQ}acFtp2 +zn%}O}A2N3*A`tigb+1T)*GjFG!Jr|$;OfC04@Xo`P^trt%&4`19Niy~6mAf>_h29@ +z<^@-venPDSRjLtd$R|8#*A`!~GD9WtJ4cNhZ8zi=FO|`7On8SAn82(@5qQj^>o%{c +z<%yjC|jkfFZ7m4m3YnmzYf&@fGaugv~v8doa +zrD-y`ajfrg0*{KTN+ApHiRj7Ah+3ye5DKI~QSm6`I^}@f$76PE@|5kavD{j9n$%)P +zPq%zv6W%{r;GSv>7Z1>;D$mK}38LT>as>BtBqw~fxw(1b%V6U#_$ND~;XjbM#Fur+ +z{zhu-iUSM0^X9|6=JT)Mzz$<^3H!Ofxq}lYcd&fUlc;l-X;!jr%@!E7H?gzdU~z7f +z-P<=fui0Y%(!CtqyUt!6xA_p3|IV1UY^B5`kJu;;$2P{?y*}o?&1`p_GLCFDtd>R_ +zs{@;`3u_mMFbblYEL%@oono2VxdthWuF|u{1?dpaI)8&_j;Cy#-r|28+r`@s@8S*X +zE9QAX(##}7X2=T6iCi}s3>s=pW>sJXfpW-F8ldP2iD|d~t}?st3xJOQZJilG?4olY +z_uvzU4}HAW4Sw}&AIsZ6dLz$%)LASnjM>nbFZ;3UdHm&1&~qO)w$E)pz|@Y*H)giJ +z2`49+)u}U`noiED9h#0gxjN5XEU~3IvQ@g$;jzVB3YIhM9|Zg7J9f_-yXIXx=ED}} +zdltvRXw)*76OB)3`a;u*u8uj8=VfGTnxKYjqp5cH-N9eqnz(v%%)PO!JqIqdsu7_; +z0YOhh1v%jb!~@?2!$C`(o+hQFAj~8@9HK{FP8FYi(`TYe +zrVY+GGsOZpxVV43aqNzG(C(FFRA`KxjJBG-^G~j|KfmFWa?@I4bEM%mjYs=!IKPLs +zU7&5oq-H=$1Hy*PeARY0My1e)fwhEn^u1{E992`I8Ywk2Kn8Mxp)$(CBaoJe5G7X) +zhePYTO_^j0qo#@4(pX85)nh=YQX)~dCd_TyX5}tn}MY?Af;AjLrZDIu_GVgip!rvl&N}!c~nVZp#V(Tuu`_9 +z2$NR?gja$9fh0Tx27T!nkLK}yLREl*P(@J?rid_w2vtRNHaEAENq>|iBP2;-icmzD +zSrBsJ)e_R?*5O)fdIi4k4e-v&|H>I>Mv$_$wCj>x$8G2Gh^}-Dvhk{)dk;sp5~LBy +z=16&l0`T~B@&jBa4+mX_-Jmu<;|RX_EXHGLveo`p+Fwih0`{z)jA=< +z0TBu$5vmXnIN`$Nbo~Ugq!h(mPDByHLcK|b5SR>%wY8fVjJ6XIh%iYY893nrRomJ) +zb~I;uP3>;^T>!X=f7BUg0y#Zdf9y}b_%W%gJ%c2Ejx_=EvXm{ShLlG}8WLtC +zxg|hRhzKH!grlgSq%H5P9qr@r(GRh_?V*H#1BDkO92bNF6GwzxPuRQvpeUT*lf +z7yfC^I5Wk1_NAB2zUfOY9o8uP?z`{i4?c7l%bCoOlv`4o!3uKapX-b>fjs9yPu=sXC)<(naKvBV +zbdsO?i%(9{V7TRBi%p&z4Vpy-t>rW#+5s&s%B{@I+QA&A0cH(SA|;c|41tjqVaA}a +ze&lMMchL)MIy(gcA;(ogLi7@inG&HMmxF+COb})gGDSr=5j}%;PF{vZ&d_-Nr@!;cKJchl{rr!8g#R^b+XIiz&9=7>=C+N^GKq|mDW!o$h$gp4 +z!Y~A2hRKkDs4@-cx|23Y!NSrNs=j6pSOlQ}0#OOIfME(y;1US~3E2ZNnW%kX*e=TZ +zP3!uNweZcCbe;YVuIu^NI^#?ruYbm)mmYuB_BFRTt;epPd*KiN;y3V5uy^<6Ke9G$ +zzcCND4+b(kHJBxcP-?PCfnf+?1}?C01eh|eEzGBkMi*d7V93M4WQvMpfWQpNKmq~~ +z5hi($M_97y)-aVfoj7&QOW}>qr_&i{0{Nq_d+HqvON;0BTGVrX=(iL9Jhrm(&?n8h +z_#ZahcHy9HE#x$oK_CJ%KmvonkxXC)qLx#vY`a`_)|t$Z0ucm`qCyBrSQa$Kfm}Ve)Z@1G+9`<>Om=`=XJCE +zN~OG`Y1^Edlrbd}G9?Q{jf(q;XoqKQs+JQ*Tw|v9XLaw|zJ2koefxH;ec%Icoc@2> +zyStF4;xG*0XJcy_V&7#((M=_JQ&?R~5TqiCY(~WfBn_g_Gf+!i>qNsq$Sh4A8I#0e +z2MMtP8;WGI)f6mZOY?JKMAppFbS{s3J3CreT}ybL9~}6@dwuTS=Xsgc1Yl>=Ho3X1 +zNOyTQwG|usS0kF7l$5sZNJ{o0aUn8sUrI*ef19NN=(lG7=(XffXZg7R&*@^V2mml( +zG^z)0<@kFoxzTR3ZXrEAU9&O*fPu=~Pj0K0URrevj|vTY#Q^|xRb1Now(4}mQ;|pR +z3hi7oYuqIc0O0wJ!jryw!_wewJ$Wk6gm_c_7Dny>IF3`kv$5sgIs?7Gn;#v#qYv1O +zSsNI+1K_Qu;~!ht??a6j$X9=!d^Y2-tI8C|$Qi&y54Yauu(&2#3u&Z@r4Nn9sP|z3 +z*1Q3xI;&g9o|MwW;}RNeDWXvaOK)sNml-(%@bdTLgC6^_DQC@?{~1S<9US@F%{1Ot +zLX&*yXRn=0WaI>pNgv>C8+UAvyQWgjWl}hFAT$$JQmpbu+MX3+={+%Gns6~W&)mSQ +zaezFCVz|eCFhQ--sgz45wffiQ`OsV}SR}20FUHyiR!$Q{g+)>-H6wd6t|YJNI~9N* +z&U_W59BH!1$s)hu^KiQk_~Nm2)tAc$|ZFdYb +Date: Sun, 14 Jun 2026 18:10:19 +0800 +Subject: [PATCH] Luminol Config System + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 9d0dc8815842a0583b212891680bd0c6461c30b9..2f5a73ac33fbb47cc025fe2d7ad16a5c34c06944 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -3033,6 +3033,17 @@ public final class CraftServer implements Server { + this.console.addPluginAllowingSleep(plugin.getName(), value); + } + ++ // Luminol start - Config API ++ @Override ++ public me.earthme.luminol.api.config.LuminolConfigsInstance getLuminolConfigs(String name) { ++ return me.earthme.luminol.config.ConfigManager.getConfigs(name); ++ } ++ ++ public me.earthme.luminol.api.config.LuminolConfigBuilder getLuminolConfigBuilder() { ++ return me.earthme.luminol.config.ConfigManager.getBuilder(); ++ } ++ // Luminol end - Config API ++ + // Folia start - region TPS API + /** + * Gets the TPS from the region which owns the specified location, or {@code null} if no region owns diff --git a/lophine-server/paper-patches/features/0003-Correct-thread-unsafe-random-sources.patch b/lophine-server/paper-patches/features/0003-Correct-thread-unsafe-random-sources.patch new file mode 100644 index 0000000..d655570 --- /dev/null +++ b/lophine-server/paper-patches/features/0003-Correct-thread-unsafe-random-sources.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Tue, 30 Jun 2026 22:34:04 +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..196301994341f7ff9eaf3b9401132df7d6eaaa8e 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; // Luminol - Correct thread unsafe random sources + + static { + instance = new CraftItemFactory(); diff --git a/lophine-server/paper-patches/features/0004-Force-disable-builtin-spark-plugin.patch b/lophine-server/paper-patches/features/0004-Force-disable-builtin-spark-plugin.patch new file mode 100644 index 0000000..baef4bd --- /dev/null +++ b/lophine-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: Fri, 17 Apr 2026 23:31:55 +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..4ecd9fe2f9ab4c7b456345665703256a6a0b37d0 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) { // Luminol - 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 2f5a73ac33fbb47cc025fe2d7ad16a5c34c06944..407191ad9a7f31cc3317a7747ec3489804807b8e 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 // Luminol - 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 // Luminol - Force disable builtin spark + } + + public boolean getCommandBlockOverride(String command) { +@@ -1049,7 +1049,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 // Luminol - Force disable builtin spark + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); + +@@ -1078,7 +1078,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 // Luminol - 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/lophine-server/paper-patches/features/0005-World-load-unload-APIs.patch b/lophine-server/paper-patches/features/0005-World-load-unload-APIs.patch new file mode 100644 index 0000000..5b0a83e --- /dev/null +++ b/lophine-server/paper-patches/features/0005-World-load-unload-APIs.patch @@ -0,0 +1,128 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Fri, 1 May 2026 18:37:51 +0800 +Subject: [PATCH] World load/unload APIs +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +好吧这里这一坨需要说明的太多了而且我也不是很会翻译所以就用中文注释了) + +这一整个实现使用了一种安全点的思路,相对于folia pr里和canvas的让每个region自己卸载, 我更偏向于将调度器拆分到每个level中然后实现类似于单个level停服的逻辑,这样会更加符合直觉而且实现要更加快速而且易于排查问题 + +由于世界卸载可能发生在玩家登录或者传送的过程中,所以我使用了引用计数器来避免这个竟态条件的发生 + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 407191ad9a7f31cc3317a7747ec3489804807b8e..606235c9af5afce55be8bbc963e8af7291a050c5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -276,7 +276,7 @@ public final class CraftServer implements Server { + private final StructureManager structureManager; + final DedicatedServer console; + private final DedicatedPlayerList playerList; +- private final Map worlds = new LinkedHashMap<>(); ++ private final Map worlds = new java.util.concurrent.ConcurrentHashMap<>(); // Luminol - Fix level hot loading/unloading apis + private YamlConfiguration configuration; + private YamlConfiguration commandsConfiguration; + private final Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); +@@ -1198,7 +1198,8 @@ public final class CraftServer implements Server { + + @Override + public World createWorld(WorldCreator creator) { +- if (true) throw new UnsupportedOperationException(); // Folia - not implemented properly yet ++ // if (true) throw new UnsupportedOperationException(); // Folia - not implemented properly yet // Luminol - Level hot load apis ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Call createWorld off global region!"); // Luminol - Level hot load apis + Preconditions.checkState(this.console.getAllLevels().iterator().hasNext(), "Cannot create additional worlds on STARTUP"); + //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot create a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. + Preconditions.checkArgument(creator != null, "WorldCreator cannot be null"); +@@ -1356,7 +1357,8 @@ public final class CraftServer implements Server { + + @Override + public boolean unloadWorld(World world, boolean save) { +- if (true) throw new UnsupportedOperationException(); // Folia - not implemented properly yet ++ // if (true) throw new UnsupportedOperationException(); // Folia - not implemented properly yet // Luminol - Level hot unload apis ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Call unloadWorld off global region!"); // Luminol - Level hot unload apis + //Preconditions.checkState(!this.console.isIteratingOverLevels, "Cannot unload a world while worlds are being ticked"); // Paper - Cat - Temp disable. We'll see how this goes. + if (world == null) { + return false; +@@ -1381,7 +1383,7 @@ public final class CraftServer implements Server { + return false; + } + +- try { ++ /*try { // Luminol - Level hot unload apis + if (save) { + handle.save(null, true, false); // Paper - Fix saving in unloadWorld + } +@@ -1393,10 +1395,70 @@ public final class CraftServer implements Server { + } + + this.worlds.remove(world.getName().toLowerCase(Locale.ROOT)); +- this.console.removeLevel(handle); ++ this.console.removeLevel(handle);*/ // Luminol - Level hot unload apis ++ // Luminol start - Level hot unload apis ++ handle.levelUnloader.getUnloadCallback().addWaiter((ret, ex) -> { ++ this.worlds.remove(world.getName().toLowerCase(Locale.ROOT)); ++ }); ++ // use the timeout of folia by default ++ handle.levelUnloader.applySettings(java.time.Duration.ofSeconds(60), save); ++ ++ if (!handle.levelUnloader.startUnloading()) { ++ return false; ++ } ++ ++ handle.levelUnloader.waitForComplete(); ++ // Luminol end + return true; + } + ++ // Luminol start - Async world unloading api ++ @Override ++ public void unloadWorld(World world, boolean save, java.util.concurrent.CompletableFuture callback) { ++ io.papermc.paper.threadedregions.RegionizedServer.ensureGlobalTickThread("Call unloadWorld off global region!"); // Luminol - Level hot unload apis ++ ++ if (world == null) { ++ callback.complete(false); ++ return; ++ } ++ ++ ServerLevel handle = ((CraftWorld) world).getHandle(); ++ ++ if (this.console.getLevel(handle.dimension()) == null) { ++ callback.complete(false); ++ return; ++ } ++ ++ if (handle.dimension() == net.minecraft.world.level.Level.OVERWORLD) { ++ callback.complete(false); ++ return; ++ } ++ ++ if (!handle.players().isEmpty()) { ++ callback.complete(false); ++ return; ++ } ++ ++ WorldUnloadEvent event = new WorldUnloadEvent(handle.getWorld()); ++ if (!event.callEvent()) { ++ callback.complete(false); ++ return; ++ } ++ ++ handle.levelUnloader.getUnloadCallback().addWaiter((_, _) -> { ++ this.worlds.remove(world.getName().toLowerCase(Locale.ROOT)); ++ ++ callback.complete(true); ++ }); ++ // use the timeout of folia by default ++ handle.levelUnloader.applySettings(java.time.Duration.ofSeconds(60), save); ++ ++ if (!handle.levelUnloader.startUnloading()) { ++ callback.complete(false); ++ } ++ } ++ // Luminol end ++ + @Override + public World getRespawnWorld() { + return this.console.findRespawnDimension().getWorld(); diff --git a/lophine-server/paper-patches/features/0006-Add-config-for-server-mod-name.patch b/lophine-server/paper-patches/features/0006-Add-config-for-server-mod-name.patch new file mode 100644 index 0000000..d105ab6 --- /dev/null +++ b/lophine-server/paper-patches/features/0006-Add-config-for-server-mod-name.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:09: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..6776af9928b3f24dd18bb3a19ad85629e42a1b5d 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); ++ me.earthme.luminol.config.modules.misc.ServerModNameConfig.fakeVanilla ? server.getServerVersion() : server.getServerModName() + ' ' + server.getServerVersion(), protocolVersion, icon);//Luminol - Fake vanilla mode + this.server = server; + } + diff --git a/lophine-server/paper-patches/features/0007-Add-config-to-disable-async-catchers.patch b/lophine-server/paper-patches/features/0007-Add-config-to-disable-async-catchers.patch new file mode 100644 index 0000000..c5f895e --- /dev/null +++ b/lophine-server/paper-patches/features/0007-Add-config-to-disable-async-catchers.patch @@ -0,0 +1,94 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 09:47:06 +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..8e5f470fd0b98d3894d7a5234386ff4cbe127f78 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() && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Luminol + 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) && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Luminol + 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) && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Luminol + 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) && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Luminol + 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) && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Luminol + 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) && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Luminol + 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) && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Luminol + 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) && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Luminol + 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..f2867e3758ebfc5ea1066b0d5d2fe6f2dfab8248 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() && !me.earthme.luminol.config.modules.experiment.DisableAsyncCatcherConfig.enabled) { // Paper - chunk system // Luminol + MinecraftServer.LOGGER.error("Thread {} failed main thread check: {}", Thread.currentThread().getName(), reason, new Throwable()); // Paper + throw new IllegalStateException("Asynchronous " + reason + "!"); + } diff --git a/lophine-server/paper-patches/features/0008-Add-config-to-avoid-of-folia-supported-check.patch b/lophine-server/paper-patches/features/0008-Add-config-to-avoid-of-folia-supported-check.patch new file mode 100644 index 0000000..ca6af77 --- /dev/null +++ b/lophine-server/paper-patches/features/0008-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..c20c04938b2f62b9e75831c5fdfabbd6ec51538a 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, 6 May 2026 18:27:15 +0800 +Subject: [PATCH] Add tpsbar with chunkhot membar and region 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 a3f2e97c1e0c9c2add3c5efd5a5cebe84ace78bf..d47c1b8cc17eb13669fbe21670c3a6eaaa995844 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/lophine-server/paper-patches/features/0010-Add-tick-regions-api.patch b/lophine-server/paper-patches/features/0010-Add-tick-regions-api.patch new file mode 100644 index 0000000..fff6082 --- /dev/null +++ b/lophine-server/paper-patches/features/0010-Add-tick-regions-api.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 10:49:40 +0800 +Subject: [PATCH] Add tick regions api + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 89d545a6d6396423a53d4c655fb7bae7a98b84fc..f5373786032b3f1db8e6c0c928c781998b2fe194 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2082,4 +2082,11 @@ public class CraftWorld extends CraftRegionAccessor implements World { + return POINTERS_SUPPLIER.view(this); + } + // Paper end ++ ++ // Luminol start - Tick regions api ++ @Override ++ public me.earthme.luminol.api.ThreadedRegionizer getThreadedRegionizer() { ++ return this.world.regioniser.threadedRegionizerAPI; ++ } ++ // Luminol end + } diff --git a/lophine-server/paper-patches/features/0011-Add-back-BlockPos-transform-to-Position-support.patch b/lophine-server/paper-patches/features/0011-Add-back-BlockPos-transform-to-Position-support.patch new file mode 100644 index 0000000..ba3ff0e --- /dev/null +++ b/lophine-server/paper-patches/features/0011-Add-back-BlockPos-transform-to-Position-support.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 18 Apr 2026 10:50:29 +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..006ed8037aad3c348f0a7e7122069b737f17d8e0 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); + } + ++ // Luminol start - add BlockPos support ++ public static Location toLocation(Level world, BlockPos pos) { ++ return CraftLocation.toBukkit(pos, world); ++ } ++ // Luminol end - add BlockPos support ++ + /** + * Converts a NMS World/Vector to Bukkit Location + * diff --git a/lophine-server/paper-patches/features/0012-Add-config-for-cpu-affinity.patch b/lophine-server/paper-patches/features/0012-Add-config-for-cpu-affinity.patch new file mode 100644 index 0000000..6134f0f --- /dev/null +++ b/lophine-server/paper-patches/features/0012-Add-config-for-cpu-affinity.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MrHua269 +Date: Sat, 20 Jun 2026 22:37:26 +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..dc69f5ae1f405bc385b7c6cec4e23faeeda182eb 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(me.earthme.luminol.config.modules.optimizations.CpuAffinityConfig.wrapForChunkSystemWorker(run), PlatformHooks.get().getBrand() + " Common Worker #" + this.idGenerator.getAndIncrement()); // Luminol - 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(me.earthme.luminol.config.modules.optimizations.CpuAffinityConfig.wrapForChunkSystemIo(run), PlatformHooks.get().getBrand() + " I/O Worker #" + this.idGenerator.getAndIncrement()); // Luminol - cpu affinity + + thread.setDaemon(true); + thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { diff --git a/lophine-server/paper-patches/features/0013-Leaf-Skip-event-if-no-listeners.patch b/lophine-server/paper-patches/features/0013-Leaf-Skip-event-if-no-listeners.patch new file mode 100644 index 0000000..b059b1b --- /dev/null +++ b/lophine-server/paper-patches/features/0013-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 2a298ec2c95e9cc913926324dbbf192caa38b05f..0db0598377283e36d59be336a2fcadf46cb4d365 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/lophine-server/paper-patches/features/0014-Leaf-Secure-seed-and-matter-seed-command.patch b/lophine-server/paper-patches/features/0014-Leaf-Secure-seed-and-matter-seed-command.patch new file mode 100644 index 0000000..1c95246 --- /dev/null +++ b/lophine-server/paper-patches/features/0014-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..fb89f6ef3adac92639d999f173383c3eeb8fabab 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 = me.earthme.luminol.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 606235c9af5afce55be8bbc963e8af7291a050c5..8cfa8a077ffc4b297f802a63f1d414ad004e45b7 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 = me.earthme.luminol.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/lophine-server/paper-patches/features/0015-Leaf-Configurable-vanilla-username-check.patch b/lophine-server/paper-patches/features/0015-Leaf-Configurable-vanilla-username-check.patch new file mode 100644 index 0000000..bffc2a8 --- /dev/null +++ b/lophine-server/paper-patches/features/0015-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/lophine-server/paper-patches/features/0016-Leaves-Leaves-plugin.patch b/lophine-server/paper-patches/features/0016-Leaves-Leaves-plugin.patch new file mode 100644 index 0000000..d958458 --- /dev/null +++ b/lophine-server/paper-patches/features/0016-Leaves-Leaves-plugin.patch @@ -0,0 +1,263 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Wed, 22 Apr 2026 12:45:32 +0800 +Subject: [PATCH] Leaves: Leaves plugin + +Co-authored by: MC_XiaoHei +As part of: Leaves (https://github.com/LeavesMC/Leaves/blob/ea91106ae57fc4cc14e2e0225009cf9919072f7f/leaves-server/paper-patches/features/0013-Leaves-Plugin.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +index 9c49bb7fb8c3ca4b62e73dcfca2669971defb181..b5fc7668d03cf314a8421e72e782da0cb7e6446a 100644 +--- a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +@@ -174,28 +174,41 @@ public class PaperPluginsCommand { + final CommandSender sender = context.getSource().getSender(); + final TreeMap> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + final TreeMap> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); ++ // Leaves start - leaves plugin ++ final TreeMap> leavesPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + for (final PluginProvider provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) { + final PluginMeta configuration = provider.getMeta(); + + if (provider instanceof SpigotPluginProvider) { + spigotPlugins.put(configuration.getDisplayName(), provider); ++ } else if (provider instanceof PaperPluginParent.LeavesServerPluginProvider) { ++ leavesPlugins.put(configuration.getDisplayName(), provider); + } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { + paperPlugins.put(configuration.getDisplayName(), provider); + } + } + ++ final int sizeLeavesPlugins = leavesPlugins.size(); + final int sizePaperPlugins = paperPlugins.size(); + final int sizeSpigotPlugins = spigotPlugins.size(); +- final int sizePlugins = sizePaperPlugins + sizeSpigotPlugins; +- final boolean hasAllPluginTypes = (sizePaperPlugins > 0 && sizeSpigotPlugins > 0); ++ final int sizePlugins = sizePaperPlugins + sizeSpigotPlugins + sizeLeavesPlugins; ++ final boolean showSize = (sizePaperPlugins > 0 && sizeSpigotPlugins > 0) || (sizePaperPlugins > 0 && sizeLeavesPlugins > 0) || (sizeSpigotPlugins > 0 && sizeLeavesPlugins > 0); + + final Component infoMessage = Component.text().append(INFO_ICON_SERVER_PLUGIN).append(Component.text("Server Plugins (%s):".formatted(sizePlugins), NamedTextColor.WHITE)).build(); + + sender.sendMessage(infoMessage); + ++ if (!leavesPlugins.isEmpty()) { ++ sender.sendMessage(header("Leaves Plugins", 0x37D1AB, sizeLeavesPlugins, showSize)); ++ } ++ ++ for (final Component component : formatProviders(leavesPlugins)) { ++ sender.sendMessage(component); ++ } ++ + if (!paperPlugins.isEmpty()) { +- sender.sendMessage(header("Paper Plugins", 0x0288D1, sizePaperPlugins, hasAllPluginTypes)); ++ sender.sendMessage(header("Paper Plugins", 0x0288D1, sizePaperPlugins, showSize)); + } + + for (final Component component : formatProviders(paperPlugins)) { +@@ -203,8 +216,9 @@ public class PaperPluginsCommand { + } + + if (!spigotPlugins.isEmpty()) { +- sender.sendMessage(header("Bukkit Plugins", 0xED8106, sizeSpigotPlugins, hasAllPluginTypes)); ++ sender.sendMessage(header("Bukkit Plugins", 0xED8106, sizeSpigotPlugins, showSize)); + } ++ // Leaves end - leaves plugin + + for (final Component component : formatProviders(spigotPlugins)) { + sender.sendMessage(component); +diff --git a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +index 5741a60faba70da485053599c9370f0e27e868f4..f3b7e21e4289812482cfbfa0bda6d9b75d7fa115 100644 +--- a/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java ++++ b/src/main/java/io/papermc/paper/plugin/PluginInitializerManager.java +@@ -112,19 +112,30 @@ public class PluginInitializerManager { + java.util.List dirs = ((java.util.List) optionSet.valuesOf("add-plugin-dir")).stream().map(File::toPath).toList(); + dirs.forEach(pluginDir -> io.papermc.paper.plugin.util.EntrypointUtil.registerProvidersFromSource(io.papermc.paper.plugin.provider.source.DirectoryProviderSource.INSTANCE_NO_CREATE, pluginDir)); + ++ // Leaves start - leaves plugin ++ final Set leavesPluginNames = new TreeSet<>(); + final Set paperPluginNames = new TreeSet<>(); + final Set legacyPluginNames = new TreeSet<>(); + LaunchEntryPointHandler.INSTANCE.getStorage().forEach((entrypoint, providerStorage) -> { + providerStorage.getRegisteredProviders().forEach(provider -> { + if (provider instanceof final SpigotPluginProvider legacy) { + legacyPluginNames.add(String.format("%s (%s)", legacy.getMeta().getName(), legacy.getMeta().getVersion())); ++ } else if (provider instanceof final PaperPluginParent.LeavesServerPluginProvider leaves) { ++ leavesPluginNames.add(String.format("%s (%s)", provider.getMeta().getName(), provider.getMeta().getVersion())); + } else if (provider instanceof final PaperPluginParent.PaperServerPluginProvider paper) { + paperPluginNames.add(String.format("%s (%s)", provider.getMeta().getName(), provider.getMeta().getVersion())); + } + }); + }); +- final int total = paperPluginNames.size() + legacyPluginNames.size(); ++ final int total = leavesPluginNames.size() + paperPluginNames.size() + legacyPluginNames.size(); + LOGGER.info("Initialized {} plugin{}", total, total == 1 ? "" : "s"); ++ if (!leavesPluginNames.isEmpty()) { ++ if (LOGGER.isDebugEnabled()) { ++ LOGGER.info("Leaves plugins ({}):\n - {}", leavesPluginNames.size(), String.join("\n - ", leavesPluginNames)); ++ } else { ++ LOGGER.info("Leaves plugins ({}):\n - {}", leavesPluginNames.size(), String.join(", ", leavesPluginNames)); ++ } ++ } // Leaves end - leaves plugin + if (!paperPluginNames.isEmpty()) { + if (LOGGER.isDebugEnabled()) { + LOGGER.info("Paper plugins ({}):\n - {}", paperPluginNames.size(), String.join("\n - ", paperPluginNames)); +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java +index 8cd649c977172f6b757d68565fcbb9eb8ae100a3..c18a94af99c827d73c31279e324e15ec8217a31d 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/LegacyPaperMeta.java +@@ -18,7 +18,7 @@ import java.util.List; + import java.util.Map; + import java.util.Set; + +-class LegacyPaperMeta { ++public class LegacyPaperMeta { + + + private static final TypeToken>> TYPE_TOKEN = new TypeToken<>() { +@@ -28,6 +28,12 @@ class LegacyPaperMeta { + ConfigurationTransformation.chain(notVersioned()).apply(node); + } + ++ // Leaves start - leaves plugin ++ public static void migrate(org.spongepowered.configurate.ConfigurationNode node) throws ConfigurateException { ++ ConfigurationTransformation.chain(notVersioned()).apply(node); ++ } ++ // Leaves end - leaves plugin ++ + private static ConfigurationTransformation notVersioned() { + return ConfigurationTransformation.builder() + .addAction(NodePath.path(), (path, value) -> { +diff --git a/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java +index fb7c6621e2805f4339c255f6c2e02c55ff4c502e..1832ee98b56b13a2e8dc6881589e1288f3774095 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/configuration/PaperPluginMeta.java +@@ -55,7 +55,7 @@ public class PaperPluginMeta implements PluginMeta { + @Required + private String version; + private String description; +- private List authors = List.of(); ++ protected List authors = List.of(); // Leaves - leaves plugin + private List contributors = List.of(); + private String website; + private String prefix; +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java +index e5a5fcaccfe85298f6fe4b47ba97aa633a8cb855..62c70d1fb20c3dbfad13a4027669d295ac9a39a8 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/PluginFileType.java +@@ -23,6 +23,7 @@ import java.util.jar.JarFile; + public abstract class PluginFileType { + + public static final String PAPER_PLUGIN_YML = "paper-plugin.yml"; ++ public static final String LEAVES_PLUGIN_JSON = "leaves-plugin.json"; // Leaves - leaves plugin + private static final List CONFIG_TYPES = new ArrayList<>(); + + public static final PluginFileType PAPER = new PluginFileType<>(PAPER_PLUGIN_YML, PaperPluginParent.FACTORY) { +@@ -43,8 +44,21 @@ public abstract class PluginFileType { + entrypointHandler.register(Entrypoint.PLUGIN, provider); + } + }; ++ // Leaves start - leaves plugin ++ public static final PluginFileType LEAVES = new PluginFileType<>(LEAVES_PLUGIN_JSON, PaperPluginParent.LEAVES_FACTORY) { ++ @Override ++ protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) { ++ PaperPluginParent.LeavesBootstrapProvider bootstrapPluginProvider = null; ++ if (parent.shouldCreateBootstrap()) { ++ bootstrapPluginProvider = parent.createLeavesBootstrapProvider(); ++ entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider); ++ } ++ entrypointHandler.register(Entrypoint.PLUGIN, parent.createLeavesPluginProvider(bootstrapPluginProvider)); ++ } ++ }; + +- private static final List> VALUES = List.of(PAPER, SPIGOT); ++ private static final List> VALUES = List.of(LEAVES, PAPER, SPIGOT); ++ // Leaves end - leaves plugin + + private final String config; + private final PluginTypeFactory factory; +diff --git a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java +index 55a6898e95704cddafda1ca5dc0951c7102fe10b..283f046c6f34c83f01e1ca1d80cbd7cc35a2b7d2 100644 +--- a/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java ++++ b/src/main/java/io/papermc/paper/plugin/provider/type/paper/PaperPluginParent.java +@@ -27,6 +27,7 @@ import java.util.jar.JarFile; + public class PaperPluginParent { + + public static final PluginTypeFactory FACTORY = new PaperPluginProviderFactory(); ++ public static final PluginTypeFactory LEAVES_FACTORY = new org.leavesmc.leaves.plugin.provider.LeavesPluginProviderFactory(); // Leaves - leaves plugin + private final Path path; + private final JarFile jarFile; + private final PaperPluginMeta description; +@@ -51,6 +52,32 @@ public class PaperPluginParent { + return new PaperBootstrapProvider(); + } + ++ // Leaves start - leaves plugin ++ public LeavesBootstrapProvider createLeavesBootstrapProvider() { ++ return new io.papermc.paper.plugin.provider.type.paper.PaperPluginParent.LeavesBootstrapProvider(); ++ } ++ public LeavesServerPluginProvider createLeavesPluginProvider(LeavesBootstrapProvider provider) { ++ return new LeavesServerPluginProvider(provider); ++ } ++ ++ public class LeavesBootstrapProvider extends PaperBootstrapProvider { ++ @Override ++ public org.leavesmc.leaves.plugin.provider.configuration.LeavesPluginMeta getMeta() { ++ return (org.leavesmc.leaves.plugin.provider.configuration.LeavesPluginMeta) super.getMeta(); ++ } ++ } ++ ++ public class LeavesServerPluginProvider extends PaperServerPluginProvider { ++ LeavesServerPluginProvider(PaperPluginParent.LeavesBootstrapProvider bootstrapProvider) { ++ super(bootstrapProvider); ++ } ++ @Override ++ public org.leavesmc.leaves.plugin.provider.configuration.LeavesPluginMeta getMeta() { ++ return (org.leavesmc.leaves.plugin.provider.configuration.LeavesPluginMeta) super.getMeta(); ++ } ++ } ++ // Leaves end - leaves plugin ++ + public PaperServerPluginProvider createPluginProvider(PaperBootstrapProvider provider) { + return new PaperServerPluginProvider(provider); + } +diff --git a/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java +index 26422904751647a061397ce978bba752149003cd..884ed63dab16acca83fe8f421762f2561400b1fb 100644 +--- a/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java ++++ b/src/main/java/io/papermc/paper/plugin/storage/SimpleProviderStorage.java +@@ -26,6 +26,16 @@ public abstract class SimpleProviderStorage implements ProviderStorage { + + @Override + public void register(PluginProvider provider) { ++ // Leaves start - leaves plugin ++ if (provider instanceof final io.papermc.paper.plugin.provider.type.paper.PaperPluginParent.LeavesServerPluginProvider leaves) { ++ org.leavesmc.leaves.plugin.provider.configuration.LeavesPluginMeta meta = leaves.getMeta(); ++ java.util.List unavailableFeatures = meta.getFeatures().getRequired().stream().filter(feature -> !org.leavesmc.leaves.plugin.ServerFeatureManager.INSTANCE.isFeatureAvailable(feature)).toList(); ++ if (!unavailableFeatures.isEmpty()) { ++ LOGGER.warn("The plugin {} ({}) requires some unavailable feature(s) {}. The plugin has been skipped.", meta.getName(), meta.getVersion(), unavailableFeatures); ++ return; ++ } ++ } ++ // Leaves end - leaves plugin + this.providers.add(provider); + } + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 8cfa8a077ffc4b297f802a63f1d414ad004e45b7..d61f38fdf84830c2e94dee8dd7c9a0400ec07318 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -3110,6 +3110,13 @@ public final class CraftServer implements Server { + } + // Luminol end - Config API + ++ // Leaves start - Feature API ++ @Override ++ public org.leavesmc.leaves.plugin.FeatureManager getFeatureManager() { ++ return org.leavesmc.leaves.plugin.ServerFeatureManager.INSTANCE; ++ } ++ // Leaves end - Feature API ++ + // Folia start - region TPS API + /** + * Gets the TPS from the region which owns the specified location, or {@code null} if no region owns diff --git a/lophine-server/paper-patches/features/0017-Purpur-Lobotomize-stuck-villagers.patch b/lophine-server/paper-patches/features/0017-Purpur-Lobotomize-stuck-villagers.patch new file mode 100644 index 0000000..5aab612 --- /dev/null +++ b/lophine-server/paper-patches/features/0017-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/lophine-server/paper-patches/features/0001-Rebrand-to-Lophine.patch b/lophine-server/paper-patches/features/0018-Rebrand-to-Lophine.patch similarity index 100% rename from lophine-server/paper-patches/features/0001-Rebrand-to-Lophine.patch rename to lophine-server/paper-patches/features/0018-Rebrand-to-Lophine.patch diff --git a/lophine-server/paper-patches/features/0002-Purpur-Barrels-and-enderchests-6-rows.patch b/lophine-server/paper-patches/features/0019-Purpur-Barrels-and-enderchests-6-rows.patch similarity index 100% rename from lophine-server/paper-patches/features/0002-Purpur-Barrels-and-enderchests-6-rows.patch rename to lophine-server/paper-patches/features/0019-Purpur-Barrels-and-enderchests-6-rows.patch diff --git a/lophine-server/paper-patches/features/0003-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch b/lophine-server/paper-patches/features/0020-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch similarity index 100% rename from lophine-server/paper-patches/features/0003-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch rename to lophine-server/paper-patches/features/0020-Add-cancel-task-by-task-id-in-FoliaGlobalRegionSched.patch diff --git a/lophine-server/paper-patches/features/0004-Leaves-Leaves-Base-Protocol-Core.patch b/lophine-server/paper-patches/features/0021-Leaves-Leaves-Base-Protocol-Core.patch similarity index 100% rename from lophine-server/paper-patches/features/0004-Leaves-Leaves-Base-Protocol-Core.patch rename to lophine-server/paper-patches/features/0021-Leaves-Leaves-Base-Protocol-Core.patch diff --git a/lophine-server/paper-patches/features/0005-LeavesHooks.patch b/lophine-server/paper-patches/features/0022-LeavesHooks.patch similarity index 100% rename from lophine-server/paper-patches/features/0005-LeavesHooks.patch rename to lophine-server/paper-patches/features/0022-LeavesHooks.patch diff --git a/lophine-server/paper-patches/features/0006-Leaves-Leaves-Fakeplayer.patch b/lophine-server/paper-patches/features/0023-Leaves-Leaves-Fakeplayer.patch similarity index 96% rename from lophine-server/paper-patches/features/0006-Leaves-Leaves-Fakeplayer.patch rename to lophine-server/paper-patches/features/0023-Leaves-Leaves-Fakeplayer.patch index b8bba42..44024b4 100644 --- a/lophine-server/paper-patches/features/0006-Leaves-Leaves-Fakeplayer.patch +++ b/lophine-server/paper-patches/features/0023-Leaves-Leaves-Fakeplayer.patch @@ -8,7 +8,7 @@ As a part of : Leaves (https://github.com/LeavesMC/Leaves) Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) 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 9180f23529a31b6b0a5b38bb7cda3e32d487f691..d0b763f07aee56f69a67c87d7dd15a44b6f44db7 100644 +index 0db0598377283e36d59be336a2fcadf46cb4d365..14fa5421107a571739f868eecb00d71dd3d29fef 100644 --- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java @@ -36,6 +36,15 @@ class PaperEventManager { @@ -96,7 +96,7 @@ index 14a8a00d9c1fc7d49d6f870bf10b9a5e178b1189..7fd08dac549261c50cf5723ef380ba90 /** * Gets the TPS from the region which owns the specified location, or {@code null} if no region owns diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 5fc4b44c05b34b64dfdfc1fe996124ec03490465..d90a194ac7ec1fd496fb305caf6752732bccfb45 100644 +index f5373786032b3f1db8e6c0c928c781998b2fe194..ca6be9faab3f93da7e91bf73997111b7d737afa9 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java @@ -247,7 +247,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { @@ -120,7 +120,7 @@ index 5fc4b44c05b34b64dfdfc1fe996124ec03490465..d90a194ac7ec1fd496fb305caf675273 HumanEntity bukkitEntity = human.getBukkitEntity(); if ((bukkitEntity != null) && (bukkitEntity instanceof Player)) { -@@ -1624,7 +1624,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { +@@ -1629,7 +1629,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { public void playSound(final net.kyori.adventure.sound.Sound sound) { org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper final long seed = sound.seed().orElseGet(this.world.getRandom()::nextLong); @@ -129,7 +129,7 @@ index 5fc4b44c05b34b64dfdfc1fe996124ec03490465..d90a194ac7ec1fd496fb305caf675273 player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player.getX(), player.getY(), player.getZ(), seed, null)); } } -@@ -1652,7 +1652,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { +@@ -1657,7 +1657,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { org.spigotmc.AsyncCatcher.catchOp("play sound"); // Paper final long seed = sound.seed().orElseGet(this.getHandle().getRandom()::nextLong); if (emitter == net.kyori.adventure.sound.Sound.Emitter.self()) { @@ -138,7 +138,7 @@ index 5fc4b44c05b34b64dfdfc1fe996124ec03490465..d90a194ac7ec1fd496fb305caf675273 player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player, seed, null)); } } else if (emitter instanceof CraftEntity craftEntity) { -@@ -1775,7 +1775,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { +@@ -1780,7 +1780,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { Preconditions.checkArgument(particle.getDataType().isInstance(data), "data (%s) should be %s", data.getClass(), particle.getDataType()); } this.getHandle().sendParticlesSource( diff --git a/lophine-server/paper-patches/features/0007-Leaves-Replay-Mod-API.patch b/lophine-server/paper-patches/features/0024-Leaves-Replay-Mod-API.patch similarity index 96% rename from lophine-server/paper-patches/features/0007-Leaves-Replay-Mod-API.patch rename to lophine-server/paper-patches/features/0024-Leaves-Replay-Mod-API.patch index 43ce47f..734d9f4 100644 --- a/lophine-server/paper-patches/features/0007-Leaves-Replay-Mod-API.patch +++ b/lophine-server/paper-patches/features/0024-Leaves-Replay-Mod-API.patch @@ -8,7 +8,7 @@ As a part of : Leaves (https://github.com/LeavesMC/Leaves) Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) 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 d0b763f07aee56f69a67c87d7dd15a44b6f44db7..bd301425f4c3cdf2b2bd22c1775b7ca9be1d4248 100644 +index 14fa5421107a571739f868eecb00d71dd3d29fef..0fce6dc8bffc341da2e7242422b4153b2743aa44 100644 --- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java @@ -57,7 +57,7 @@ class PaperEventManager { @@ -66,10 +66,10 @@ index 4722e027ed5869516adddf696ac030fd84df834d..4d20f07ff4bf782e938ac990e2b9aaeb // Special case complex part, since there is no extra entity type for them if (entity instanceof EnderDragonPart complexPart) { diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index e655c6b817b1ef2b3186c4b16a1039081139e97f..96d8ed40eae0765d5d48b1397de525581b8bf2fb 100644 +index d47c1b8cc17eb13669fbe21670c3a6eaaa995844..0f522b90f300431f1d5daa62c9a3fd6d260e1feb 100644 --- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -2085,7 +2085,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player, PluginMessa +@@ -2084,7 +2084,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player, PluginMessa @Override public boolean canSee(Player player) { diff --git a/lophine-server/src/main/java/abomination/IRegionFile.java b/lophine-server/src/main/java/abomination/IRegionFile.java new file mode 100644 index 0000000..52d72f0 --- /dev/null +++ b/lophine-server/src/main/java/abomination/IRegionFile.java @@ -0,0 +1,43 @@ +package abomination; + +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 IRegionFile 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/lophine-server/src/main/java/abomination/LinearRegionFile.java b/lophine-server/src/main/java/abomination/LinearRegionFile.java new file mode 100644 index 0000000..af5b1e2 --- /dev/null +++ b/lophine-server/src/main/java/abomination/LinearRegionFile.java @@ -0,0 +1,620 @@ +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 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 IRegionFile { + 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 LinearRegionFile.ChunkBuffer(pos))); + } + + @Override + public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) throws IOException { + final DataOutputStream out = this.getChunkDataOutputStream(pos); + + return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData( + data, ca.spottedleaf.moonrise.patches.chunk_system.io.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/lophine-server/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulableTickHack.java b/lophine-server/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulableTickHack.java new file mode 100644 index 0000000..add1d97 --- /dev/null +++ b/lophine-server/src/main/java/ca/spottedleaf/concurrentutil/scheduler/SchedulableTickHack.java @@ -0,0 +1,24 @@ +package ca.spottedleaf.concurrentutil.scheduler; + +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +public class SchedulableTickHack { + public static boolean setState(@NonNull SchedulableTick tick, Object state) { + return tick.setState(state); + } + + @Contract(pure = true) + public static T getState(@NonNull SchedulableTick tick) { + return (T) tick.state; + } + + @Contract(pure = true) + public static long getScheduledStart(@NonNull SchedulableTick tick) { + return tick.getScheduledStart(); + } + + public static void setScheduledStart(@NonNull SchedulableTick tick, long newValue) { + tick.setScheduledStart(newValue); + } +} diff --git a/lophine-server/src/main/java/com/kiocg/ChunkHot.java b/lophine-server/src/main/java/com/kiocg/ChunkHot.java new file mode 100644 index 0000000..387bdaf --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityLimits.java b/lophine-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityLimits.java new file mode 100644 index 0000000..cca144d --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityThrottler.java b/lophine-server/src/main/java/dev/kaiijumc/kaiiju/KaiijuEntityThrottler.java new file mode 100644 index 0000000..330c206 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java b/lophine-server/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java new file mode 100644 index 0000000..74080d1 --- /dev/null +++ b/lophine-server/src/main/java/gg/pufferfish/pufferfish/sentry/PufferfishSentryAppender.java @@ -0,0 +1,149 @@ +/* + * 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.sentry; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import io.sentry.Breadcrumb; +import io.sentry.Sentry; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.protocol.Message; +import io.sentry.protocol.User; +import me.earthme.luminol.config.modules.misc.SentryConfig; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Marker; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.Logger; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.filter.AbstractFilter; + +import java.util.Map; + +public class PufferfishSentryAppender extends AbstractAppender { + + private static final org.apache.logging.log4j.Logger logger = LogManager.getLogger(PufferfishSentryAppender.class.getSimpleName()); + private static final Gson GSON = new Gson(); + private final Level logLevel; + + public PufferfishSentryAppender(Level logLevel) { + super("PufferfishSentryAdapter", new SentryFilter(), null); + this.logLevel = logLevel; + } + + @Override + public void append(LogEvent logEvent) { + if (logEvent.getLevel().isMoreSpecificThan(logLevel) && (logEvent.getThrown() != null || !SentryConfig.onlyLogThrown)) { + try { + logException(logEvent); + } catch (Exception e) { + logger.warn("Failed to log event with sentry", e); + } + } else { + try { + logBreadcrumb(logEvent); + } catch (Exception e) { + logger.warn("Failed to log event with sentry", e); + } + } + } + + private void logException(LogEvent e) { + SentryEvent event = new SentryEvent(e.getThrown()); + + Message sentryMessage = new Message(); + sentryMessage.setMessage(e.getMessage().getFormattedMessage()); + + event.setThrowable(e.getThrown()); + event.setLevel(getLevel(e.getLevel())); + event.setLogger(e.getLoggerName()); + event.setTransaction(e.getLoggerName()); + event.setExtra("thread_name", e.getThreadName()); + + boolean hasContext = e.getContextData() != null; + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_playerid")) { + User user = new User(); + user.setId(e.getContextData().getValue("pufferfishsentry_playerid")); + user.setUsername(e.getContextData().getValue("pufferfishsentry_playername")); + event.setUser(user); + } + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_pluginname")) { + event.setExtra("plugin.name", e.getContextData().getValue("pufferfishsentry_pluginname")); + event.setExtra("plugin.version", e.getContextData().getValue("pufferfishsentry_pluginversion")); + event.setTransaction(e.getContextData().getValue("pufferfishsentry_pluginname")); + } + + if (hasContext && e.getContextData().containsKey("pufferfishsentry_eventdata")) { + Map eventFields = GSON.fromJson((String) e.getContextData().getValue("pufferfishsentry_eventdata"), new TypeToken>() { + }.getType()); + if (eventFields != null) { + event.setExtra("event", eventFields); + } + } + + Sentry.captureEvent(event); + } + + private void logBreadcrumb(LogEvent e) { + Breadcrumb breadcrumb = new Breadcrumb(); + + breadcrumb.setLevel(getLevel(e.getLevel())); + breadcrumb.setCategory(e.getLoggerName()); + breadcrumb.setType(e.getLoggerName()); + breadcrumb.setMessage(e.getMessage().getFormattedMessage()); + + Sentry.addBreadcrumb(breadcrumb); + } + + private SentryLevel getLevel(Level level) { + return switch (level.getStandardLevel()) { + case TRACE, DEBUG -> SentryLevel.DEBUG; + case WARN -> SentryLevel.WARNING; + case ERROR -> SentryLevel.ERROR; + case FATAL -> SentryLevel.FATAL; + default -> SentryLevel.INFO; + }; + } + + private static class SentryFilter extends AbstractFilter { + + @Override + public Result filter(Logger logger, Level level, Marker marker, String msg, + Object... params) { + return this.filter(logger.getName()); + } + + @Override + public Result filter(Logger logger, Level level, Marker marker, Object msg, Throwable t) { + return this.filter(logger.getName()); + } + + @Override + public Result filter(LogEvent event) { + return this.filter(event == null ? null : event.getLoggerName()); + } + + private Result filter(String loggerName) { + return loggerName != null && loggerName.startsWith("gg.castaway.pufferfish.sentry") ? Result.DENY + : Result.NEUTRAL; + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java b/lophine-server/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java new file mode 100644 index 0000000..a96be98 --- /dev/null +++ b/lophine-server/src/main/java/gg/pufferfish/pufferfish/sentry/SentryManager.java @@ -0,0 +1,61 @@ +/* + * 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.sentry; + +import io.sentry.Sentry; +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class SentryManager { + + private static final Logger logger = LogManager.getLogger(SentryManager.class); + + private SentryManager() { + + } + + private static boolean initialized = false; + + public static synchronized void init(Level logLevel) { + if (initialized) { + return; + } + if (logLevel == null) { + logger.error("Invalid log level, defaulting to WARN."); + logLevel = Level.WARN; + } + try { + initialized = true; + + Sentry.init(options -> { + options.setDsn(me.earthme.luminol.config.modules.misc.SentryConfig.sentryDsn); + options.setMaxBreadcrumbs(100); + }); + + PufferfishSentryAppender appender = new PufferfishSentryAppender(logLevel); + appender.start(); + ((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addAppender(appender); + logger.info("Sentry logging started!"); + } catch (Exception e) { + logger.warn("Failed to initialize sentry!", e); + initialized = false; + } + } + +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/api/impl/RegionStatsImpl.java b/lophine-server/src/main/java/me/earthme/luminol/api/impl/RegionStatsImpl.java new file mode 100644 index 0000000..410174f --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/api/impl/RegionStatsImpl.java @@ -0,0 +1,27 @@ +package me.earthme.luminol.api.impl; + +import io.papermc.paper.threadedregions.TickRegions; +import me.earthme.luminol.api.RegionStats; + +public class RegionStatsImpl implements RegionStats { + private final TickRegions.RegionStats internal; + + public RegionStatsImpl(TickRegions.RegionStats internal) { + this.internal = internal; + } + + @Override + public int getEntityCount() { + return this.internal.getEntityCount(); + } + + @Override + public int getPlayerCount() { + return this.internal.getPlayerCount(); + } + + @Override + public int getChunkCount() { + return this.internal.getChunkCount(); + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/api/impl/ThreadedRegionImpl.java b/lophine-server/src/main/java/me/earthme/luminol/api/impl/ThreadedRegionImpl.java new file mode 100644 index 0000000..ab7a394 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/api/impl/ThreadedRegionImpl.java @@ -0,0 +1,52 @@ +package me.earthme.luminol.api.impl; + +import io.papermc.paper.threadedregions.ThreadedRegionizer; +import io.papermc.paper.threadedregions.TickRegions; +import me.earthme.luminol.api.ThreadedRegion; +import me.earthme.luminol.api.TickRegionData; +import net.minecraft.world.level.ChunkPos; +import org.bukkit.Location; +import org.bukkit.World; + +import javax.annotation.Nullable; + +public class ThreadedRegionImpl implements ThreadedRegion { + private final ThreadedRegionizer.ThreadedRegion internal; + + public ThreadedRegionImpl(ThreadedRegionizer.ThreadedRegion internal) { + this.internal = internal; + } + + @Nullable + @Override + public Location getCenterChunkPos() { + final ChunkPos centerChunkPos = this.internal.getCenterChunk(); + + if (centerChunkPos == null) { + return null; + } + + return new Location(this.internal.regioniser.world.getWorld(), centerChunkPos.getMiddleBlockX(), 0, centerChunkPos.getMiddleBlockZ()); + } + + @Override + public double getDeadSectionPercent() { + return this.internal.getDeadSectionPercent(); + } + + @Override + public TickRegionData getTickRegionData() { + return this.internal.getData().tickRegionDataAPI; + } + + @Nullable + @Override + public World getWorld() { + return this.internal.regioniser.world.getWorld(); + } + + @Override + public long getId() { + return this.internal.id; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/api/impl/ThreadedRegionizerImpl.java b/lophine-server/src/main/java/me/earthme/luminol/api/impl/ThreadedRegionizerImpl.java new file mode 100644 index 0000000..8499915 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/api/impl/ThreadedRegionizerImpl.java @@ -0,0 +1,49 @@ +package me.earthme.luminol.api.impl; + +import io.papermc.paper.threadedregions.TickRegions; +import me.earthme.luminol.api.ThreadedRegion; +import me.earthme.luminol.api.ThreadedRegionizer; +import net.minecraft.server.level.ServerLevel; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ThreadedRegionizerImpl implements ThreadedRegionizer { + private final ServerLevel internal; + + public ThreadedRegionizerImpl(ServerLevel internal) { + this.internal = internal; + } + + @Override + public Collection getAllRegions() { + final List ret = new ArrayList<>(); + + this.internal.regioniser.computeForAllRegions(region -> ret.add(region.threadedRegionAPI)); + + return ret; + } + + @Override + public ThreadedRegion getAtSynchronized(int chunkX, int chunkZ) { + final io.papermc.paper.threadedregions.ThreadedRegionizer.ThreadedRegion got = this.internal.regioniser.getRegionAtSynchronised(chunkX, chunkZ); + + if (got == null) { + return null; + } + + return got.threadedRegionAPI; + } + + @Override + public ThreadedRegion getAtUnSynchronized(int chunkX, int chunkZ) { + final io.papermc.paper.threadedregions.ThreadedRegionizer.ThreadedRegion got = this.internal.regioniser.getRegionAtUnsynchronised(chunkX, chunkZ); + + if (got == null) { + return null; + } + + return got.threadedRegionAPI; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/api/impl/TickRegionDataImpl.java b/lophine-server/src/main/java/me/earthme/luminol/api/impl/TickRegionDataImpl.java new file mode 100644 index 0000000..d0cb80c --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/api/impl/TickRegionDataImpl.java @@ -0,0 +1,30 @@ +package me.earthme.luminol.api.impl; + +import io.papermc.paper.threadedregions.TickRegions; +import me.earthme.luminol.api.RegionStats; +import me.earthme.luminol.api.TickRegionData; +import org.bukkit.World; + +public class TickRegionDataImpl implements TickRegionData { + private final TickRegions.TickRegionData internal; + + public TickRegionDataImpl(TickRegions.TickRegionData internal) { + this.internal = internal; + } + + @Override + public World getWorld() { + return this.internal.world.getWorld(); + } + + @Override + public long getCurrentTickCount() { + return this.internal.getCurrentTick(); + } + + @Override + public RegionStats getRegionStats() { + return this.internal.getRegionStats().regionStatsAPI; + } + +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/commands/CommandRegister.java b/lophine-server/src/main/java/me/earthme/luminol/commands/CommandRegister.java new file mode 100644 index 0000000..a4821ce --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/CommandRegister.java @@ -0,0 +1,14 @@ +package me.earthme.luminol.commands; + +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/bar/BarCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/bar/BarCommand.java new file mode 100644 index 0000000..e4abff9 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/bar/BarCommand.java @@ -0,0 +1,52 @@ +package me.earthme.luminol.commands.bar; + +import me.earthme.luminol.commands.bar.sub.ToggleCommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/bar/BarSubcommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/bar/BarSubcommand.java new file mode 100644 index 0000000..ed53d25 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/bar/BarSubcommand.java @@ -0,0 +1,35 @@ +package me.earthme.luminol.commands.bar; + +import io.papermc.paper.command.brigadier.CommandSourceStack; +import me.earthme.luminol.commands.bar.sub.ConfigEditCommand; +import me.earthme.luminol.commands.bar.sub.ToggleCommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/bar/sub/ConfigEditCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/bar/sub/ConfigEditCommand.java new file mode 100644 index 0000000..fd390b0 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/bar/sub/ConfigEditCommand.java @@ -0,0 +1,74 @@ +package me.earthme.luminol.commands.bar.sub; + +import com.mojang.brigadier.arguments.BoolArgumentType; +import me.earthme.luminol.api.config.LuminolConfigsInstance; +import me.earthme.luminol.config.ConfigManager; +import me.earthme.luminol.config.modules.function.MembarConfig; +import me.earthme.luminol.config.modules.function.RegionBarConfig; +import me.earthme.luminol.config.modules.function.TpsBarConfig; +import me.earthme.luminol.enums.EnumBarType; +import me.earthme.luminol.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 { + LuminolConfigsInstance 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/lophine-server/src/main/java/me/earthme/luminol/commands/bar/sub/ToggleCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/bar/sub/ToggleCommand.java new file mode 100644 index 0000000..e7a4a69 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/bar/sub/ToggleCommand.java @@ -0,0 +1,127 @@ +package me.earthme.luminol.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 me.earthme.luminol.commands.bar.BarCommand; +import me.earthme.luminol.enums.EnumBarType; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/ConfigCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/ConfigCommand.java new file mode 100644 index 0000000..982cff4 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/ConfigCommand.java @@ -0,0 +1,37 @@ +package me.earthme.luminol.commands.config; + +import me.earthme.luminol.commands.config.sub.*; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/ConfigSubcommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/ConfigSubcommand.java new file mode 100644 index 0000000..c533d5f --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/ConfigSubcommand.java @@ -0,0 +1,24 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/CleanCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/CleanCommand.java new file mode 100644 index 0000000..9060407 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/CleanCommand.java @@ -0,0 +1,69 @@ +package me.earthme.luminol.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 me.earthme.luminol.commands.config.ConfigCommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/OpenGuiCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/OpenGuiCommand.java new file mode 100644 index 0000000..ac717ef --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/OpenGuiCommand.java @@ -0,0 +1,83 @@ +package me.earthme.luminol.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 me.earthme.luminol.commands.config.ConfigCommand; +import me.earthme.luminol.commands.config.ConfigSubcommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ReloadCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ReloadCommand.java new file mode 100644 index 0000000..ed73407 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ReloadCommand.java @@ -0,0 +1,25 @@ +package me.earthme.luminol.commands.config.sub; + +import com.mojang.brigadier.exceptions.CommandSyntaxException; +import me.earthme.luminol.commands.config.ConfigCommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ResetCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ResetCommand.java new file mode 100644 index 0000000..f41c373 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ResetCommand.java @@ -0,0 +1,59 @@ +package me.earthme.luminol.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import me.earthme.luminol.commands.config.ConfigCommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ResetCommentsCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ResetCommentsCommand.java new file mode 100644 index 0000000..c42cd6e --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/ResetCommentsCommand.java @@ -0,0 +1,68 @@ +package me.earthme.luminol.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 me.earthme.luminol.commands.config.ConfigCommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/SetCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/SetCommand.java new file mode 100644 index 0000000..a39420d --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/SetCommand.java @@ -0,0 +1,123 @@ +package me.earthme.luminol.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import com.mojang.brigadier.suggestion.Suggestions; +import com.mojang.brigadier.suggestion.SuggestionsBuilder; +import me.earthme.luminol.commands.config.ConfigCommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/SubmitCommand.java b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/SubmitCommand.java new file mode 100644 index 0000000..fa5390a --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/commands/config/sub/SubmitCommand.java @@ -0,0 +1,37 @@ +package me.earthme.luminol.commands.config.sub; + +import com.mojang.brigadier.arguments.StringArgumentType; +import me.earthme.luminol.commands.config.ConfigCommand; +import me.earthme.luminol.commands.config.ConfigSubcommand; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/ConfigManager.java b/lophine-server/src/main/java/me/earthme/luminol/config/ConfigManager.java new file mode 100644 index 0000000..2a2d6af --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/ConfigManager.java @@ -0,0 +1,155 @@ +package me.earthme.luminol.config; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import me.earthme.luminol.commands.CommandRegister; +import me.earthme.luminol.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("luminol", builder.of("luminol", "me.earthme.luminol.config.modules")); + registerConfig("lophine", builder.of("lophine", "fun.bm.lophine.config.modules")); // add lophine global config + // Lophine start - add carpet config + registerConfig("lophine_carpet", builder.of( + new java.io.File("lophine_config"), + "lophine_carpet", + "lophine_carpet_config.toml", + "lophinecarpetconfig", + "fun.bm.lophine.carpet.config.modules" + )); + // Lophine end - add carpet config + 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/lophine-server/src/main/java/me/earthme/luminol/config/ConfigsInstance.java b/lophine-server/src/main/java/me/earthme/luminol/config/ConfigsInstance.java new file mode 100644 index 0000000..f2cab04 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/ConfigsInstance.java @@ -0,0 +1,1065 @@ +package me.earthme.luminol.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 me.earthme.luminol.api.config.ConfigDataPair; +import me.earthme.luminol.api.config.LuminolConfigsInstance; +import me.earthme.luminol.commands.config.ConfigCommand; +import me.earthme.luminol.config.flags.*; +import me.earthme.luminol.enums.EnumConfigCategory; +import me.earthme.luminol.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 LuminolConfigsInstance { + 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/lophine-server/src/main/java/me/earthme/luminol/config/ConfigsInstanceBuilder.java b/lophine-server/src/main/java/me/earthme/luminol/config/ConfigsInstanceBuilder.java new file mode 100644 index 0000000..e85093f --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/ConfigsInstanceBuilder.java @@ -0,0 +1,82 @@ +package me.earthme.luminol.config; + +import me.earthme.luminol.api.config.LuminolConfigBuilder; +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +public class ConfigsInstanceBuilder implements LuminolConfigBuilder { + // 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/lophine-server/src/main/java/me/earthme/luminol/config/DefaultTransformLogic.java b/lophine-server/src/main/java/me/earthme/luminol/config/DefaultTransformLogic.java new file mode 100644 index 0000000..6d4a11e --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/DefaultTransformLogic.java @@ -0,0 +1,7 @@ +package me.earthme.luminol.config; + +public class DefaultTransformLogic { + public Object transform(Object obj) { + return obj; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/IConfigModule.java b/lophine-server/src/main/java/me/earthme/luminol/config/IConfigModule.java new file mode 100644 index 0000000..dc880ce --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/IConfigModule.java @@ -0,0 +1,27 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/IllegalFormatConversionExceptionWithOrigin.java b/lophine-server/src/main/java/me/earthme/luminol/config/IllegalFormatConversionExceptionWithOrigin.java new file mode 100644 index 0000000..60633a6 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/IllegalFormatConversionExceptionWithOrigin.java @@ -0,0 +1,24 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/flags/CommandSuggestions.java b/lophine-server/src/main/java/me/earthme/luminol/config/flags/CommandSuggestions.java new file mode 100644 index 0000000..c7ffcc5 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/flags/CommandSuggestions.java @@ -0,0 +1,9 @@ +package me.earthme.luminol.config.flags; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface CommandSuggestions { + String[] suggest(); +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/flags/ConfigClassInfo.java b/lophine-server/src/main/java/me/earthme/luminol/config/flags/ConfigClassInfo.java new file mode 100644 index 0000000..4f5d8fd --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/flags/ConfigClassInfo.java @@ -0,0 +1,17 @@ +package me.earthme.luminol.config.flags; + +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/flags/ConfigInfo.java b/lophine-server/src/main/java/me/earthme/luminol/config/flags/ConfigInfo.java new file mode 100644 index 0000000..efa569b --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/flags/ConfigInfo.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/flags/DoNotLoad.java b/lophine-server/src/main/java/me/earthme/luminol/config/flags/DoNotLoad.java new file mode 100644 index 0000000..aab8928 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/flags/DoNotLoad.java @@ -0,0 +1,8 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/flags/HotReloadUnsupported.java b/lophine-server/src/main/java/me/earthme/luminol/config/flags/HotReloadUnsupported.java new file mode 100644 index 0000000..ea56eae --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/flags/HotReloadUnsupported.java @@ -0,0 +1,8 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/flags/TransformedConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/flags/TransformedConfig.java new file mode 100644 index 0000000..76a7ee9 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/flags/TransformedConfig.java @@ -0,0 +1,28 @@ +package me.earthme.luminol.config.flags; + +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java new file mode 100644 index 0000000..30f78cb --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/CommandConfig.java @@ -0,0 +1,26 @@ +package me.earthme.luminol.config.modules.experiment; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.HotReloadUnsupported; +import me.earthme.luminol.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_waypoints_and_waypoint_command", comments = """ + Enable waypoint and waypoint command. + WARN: Still under testing + """) + @HotReloadUnsupported + public static boolean waypointsAndWaypointCommand = false; +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/DisableAsyncCatcherConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/DisableAsyncCatcherConfig.java new file mode 100644 index 0000000..7e5e671 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/DisableAsyncCatcherConfig.java @@ -0,0 +1,17 @@ +package me.earthme.luminol.config.modules.experiment; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/DisableEntityCatchConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/DisableEntityCatchConfig.java new file mode 100644 index 0000000..633c462 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/experiment/DisableEntityCatchConfig.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.config.modules.experiment; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/CollisionBehaviorConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/CollisionBehaviorConfig.java new file mode 100644 index 0000000..dcefbe2 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/CollisionBehaviorConfig.java @@ -0,0 +1,22 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.CommandSuggestions; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumCollisionBehaviorMode; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/FoliaEntityMovingFixConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/FoliaEntityMovingFixConfig.java new file mode 100644 index 0000000..46cce4e --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/FoliaEntityMovingFixConfig.java @@ -0,0 +1,21 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/ForceCleanupEntityBrainMemoryConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/ForceCleanupEntityBrainMemoryConfig.java new file mode 100644 index 0000000..4dad349 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/ForceCleanupEntityBrainMemoryConfig.java @@ -0,0 +1,18 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/ItemMultitaskConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/ItemMultitaskConfig.java new file mode 100644 index 0000000..9b296c9 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/ItemMultitaskConfig.java @@ -0,0 +1,14 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/LongCommandSupportConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/LongCommandSupportConfig.java new file mode 100644 index 0000000..78a99e5 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/LongCommandSupportConfig.java @@ -0,0 +1,14 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/POIRangeFixes.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/POIRangeFixes.java new file mode 100644 index 0000000..e40c1a3 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/POIRangeFixes.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/PathfindingFixesConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/PathfindingFixesConfig.java new file mode 100644 index 0000000..3ff1087 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/PathfindingFixesConfig.java @@ -0,0 +1,14 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/PreventIncorrectTeleportAsyncConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/PreventIncorrectTeleportAsyncConfig.java new file mode 100644 index 0000000..71f9849 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/PreventIncorrectTeleportAsyncConfig.java @@ -0,0 +1,18 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "prevent_incorrect_teleport_async_calls_during_move_event") +public class PreventIncorrectTeleportAsyncConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + When enabled, the server would reject some incorrect teleportAsync calls during move events. + And this will reduce the crashes which caused by plugins(Residence etc.) + But you should notice that it might break the compatibility with some plugins.""") + public static boolean enabled = false; + + @ConfigInfo(name = "throw_when_caught") + public static boolean throwWhenCaught = true; +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/UnsafeTeleportationConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/UnsafeTeleportationConfig.java new file mode 100644 index 0000000..f13d9c4 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/UnsafeTeleportationConfig.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "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/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/VanillaRandomSourceConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/VanillaRandomSourceConfig.java new file mode 100644 index 0000000..39f1488 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/fixes/VanillaRandomSourceConfig.java @@ -0,0 +1,12 @@ +package me.earthme.luminol.config.modules.fixes; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "use_vanilla_random_source") +public class VanillaRandomSourceConfig implements IConfigModule { + @ConfigInfo(name = "enable_for_player_entity", comments = "Related with RNG cracks") + public static boolean useLegacyRandomSourceForPlayers = false; +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/MembarConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/MembarConfig.java new file mode 100644 index 0000000..f61c928 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/MembarConfig.java @@ -0,0 +1,50 @@ +package me.earthme.luminol.config.modules.function; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; +import me.earthme.luminol.enums.EnumBarType; +import me.earthme.luminol.enums.EnumConfigCategory; +import me.earthme.luminol.enums.EnumStatusBarDisplay; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/PortalRateLimiterConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/PortalRateLimiterConfig.java new file mode 100644 index 0000000..ee7650d --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/PortalRateLimiterConfig.java @@ -0,0 +1,66 @@ +package me.earthme.luminol.config.modules.function; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.HotReloadUnsupported; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionBarConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionBarConfig.java new file mode 100644 index 0000000..98294a1 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionBarConfig.java @@ -0,0 +1,50 @@ +package me.earthme.luminol.config.modules.function; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; +import me.earthme.luminol.enums.EnumBarType; +import me.earthme.luminol.enums.EnumConfigCategory; +import me.earthme.luminol.enums.EnumStatusBarDisplay; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionFormatConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionFormatConfig.java new file mode 100644 index 0000000..9f5bbc4 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/RegionFormatConfig.java @@ -0,0 +1,81 @@ +package me.earthme.luminol.config.modules.function; + +import abomination.LinearRegionFile; +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.IllegalFormatConversionExceptionWithOrigin; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; +import me.earthme.luminol.config.flags.HotReloadUnsupported; +import me.earthme.luminol.enums.EnumConfigCategory; +import me.earthme.luminol.enums.EnumRegionFormat; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/SecureSeedConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/SecureSeedConfig.java new file mode 100644 index 0000000..57a28bd --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/SecureSeedConfig.java @@ -0,0 +1,41 @@ +package me.earthme.luminol.config.modules.function; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.HotReloadUnsupported; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/TpsBarConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/TpsBarConfig.java new file mode 100644 index 0000000..31b1b12 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/TpsBarConfig.java @@ -0,0 +1,58 @@ +package me.earthme.luminol.config.modules.function; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; +import me.earthme.luminol.enums.EnumBarType; +import me.earthme.luminol.enums.EnumConfigCategory; +import me.earthme.luminol.enums.EnumStatusBarDisplay; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/TripwireBehaviorConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/TripwireBehaviorConfig.java new file mode 100644 index 0000000..1895d5d --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/function/TripwireBehaviorConfig.java @@ -0,0 +1,21 @@ +package me.earthme.luminol.config.modules.function; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/AutoUpdateConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/AutoUpdateConfig.java new file mode 100644 index 0000000..9b5623c --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/AutoUpdateConfig.java @@ -0,0 +1,57 @@ +package me.earthme.luminol.config.modules.misc; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; +import me.earthme.luminol.enums.EnumConfigCategory; +import me.earthme.luminol.utils.AutoUpdateHelper; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +@ConfigClassInfo( + category = EnumConfigCategory.MISC, + name = "auto_update", + comments = """ + Checks GitHub Releases for newer version's jars on a schedule. + Downloads are staged under auto_update/lophine and written to auto_update/core.path, + which Hyacinthusclip can consume on the next restart. + If target_jar_path is set, server will also try to replace that launcher jar directly.""" +) +public class AutoUpdateConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = "Whether the server should check for updates automatically.") + public static boolean enabled = false; + + @ConfigInfo(name = "check_times", comments = "List of daily check times in HH:mm, based on the server's local time zone.") + public static List checkTimes = List.of("06:00"); + + @ConfigInfo(name = "allow_prerelease", comments = "Whether prerelease GitHub releases are allowed when selecting an update.") + public static boolean allowPrerelease = false; + + @ConfigInfo(name = "target_jar_path", comments = """ + Optional launcher jar path to replace after a successful download. + Leave this blank to keep the downloaded jar staged in auto_update/lophine + and let Hyacinthusclip switch to it through auto_update/core.path on restart.""") + public static String targetJarPath = ""; + + @DoNotLoad + public AutoUpdateHelper instance = null; + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set exs) { + if (enabled) { + if (instance == null) { + instance = new AutoUpdateHelper(); + } + instance.load(false); + } + } + + @Override + public void onUnloaded(CommentedFileConfig configInstance) { + if (instance != null) instance.shutdown(); + } +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/DisableWarningConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/DisableWarningConfig.java new file mode 100644 index 0000000..2272119 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/DisableWarningConfig.java @@ -0,0 +1,18 @@ +package me.earthme.luminol.config.modules.misc; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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; + @ConfigInfo(name = "disable_offline_mode_warning", comments = "Disable offline warns popped in the log when starting the server") + public static boolean disableOfflineModeWarning = false; + @ConfigInfo(name = "disable_moved_wrongly_threshold_warning", comments = "Disable wrongly move warns and checks") + public static boolean disableMovedWronglyThresholdWarning = false; +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/FoliaWatchogConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/FoliaWatchogConfig.java new file mode 100644 index 0000000..0306e7f --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/FoliaWatchogConfig.java @@ -0,0 +1,12 @@ +package me.earthme.luminol.config.modules.misc; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/InorderChatConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/InorderChatConfig.java new file mode 100644 index 0000000..0fb9a2a --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/InorderChatConfig.java @@ -0,0 +1,12 @@ +package me.earthme.luminol.config.modules.misc; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "mojang_out_of_order_chat_check") +public class InorderChatConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean enabled = true; +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/PaperPacketLimiterConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/PaperPacketLimiterConfig.java new file mode 100644 index 0000000..e80d178 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/PaperPacketLimiterConfig.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.config.modules.misc; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/PublickeyVerifyConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/PublickeyVerifyConfig.java new file mode 100644 index 0000000..74c1d07 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/PublickeyVerifyConfig.java @@ -0,0 +1,12 @@ +package me.earthme.luminol.config.modules.misc; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "verify_publickey_only_in_online_mode", comments = "Only verify the public key in online mode, could be useful when using plugins like MultiLogin with custom auth server configured") +public class PublickeyVerifyConfig implements IConfigModule { + @ConfigInfo(name = "enabled") + public static boolean enabled = false; +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/SavePortalTicketsConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/SavePortalTicketsConfig.java new file mode 100644 index 0000000..9037078 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/SavePortalTicketsConfig.java @@ -0,0 +1,14 @@ +package me.earthme.luminol.config.modules.misc; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/SentryConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/SentryConfig.java new file mode 100644 index 0000000..af44145 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/SentryConfig.java @@ -0,0 +1,45 @@ +package me.earthme.luminol.config.modules.misc; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import gg.pufferfish.pufferfish.sentry.SentryManager; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.CommandSuggestions; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; +import org.apache.logging.log4j.Level; +import org.jetbrains.annotations.Nullable; + +import java.util.Set; + +@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "sentry") +public class SentryConfig implements IConfigModule { + + @ConfigInfo(name = "dsn", comments = + " Sentry DSN for improved error logging, leave blank to disable,\n" + + " Obtain from https://sentry.io/") + public static String sentryDsn = ""; + + @CommandSuggestions(suggest = {"OFF", "FATAL", "ERROR", "WARN", "INFO", "DEBUG", "TRACE", "ALL"}) + @ConfigInfo(name = "log_level", comments = " Logs with a level higher than or equal to this level will be recorded.") + public static String logLevel = "WARN"; + + @ConfigInfo(name = "only_log_thrown", comments = " Only log with a Throwable will be recorded after enabling this.") + public static boolean onlyLogThrown = true; + + @Override + public void onLoaded(CommentedFileConfig configInstance, @Nullable Set exs) { + String sentryEnvironment = System.getenv("SENTRY_DSN"); + + sentryDsn = sentryEnvironment != null && !sentryEnvironment.isBlank() + ? sentryEnvironment + : configInstance.getOrElse("sentry.dsn", sentryDsn); + + logLevel = configInstance.getOrElse("sentry.log-level", logLevel); + onlyLogThrown = configInstance.getOrElse("sentry.only-log-thrown", onlyLogThrown); + + if (sentryDsn != null && !sentryDsn.isBlank()) { + SentryManager.init(Level.getLevel(logLevel)); + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java new file mode 100644 index 0000000..a4188ef --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/ServerModNameConfig.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.config.modules.misc; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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 = "Lophine"; + + @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/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/UsernameCheckConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/UsernameCheckConfig.java new file mode 100644 index 0000000..50001d7 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/misc/UsernameCheckConfig.java @@ -0,0 +1,63 @@ +package me.earthme.luminol.config.modules.misc; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.mojang.logging.LogUtils; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/AsyncProtocolChangeConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/AsyncProtocolChangeConfig.java new file mode 100644 index 0000000..16fd3c0 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/AsyncProtocolChangeConfig.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/CpuAffinityConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/CpuAffinityConfig.java new file mode 100644 index 0000000..6e8ddff --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/CpuAffinityConfig.java @@ -0,0 +1,122 @@ +package me.earthme.luminol.config.modules.optimizations; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.mojang.logging.LogUtils; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; +import me.earthme.luminol.config.flags.HotReloadUnsupported; +import me.earthme.luminol.enums.EnumConfigCategory; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/EntityGoalSelectorInactiveTickConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/EntityGoalSelectorInactiveTickConfig.java new file mode 100644 index 0000000..4e2207c --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/EntityGoalSelectorInactiveTickConfig.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/GaleVariableEntityWakeupConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/GaleVariableEntityWakeupConfig.java new file mode 100644 index 0000000..31a5097 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/GaleVariableEntityWakeupConfig.java @@ -0,0 +1,17 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/KaiijuEntityLimiterConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/KaiijuEntityLimiterConfig.java new file mode 100644 index 0000000..e5573a0 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/KaiijuEntityLimiterConfig.java @@ -0,0 +1,18 @@ +package me.earthme.luminol.config.modules.optimizations; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import dev.kaiijumc.kaiiju.KaiijuEntityLimits; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/LeavesSleepingBlockEntityConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/LeavesSleepingBlockEntityConfig.java new file mode 100644 index 0000000..1625487 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/LeavesSleepingBlockEntityConfig.java @@ -0,0 +1,17 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.HotReloadUnsupported; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/LobotomizeVillageConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/LobotomizeVillageConfig.java new file mode 100644 index 0000000..f0271c0 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/LobotomizeVillageConfig.java @@ -0,0 +1,16 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/PetalReduceSensorWorkConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/PetalReduceSensorWorkConfig.java new file mode 100644 index 0000000..85aa080 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/PetalReduceSensorWorkConfig.java @@ -0,0 +1,14 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/ProjectileChunkReduceConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/ProjectileChunkReduceConfig.java new file mode 100644 index 0000000..0719c8d --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/ProjectileChunkReduceConfig.java @@ -0,0 +1,14 @@ +package me.earthme.luminol.config.modules.optimizations; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/SIMDConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/SIMDConfig.java new file mode 100644 index 0000000..3ab6f04 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/optimizations/SIMDConfig.java @@ -0,0 +1,45 @@ +package me.earthme.luminol.config.modules.optimizations; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import com.mojang.logging.LogUtils; +import gg.pufferfish.pufferfish.simd.SIMDDetection; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.config.flags.DoNotLoad; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/removed/RemovedConfig.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/removed/RemovedConfig.java new file mode 100644 index 0000000..717572e --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/removed/RemovedConfig.java @@ -0,0 +1,14 @@ +package me.earthme.luminol.config.modules.removed; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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/lophine-server/src/main/java/me/earthme/luminol/config/modules/unsupported/DisableCheckForFoliaSupported.java b/lophine-server/src/main/java/me/earthme/luminol/config/modules/unsupported/DisableCheckForFoliaSupported.java new file mode 100644 index 0000000..fa3aae5 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/config/modules/unsupported/DisableCheckForFoliaSupported.java @@ -0,0 +1,19 @@ +package me.earthme.luminol.config.modules.unsupported; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.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; + + @ConfigInfo(name = "disable_for_leaves", comments = """ + Disable check for folia-supported for leaves plugin. + ATTENTION: No support will be provided if you enabled this.""") + public static boolean disableForLeaves = false; +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/data/BufferedLinearRegionFile.java b/lophine-server/src/main/java/me/earthme/luminol/data/BufferedLinearRegionFile.java new file mode 100644 index 0000000..516b393 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/data/BufferedLinearRegionFile.java @@ -0,0 +1,1442 @@ +package me.earthme.luminol.data; + +import abomination.IRegionFile; +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 me.earthme.luminol.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 IRegionFile { + 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/lophine-server/src/main/java/me/earthme/luminol/enums/EnumBarType.java b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumBarType.java new file mode 100644 index 0000000..5efe2db --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumBarType.java @@ -0,0 +1,92 @@ +package me.earthme.luminol.enums; + +import com.mojang.datafixers.util.Pair; +import me.earthme.luminol.functions.bars.TickableStatusBar; +import me.earthme.luminol.functions.bars.impl.Membar; +import me.earthme.luminol.functions.bars.impl.RegionBar; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/enums/EnumCollisionBehaviorMode.java b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumCollisionBehaviorMode.java new file mode 100644 index 0000000..07c3448 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumCollisionBehaviorMode.java @@ -0,0 +1,7 @@ +package me.earthme.luminol.enums; + +public enum EnumCollisionBehaviorMode { + VANILLA, + BLOCK_SHAPE_VANILLA, + PAPER +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/enums/EnumConfigCategory.java b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumConfigCategory.java new file mode 100644 index 0000000..5244b7b --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumConfigCategory.java @@ -0,0 +1,33 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/enums/EnumRegionFormat.java b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumRegionFormat.java new file mode 100644 index 0000000..174a196 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumRegionFormat.java @@ -0,0 +1,29 @@ +package me.earthme.luminol.enums; + +import abomination.LinearRegionFile; +import me.earthme.luminol.config.modules.function.RegionFormatConfig; +import me.earthme.luminol.data.BufferedLinearRegionFile; +import me.earthme.luminol.utils.IRegionCreateFunction; +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 IRegionCreateFunction creator; + + EnumRegionFormat(String argument, IRegionCreateFunction creator) { + this.argument = argument; + this.creator = creator; + } + + public IRegionCreateFunction getCreator() { + return this.creator; + } + + public String getArgument() { + return this.argument; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/enums/EnumStatusBarDisplay.java b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumStatusBarDisplay.java new file mode 100644 index 0000000..d951d02 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumStatusBarDisplay.java @@ -0,0 +1,21 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/enums/EnumTripwireBehavior.java b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumTripwireBehavior.java new file mode 100644 index 0000000..c988e40 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/enums/EnumTripwireBehavior.java @@ -0,0 +1,7 @@ +package me.earthme.luminol.enums; + +public enum EnumTripwireBehavior { + VANILLA20, + VANILLA21, + MIXED +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/functions/bars/TickableStatusBar.java b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/TickableStatusBar.java new file mode 100644 index 0000000..f77ad7c --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/TickableStatusBar.java @@ -0,0 +1,194 @@ +package me.earthme.luminol.functions.bars; + +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/functions/bars/TickableStatusBarList.java b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/TickableStatusBarList.java new file mode 100644 index 0000000..905c08b --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/TickableStatusBarList.java @@ -0,0 +1,171 @@ +package me.earthme.luminol.functions.bars; + +import com.mojang.logging.LogUtils; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/Membar.java b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/Membar.java new file mode 100644 index 0000000..e949d16 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/Membar.java @@ -0,0 +1,110 @@ +package me.earthme.luminol.functions.bars.impl; + +import me.earthme.luminol.config.modules.function.MembarConfig; +import me.earthme.luminol.enums.EnumStatusBarDisplay; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/RegionBar.java b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/RegionBar.java new file mode 100644 index 0000000..0e88f53 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/RegionBar.java @@ -0,0 +1,129 @@ +package me.earthme.luminol.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 me.earthme.luminol.config.modules.function.RegionBarConfig; +import me.earthme.luminol.enums.EnumStatusBarDisplay; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/TpsBar.java b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/TpsBar.java new file mode 100644 index 0000000..e8a9367 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/functions/bars/impl/TpsBar.java @@ -0,0 +1,167 @@ +package me.earthme.luminol.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 me.earthme.luminol.config.modules.function.TpsBarConfig; +import me.earthme.luminol.enums.EnumStatusBarDisplay; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/AffinityRunnableWrapper.java b/lophine-server/src/main/java/me/earthme/luminol/utils/AffinityRunnableWrapper.java new file mode 100644 index 0000000..2abfa01 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/AffinityRunnableWrapper.java @@ -0,0 +1,34 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/AutoUpdateHelper.java b/lophine-server/src/main/java/me/earthme/luminol/utils/AutoUpdateHelper.java new file mode 100644 index 0000000..d0d0cf5 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/AutoUpdateHelper.java @@ -0,0 +1,466 @@ +package me.earthme.luminol.utils; + +import com.google.gson.*; +import com.mojang.logging.LogUtils; +import io.papermc.paper.ServerBuildInfo; +import me.earthme.luminol.config.modules.misc.AutoUpdateConfig; +import net.minecraft.util.Util; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.time.Duration; +import java.time.LocalTime; +import java.util.Locale; +import java.util.Optional; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static java.nio.file.StandardOpenOption.*; + +public final class AutoUpdateHelper { + private final Logger LOGGER = LogUtils.getClassLogger(); + private final Gson GSON = new Gson(); + + private final String GITHUB_API_BASE = "https://api.github.com/repos/lophineMC/lophine"; + private final Path AUTO_UPDATE_DIR = Path.of("auto_update"); + private final Path CORE_PATH_FILE = AUTO_UPDATE_DIR.resolve("core.path"); + private final Path LUMINOL_UPDATE_DIR = AUTO_UPDATE_DIR.resolve("lophine"); + private final Path LATEST_PATH_FILE = LUMINOL_UPDATE_DIR.resolve("latest.path"); + private final long DAILY_TASK_PERIOD_MILLIS = TimeUnit.DAYS.toMillis(1); + + private final AtomicBoolean UPDATE_RUNNING = new AtomicBoolean(false); + + private volatile @Nullable ScheduledExecutorService scheduler; + + public synchronized void load(boolean reload) { + if (reload) shutdown(); + ensureWorkingDirectories(); + + scheduler = Executors.newSingleThreadScheduledExecutor(task -> { + final Thread thread = new Thread(task, "Lophine Auto Update Scheduler"); + thread.setDaemon(true); + return thread; + }); + + if (AutoUpdateConfig.checkTimes.isEmpty()) { + LOGGER.warn("Lophine auto-update is enabled, but misc.auto_update.check_times is empty."); + return; + } + + final LocalTime currentTime = LocalTime.now(); + + for (String time : AutoUpdateConfig.checkTimes) { + try { + final String[] parts = time.split(":"); + final LocalTime taskTime = LocalTime.of(Integer.parseInt(parts[0]), Integer.parseInt(parts[1])); + Duration taskDelay = Duration.between(currentTime, taskTime); + if (taskDelay.isNegative()) { + taskDelay = taskDelay.plusDays(1); + } + + scheduler.scheduleAtFixedRate( + this::checkForUpdatesSafely, + taskDelay.toMillis(), + DAILY_TASK_PERIOD_MILLIS, + TimeUnit.MILLISECONDS + ); + } catch (Exception e) { + LOGGER.warn("Illegal auto-update time ignored: {}", time); + } + } + } + + public synchronized void shutdown() { + if (scheduler != null) { + scheduler.shutdownNow(); + scheduler = null; + } + + UPDATE_RUNNING.set(false); + } + + private void checkForUpdatesSafely() { + if (!UPDATE_RUNNING.compareAndSet(false, true)) { + LOGGER.info("A Lophine auto-update check is already running, skipping this schedule."); + return; + } + + try { + checkForUpdates(); + } catch (Exception e) { + LOGGER.warn("Lophine auto-update failed.", e); + } finally { + UPDATE_RUNNING.set(false); + } + } + + private void checkForUpdates() { + final ServerBuildInfo buildInfo = ServerBuildInfo.buildInfo(); + final Optional gitCommit = buildInfo.gitCommit(); + + if (gitCommit.isEmpty()) { + LOGGER.info("Current build does not expose a git hash. Skipping auto-update."); + return; + } + + final String currentGitHash = gitCommit.get(); + final String mcVersion = buildInfo.minecraftVersionId(); + final String gitBranch = buildInfo.gitBranch().orElse(null); + + final ReleaseAssetInfo latestRelease = getLatestCompatibleRelease(mcVersion, gitBranch); + if (latestRelease == null) { + LOGGER.warn("No compatible Lophine release was found for Minecraft {}{}.", + mcVersion, + gitBranch == null ? "" : " on branch " + gitBranch + ); + return; + } + + final UpdateStatus updateStatus = compareReleaseWithCurrentBuild(latestRelease.releaseCommitHash(), currentGitHash); + if (updateStatus == UpdateStatus.UP_TO_DATE) { + LOGGER.info("You are already running the latest compatible Lophine release: {}", latestRelease.tagName()); + return; + } + + if (updateStatus == UpdateStatus.CURRENT_BUILD_AHEAD_OR_DIVERGED) { + LOGGER.info( + "Current build {} is not behind release {}, skipping auto-update to avoid downgrading.", + currentGitHash, + latestRelease.tagName() + ); + return; + } + + if (updateStatus == UpdateStatus.UNKNOWN) { + LOGGER.warn( + "Could not compare current build {} with release {}, skipping auto-update to avoid unsafe replacement.", + currentGitHash, + latestRelease.tagName() + ); + return; + } + + LOGGER.info("Found a newer Lophine release: {}", latestRelease.tagName()); + + final Path stagedJar = downloadAndStageRelease(latestRelease); + final Path finalJarPath = tryApplyTargetJar(stagedJar); + writeCorePath(finalJarPath); + writeLatestPath(finalJarPath); + + if (finalJarPath.equals(stagedJar)) { + LOGGER.info( + "Downloaded the latest Lophine jar to {} and refreshed auto_update/core.path for Hyacinthusclip. Please restart your server.", + finalJarPath.toAbsolutePath() + ); + } else { + LOGGER.info( + "Updated target jar at {} using release {} and refreshed auto_update/core.path. Please restart your server.", + finalJarPath.toAbsolutePath(), + latestRelease.tagName() + ); + } + } + + private void ensureWorkingDirectories() { + try { + Files.createDirectories(LUMINOL_UPDATE_DIR); + } catch (IOException e) { + throw new RuntimeException("Failed to create Lophine auto update directory", e); + } + } + + private @Nullable ReleaseAssetInfo getLatestCompatibleRelease(String mcVersion, @Nullable String gitBranch) { + final JsonArray releases = requestJsonArray(GITHUB_API_BASE + "/releases?per_page=100"); + if (releases == null) { + return null; + } + + ReleaseAssetInfo fallbackMatch = null; + for (JsonElement element : releases) { + if (!element.isJsonObject()) { + continue; + } + + final ReleaseAssetInfo releaseInfo = parseReleaseInfo(element.getAsJsonObject()); + if (releaseInfo == null) { + continue; + } + + if (!releaseInfo.tagName().startsWith(mcVersion + "-")) { + continue; + } + + if (releaseInfo.prerelease() && !AutoUpdateConfig.allowPrerelease) { + continue; + } + + if (gitBranch != null && gitBranch.equals(releaseInfo.targetCommitish())) { + return releaseInfo; + } + + if (fallbackMatch == null) { + fallbackMatch = releaseInfo; + } + } + + return fallbackMatch; + } + + private @Nullable ReleaseAssetInfo parseReleaseInfo(JsonObject releaseObject) { + if (releaseObject.get("draft").getAsBoolean()) { + return null; + } + + final String tagName = releaseObject.get("tag_name").getAsString(); + final int hashSeparatorIndex = tagName.lastIndexOf('-'); + if (hashSeparatorIndex < 0 || hashSeparatorIndex == tagName.length() - 1) { + return null; + } + + final JsonArray assets = releaseObject.getAsJsonArray("assets"); + if (assets == null || assets.isEmpty()) { + return null; + } + + for (JsonElement assetElement : assets) { + if (!assetElement.isJsonObject()) { + continue; + } + + final JsonObject assetObject = assetElement.getAsJsonObject(); + final String assetName = assetObject.get("name").getAsString(); + if (!assetName.endsWith(".jar")) { + continue; + } + + final String digest = assetObject.has("digest") && !assetObject.get("digest").isJsonNull() + ? assetObject.get("digest").getAsString() + : ""; + final String sha256 = normalizeSha256Digest(digest); + if (sha256 == null) { + LOGGER.warn("Skipping release {} because asset {} does not expose a SHA-256 digest.", tagName, assetName); + continue; + } + + return new ReleaseAssetInfo( + tagName, + tagName.substring(hashSeparatorIndex + 1), + releaseObject.get("target_commitish").getAsString(), + releaseObject.get("prerelease").getAsBoolean(), + assetName, + sha256, + assetObject.get("browser_download_url").getAsString() + ); + } + + return null; + } + + private @Nullable String normalizeSha256Digest(String digest) { + if (digest == null || digest.isBlank()) { + return null; + } + + final String normalized = digest.toLowerCase(Locale.ROOT); + if (normalized.startsWith("sha256:")) { + return normalized.substring("sha256:".length()); + } + + return normalized; + } + + private UpdateStatus compareReleaseWithCurrentBuild(String releaseGitHash, String currentGitHash) { + if (releaseGitHash.equalsIgnoreCase(currentGitHash)) { + return UpdateStatus.UP_TO_DATE; + } + + final JsonObject compareObject = requestJsonObject( + GITHUB_API_BASE + "/compare/" + releaseGitHash + "..." + currentGitHash + ); + if (compareObject == null || !compareObject.has("status")) { + return UpdateStatus.UNKNOWN; + } + + return switch (compareObject.get("status").getAsString().toLowerCase(Locale.ROOT)) { + case "identical" -> UpdateStatus.UP_TO_DATE; + case "behind" -> UpdateStatus.NEEDS_UPDATE; + case "ahead", "diverged" -> UpdateStatus.CURRENT_BUILD_AHEAD_OR_DIVERGED; + default -> UpdateStatus.UNKNOWN; + }; + } + + private @NotNull Path downloadAndStageRelease(ReleaseAssetInfo releaseInfo) { + final Path tempPath = LUMINOL_UPDATE_DIR.resolve(releaseInfo.assetName() + ".cache"); + final Path stagedPath = LUMINOL_UPDATE_DIR.resolve(releaseInfo.assetName()); + final Path backupPath = LUMINOL_UPDATE_DIR.resolve(releaseInfo.assetName() + ".old"); + + try { + Files.deleteIfExists(tempPath); + + try ( + ReadableByteChannel source = Channels.newChannel(openConnection(releaseInfo.downloadUrl()).getInputStream()); + FileChannel fileChannel = FileChannel.open(tempPath, CREATE, WRITE, TRUNCATE_EXISTING) + ) { + fileChannel.transferFrom(source, 0, Long.MAX_VALUE); + } + + if (!isFileValid(tempPath, releaseInfo.sha256())) { + Files.deleteIfExists(tempPath); + throw new IllegalStateException("SHA-256 check failed for " + releaseInfo.assetName()); + } + + Util.safeReplaceFile(stagedPath, tempPath, backupPath); + return stagedPath; + } catch (IOException | URISyntaxException e) { + throw new RuntimeException("Failed to download release " + releaseInfo.tagName(), e); + } + } + + private @NotNull Path tryApplyTargetJar(Path stagedJar) { + if (AutoUpdateConfig.targetJarPath.isBlank()) { + return stagedJar; + } + + final Path targetJar = Path.of(AutoUpdateConfig.targetJarPath).toAbsolutePath().normalize(); + final Path targetParent = targetJar.getParent(); + final Path tempTarget = LUMINOL_UPDATE_DIR.resolve(targetJar.getFileName().toString() + ".apply.cache"); + final Path backupTarget = targetJar.resolveSibling(targetJar.getFileName().toString() + ".old"); + + try { + if (targetParent != null) { + Files.createDirectories(targetParent); + } + + Files.deleteIfExists(tempTarget); + Files.copy(stagedJar, tempTarget); + + if (!Util.safeReplaceOrMoveFile(targetJar, tempTarget, backupTarget, false)) { + Files.deleteIfExists(tempTarget); + LOGGER.warn("Failed to replace configured target jar {}. Keeping the staged update instead.", targetJar); + return stagedJar; + } + + return targetJar; + } catch (IOException e) { + throw new RuntimeException("Failed to apply update to target jar " + targetJar, e); + } + } + + private void writeCorePath(Path path) { + try (BufferedWriter writer = Files.newBufferedWriter(CORE_PATH_FILE, StandardCharsets.UTF_8, CREATE, WRITE, TRUNCATE_EXISTING)) { + writer.write(path.toAbsolutePath().normalize().toString()); + } catch (IOException e) { + LOGGER.warn("Failed to write auto-update core path file: {}", CORE_PATH_FILE, e); + } + } + + private void writeLatestPath(Path path) { + try (BufferedWriter writer = Files.newBufferedWriter(LATEST_PATH_FILE, StandardCharsets.UTF_8, CREATE, WRITE, TRUNCATE_EXISTING)) { + writer.write(path.toAbsolutePath().normalize().toString()); + } catch (IOException e) { + LOGGER.warn("Failed to write latest update path file: {}", LATEST_PATH_FILE, e); + } + } + + private boolean isFileValid(Path file, String expectedHash) { + try (FileInputStream inputStream = new FileInputStream(file.toFile())) { + final byte[] buffer = new byte[8192]; + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + + for (int numRead; (numRead = inputStream.read(buffer)) > 0; ) { + digest.update(buffer, 0, numRead); + } + + return toHexString(digest.digest()).equalsIgnoreCase(expectedHash); + } catch (Exception e) { + LOGGER.warn("Failed to validate file {}", file, e); + return false; + } + } + + private @NotNull String toHexString(byte @NotNull [] bytes) { + final StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + builder.append(String.format("%02x", b)); + } + return builder.toString(); + } + + private @Nullable JsonArray requestJsonArray(String url) { + try { + final JsonElement element = requestJsonElement(url); + return element != null && element.isJsonArray() ? element.getAsJsonArray() : null; + } catch (Exception e) { + LOGGER.warn("Failed to request JSON array from {}", url, e); + return null; + } + } + + private @Nullable JsonObject requestJsonObject(String url) { + try { + final JsonElement element = requestJsonElement(url); + return element != null && element.isJsonObject() ? element.getAsJsonObject() : null; + } catch (Exception e) { + LOGGER.warn("Failed to request JSON object from {}", url, e); + return null; + } + } + + private @Nullable JsonElement requestJsonElement(String url) throws IOException, URISyntaxException { + final HttpURLConnection connection = openConnection(url); + + final int responseCode = connection.getResponseCode(); + if (responseCode >= 400) { + LOGGER.warn("GitHub API request failed with status {} for {}", responseCode, url); + return null; + } + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) { + return GSON.fromJson(reader, JsonElement.class); + } catch (JsonSyntaxException e) { + LOGGER.warn("Failed to parse GitHub API response from {}", url, e); + return null; + } + } + + private @NotNull HttpURLConnection openConnection(String url) throws IOException, URISyntaxException { + final HttpURLConnection connection = (HttpURLConnection) new URI(url).toURL().openConnection(); + connection.setRequestProperty("Accept", "application/vnd.github+json"); + connection.setRequestProperty("User-Agent", "Lophine Auto Update"); + connection.setConnectTimeout(10000); + connection.setReadTimeout(10000); + return connection; + } + + private enum UpdateStatus { + UP_TO_DATE, + NEEDS_UPDATE, + CURRENT_BUILD_AHEAD_OR_DIVERGED, + UNKNOWN + } + + private record ReleaseAssetInfo( + String tagName, + String releaseCommitHash, + String targetCommitish, + boolean prerelease, + String assetName, + String sha256, + String downloadUrl + ) { + } +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/BufferedLinearRegionFileFlusher.java b/lophine-server/src/main/java/me/earthme/luminol/utils/BufferedLinearRegionFileFlusher.java new file mode 100644 index 0000000..081fe9f --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/BufferedLinearRegionFileFlusher.java @@ -0,0 +1,133 @@ +package me.earthme.luminol.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 me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/ClassLoadUtil.java b/lophine-server/src/main/java/me/earthme/luminol/utils/ClassLoadUtil.java new file mode 100644 index 0000000..9e5b4a2 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/ClassLoadUtil.java @@ -0,0 +1,97 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/EntityMoveOutOfRegionException.java b/lophine-server/src/main/java/me/earthme/luminol/utils/EntityMoveOutOfRegionException.java new file mode 100644 index 0000000..9c73276 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/EntityMoveOutOfRegionException.java @@ -0,0 +1,30 @@ +package me.earthme.luminol.utils; + +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/lophine-server/src/main/java/me/earthme/luminol/utils/FoliaServerWaypointManager.java b/lophine-server/src/main/java/me/earthme/luminol/utils/FoliaServerWaypointManager.java new file mode 100644 index 0000000..0192668 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/FoliaServerWaypointManager.java @@ -0,0 +1,235 @@ +package me.earthme.luminol.utils; + +import ca.spottedleaf.moonrise.common.util.TickThread; +import com.google.common.collect.Sets; +import me.earthme.luminol.config.modules.experiment.CommandConfig; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.waypoints.ServerWaypointManager; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.gamerules.GameRules; +import net.minecraft.world.waypoints.WaypointTransmitter; +import org.jetbrains.annotations.NotNull; + +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class FoliaServerWaypointManager extends ServerWaypointManager { + private final Set waypoints = ConcurrentHashMap.newKeySet(); + private final Set trackingPlayers = ConcurrentHashMap.newKeySet(); + private final Map> connections = new ConcurrentHashMap<>(); + + public FoliaServerWaypointManager(ServerLevel serverLevel) { + super(serverLevel); + } + + @Override + public void breakAllConnections() { + Iterator>> connectionTablesEntryIterator = this.connections.entrySet().iterator(); + while (connectionTablesEntryIterator.hasNext()) { + final Map table = connectionTablesEntryIterator.next().getValue(); + connectionTablesEntryIterator.remove(); + + for (WaypointTransmitter.Connection connection : table.values()) { + connection.disconnect(); + } + } + } + + @Override + public void remakeConnections(WaypointTransmitter waypoint) { + throw new UnsupportedOperationException("Unused"); + } + + @Override + public Set transmitters() { + return this.waypoints; + } + + @Override + public void trackWaypoint(WaypointTransmitter waypoint) { + if (!CommandConfig.waypointsAndWaypointCommand) { + return; + } + + // Leaves start - fakeplayer + if (waypoint instanceof org.leavesmc.leaves.bot.ServerBot bot && !bot.getConfigValue(org.leavesmc.leaves.bot.agent.Configs.ENABLE_LOCATOR_BAR)) { + return; + } + // Leaves end - fakeplayer + + this.waypoints.add(waypoint); + + for (ServerPlayer toCreateFor : this.trackingPlayers) { + this.createConnection(toCreateFor, waypoint); + } + } + + @Override + public void updateWaypoint(WaypointTransmitter waypoint) { + if (!CommandConfig.waypointsAndWaypointCommand) { + return; + } + + if (this.waypoints.contains(waypoint)) { + for (ServerPlayer player : this.trackingPlayers) { + final Map connections = this.connections.get(player); + + if (connections != null) { + final WaypointTransmitter.Connection connection = connections.get(waypoint); + + if (connection != null) { + this.updateConnection(player, waypoint, connection); + } else { + this.createConnection(player, waypoint); + } + } else { + this.createConnection(player, waypoint); + } + } + } + } + + @Override + public void untrackWaypoint(WaypointTransmitter waypoint) { + if (!CommandConfig.waypointsAndWaypointCommand) { + return; + } + + for (Map.Entry> connectionMapEntry : this.connections.entrySet()) { + final Map connectionsOfCurr = connectionMapEntry.getValue(); + final ServerPlayer ownerOfCurr = connectionMapEntry.getKey(); + + final WaypointTransmitter.Connection removed = connectionsOfCurr.remove(waypoint); + if (removed != null) { + scheduleIfOffTarget(ownerOfCurr, removed::disconnect); + } + } + + this.waypoints.remove(waypoint); + } + + @Override + public void addPlayer(ServerPlayer player) { + this.trackingPlayers.add(player); + + scheduleIfOffTarget(player, () -> { + // Leaves start - fakeplayer + if (player instanceof org.leavesmc.leaves.bot.ServerBot) { + return; + } + // Leaves end - fakeplayer + for (WaypointTransmitter transmitter : this.waypoints) { + this.createConnection(player, transmitter); + } + + if (player.isTransmittingWaypoint()) { + this.trackWaypoint(player); + } + }); + } + + @Override + public void updatePlayer(ServerPlayer player) { + Map conneections = this.connections.get(player); + + if (conneections == null) { + return; + } + + Sets.SetView newTransmitters = Sets.difference(this.waypoints, conneections.keySet()); + + for (Map.Entry entry : conneections.entrySet()) { + this.updateConnection(player, entry.getKey(), entry.getValue()); + } + + for (WaypointTransmitter waypointTransmitter : newTransmitters) { + this.createConnection(player, waypointTransmitter); + } + } + + @Override + public void removePlayer(ServerPlayer player) { + this.trackingPlayers.remove(player); + + scheduleIfOffTarget(player, () -> { + final Map removedConnections = this.connections.remove(player); + + if (removedConnections != null) { + for (WaypointTransmitter.Connection connection : removedConnections.values()) { + connection.disconnect(); + } + } + + this.untrackWaypoint(player); + }); + } + + private static boolean isLocatorBarEnabledFor(@NotNull ServerPlayer player) { + return player.level().getGameRules().get(GameRules.LOCATOR_BAR); + } + + private static void scheduleIfOffTarget(Entity entity, Runnable action) { + if (!TickThread.isTickThreadFor(entity)) { + entity.getBukkitEntity().taskScheduler.schedule(_ -> action.run(), _ -> action.run(), 1L); + return; + } + + action.run(); + } + + private void createConnection(ServerPlayer player, WaypointTransmitter waypoint) { + if (player != waypoint) { + if (isLocatorBarEnabledFor(player)) { + // -> waypoint + scheduleIfOffTarget(player, () -> waypoint.makeWaypointConnectionWith(player).ifPresentOrElse(newConnection -> { + final Map connectionsOfThisPlayer = this.connections.computeIfAbsent(player, _ -> new ConcurrentHashMap<>()); + + connectionsOfThisPlayer.put(waypoint, newConnection); + + newConnection.connect(); + }, () -> { + final Map connectionsOfThisPlayer = this.connections.get(player); + + if (connectionsOfThisPlayer != null) { + WaypointTransmitter.Connection removedConnection = connectionsOfThisPlayer.remove(waypoint); + if (removedConnection != null) { + removedConnection.disconnect(); + } + } + })); + } + } + } + + private void updateConnection(ServerPlayer player, WaypointTransmitter waypoint, WaypointTransmitter.Connection connection) { + if (player != waypoint) { + if (isLocatorBarEnabledFor(player)) { + scheduleIfOffTarget(player, () -> { + if (!connection.isBroken()) { + connection.update(); + } else { + waypoint.makeWaypointConnectionWith(player).ifPresentOrElse(newConnection -> { + final Map connectionsOfThisPlayer = this.connections.computeIfAbsent(player, _ -> new ConcurrentHashMap<>()); + + connectionsOfThisPlayer.put(waypoint, newConnection); + + + newConnection.connect(); + }, () -> { + final Map connectionsOfThisPlayer = this.connections.get(player); + + if (connectionsOfThisPlayer != null) { + connectionsOfThisPlayer.remove(waypoint); + } + + connection.disconnect(); + }); + } + }); + } + } + } +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/IRegionCreateFunction.java b/lophine-server/src/main/java/me/earthme/luminol/utils/IRegionCreateFunction.java new file mode 100644 index 0000000..e041632 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/IRegionCreateFunction.java @@ -0,0 +1,10 @@ +package me.earthme.luminol.utils; + +import abomination.IRegionFile; + +import java.io.IOException; + +@FunctionalInterface +public interface IRegionCreateFunction { + IRegionFile create(RegionCreatorInfo info) throws IOException; +} \ No newline at end of file diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/NullPlugin.java b/lophine-server/src/main/java/me/earthme/luminol/utils/NullPlugin.java new file mode 100644 index 0000000..dbe5e87 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/NullPlugin.java @@ -0,0 +1,152 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/RateThrottler.java b/lophine-server/src/main/java/me/earthme/luminol/utils/RateThrottler.java new file mode 100644 index 0000000..7c47df0 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/RateThrottler.java @@ -0,0 +1,87 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/RegionCreatorInfo.java b/lophine-server/src/main/java/me/earthme/luminol/utils/RegionCreatorInfo.java new file mode 100644 index 0000000..aafb3b9 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/RegionCreatorInfo.java @@ -0,0 +1,8 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/TeleportRecord.java b/lophine-server/src/main/java/me/earthme/luminol/utils/TeleportRecord.java new file mode 100644 index 0000000..3267a3d --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/TeleportRecord.java @@ -0,0 +1,15 @@ +package me.earthme.luminol.utils; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Contract; +import org.jspecify.annotations.NonNull; + +import java.lang.ref.WeakReference; + +public record TeleportRecord(WeakReference lastLevel, Vec3 lastPos, float yaw, float pitch) { + @Contract("_, _, _, _ -> new") + public static @NonNull TeleportRecord create(ServerLevel lastLevel, Vec3 lastPos, float yaw, float pitch) { + return new TeleportRecord(new WeakReference<>(lastLevel), lastPos, yaw, pitch); + } +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/ConfigCommandDialog.java b/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/ConfigCommandDialog.java new file mode 100644 index 0000000..77f108a --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/ConfigCommandDialog.java @@ -0,0 +1,117 @@ +package me.earthme.luminol.utils.dialog; + +import com.google.common.reflect.TypeToken; +import com.google.gson.Gson; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/ConfigDialogUtil.java b/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/ConfigDialogUtil.java new file mode 100644 index 0000000..ece053e --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/ConfigDialogUtil.java @@ -0,0 +1,55 @@ +package me.earthme.luminol.utils.dialog; + +import com.mojang.datafixers.util.Pair; +import me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/DialogUtil.java b/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/DialogUtil.java new file mode 100644 index 0000000..e8e278b --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/dialog/DialogUtil.java @@ -0,0 +1,472 @@ +package me.earthme.luminol.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/lophine-server/src/main/java/me/earthme/luminol/utils/thread/LevelAwareRegionScheduler.java b/lophine-server/src/main/java/me/earthme/luminol/utils/thread/LevelAwareRegionScheduler.java new file mode 100644 index 0000000..70cabc7 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/thread/LevelAwareRegionScheduler.java @@ -0,0 +1,222 @@ +package me.earthme.luminol.utils.thread; + +import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue; +import ca.spottedleaf.concurrentutil.scheduler.SchedulableTick; +import ca.spottedleaf.concurrentutil.scheduler.SchedulableTickHack; +import ca.spottedleaf.concurrentutil.scheduler.Scheduler; + +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BooleanSupplier; + +public class LevelAwareRegionScheduler extends Scheduler { + private final Scheduler parent; + private final AtomicBoolean halted = new AtomicBoolean(false); + + private final AtomicInteger activeCount = new AtomicInteger(0); + private final MultiThreadedQueue haltedCallbacks = new MultiThreadedQueue<>(); + private final Set managedTasks = new CopyOnWriteArraySet<>(); + + private final AtomicBoolean fullyExited = new AtomicBoolean(false); + + public LevelAwareRegionScheduler(Scheduler parent) { + this.parent = parent; + } + + private void allTaskExitedCallback() { + try { + Runnable callback; + while ((callback = this.haltedCallbacks.pollOrBlockAdds()) != null) { + callback.run(); + } + } finally { + this.fullyExited.set(true); + } + } + + public boolean isFullyExited() { + return this.fullyExited.get(); + } + + public boolean addHaltCallback(Runnable task) { + return this.haltedCallbacks.offer(task); + } + + public boolean isHaltSignalled() { + return this.halted.get(); + } + + public boolean isAllHalted() { + return this.haltedCallbacks.isAddBlocked(); + } + + // the parent is already shut down and no tasks will be executed, so just directly retire all callbacks + public void onParentHalted() { + this.halted.set(true); + + this.allTaskExitedCallback(); + } + + private final class WrappedTask extends SchedulableTick { + private final SchedulableTick handle; + private final AtomicBoolean thisHalted = new AtomicBoolean(false); + + private WrappedTask(SchedulableTick handle) { + this.handle = handle; + + // resync deadline + SchedulableTickHack.setScheduledStart(this, SchedulableTickHack.getScheduledStart(this.handle)); + } + + public void register() { + LevelAwareRegionScheduler.this.activeCount.getAndIncrement(); + LevelAwareRegionScheduler.this.managedTasks.add(this); + } + + private void handleParentHalted() { + if (this.thisHalted.compareAndSet(false, true)) { + final int remaining = LevelAwareRegionScheduler.this.activeCount.decrementAndGet(); + + LevelAwareRegionScheduler.this.managedTasks.remove(this); + + if (remaining == 0) { + LevelAwareRegionScheduler.this.allTaskExitedCallback(); + } + } + } + + @Override + public boolean runTick() { + if (LevelAwareRegionScheduler.this.halted.get()) { + this.handleParentHalted(); + + return false; + } + + final boolean notCancelled = this.handle.runTick(); + + if (!notCancelled && this.thisHalted.compareAndSet(false, true)) { + LevelAwareRegionScheduler.this.activeCount.decrementAndGet(); + LevelAwareRegionScheduler.this.managedTasks.remove(this); + } + + // resync deadline + SchedulableTickHack.setScheduledStart(this, SchedulableTickHack.getScheduledStart(this.handle)); + return notCancelled; + } + + @Override + public boolean hasTasks() { + return this.handle.hasTasks() || LevelAwareRegionScheduler.this.halted.get(); + } + + @Override + public boolean runTasks(BooleanSupplier canContinue) { + if (LevelAwareRegionScheduler.this.halted.get()) { + this.handleParentHalted(); + + return false; + } + + final boolean notCancelled = this.handle.runTasks(canContinue); + + if (!notCancelled && this.thisHalted.compareAndSet(false, true)) { + LevelAwareRegionScheduler.this.activeCount.decrementAndGet(); + LevelAwareRegionScheduler.this.managedTasks.remove(this); + } + + return notCancelled; + } + } + + @Override + public Thread[] getAliveThreads() { + return this.parent.getAliveThreads(); + } + + @Override + public Thread[] getCoreThreads() { + return this.parent.getCoreThreads(); + } + + @Override + public void halt() { + if (!this.halted.compareAndSet(false, true)) { + return; + } + + // the parent schedule has its ability to handle notify when task is already canceled + // so just do it directly + for (WrappedTask task : this.managedTasks) { + this.parent.notifyTasks(task); + } + + if (this.activeCount.get() == 0) { + this.allTaskExitedCallback(); + } + } + + @Override + public boolean join(long msToWait) { + return false; // TODO + } + + @Override + public boolean joinInterruptable(long msToWait) { + return false; // TODO + } + + @Override + public void schedule(SchedulableTick tick) { + if (this.halted.get()) { + return; + } + + final WrappedTask toSchedule = new WrappedTask(tick); + + if (!SchedulableTickHack.setState(tick, toSchedule)) { + throw new IllegalStateException("Already scheduled!"); + } + + toSchedule.register(); + + this.parent.schedule(toSchedule); + } + + @Override + public void notifyTasks(SchedulableTick tick) { + if (this.halted.get()) { + return; + } + + final WrappedTask wrapped = SchedulableTickHack.getState(tick); + + if (wrapped != null) { + this.parent.notifyTasks(wrapped); + } + } + + @Override + public boolean cancel(SchedulableTick tick) { + final WrappedTask wrapped = SchedulableTickHack.getState(tick); + + if (wrapped != null) { + final boolean cancelled = this.parent.cancel(wrapped); + + if (cancelled && wrapped.thisHalted.compareAndSet(false, true)) { + final int remaining = this.activeCount.decrementAndGet(); + this.managedTasks.remove(wrapped); + + if (remaining == 0 && this.halted.get()) { + this.allTaskExitedCallback(); + } + } + + return cancelled; + } + + return false; + } +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/thread/LevelSubRegionShutdownThread.java b/lophine-server/src/main/java/me/earthme/luminol/utils/thread/LevelSubRegionShutdownThread.java new file mode 100644 index 0000000..45219b8 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/thread/LevelSubRegionShutdownThread.java @@ -0,0 +1,258 @@ +package me.earthme.luminol.utils.thread; + +import ca.spottedleaf.concurrentutil.completable.CallbackCompletable; +import ca.spottedleaf.moonrise.common.util.WorldUtil; +import io.papermc.paper.threadedregions.*; +import me.earthme.luminol.utils.TeleportRecord; +import net.minecraft.network.DisconnectionDetails; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.phys.Vec3; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.jetbrains.annotations.NotNull; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class LevelSubRegionShutdownThread extends RegionShutdownThread { + private final ServerLevel toUnload; + private final CallbackCompletable unloadCallback = new CallbackCompletable<>(); + + @NotNull + private Duration schedulerHaltTimeout = Duration.ZERO; + private boolean doSaving = true; + private boolean started = false; + + public LevelSubRegionShutdownThread(String name, ServerLevel toUnload) { + super(name); + this.toUnload = toUnload; + } + + public void applySettings( + Duration schedulerHaltTimeout, + boolean doSaving + ) { + Objects.requireNonNull(schedulerHaltTimeout); + + RegionizedServer.ensureGlobalTickThread("Apply world shutdown settings off global region!"); + + this.schedulerHaltTimeout = schedulerHaltTimeout; + this.doSaving = doSaving; + } + + public CallbackCompletable getUnloadCallback() { + return this.unloadCallback; + } + + public boolean startUnloading() { + if (this.started) { + return false; + } + + this.started = true; + this.start(); + return true; + } + + @Override + public void run() { + LOGGER.info("Beginning unload task of level {}.", this.toUnload.dimension()); + + this.toUnload.levelUnloadStateLock.blockReadingReferencing(); // stop accepting any new reading reference prely + + final boolean shutdownWriteLockHeld = this.toUnload.levelUnloadStateLock.acquireWrite(); + if (!shutdownWriteLockHeld) { + // already unreachable + LOGGER.info("Level {} is already unloaded during global shutdown! Quitting.", this.toUnload.dimension()); + return; + } + + final CompletableFuture haltCallbackTask = new CompletableFuture<>(); + + if (!this.toUnload.levelScheduler.addHaltCallback(() -> haltCallbackTask.complete(null))) { + haltCallbackTask.complete(null); + } + + this.toUnload.levelScheduler.halt(); + + // wait full scheduler exit + try { + if (this.schedulerHaltTimeout != Duration.ZERO) { + try { + haltCallbackTask.get(this.schedulerHaltTimeout.toNanos(), TimeUnit.NANOSECONDS); + } catch (InterruptedException e) { + LOGGER.error("Shutdown thread was interrupted by another threads! An incorrect nms call?", e); + } catch (TimeoutException e) { + LOGGER.warn("Timed out for waiting scheduler exit of level {}! Forcing unloading.", this.toUnload.dimension()); + } catch (ExecutionException e) { + LOGGER.error("Exception caught while waiting scheduler exit!", e); + } + } else { + haltCallbackTask.join(); + } + } catch (Throwable ex) { + LOGGER.error("Exception caught while await sub scheduler termination of level !", ex); + } + + LOGGER.info("Halted sub scheduler of level {}.", this.toUnload.dimension()); + + try { + this.handleOnHalted(); + } catch (Throwable ex) { + LOGGER.error("Exception caught while processing unload logics!", ex); + } finally { + this.doRemoveLevel(); + this.finalizeLock(); + this.retireCallback(); + } + } + + private void doRemoveLevel() { + LOGGER.info("Removing level..."); + RegionizedServer.getInstance().addTask(() -> { + MinecraftServer.getServer().removeLevel(this.toUnload); + RegionizedServer.getInstance().removeWorld(this.toUnload); + + LOGGER.info("Removed level"); + }); + } + + public void waitForComplete() { + try { + this.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + + private void retireCallback() { + this.unloadCallback.complete(this.toUnload); + } + + private void finalizeLock() { + LOGGER.info("Leaving finalization safe point area of level {}", this.toUnload.dimension()); + this.toUnload.levelUnloadStateLock.acquireUnreachable(); + } + + private void migrateAllPlayersOnThisRegion(final ThreadedRegionizer.ThreadedRegion region) { + ChunkPos center = null; + try { + this.shuttingDown = region; + center = region.getCenterChunk(); + + final RegionizedWorldData worldData = region.regioniser.world.worldRegionData.get(); + + for (final ServerPlayer player : worldData.getLocalPlayers()) { + try { + final TeleportRecord lastTeleportPos = player.lastTeleportRecord; + + ServerLevel toMigrateLevel = MinecraftServer.getServer().overworld(); + + if (lastTeleportPos != null) { + final ServerLevel lastLevel = lastTeleportPos.lastLevel().get(); + + if (lastLevel != null && lastLevel != this.toUnload) { + toMigrateLevel = lastLevel; + } + } + + Vec3 destinationPosition = lastTeleportPos == null ? Vec3.ZERO : lastTeleportPos.lastPos(); + + // we can acquire read lock multiple times so we don't care about these re-entering problems + final boolean teleported = player.teleportAsync( + toMigrateLevel, + destinationPosition, + lastTeleportPos == null ? 0.0F : lastTeleportPos.yaw(), + lastTeleportPos == null ? 0.0F : lastTeleportPos.pitch(), + player.getDeltaMovement(), + PlayerTeleportEvent.TeleportCause.UNKNOWN, + Entity.TELEPORT_FLAG_TELEPORT_PASSENGERS | Entity.TELEPORT_FLAG_LOAD_CHUNK, + null + ); + + if (!teleported) { + LOGGER.error("Failed to migrate player {}! Force kicking.", player.getScoreboardName()); + + // don't care that the player is not removed from the world + // as the players would be collected with their unloaded level by GC after disconnection + // and the logic of mc will help us correct it to an unloaded level + player.connection.connection.disconnect((DisconnectionDetails) null); + } + } catch (final Throwable thr) { + LOGGER.error("Failed to migrate out player: " + player, thr); + } + } + } catch (final Throwable thr) { + LOGGER.error("Failed to migrate player out for region around chunk " + center + " in world '" + region.regioniser.world.getWorld().getName() + "'", thr); + } finally { + this.shuttingDown = null; + } + } + + private void closeOtherMiscResources() { + this.toUnload.noSave = false; + this.toUnload.getChunkSource().getDataStorage().close(); + + io.papermc.paper.FeatureHooks.closeEntityManager(this.toUnload, this.doSaving); + + MinecraftServer.getServer().saveGlobalData(true); + } + + private void handleOnHalted() { + this.toUnload.moonrise$getChunkTaskScheduler().halt(false, 0L); + this.haltChunkSystem(this.toUnload); + + LOGGER.info("Halted chunk systems"); + + final List> regions = new ArrayList<>(); + + this.toUnload.regioniser.computeForAllRegions(regions::add); // COW + + LOGGER.info("Finishing pending teleports..."); + for (var region : regions) { + this.finishTeleportations(region, this.toUnload); + } + LOGGER.info("Finished pending teleports"); + + LOGGER.info("Saving world data for world '" + WorldUtil.getWorldName(this.toUnload) + "'"); + + LOGGER.info("Closing player inventories..."); + for (var region : regions) { + this.closePlayerInventories(region); + } + LOGGER.info("Closed player inventories"); + + LOGGER.info("Migrating out players"); + for (var region : regions) { + this.migrateAllPlayersOnThisRegion(region); + } + LOGGER.info("Migrated out players"); + + if (this.doSaving) { + LOGGER.info("Saving chunks..."); + for (int i = 0, len = regions.size(); i < len; ++i) { + this.saveRegionChunks(regions.get(i), (i + 1) == len); + } + LOGGER.info("Saved chunks"); + } else { + LOGGER.info("Do saving is not required for level {}. Skipping saving chunks.", this.toUnload.dimension()); + } + + LOGGER.info("Saving level data..."); + this.saveLevelData(this.toUnload); + LOGGER.info("Saved level data"); + + LOGGER.info("Closing other misc resources..."); + this.closeOtherMiscResources(); + LOGGER.info("Closed other misc resources"); + } +} diff --git a/lophine-server/src/main/java/me/earthme/luminol/utils/thread/SimpleReferenceRWLock.java b/lophine-server/src/main/java/me/earthme/luminol/utils/thread/SimpleReferenceRWLock.java new file mode 100644 index 0000000..77eaae4 --- /dev/null +++ b/lophine-server/src/main/java/me/earthme/luminol/utils/thread/SimpleReferenceRWLock.java @@ -0,0 +1,224 @@ +package me.earthme.luminol.utils.thread; + +import ca.spottedleaf.concurrentutil.util.ConcurrentUtil; + +import java.lang.invoke.VarHandle; + +// -1 -> write locked +// -2 -> unreachable +// >= 0 read locked or not locked (0) + +// this is a simple version of rw lock which removed the owner thread check +// only used for reference counting + +// we only use this for preventing a player joining an unloading level or halt the level scheduler during some unfinished place logics +// such as async teleportation and some player join logics + +// in this lock, the write area is designed for a single threaded env(the shutdown logic itself is single threaded) +// then the read area could be happened on multiple threads + +// if the write lock is held, then it means that the scheduler is begun to shut down +// we could just do a simple check (try acquiring read) and if it's failed then it means the level is being unloaded so we could correct the player's level safely +// additionally, the blocking write lock is to ensure those async place tasks +public class SimpleReferenceRWLock { + private int count; + private boolean readReferencingBlocked = false; + + private static final VarHandle COUNT_HANDLE = ConcurrentUtil.getVarHandle(SimpleReferenceRWLock.class, "count", int.class); + private static final VarHandle READ_REFERENCE_BLOCKED = ConcurrentUtil.getVarHandle(SimpleReferenceRWLock.class, "readReferencingBlocked", boolean.class); + + public void acquireUnreachable() { + final int curr = (int) COUNT_HANDLE.getVolatile(this); + + // we require the unreachable state should be upgraded from write held state + if (curr != -1) { + throw new IllegalStateException("Not write held!"); + } + + if (!COUNT_HANDLE.compareAndSet(this, curr, -2)) + throw new IllegalStateException("race cond!"); + } + + public void blockReadingReferencing() { + READ_REFERENCE_BLOCKED.setVolatile(this, true); + } + + public boolean isReadReferencingBlocked() { + return (boolean) READ_REFERENCE_BLOCKED.getVolatile(this); + } + + public void releaseWrite() { + long failures = 1L; + + for (; ; ) { + final int curr = (int) COUNT_HANDLE.getVolatile(this); + + // unreachable acquired + if (curr == -2) { + throw new IllegalStateException("Unreachable set!"); + } + + // write acquired (we do not allow multiple write competitions) + if (curr != -1) { + throw new IllegalStateException("Not write held!"); + } + + if (COUNT_HANDLE.compareAndSet(this, curr, 0)) { + break; + } + + // max sleep for 2us + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 100L, 2000); + } + } + + public boolean acquireWrite(long deadline) { + long failures = 1L; + + for (; ; ) { + // deadline reached + if (deadline <= System.nanoTime()) { + return false; + } + + final int curr = (int) COUNT_HANDLE.getVolatile(this); + + // unreachable acquired + if (curr == -2) { + return false; + } + + // have remaining read operations + if (curr > 0) { + // max sleep for 2us + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 100L, 2000); + continue; + } + + // write acquired (we do not allow multiple write competitions) + if (curr == -1) { + return false; + } + + if (COUNT_HANDLE.compareAndSet(this, curr, -1)) { + break; + } + + // max sleep for 2us + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 100L, 2000); + } + + // gained + return true; + } + + public boolean acquireWrite() { + long failures = 1L; + + for (; ; ) { + final int curr = (int) COUNT_HANDLE.getVolatile(this); + + // unreachable acquired + if (curr == -2) { + return false; + } + + // write acquired (we do not allow multiple write competitions) + if (curr == -1) { + return false; + } + + // have remaining read operations + if (curr > 0) { + // max sleep for 2us + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 100L, 2000); + continue; + } + + if (COUNT_HANDLE.compareAndSet(this, curr, -1)) { + break; + } + + // max sleep for 2us + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 100L, 2000); + } + + return true; + } + + public boolean isReadLocked() { + return (int) COUNT_HANDLE.getVolatile(this) > 0; + } + + public void releaseRead() { + long failures = 1L; + + for (; ; ) { + final int curr = (int) COUNT_HANDLE.getVolatile(this); + + // unreachable acquired + if (curr == -2) { + throw new IllegalStateException("Unreachable set!"); + } + + // write acquired + if (curr == -1) { + throw new IllegalStateException("Unexpected state! Expected is read locked but got write locked!"); + } + + if (curr == 0) { + throw new IllegalStateException("Not read held!"); + } + + if (COUNT_HANDLE.compareAndSet(this, curr, curr - 1)) { + break; + } + + // max sleep for 2us + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 100L, 2000); + } + } + + public boolean acquireRead() { + long failures = 1L; + + for (; ; ) { + final int curr = (int) COUNT_HANDLE.getVolatile(this); + + // read referencing blocked + if ((boolean) READ_REFERENCE_BLOCKED.getVolatile(this)) { + return false; + } + + // unreachable acquired + if (curr == -2) { + return false; + } + + // write acquired + if (curr == -1) { + return false; + } + + if (COUNT_HANDLE.compareAndSet(this, curr, curr + 1)) { + break; + } + + // max sleep for 2us + Thread.yield(); + failures = ConcurrentUtil.linearLongBackoff(failures, 100L, 2000); + } + + return true; + } + + public boolean isUnreachable() { + return (int) COUNT_HANDLE.getVolatile(this) == -2; + } +} diff --git a/lophine-server/src/main/java/me/titaniumtown/ArrayConstants.java b/lophine-server/src/main/java/me/titaniumtown/ArrayConstants.java new file mode 100644 index 0000000..5318ebf --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/dreeam/leaf/util/FastBitRadixSort.java b/lophine-server/src/main/java/org/dreeam/leaf/util/FastBitRadixSort.java new file mode 100644 index 0000000..7541146 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java b/lophine-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java new file mode 100644 index 0000000..7a53e2a --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/command/ArgumentNode.java b/lophine-server/src/main/java/org/leavesmc/leaves/command/ArgumentNode.java new file mode 100644 index 0000000..b276c8f --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/command/CommandContext.java b/lophine-server/src/main/java/org/leavesmc/leaves/command/CommandContext.java new file mode 100644 index 0000000..9d13a58 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/command/CommandNode.java b/lophine-server/src/main/java/org/leavesmc/leaves/command/CommandNode.java new file mode 100644 index 0000000..335bdc4 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/command/CommandUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/command/CommandUtils.java new file mode 100644 index 0000000..cf11c85 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/command/LiteralNode.java b/lophine-server/src/main/java/org/leavesmc/leaves/command/LiteralNode.java new file mode 100644 index 0000000..4495946 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/command/RootNode.java b/lophine-server/src/main/java/org/leavesmc/leaves/command/RootNode.java new file mode 100644 index 0000000..cc3163b --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/command/WrappedArgument.java b/lophine-server/src/main/java/org/leavesmc/leaves/command/WrappedArgument.java new file mode 100644 index 0000000..5e4bb87 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumCooldownReceivingInventory.java new file mode 100644 index 0000000..42e7368 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumDefaultedList.java new file mode 100644 index 0000000..a03e559 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumInventory.java new file mode 100644 index 0000000..e92d9c2 --- /dev/null +++ b/lophine-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 net.minecraft.world.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 net.minecraft.world.entity.vehicle.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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/api/inventory/LithiumTransferConditionInventory.java new file mode 100644 index 0000000..f30d34b --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetBlockStateHandlingBlockEntity.java new file mode 100644 index 0000000..fa291e4 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SetChangedHandlingBlockEntity.java new file mode 100644 index 0000000..b520461 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/ShapeUpdateHandlingBlockBehaviour.java new file mode 100644 index 0000000..1f65c34 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java new file mode 100644 index 0000000..22eea64 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/SleepingBlockEntity.java new file mode 100644 index 0000000..66a56dc --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeEmitter.java b/lophine-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/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeListener.java b/lophine-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/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_change_tracking/InventoryChangeTracker.java b/lophine-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/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracker.java b/lophine-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/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/block/entity/inventory_comparator_tracking/ComparatorTracking.java b/lophine-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/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/BlockStateOnlyInventory.java new file mode 100644 index 0000000..4889fd0 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/ComparatorUpdatePattern.java new file mode 100644 index 0000000..7890837 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperCachingState.java new file mode 100644 index 0000000..b5212f8 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/HopperHelper.java new file mode 100644 index 0000000..f85a1a2 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/InventoryHelper.java new file mode 100644 index 0000000..897d392 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleInventory.java new file mode 100644 index 0000000..c8ecadf --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumDoubleStackList.java new file mode 100644 index 0000000..0c138d3 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/LithiumStackList.java new file mode 100644 index 0000000..80ec678 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/hopper/UpdateReceiver.java new file mode 100644 index 0000000..ab96fa2 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementListener.java new file mode 100644 index 0000000..16ba4dd --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionEntityMovementTracker.java new file mode 100644 index 0000000..962e3af --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionInventoryEntityTracker.java new file mode 100644 index 0000000..cfd7977 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/tracking/entity/ChunkSectionItemEntityMovementTracker.java new file mode 100644 index 0000000..b75c4f7 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/DirectionConstants.java new file mode 100644 index 0000000..817be4b --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangePublisher.java new file mode 100644 index 0000000..cd0a987 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/change_tracking/ChangeSubscriber.java new file mode 100644 index 0000000..3cbb275 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/util/tuples/WorldSectionBox.java new file mode 100644 index 0000000..c93b080 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java b/lophine-server/src/main/java/org/leavesmc/leaves/lithium/common/world/WorldHelper.java new file mode 100644 index 0000000..7a467f4 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java new file mode 100644 index 0000000..21bfb28 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java @@ -0,0 +1,170 @@ +/* + * 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.plugin; + +import io.papermc.paper.plugin.configuration.PluginMeta; +import io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager; +import org.bukkit.Bukkit; +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.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.InputStream; +import java.util.List; + +public class MinecraftInternalPlugin extends PluginBase { + + public static final MinecraftInternalPlugin INSTANCE = new MinecraftInternalPlugin(); + + private boolean enabled = true; + + private final PluginDescriptionFile pdf; + private final PluginLogger logger; + + public MinecraftInternalPlugin() { + String pluginName = "Minecraft"; + pdf = new PluginDescriptionFile(pluginName, "1.0", "nms"); + logger = new PluginLogger(this); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public @NotNull PluginDescriptionFile getDescription() { + return pdf; + } + + @Override + public @NotNull PluginMeta getPluginMeta() { + return pdf; + } + + @Override + public @NotNull PluginLogger getLogger() { + return logger; + } + + @Override + public @NotNull Server getServer() { + return Bukkit.getServer(); + } + + @Override + public boolean isEnabled() { + return enabled; + } + + @Override + public @NotNull PluginLoader getPluginLoader() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public @NotNull File getDataFolder() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public @NotNull FileConfiguration getConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public InputStream getResource(@NotNull 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(@NotNull String resourcePath, boolean replace) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public void reloadConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @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(@NotNull 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(@NotNull CommandSender sender, @NotNull Command command, @NotNull String label, String @NotNull [] args) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public List onTabComplete(@NotNull CommandSender sender, @NotNull Command command, @NotNull String alias, String @NotNull [] args) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public @NotNull LifecycleEventManager<@NotNull Plugin> getLifecycleManager() { + throw new UnsupportedOperationException("Not supported."); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java new file mode 100644 index 0000000..c3db6c5 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/ServerFeatureManager.java @@ -0,0 +1,31 @@ +// This file is licensed under the MIT license. +package org.leavesmc.leaves.plugin; + +import java.util.HashSet; +import java.util.Set; + +import static org.leavesmc.leaves.plugin.Features.*; + +public class ServerFeatureManager implements FeatureManager { + public static ServerFeatureManager INSTANCE = new ServerFeatureManager(); + private final Set availableFeatures = new HashSet<>(); + + private ServerFeatureManager() { + availableFeatures.add(FAKEPLAYER); // Lophine - fakeplayer support + availableFeatures.add(PHOTOGRAPHER); // Lophine - Replay Mod API support + availableFeatures.add(UPDATE_SUPPRESSION_EVENT); // Lophine - update suppression event support + if (Boolean.getBoolean("leavesclip.enable.mixin") || Boolean.getBoolean("hyacinthusclip.enable.mixin")) { + availableFeatures.add(MIXIN); + } + } + + @Override + public Set getAvailableFeatures() { + return availableFeatures; + } + + @Override + public boolean isFeatureAvailable(String feature) { + return availableFeatures.contains(feature); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java new file mode 100644 index 0000000..3548aec --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java @@ -0,0 +1,78 @@ +/* + * 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.plugin.provider; + +import com.destroystokyo.paper.utils.PaperPluginLogger; +import io.papermc.paper.plugin.bootstrap.PluginProviderContext; +import io.papermc.paper.plugin.bootstrap.PluginProviderContextImpl; +import io.papermc.paper.plugin.entrypoint.classloader.PaperPluginClassLoader; +import io.papermc.paper.plugin.entrypoint.classloader.PaperSimplePluginClassLoader; +import io.papermc.paper.plugin.loader.PaperClasspathBuilder; +import io.papermc.paper.plugin.loader.PluginLoader; +import io.papermc.paper.plugin.provider.type.PluginTypeFactory; +import io.papermc.paper.plugin.provider.type.paper.PaperPluginParent; +import io.papermc.paper.plugin.provider.util.ProviderUtil; +import me.earthme.luminol.config.modules.unsupported.DisableCheckForFoliaSupported; +import net.kyori.adventure.text.logger.slf4j.ComponentLogger; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.plugin.provider.configuration.LeavesPluginMeta; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.file.Path; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; +import java.util.logging.Logger; + +public class LeavesPluginProviderFactory implements PluginTypeFactory { + @Override + public PaperPluginParent build(JarFile file, LeavesPluginMeta configuration, Path source) { + if (!configuration.isFoliaSupported() && !DisableCheckForFoliaSupported.disableForLeaves) { + throw new RuntimeException("Could not load plugin '" + configuration.getDisplayName() + "' as it is not marked as supporting Folia!"); + } + Logger jul = PaperPluginLogger.getLogger(configuration); + ComponentLogger logger = ComponentLogger.logger(jul.getName()); + PluginProviderContext context = PluginProviderContextImpl.create(configuration, logger, source); + + PaperClasspathBuilder builder = new PaperClasspathBuilder(context); + + if (configuration.getLoader() != null) { + try ( + PaperSimplePluginClassLoader simplePluginClassLoader = new PaperSimplePluginClassLoader(source, file, configuration, this.getClass().getClassLoader()) + ) { + PluginLoader loader = ProviderUtil.loadClass(configuration.getLoader(), PluginLoader.class, simplePluginClassLoader); + loader.classloader(builder); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + PaperPluginClassLoader classLoader = builder.buildClassLoader(jul, source, file, configuration); + return new PaperPluginParent(source, file, configuration, classLoader, context); + } + + @Override + public LeavesPluginMeta create(@NotNull JarFile file, JarEntry config) throws IOException { + LeavesPluginMeta configuration; + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(file.getInputStream(config)))) { + configuration = LeavesPluginMeta.create(bufferedReader); + } + return configuration; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/FeaturesConfiguration.java b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/FeaturesConfiguration.java new file mode 100644 index 0000000..37e0cfa --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/FeaturesConfiguration.java @@ -0,0 +1,38 @@ +// This file is licensed under the MIT license. +/* + * 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.plugin.provider.configuration; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; + +import java.util.List; + +@SuppressWarnings("FieldMayBeFinal") +@ConfigSerializable +public class FeaturesConfiguration { + private List required = List.of(); + private List optional = List.of(); + + public List getRequired() { + return required; + } + + public List getOptional() { + return optional; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java new file mode 100644 index 0000000..eae1159 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java @@ -0,0 +1,116 @@ +// This file is licensed under the MIT license. +/* + * 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.plugin.provider.configuration; + +import com.google.common.collect.ImmutableList; +import io.papermc.paper.configuration.constraint.Constraint; +import io.papermc.paper.configuration.serializer.ComponentSerializer; +import io.papermc.paper.configuration.serializer.EnumValueSerializer; +import io.papermc.paper.plugin.provider.configuration.FlattenedResolver; +import io.papermc.paper.plugin.provider.configuration.LegacyPaperMeta; +import io.papermc.paper.plugin.provider.configuration.PaperPluginMeta; +import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer; +import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints; +import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration; +import org.bukkit.craftbukkit.util.ApiVersion; +import org.jetbrains.annotations.NotNull; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.ConfigurationNode; +import org.spongepowered.configurate.gson.GsonConfigurationLoader; +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.ObjectMapper; +import org.spongepowered.configurate.serialize.ScalarSerializer; +import org.spongepowered.configurate.serialize.SerializationException; + +import java.io.BufferedReader; +import java.lang.reflect.Type; +import java.util.function.Predicate; + +@SuppressWarnings("FieldMayBeFinal") +@ConfigSerializable +public class LeavesPluginMeta extends PaperPluginMeta { + private FeaturesConfiguration features = new FeaturesConfiguration(); + private MixinConfiguration mixin = new MixinConfiguration(); + static final ApiVersion MINIMUM = ApiVersion.getOrCreateVersion("1.21.4"); + + public static LeavesPluginMeta create(BufferedReader reader) throws ConfigurateException { + GsonConfigurationLoader loader = GsonConfigurationLoader.builder() + .source(() -> reader) + .defaultOptions((options) -> + options.serializers((serializers) -> + serializers.register(new ScalarSerializer<>(ApiVersion.class) { + @Override + public ApiVersion deserialize(final @NotNull Type type, final @NotNull Object obj) throws SerializationException { + try { + final ApiVersion version = ApiVersion.getOrCreateVersion(obj.toString()); + if (version.isOlderThan(MINIMUM)) { + throw new SerializationException(version + " is too old for a leaves plugin!"); + } + return version; + } catch (final IllegalArgumentException e) { + throw new SerializationException(e); + } + } + + @Override + protected @NotNull Object serialize(final ApiVersion item, final @NotNull Predicate> typeSupported) { + return item.getVersionString(); + } + }) + .register(new EnumValueSerializer()) + .register(PermissionConfiguration.class, PermissionConfigurationSerializer.SERIALIZER) + .register(new ComponentSerializer()) + .registerAnnotatedObjects( + ObjectMapper.factoryBuilder() + .addConstraint(Constraint.class, new Constraint.Factory()) + .addConstraint(PluginConfigConstraints.PluginName.class, String.class, new PluginConfigConstraints.PluginName.Factory()) + .addConstraint(PluginConfigConstraints.PluginNameSpace.class, String.class, new PluginConfigConstraints.PluginNameSpace.Factory()) + .addNodeResolver(new FlattenedResolver.Factory()) + .build() + ) + ) + ) + .build(); + ConfigurationNode node = loader.load(); + LegacyPaperMeta.migrate(node); + LeavesPluginMeta pluginConfiguration = node.require(LeavesPluginMeta.class); + + var authorNode = node.node("author"); + if (!authorNode.virtual()) { + String author = authorNode.getString(); + var authorsBuilder = ImmutableList.builder(); + if (author != null) { + authorsBuilder.add(author); + } + pluginConfiguration.authors = authorsBuilder + .addAll(pluginConfiguration.authors) + .build(); + } + + return pluginConfiguration; + } + + public FeaturesConfiguration getFeatures() { + return features; + } + + public MixinConfiguration getMixin() { + return mixin; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/MixinConfiguration.java b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/MixinConfiguration.java new file mode 100644 index 0000000..7213755 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/MixinConfiguration.java @@ -0,0 +1,54 @@ +// This file is licensed under the MIT license. +/* + * 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.plugin.provider.configuration; + +import org.spongepowered.configurate.objectmapping.ConfigSerializable; +import org.spongepowered.configurate.objectmapping.meta.PostProcess; + +import java.util.List; + +@SuppressWarnings({"FieldMayBeFinal", "unused"}) +@ConfigSerializable +public class MixinConfiguration { + private String packageName; + private List mixins = List.of(); + private String accessWidener; + + @PostProcess + public void postProcess() { + if (mixins.isEmpty()) { + return; + } + if (packageName == null) { + throw new IllegalStateException("Already define mixins: " + mixins + ", but no mixin package-name provided"); + } + } + + public List getMixins() { + return mixins; + } + + public String getAccessWidener() { + return accessWidener; + } + + public String getPackageName() { + return packageName; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java b/lophine-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java new file mode 100644 index 0000000..88a4801 --- /dev/null +++ b/lophine-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/lophine-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java new file mode 100644 index 0000000..84d0e1a --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java @@ -0,0 +1,195 @@ +/* + * 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 fun.bm.lophine.config.modules.function.ContainerExpansionConfig; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.level.block.ShulkerBoxBlock; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +// Only used part of the original code was copied +public class ItemOverstackUtils { + + private static final List overstackUtils = List.of( + new ShulkerBox() + ); + + public static int getItemStackMaxCount(ItemStack stack) { + int size; + for (ItemUtil util : overstackUtils) { + if (util.isEnabled() && (size = util.getMaxServerStackCount(stack)) != -1) { + return size; + } + } + return stack.getMaxStackSize(); + } + + public static int getNetworkMaxCount(ItemStack stack) { + int size; + for (ItemUtil util : overstackUtils) { + if (util.isEnabled() && (size = util.getMaxClientStackCount(stack)) != -1) { + return size; + } + } + return stack.getMaxStackSize(); + } + + public static boolean tryStackItems(ItemEntity self, ItemEntity other) { + for (ItemUtil util : overstackUtils) { + if (util.isEnabled() && util.tryStackItems(self, other)) { + return true; + } + } + return false; + } + + public static boolean hasOverstackingItem() { + for (ItemUtil util : overstackUtils) { + if (util.isEnabled()) { + return true; + } + } + return false; + } + + public static int getItemStackMaxCountReal(ItemStack stack) { + CompoundTag nbt = Optional.ofNullable(stack.get(DataComponents.CUSTOM_DATA)).orElse(CustomData.EMPTY).copyTag(); + return nbt.getInt("Leaves.RealStackSize").orElse(stack.getMaxStackSize()); + } + + public static ItemStack encodeMaxStackSize(ItemStack itemStack) { + int realMaxStackSize = getItemStackMaxCountReal(itemStack); + int modifiedMaxStackSize = getNetworkMaxCount(itemStack); + if (itemStack.getMaxStackSize() != modifiedMaxStackSize) { + itemStack.set(DataComponents.MAX_STACK_SIZE, modifiedMaxStackSize); + CompoundTag nbt = itemStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + nbt.putInt("Leaves.RealStackSize", realMaxStackSize); + itemStack.set(DataComponents.CUSTOM_DATA, CustomData.of(nbt)); + } + return itemStack; + } + + public static ItemStack decodeMaxStackSize(ItemStack itemStack) { + int realMaxStackSize = getItemStackMaxCountReal(itemStack); + if (itemStack.getMaxStackSize() != realMaxStackSize) { + itemStack.set(DataComponents.MAX_STACK_SIZE, realMaxStackSize); + CompoundTag nbt = itemStack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + nbt.remove("Leaves.RealStackSize"); + if (nbt.isEmpty()) { + itemStack.remove(DataComponents.CUSTOM_DATA); + } else { + itemStack.set(DataComponents.CUSTOM_DATA, CustomData.of(nbt)); + } + } + return itemStack; + } + + 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); + } + + public static boolean isStackable(ItemStack itemStack) { + return getItemStackMaxCount(itemStack) > 1 && (!itemStack.isDamageableItem() || !itemStack.isDamaged()); + } + + private interface ItemUtil { + boolean isEnabled(); + + boolean tryStackItems(ItemEntity self, ItemEntity other); + + // number -> modified count, -1 -> I don't care + int getMaxServerStackCount(ItemStack stack); + + // number -> modified count, -1 -> I don't care + default int getMaxClientStackCount(ItemStack stack) { + return getMaxServerStackCount(stack); + } + } + + private static class ShulkerBox implements ItemUtil { + public static boolean shulkerBoxCheck(@NotNull ItemStack stack1, @NotNull ItemStack stack2) { + if (ContainerExpansionConfig.nbtShulkerStackable) { + return Objects.equals(stack1.getComponents(), stack2.getComponents()); + } + return shulkerBoxNoItem(stack1) && shulkerBoxNoItem(stack2) && Objects.equals(stack1.getComponents(), stack2.getComponents()); + } + + public static boolean shulkerBoxNoItem(@NotNull ItemStack stack) { + // Leaves - Paper 26.1: ItemContainerContents#stream() removed, use nonEmptyItemCopyStream() + return stack.getComponents().getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY).nonEmptyItemCopyStream().findAny().isEmpty(); + } + + @Override + public boolean isEnabled() { + return ContainerExpansionConfig.shulkerCount > 1; + } + + @Override + public boolean tryStackItems(ItemEntity self, ItemEntity other) { + ItemStack selfStack = self.getItem(); + if (!(selfStack.getItem() instanceof BlockItem blockItem) || !(blockItem.getBlock() instanceof ShulkerBoxBlock)) { + return false; + } + + ItemStack otherStack = other.getItem(); + if (selfStack.getItem() == otherStack.getItem() + && shulkerBoxCheck(selfStack, otherStack) + && selfStack.getCount() != ContainerExpansionConfig.shulkerCount) { + int amount = Math.min(otherStack.getCount(), ContainerExpansionConfig.shulkerCount - selfStack.getCount()); + + selfStack.grow(amount); + self.setItem(selfStack); + + self.pickupDelay = Math.max(other.pickupDelay, self.pickupDelay); + self.age = Math.min(other.getAge(), self.age); + + otherStack.shrink(amount); + if (otherStack.isEmpty()) { + other.discard(); + } else { + other.setItem(otherStack); + } + return true; + } + return false; + } + + @Override + public int getMaxServerStackCount(ItemStack stack) { + if (stack.getItem() instanceof BlockItem bi && + bi.getBlock() instanceof ShulkerBoxBlock && (ContainerExpansionConfig.nbtShulkerStackable || shulkerBoxNoItem(stack))) { + return ContainerExpansionConfig.shulkerCount; + } + return -1; + } + } +} diff --git a/lophine-server/src/main/java/su/plo/matter/Globals.java b/lophine-server/src/main/java/su/plo/matter/Globals.java new file mode 100644 index 0000000..6d55f08 --- /dev/null +++ b/lophine-server/src/main/java/su/plo/matter/Globals.java @@ -0,0 +1,95 @@ +package su.plo.matter; + +import com.google.common.collect.Iterables; +import me.earthme.luminol.config.modules.function.SecureSeedConfig; +import net.minecraft.server.level.ServerLevel; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.Optional; + +public class Globals { + public static final int WORLD_SEED_LONGS = 16; + public static final int WORLD_SEED_BITS = WORLD_SEED_LONGS * 64; + + public static final long[] worldSeed = new long[WORLD_SEED_LONGS]; + public static final ThreadLocal dimension = ThreadLocal.withInitial(() -> 0); + + public enum Salt { + UNDEFINED, + BASTION_FEATURE, + WOODLAND_MANSION_FEATURE, + MINESHAFT_FEATURE, + BURIED_TREASURE_FEATURE, + NETHER_FORTRESS_FEATURE, + PILLAGER_OUTPOST_FEATURE, + GEODE_FEATURE, + NETHER_FOSSIL_FEATURE, + OCEAN_MONUMENT_FEATURE, + RUINED_PORTAL_FEATURE, + POTENTIONAL_FEATURE, + GENERATE_FEATURE, + JIGSAW_PLACEMENT, + STRONGHOLDS, + POPULATION, + DECORATION, + SLIME_CHUNK + } + + public static void setupGlobals(ServerLevel world) { + if (!SecureSeedConfig.enabled) return; + + long[] seed = world.worldGenSettings.options().featureSeed(); + System.arraycopy(seed, 0, worldSeed, 0, WORLD_SEED_LONGS); + int worldIndex = Iterables.indexOf(world.getServer().levelKeys(), it -> it == world.dimension()); + if (worldIndex == -1) + worldIndex = world.getServer().levelKeys().size(); // if we are in world construction it may not have been added to the map yet + dimension.set(worldIndex); + } + + public static long[] createRandomWorldSeed() { + long[] seed = new long[WORLD_SEED_LONGS]; + SecureRandom rand = new SecureRandom(); + for (int i = 0; i < WORLD_SEED_LONGS; i++) { + seed[i] = rand.nextLong(); + } + return seed; + } + + // 1024-bit string -> 16 * 64 long[] + public static Optional parseSeed(String seedStr) { + if (seedStr.isEmpty()) return Optional.empty(); + + if (seedStr.length() != WORLD_SEED_BITS) { + throw new IllegalArgumentException("Secure seed length must be " + WORLD_SEED_BITS + "-bit but found " + seedStr.length() + "-bit."); + } + + long[] seed = new long[WORLD_SEED_LONGS]; + + for (int i = 0; i < WORLD_SEED_LONGS; i++) { + int start = i * 64; + int end = start + 64; + String seedSection = seedStr.substring(start, end); + + BigInteger seedInDecimal = new BigInteger(seedSection, 2); + seed[i] = seedInDecimal.longValue(); + } + + return Optional.of(seed); + } + + // 16 * 64 long[] -> 1024-bit string + public static String seedToString(long[] seed) { + StringBuilder sb = new StringBuilder(); + + for (long longV : seed) { + // Convert to 64-bit binary string per long + // Use format to keep 64-bit length, and use 0 to complete space + String binaryStr = String.format("%64s", Long.toBinaryString(longV)).replace(' ', '0'); + + sb.append(binaryStr); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/su/plo/matter/Hashing.java b/lophine-server/src/main/java/su/plo/matter/Hashing.java new file mode 100644 index 0000000..6f0fca1 --- /dev/null +++ b/lophine-server/src/main/java/su/plo/matter/Hashing.java @@ -0,0 +1,73 @@ +package su.plo.matter; + +public class Hashing { + // https://en.wikipedia.org/wiki/BLAKE_(hash_function) + // https://github.com/bcgit/bc-java/blob/master/core/src/main/java/org/bouncycastle/crypto/digests/Blake2bDigest.java + + private final static long[] blake2b_IV = { + 0x6a09e667f3bcc908L, 0xbb67ae8584caa73bL, 0x3c6ef372fe94f82bL, + 0xa54ff53a5f1d36f1L, 0x510e527fade682d1L, 0x9b05688c2b3e6c1fL, + 0x1f83d9abfb41bd6bL, 0x5be0cd19137e2179L + }; + + private final static byte[][] blake2b_sigma = { + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}, + {11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4}, + {7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8}, + {9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13}, + {2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9}, + {12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11}, + {13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10}, + {6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5}, + {10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0}, + {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}, + {14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3} + }; + + public static long[] hashWorldSeed(long[] worldSeed) { + long[] result = blake2b_IV.clone(); + result[0] ^= 0x01010040; + hash(worldSeed, result, new long[16], 0, false); + return result; + } + + public static void hash(long[] message, long[] chainValue, long[] internalState, long messageOffset, boolean isFinal) { + assert message.length == 16; + assert chainValue.length == 8; + assert internalState.length == 16; + + System.arraycopy(chainValue, 0, internalState, 0, chainValue.length); + System.arraycopy(blake2b_IV, 0, internalState, chainValue.length, 4); + internalState[12] = messageOffset ^ blake2b_IV[4]; + internalState[13] = blake2b_IV[5]; + if (isFinal) internalState[14] = ~blake2b_IV[6]; + internalState[15] = blake2b_IV[7]; + + for (int round = 0; round < 12; round++) { + G(message[blake2b_sigma[round][0]], message[blake2b_sigma[round][1]], 0, 4, 8, 12, internalState); + G(message[blake2b_sigma[round][2]], message[blake2b_sigma[round][3]], 1, 5, 9, 13, internalState); + G(message[blake2b_sigma[round][4]], message[blake2b_sigma[round][5]], 2, 6, 10, 14, internalState); + G(message[blake2b_sigma[round][6]], message[blake2b_sigma[round][7]], 3, 7, 11, 15, internalState); + G(message[blake2b_sigma[round][8]], message[blake2b_sigma[round][9]], 0, 5, 10, 15, internalState); + G(message[blake2b_sigma[round][10]], message[blake2b_sigma[round][11]], 1, 6, 11, 12, internalState); + G(message[blake2b_sigma[round][12]], message[blake2b_sigma[round][13]], 2, 7, 8, 13, internalState); + G(message[blake2b_sigma[round][14]], message[blake2b_sigma[round][15]], 3, 4, 9, 14, internalState); + } + + for (int i = 0; i < 8; i++) { + chainValue[i] ^= internalState[i] ^ internalState[i + 8]; + } + } + + private static void G(long m1, long m2, int posA, int posB, int posC, int posD, long[] internalState) { + internalState[posA] = internalState[posA] + internalState[posB] + m1; + internalState[posD] = Long.rotateRight(internalState[posD] ^ internalState[posA], 32); + internalState[posC] = internalState[posC] + internalState[posD]; + internalState[posB] = Long.rotateRight(internalState[posB] ^ internalState[posC], 24); // replaces 25 of BLAKE + internalState[posA] = internalState[posA] + internalState[posB] + m2; + internalState[posD] = Long.rotateRight(internalState[posD] ^ internalState[posA], 16); + internalState[posC] = internalState[posC] + internalState[posD]; + internalState[posB] = Long.rotateRight(internalState[posB] ^ internalState[posC], 63); // replaces 11 of BLAKE + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/su/plo/matter/HashingV2.java b/lophine-server/src/main/java/su/plo/matter/HashingV2.java new file mode 100644 index 0000000..89ffd1b --- /dev/null +++ b/lophine-server/src/main/java/su/plo/matter/HashingV2.java @@ -0,0 +1,519 @@ +package su.plo.matter; + +import me.earthme.luminol.config.modules.function.SecureSeedConfig; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; + +public class HashingV2 { + private static final int BLOCK_LEN = 64; + private static final int CHUNK_LEN = 1024; + private static final int OUT_LEN = 32; + private static final int KEY_LEN = 32; + + private static final int CHUNK_START = 1 << 0; + private static final int CHUNK_END = 1 << 1; + private static final int PARENT = 1 << 2; + private static final int ROOT = 1 << 3; + private static final int KEYED_HASH = 1 << 4; + private static final int DERIVE_KEY_CONTEXT = 1 << 5; + private static final int DERIVE_KEY_MATERIAL = 1 << 6; + + private static final int[] IV = { + 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, + 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19, + }; + + private static final int[] MSG_PERMUTATION = {2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8}; + + private static final ThreadLocal saltHolderLocal = ThreadLocal.withInitial(() -> { + final SaltHolder newHolder = new SaltHolder(); + + newHolder.init(SecureSeedConfig.salt); + + return newHolder; + }); + private static final ThreadLocal threadBuffer = ThreadLocal.withInitial(() -> new byte[128]); + private static final ThreadLocal threadByteBuffer = ThreadLocal.withInitial(() -> ByteBuffer.allocate(128).order(ByteOrder.LITTLE_ENDIAN)); + + private static int rotr32(int x, int k) { + return ((x >>> k) | (x << (32 - k))) >>> 0; + } + + private static void g(int[] state, int a, int b, int c, int d, int mx, int my) { + state[a] = (state[a] + state[b] + mx) >>> 0; + state[d] = rotr32(state[d] ^ state[a], 16); + state[c] = (state[c] + state[d]) >>> 0; + state[b] = rotr32(state[b] ^ state[c], 12); + state[a] = (state[a] + state[b] + my) >>> 0; + state[d] = rotr32(state[d] ^ state[a], 8); + state[c] = (state[c] + state[d]) >>> 0; + state[b] = rotr32(state[b] ^ state[c], 7); + } + + private static void round(int[] state, int[] m) { + g(state, 0, 4, 8, 12, m[0], m[1]); + g(state, 1, 5, 9, 13, m[2], m[3]); + g(state, 2, 6, 10, 14, m[4], m[5]); + g(state, 3, 7, 11, 15, m[6], m[7]); + g(state, 0, 5, 10, 15, m[8], m[9]); + g(state, 1, 6, 11, 12, m[10], m[11]); + g(state, 2, 7, 8, 13, m[12], m[13]); + g(state, 3, 4, 9, 14, m[14], m[15]); + } + + private static int[] permute(int[] m) { + int[] permuted = new int[16]; + for (int i = 0; i < 16; i++) { + permuted[i] = m[MSG_PERMUTATION[i]]; + } + return permuted; + } + + private static int[] compress(int[] chainingValue, int[] blockWords, long counter, int blockLen, int flags) { + int counterLow = (int) (counter >>> 0); + int counterHigh = (int) ((counter / 0x100000000L) >>> 0); + + int[] state = new int[16]; + System.arraycopy(chainingValue, 0, state, 0, 8); + state[8] = IV[0]; + state[9] = IV[1]; + state[10] = IV[2]; + state[11] = IV[3]; + state[12] = counterLow; + state[13] = counterHigh; + state[14] = blockLen; + state[15] = flags; + + int[] block = Arrays.copyOf(blockWords, 16); + + for (int i = 0; i < 7; i++) { + round(state, block); + block = permute(block); + } + + int[] output = new int[16]; + for (int i = 0; i < 8; i++) { + output[i] = state[i] ^ state[i + 8]; + output[i + 8] = state[i + 8] ^ chainingValue[i]; + } + + return output; + } + + private static int[] first8Words(int[] compressionOutput) { + return Arrays.copyOfRange(compressionOutput, 0, 8); + } + + private static int[] wordsFromLittleEndianBytes(byte[] bytes) { + int[] words = new int[bytes.length / 4]; + ByteBuffer buffer = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < words.length; i++) { + words[i] = buffer.getInt(i * 4); + } + return words; + } + + private static byte[] wordsToLittleEndianBytes(int[] words, int outputLen) { + byte[] output = new byte[outputLen]; + ByteBuffer buffer = ByteBuffer.wrap(output).order(ByteOrder.LITTLE_ENDIAN); + for (int i = 0; i < words.length && i * 4 < outputLen; i++) { + buffer.putInt(i * 4, words[i]); + } + return output; + } + + private static class ChunkState { + int[] chainingValue; + long chunkCounter; + byte[] block; + int blockLen; + int blocksCompressed; + int flags; + + ChunkState(int[] keyWords, long chunkCounter, int flags) { + this.chainingValue = keyWords.clone(); + this.chunkCounter = chunkCounter; + this.block = new byte[BLOCK_LEN]; + this.blockLen = 0; + this.blocksCompressed = 0; + this.flags = flags; + } + + int len() { + return BLOCK_LEN * this.blocksCompressed + this.blockLen; + } + + int startFlag() { + return this.blocksCompressed == 0 ? CHUNK_START : 0; + } + + void update(byte[] input) { + int offset = 0; + + while (offset < input.length) { + if (this.blockLen == BLOCK_LEN) { + int[] blockWords = wordsFromLittleEndianBytes(this.block); + this.chainingValue = first8Words(compress( + this.chainingValue, + blockWords, + this.chunkCounter, + BLOCK_LEN, + this.flags | this.startFlag() + )); + this.blocksCompressed++; + this.block = new byte[BLOCK_LEN]; + this.blockLen = 0; + } + + int want = BLOCK_LEN - this.blockLen; + int take = Math.min(want, input.length - offset); + System.arraycopy(input, offset, this.block, this.blockLen, take); + this.blockLen += take; + offset += take; + } + } + + int[] outputBlockWords() { + int[] blockWords = new int[16]; + for (int i = 0; i < 16 && i * 4 < this.blockLen; i++) { + blockWords[i] = ((this.block[i * 4 + 0] & 0xFF)) | + ((this.block[i * 4 + 1] & 0xFF) << 8) | + ((this.block[i * 4 + 2] & 0xFF) << 16) | + ((this.block[i * 4 + 3] & 0xFF) << 24); + } + return blockWords; + } + + Output output() { + return new Output( + this.chainingValue, + outputBlockWords(), + this.chunkCounter, + this.blockLen, + this.flags | this.startFlag() | CHUNK_END + ); + } + } + + private static class Output { + int[] inputChainingValue; + int[] blockWords; + long counter; + int blockLen; + int flags; + + Output(int[] inputChainingValue, int[] blockWords, long counter, int blockLen, int flags) { + this.inputChainingValue = inputChainingValue; + this.blockWords = blockWords; + this.counter = counter; + this.blockLen = blockLen; + this.flags = flags; + } + + int[] chainingValue() { + return first8Words(compress( + this.inputChainingValue, + this.blockWords, + this.counter, + this.blockLen, + this.flags + )); + } + + byte[] rootOutputBytes(int outLen) { + byte[] output = new byte[outLen]; + long outputBlockCounter = 0; + + for (int offset = 0; offset < outLen; offset += 2 * OUT_LEN) { + int[] words = compress( + this.inputChainingValue, + this.blockWords, + outputBlockCounter, + this.blockLen, + this.flags | ROOT + ); + + byte[] blockOutput = wordsToLittleEndianBytes(words, 64); + + int remaining = outLen - offset; + int copyLen = Math.min(remaining, 64); + System.arraycopy(blockOutput, 0, output, offset, copyLen); + + outputBlockCounter++; + } + + return output; + } + } + + private static Output parentOutput(int[] leftChildCv, int[] rightChildCv, int[] keyWords, int flags) { + int[] blockWords = new int[16]; + System.arraycopy(leftChildCv, 0, blockWords, 0, 8); + System.arraycopy(rightChildCv, 0, blockWords, 8, 8); + + return new Output( + keyWords, + blockWords, + 0, + BLOCK_LEN, + PARENT | flags + ); + } + + private static int[] parentCv(int[] leftChildCv, int[] rightChildCv, int[] keyWords, int flags) { + return parentOutput(leftChildCv, rightChildCv, keyWords, flags).chainingValue(); + } + + private static class SaltHolder { + private long[] cachedSaltHash = null; + private String lastSalt = null; + + public void init(String currentSalt) { + if (cachedSaltHash == null || !currentSalt.equals(lastSalt)) { + byte[] saltBytes = currentSalt.getBytes(); + byte[] hashBytes = blake3(saltBytes); + int[] hashInts = bytesToIntsLittleEndian(hashBytes); + + cachedSaltHash = new long[8]; + for (int i = 0; i < 8; i++) { + cachedSaltHash[i] = (hashInts[i] & 0xFFFFFFFFL); + } + lastSalt = currentSalt; + } + } + } + + private static class Hasher { + ChunkState chunkState; + int[] keyWords; + java.util.ArrayList cvStack; + int flags; + + Hasher(int[] keyWords, int flags) { + this.chunkState = new ChunkState(keyWords, 0, flags); + this.keyWords = keyWords.clone(); + this.cvStack = new java.util.ArrayList<>(); + this.flags = flags; + } + + static Hasher createNew() { + int[] iv = {IV[0], IV[1], IV[2], IV[3], IV[4], IV[5], IV[6], IV[7]}; + return new Hasher(iv, 0); + } + + static Hasher createKeyed(byte[] key) { + if (key.length != KEY_LEN) { + throw new IllegalArgumentException("Key must be " + KEY_LEN + " bytes"); + } + int[] keyWords = wordsFromLittleEndianBytes(key); + return new Hasher(keyWords, KEYED_HASH); + } + + void pushStack(int[] cv) { + cvStack.add(cv); + } + + int[] popStack() { + return cvStack.remove(cvStack.size() - 1); + } + + void addChunkChainingValue(int[] newCv, long totalChunks) { + int[] cv = newCv; + long chunks = totalChunks; + + while ((chunks & 1) == 0) { + cv = parentCv(popStack(), cv, this.keyWords, this.flags); + chunks >>>= 1; + } + + pushStack(cv); + } + + void update(byte[] input) { + int offset = 0; + + while (offset < input.length) { + if (this.chunkState.len() == CHUNK_LEN) { + int[] chunkCv = this.chunkState.output().chainingValue(); + long totalChunks = this.chunkState.chunkCounter + 1; + addChunkChainingValue(chunkCv, totalChunks); + this.chunkState = new ChunkState(this.keyWords, totalChunks, this.flags); + } + + int want = CHUNK_LEN - this.chunkState.len(); + int take = Math.min(want, input.length - offset); + byte[] chunk = new byte[take]; + System.arraycopy(input, offset, chunk, 0, take); + this.chunkState.update(chunk); + offset += take; + } + } + + byte[] finalize(int outLen) { + Output output = this.chunkState.output(); + int parentNodesRemaining = this.cvStack.size(); + + while (parentNodesRemaining > 0) { + parentNodesRemaining--; + output = parentOutput( + this.cvStack.get(parentNodesRemaining), + output.chainingValue(), + this.keyWords, + this.flags + ); + } + + return output.rootOutputBytes(outLen); + } + } + + private static byte[] blake3(byte[] data) { + Hasher hasher = Hasher.createNew(); + hasher.update(data); + return hasher.finalize(OUT_LEN); + } + + private static byte[] blake3Keyed(byte[] data, byte[] key) { + Hasher hasher = Hasher.createKeyed(key); + hasher.update(data); + return hasher.finalize(OUT_LEN); + } + + private static byte[] blake3Keyed(byte[] data, byte[] key, int outLen) { + Hasher hasher = Hasher.createKeyed(key); + hasher.update(data); + return hasher.finalize(outLen); + } + + private static int[] bytesToIntsLittleEndian(byte[] bytes) { + int[] ints = new int[bytes.length / 4]; + for (int i = 0; i < ints.length; i++) { + ints[i] = ((bytes[i * 4 + 0] & 0xFF)) | + ((bytes[i * 4 + 1] & 0xFF) << 8) | + ((bytes[i * 4 + 2] & 0xFF) << 16) | + ((bytes[i * 4 + 3] & 0xFF) << 24); + } + return ints; + } + + private static int[] blake3Ints(byte[] data, int outLen) { + byte[] result = blake3(data); + if (outLen <= 32) { + return bytesToIntsLittleEndian(Arrays.copyOf(result, outLen)); + } + return bytesToIntsLittleEndian(result); + } + + private static long[] getSaltHash() { + final SaltHolder currHolder = saltHolderLocal.get(); + + return currHolder.cachedSaltHash; + } + + private static long[] hashWorldSeedInternal(long[] worldSeed) { + byte[] seedBytes = new byte[worldSeed.length * 8]; + ByteBuffer buffer = ByteBuffer.wrap(seedBytes).order(ByteOrder.LITTLE_ENDIAN); + for (long l : worldSeed) { + buffer.putLong(l); + } + + byte[] hashBytes = blake3(seedBytes); + int[] hashInts = bytesToIntsLittleEndian(hashBytes); + + long[] result = new long[8]; + for (int i = 0; i < 8; i++) { + result[i] = hashInts[i] & 0xFFFFFFFFL; + } + return result; + } + + public static long[] hashWorldSeed(long[] worldSeed) { + if (!SecureSeedConfig.enabled && SecureSeedConfig.version == 2) { + return worldSeed.clone(); + } + + long[] saltHashValue = getSaltHash(); + long[] saltedSeed = new long[worldSeed.length]; + + for (int i = 0; i < worldSeed.length; i++) { + saltedSeed[i] = worldSeed[i] ^ saltHashValue[i % saltHashValue.length]; + } + + return hashWorldSeedInternal(saltedSeed); + } + + public static long[] expandLevelSeedTo1024Bits(long levelSeed) { + if (!SecureSeedConfig.enabled && SecureSeedConfig.version == 2) { + long[] result = new long[Globals.WORLD_SEED_LONGS]; + for (int i = 0; i < Globals.WORLD_SEED_LONGS; i++) { + result[i] = levelSeed ^ (i * 0x9E3779B97F4A7C15L); + } + return result; + } + + String salt = SecureSeedConfig.salt; + byte[] saltBytes = salt.getBytes(java.nio.charset.StandardCharsets.UTF_8); + byte[] saltKey = blake3(saltBytes); + + long[] result = new long[Globals.WORLD_SEED_LONGS]; + + for (int segment = 0; segment < Globals.WORLD_SEED_LONGS; segment++) { + byte[] input = new byte[16]; + ByteBuffer buf = ByteBuffer.wrap(input).order(ByteOrder.LITTLE_ENDIAN); + buf.putLong(0, levelSeed); + buf.putLong(8, segment); + + byte[] hash = blake3Keyed(input, saltKey, 32); + int[] hashInts = bytesToIntsLittleEndian(hash); + + result[segment] = ((long) hashInts[0] & 0xFFFFFFFFL) | + (((long) hashInts[1] & 0xFFFFFFFFL) << 32); + } + + return result; + } + + public static long getTerrainSeed(long[] hashedSeed, TerrainType type) { + return hashedSeed[type.ordinal() % hashedSeed.length]; + } + + public enum TerrainType { + BASE_TERRAIN, + BIOME_NOISE, + CLIMATE, + AQUIFER, + ORE, + SURFACE, + VEGETATION, + SHIFT + } + + public static void hash(long[] message, long[] output, long[] state, int outputBytes, boolean finalBlock) { + int len = message.length * 8; + byte[] buffer = threadBuffer.get(); + if (buffer.length < len) { + buffer = new byte[len]; + threadBuffer.set(buffer); + } + + ByteBuffer buf = threadByteBuffer.get(); + if (buf.capacity() < len) { + buf = ByteBuffer.allocate(len).order(ByteOrder.LITTLE_ENDIAN); + threadByteBuffer.set(buf); + } + buf.clear(); + + for (long l : message) { + buf.putLong(l); + } + + byte[] hashBytes = blake3(Arrays.copyOf(buffer, len)); + int[] hashInts = bytesToIntsLittleEndian(hashBytes); + + for (int i = 0; i < Math.min(hashInts.length, output.length); i++) { + output[i] = hashInts[i] & 0xFFFFFFFFL; + } + } +} diff --git a/lophine-server/src/main/java/su/plo/matter/WorldgenCryptoRandom.java b/lophine-server/src/main/java/su/plo/matter/WorldgenCryptoRandom.java new file mode 100644 index 0000000..dc53035 --- /dev/null +++ b/lophine-server/src/main/java/su/plo/matter/WorldgenCryptoRandom.java @@ -0,0 +1,190 @@ +package su.plo.matter; + +import me.earthme.luminol.config.modules.function.SecureSeedConfig; +import net.minecraft.util.Mth; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.levelgen.LegacyRandomSource; +import net.minecraft.world.level.levelgen.WorldgenRandom; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; + +public class WorldgenCryptoRandom extends WorldgenRandom { + // hash the world seed to guard against badly chosen world seeds + private static final long[] HASHED_ZERO_SEED = Hashing.hashWorldSeed(new long[Globals.WORLD_SEED_LONGS]); + private static final ThreadLocal LAST_SEEN_WORLD_SEED = ThreadLocal.withInitial(() -> new long[Globals.WORLD_SEED_LONGS]); + private static final ThreadLocal HASHED_WORLD_SEED = ThreadLocal.withInitial(() -> HASHED_ZERO_SEED); + + private final long[] worldSeed = new long[Globals.WORLD_SEED_LONGS]; + private final long[] randomBits = new long[8]; + private int randomBitIndex; + private static final int MAX_RANDOM_BIT_INDEX = 64 * 8; + private static final int LOG2_MAX_RANDOM_BIT_INDEX = 9; + private long counter; + private final long[] message = new long[16]; + private final long[] cachedInternalState = new long[16]; + + public WorldgenCryptoRandom(int x, int z, Globals.Salt typeSalt, long salt) { + super(new LegacyRandomSource(0L)); + + if (typeSalt == null) { + return; + } + + if (SecureSeedConfig.enabled) { + this.setSecureSeed(x, z, typeSalt, salt); + } else { + super.setSeed(((long) x << 32) | ((long) z & 0xffffffffL) ^ salt); + } + } + + public void setSecureSeed(int x, int z, Globals.Salt typeSalt, long salt) { + if (!SecureSeedConfig.enabled) { + super.setSeed(((long) x << 32) | ((long) z & 0xffffffffL) ^ salt); + return; + } + + System.arraycopy(Globals.worldSeed, 0, this.worldSeed, 0, Globals.WORLD_SEED_LONGS); + message[0] = ((long) x << 32) | ((long) z & 0xffffffffL); + message[1] = ((long) Globals.dimension.get() << 32) | ((long) salt & 0xffffffffL); + message[2] = typeSalt.ordinal(); + message[3] = counter = 0; + randomBitIndex = MAX_RANDOM_BIT_INDEX; + } + + private long[] getHashedWorldSeed() { + if (!Arrays.equals(worldSeed, LAST_SEEN_WORLD_SEED.get())) { + HASHED_WORLD_SEED.set(Hashing.hashWorldSeed(worldSeed)); + System.arraycopy(worldSeed, 0, LAST_SEEN_WORLD_SEED.get(), 0, Globals.WORLD_SEED_LONGS); + } + return HASHED_WORLD_SEED.get(); + } + + private void moreRandomBits() { + message[3] = counter++; + System.arraycopy(getHashedWorldSeed(), 0, randomBits, 0, 8); + Hashing.hash(message, randomBits, cachedInternalState, 64, true); + } + + private long getBits(int count) { + if (randomBitIndex >= MAX_RANDOM_BIT_INDEX) { + moreRandomBits(); + randomBitIndex -= MAX_RANDOM_BIT_INDEX; + } + + int alignment = randomBitIndex & 63; + if ((randomBitIndex >>> 6) == ((randomBitIndex + count) >>> 6)) { + long result = (randomBits[randomBitIndex >>> 6] >>> alignment) & ((1L << count) - 1); + randomBitIndex += count; + return result; + } else { + long result = (randomBits[randomBitIndex >>> 6] >>> alignment) & ((1L << (64 - alignment)) - 1); + randomBitIndex += count; + if (randomBitIndex >= MAX_RANDOM_BIT_INDEX) { + moreRandomBits(); + randomBitIndex -= MAX_RANDOM_BIT_INDEX; + } + alignment = randomBitIndex & 63; + result <<= alignment; + result |= (randomBits[randomBitIndex >>> 6] >>> (64 - alignment)) & ((1L << alignment) - 1); + + return result; + } + } + + @Override + public @NotNull RandomSource fork() { + if (!SecureSeedConfig.enabled) { + return super.fork(); + } + + WorldgenCryptoRandom fork = new WorldgenCryptoRandom(0, 0, null, 0); + + System.arraycopy(Globals.worldSeed, 0, fork.worldSeed, 0, Globals.WORLD_SEED_LONGS); + fork.message[0] = this.message[0]; + fork.message[1] = this.message[1]; + fork.message[2] = this.message[2]; + fork.message[3] = this.message[3]; + fork.randomBitIndex = this.randomBitIndex; + fork.counter = this.counter; + fork.nextLong(); + + return fork; + } + + @Override + public int next(int bits) { + return SecureSeedConfig.enabled ? (int) getBits(bits) : super.next(bits); + } + + @Override + public void consumeCount(int count) { + if (!SecureSeedConfig.enabled) { + return; + } + + randomBitIndex += count; + if (randomBitIndex >= MAX_RANDOM_BIT_INDEX * 2) { + randomBitIndex -= MAX_RANDOM_BIT_INDEX; + counter += randomBitIndex >>> LOG2_MAX_RANDOM_BIT_INDEX; + randomBitIndex &= MAX_RANDOM_BIT_INDEX - 1; + randomBitIndex += MAX_RANDOM_BIT_INDEX; + } + } + + @Override + public int nextInt(int bound) { + if (!SecureSeedConfig.enabled) { + return super.nextInt(bound); + } + int bits = Mth.ceillog2(bound); + int result; + do { + result = (int) getBits(bits); + } while (result >= bound); + + return result; + } + + @Override + public long nextLong() { + return SecureSeedConfig.enabled ? getBits(64) : super.nextLong(); + } + + @Override + public double nextDouble() { + return SecureSeedConfig.enabled ? (getBits(53) * 0x1.0p-53) : super.nextDouble(); + } + + @Override + public long setDecorationSeed(long worldSeed, int blockX, int blockZ) { + if (!SecureSeedConfig.enabled) { + return super.setDecorationSeed(worldSeed, blockX, blockZ); + } + setSecureSeed(blockX, blockZ, Globals.Salt.POPULATION, 0); + return ((long) blockX << 32) | ((long) blockZ & 0xffffffffL); + } + + @Override + public void setFeatureSeed(long populationSeed, int index, int step) { + if (!SecureSeedConfig.enabled) { + super.setFeatureSeed(populationSeed, index, step); + return; + } + setSecureSeed((int) (populationSeed >> 32), (int) populationSeed, Globals.Salt.DECORATION, index + 10000L * step); + } + + @Override + public void setLargeFeatureSeed(long worldSeed, int chunkX, int chunkZ) { + super.setLargeFeatureSeed(worldSeed, chunkX, chunkZ); + } + + @Override + public void setLargeFeatureWithSalt(long worldSeed, int regionX, int regionZ, int salt) { + super.setLargeFeatureWithSalt(worldSeed, regionX, regionZ, salt); + } + + public static RandomSource seedSlimeChunk(int chunkX, int chunkZ) { + return new WorldgenCryptoRandom(chunkX, chunkZ, Globals.Salt.SLIME_CHUNK, 0); + } +} \ No newline at end of file