From d5c9306a7f5107e627b5f3733e366d0b5316292b Mon Sep 17 00:00:00 2001 From: violetc <58360096+s-yh-china@users.noreply.github.com> Date: Fri, 14 Feb 2025 23:55:46 +0800 Subject: [PATCH] 1.21.4 (#413) * init 1.21.4, and boom! * build change, but weight not work * just work * Build changes, and delete timings * Fix API patches (#406) * Fix API patches * merge --------- Co-authored-by: violetc <58360096+s-yh-china@users.noreply.github.com> * 0006/0129 * 0009/0129 * 0011/0129 * 0018/0129 * 0030/0129 * 0035/0129 * 0043/0129 * 0048/0129 * 0049/0129 * 0057/0129 * 0065/0129 * 0086/0129 (#408) * 0072/0129 * 0080/0129 * Readd patch infos * 0086/0129 * Delete applied patches * 0087/0129 * 0091/0129 * 0097/0129 * 0101/0129 * 102/129 * 0107/0129 * 0112/0129 * 0118/0129 * 0129/0129, 100% patched * fix some * server work * Protocol... (#409) * Jade v7 * Fix changed part for Jade * Formatting imports, add Lms Paster protocol * REI payloads 5/8 * Add REI support, remove unnecessary content in Jade * Rename * Make jade better * Make action work * fix action jar * Fix some protocol * Fix bot action, and entity tickCount * Fix Warden GameEventListener register on load * Fix extra Raider drop * Fix grindstone overstacking * Update Paper, and some doc * Merge * [ci skip] Update Action --------- Co-authored-by: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/test.yml | 8 +- .gitignore | 23 +- README.md | 8 +- README_cn.md | 8 +- build.gradle.kts | 104 +- docs/CONTRIBUTING.md | 6 +- docs/CONTRIBUTING_cn.md | 6 +- gradle.properties | 8 +- gradle/wrapper/gradle-wrapper.properties | 2 +- leaves-api/build.gradle.kts.patch | 84 + .../features/0001-Delete-Timings.patch | 816 +- ...Add-isShrink-to-EntityResurrectEvent.patch | 0 .../features}/0003-Add-fakeplayer-api.patch | 10 +- .../0004-Player-operation-limiter.patch | 0 .../0005-Force-peaceful-mode-switch.patch | 4 +- .../features/0006-Replay-Mod-API.patch | 14 +- .../features/0007-Bytebuf-API.patch | 14 +- .../features/0008-Revert-raid-changes.patch | 20 + leaves-server/build.gradle.kts.patch | 155 + .../features/0001-Build-changes.patch | 70 + ...002-Leaves-Server-Config-And-Command.patch | 19 + .../features/0003-Leaves-Protocol-Core.patch | 94 + .../0004-Fix-trading-with-the-void.patch | 12 +- ...nowball-and-egg-can-knockback-player.patch | 33 +- .../features/0006-Leaves-Fakeplayer.patch | 479 ++ ...hears-in-dispenser-can-unlimited-use.patch | 19 + .../0008-Redstone-Shears-Wrench.patch | 102 + ...Add-isShrink-to-EntityResurrectEvent.patch | 27 + ...-Budding-Amethyst-can-push-by-piston.patch | 38 +- .../0011-Spectator-dont-get-Advancement.patch | 12 +- ...ick-can-change-ArmorStand-arm-status.patch | 23 + .../features/0013-Configurable-MC-59471.patch | 39 + .../features/0014-No-chat-sign.patch | 147 + ...015-Dont-send-useless-entity-packets.patch | 28 +- .../features/0016-Optimize-suffocation.patch | 33 + ...check-for-spooky-season-once-an-hour.patch | 32 +- ...18-Config-to-disable-method-profiler.patch | 32 + ...oal-selector-during-inactive-ticking.patch | 10 +- .../0020-Reduce-entity-allocations.patch | 16 +- ...021-Remove-lambda-from-ticking-guard.patch | 36 + ...-Cache-climbing-check-for-activation.patch | 47 + .../0023-Reduce-chunk-loading-lookups.patch | 43 + ...024-InstantBlockUpdater-Reintroduced.patch | 24 +- ...ndom-flatten-triangular-distribution.patch | 39 + .../features/0026-BBOR-Protocol.patch | 34 + .../features/0027-PCA-sync-protocol.patch | 289 + .../features/0028-Jade-Protocol.patch | 111 + ...Alternative-block-placement-Protocol.patch | 101 + .../0030-Player-operation-limiter.patch | 66 +- .../features/0031-Renewable-Elytra.patch | 18 +- .../0032-Stackable-ShulkerBoxes.patch | 393 + .../0033-MC-Technical-Survival-Mode.patch | 81 + .../0034-Return-nether-portal-fix.patch | 95 + .../0035-Leaves-Extra-Yggdrasil-Service.patch | 45 + .../0036-Configurable-vanilla-random.patch | 86 + .../0037-Catch-update-suppression-crash.patch | 81 + .../features/0038-Bedrock-break-list.patch | 81 + .../features/0039-Fix-trapdoor-feature.patch | 31 + ...e-distance-check-for-UseItemOnPacket.patch | 19 + .../0041-No-feather-falling-trample.patch | 24 + .../features/0042-Syncmatica-Protocol.patch | 23 + .../0043-Shared-villager-discounts.patch | 18 +- ...one-wire-dont-connect-if-on-trapdoor.patch | 28 + ...5-Disable-check-out-of-order-command.patch | 8 +- .../0046-Despawn-enderman-with-block.patch | 12 +- .../features/0047-Creative-fly-no-clip.patch | 121 + .../0048-Optimized-dragon-respawn.patch | 75 +- .../features/0049-Shave-snow-layers.patch | 37 + ...050-Elytra-aeronautics-no-chunk-load.patch | 80 + .../features/0051-Cache-ignite-odds.patch | 63 + .../features/0052-Lava-riptide.patch | 28 + .../0053-No-block-update-command.patch | 93 + ...0054-Raider-die-skip-self-raid-check.patch | 19 + .../0055-Container-open-passthrough.patch | 80 + ...Dont-respond-ping-before-start-fully.patch | 24 + .../0057-Faster-chunk-serialization.patch | 272 + ...-Skip-secondary-POI-sensor-if-absent.patch | 12 +- .../0059-Store-mob-counts-in-an-array.patch | 24 +- .../0060-Optimize-noise-generation.patch | 281 + .../0061-Reduce-array-allocations.patch | 281 + .../0062-Optimize-sun-burn-tick.patch | 55 +- ...d-Optional-allocation-in-EntityBased.patch | 22 +- ...signableFrom-call-in-ClassInstanceMu.patch | 10 +- .../0065-Optimized-CubePointRange.patch | 30 +- ...ck-frozen-ticks-before-landing-block.patch | 24 + ...Skip-entity-move-if-movement-is-zero.patch | 24 +- ...68-Skip-cloning-advancement-criteria.patch | 8 +- ...69-Fix-villagers-dont-release-memory.patch | 43 + .../0070-Avoid-anvil-too-expensive.patch | 12 +- .../features/0071-Bow-infinity-fix.patch | 12 +- .../features/0072-Zero-tick-plants.patch | 85 + .../0073-Force-peaceful-mode-switch.patch | 100 + .../features/0074-Replay-Mod-API.patch | 357 + .../features/0075-Leaves-I18n.patch | 65 + ...craft-hopper-not-work-without-player.patch | 10 +- .../features/0077-RNG-Fishing.patch | 19 + .../features/0078-Wool-Hopper-Counter.patch | 52 + .../0079-Spider-jockeys-drop-gapples.patch | 14 +- .../features/0080-Force-Void-Trade.patch | 139 +- .../0081-Villager-infinite-discounts.patch | 22 + .../0082-CCE-update-suppression.patch | 29 + ...83-Disable-offline-warn-if-use-proxy.patch | 14 +- ...or-stand-cant-kill-by-mob-projectile.patch | 26 + .../0085-Make-Item-tick-vanilla.patch | 29 + .../0086-Copper-Bulb-1-gt-delay.patch | 20 +- .../features/0087-Crafter-1-gt-delay.patch | 28 + .../0088-More-Region-Format-Support.patch | 399 + .../features/0089-No-TNT-place-update.patch | 19 + .../features/0090-Servux-Protocol.patch | 18 + ...ked-hopper-no-longer-send-NC-updates.patch | 10 +- .../features/0092-Renewable-deepslate.patch | 32 + .../features/0093-Renewable-sponges.patch | 22 +- .../features/0094-Renewable-coral.patch | 109 + .../features/0095-Fast-resume.patch | 80 + .../features/0096-Vanilla-hopper.patch | 42 +- .../0097-Old-hopper-suckin-behavior.patch | 19 + ...8-Fix-falling-block-s-block-location.patch | 26 + .../features/0099-Bytebuf-API.patch | 36 + .../0100-Allow-grindstone-overstacking.patch | 63 + .../features/0101-Configurable-MC-67.patch | 18 + ...le-end-gateway-portal-entity-ticking.patch | 27 + ...sable-crystal-portal-proximity-check.patch | 64 + ...able-LivingEntity-aiStep-alive-check.patch | 10 +- .../0105-Fix-fortress-mob-spawn.patch | 12 +- ...106-Fix-FallingBlockEntity-Duplicate.patch | 19 + .../0107-Old-BlockEntity-behaviour.patch | 90 + .../features/0108-Revert-raid-changes.patch | 70 + ...09-Allow-anvil-destroy-item-entities.patch | 18 +- ...t-Collision-Behavior-for-Block-Shape.patch | 10 +- .../0111-Disable-vault-blacklist.patch | 42 + ...0112-Fix-EntityPortalExitEvent-logic.patch | 44 + .../0113-Fix-CraftPortalEvent-logic.patch | 26 + .../features/0114-Xaero-Map-Protocol.patch | 18 + .../0115-Dont-save-Entity-tickCount.patch | 31 + ...n-GameEventListener-register-on-load.patch | 20 + .../minecraft/world/entity/Entity.java.patch | 40 + .../features}/0001-Build-changes.patch | 274 +- .../features/0002-Delete-Timings.patch | 24 +- ...003-Leaves-Server-Config-And-Command.patch | 50 + .../features/0004-Leaves-Protocol-Core.patch | 26 + .../features/0005-Leaves-Fakeplayer.patch | 158 + .../features/0006-No-chat-sign.patch | 19 + .../0007-MC-Technical-Survival-Mode.patch | 19 + .../0008-Leaves-Extra-Yggdrasil-Service.patch | 19 + .../features/0009-Disable-packet-limit.patch | 4 +- .../0010-Reduce-array-allocations.patch | 62 + .../0011-Force-peaceful-mode-switch.patch | 29 + .../features/0012-Replay-Mod-API.patch | 78 + .../0013-Force-minecraft-command.patch | 0 .../features/0014-Bytebuf-API.patch | 55 + .../features/0015-Leaves-plugins.patch | 145 + .../paper/PaperVersionFetcher.java.patch | 20 + .../util/CraftMagicNumbers.java.patch | 11 + .../java/org/leavesmc/leaves/.editorconfig | 2 + .../org/leavesmc/leaves/LeavesConfig.java | 980 +++ .../org/leavesmc/leaves/LeavesLogger.java | 24 + .../org/leavesmc/leaves/bot/BotCommand.java | 544 ++ .../leavesmc/leaves/bot/BotCreateState.java | 120 + .../leavesmc/leaves/bot/BotDataStorage.java | 121 + .../leaves/bot/BotInventoryContainer.java | 191 + .../java/org/leavesmc/leaves/bot/BotList.java | 297 + .../leavesmc/leaves/bot/BotStatsCounter.java | 36 + .../java/org/leavesmc/leaves/bot/BotUtil.java | 73 + .../leaves/bot/IPlayerDataStorage.java | 13 + .../org/leavesmc/leaves/bot/MojangAPI.java | 39 + .../org/leavesmc/leaves/bot/ServerBot.java | 553 ++ .../leaves/bot/ServerBotGameMode.java | 127 + .../bot/ServerBotPacketListenerImpl.java | 85 + .../leavesmc/leaves/bot/agent/Actions.java | 67 + .../leavesmc/leaves/bot/agent/BotAction.java | 163 + .../leavesmc/leaves/bot/agent/BotConfig.java | 62 + .../leavesmc/leaves/bot/agent/Configs.java | 44 + .../agent/actions/AbstractTimerAction.java | 25 + .../bot/agent/actions/AttackAction.java | 22 + .../bot/agent/actions/BreakBlockAction.java | 75 + .../bot/agent/actions/CraftBotAction.java | 54 + .../agent/actions/CraftCustomBotAction.java | 49 + .../leaves/bot/agent/actions/DropAction.java | 25 + .../leaves/bot/agent/actions/FishAction.java | 73 + .../leaves/bot/agent/actions/JumpAction.java | 21 + .../leaves/bot/agent/actions/LookAction.java | 63 + .../bot/agent/actions/RotateAction.java | 51 + .../bot/agent/actions/RotationAction.java | 65 + .../leaves/bot/agent/actions/SneakAction.java | 27 + .../leaves/bot/agent/actions/SwimAction.java | 30 + .../bot/agent/actions/UseItemAction.java | 26 + .../agent/actions/UseItemOffHandAction.java | 26 + .../bot/agent/actions/UseItemOnAction.java | 45 + .../agent/actions/UseItemOnOffhandAction.java | 45 + .../bot/agent/actions/UseItemToAction.java | 27 + .../agent/actions/UseItemToOffhandAction.java | 27 + .../agent/configs/AlwaysSendDataConfig.java | 45 + .../configs/SimulationDistanceConfig.java | 47 + .../bot/agent/configs/SkipSleepConfig.java | 42 + .../bot/agent/configs/SpawnPhantomConfig.java | 51 + .../leaves/bytebuf/SimpleBytebufManager.java | 35 + .../leaves/bytebuf/WrappedBytebuf.java | 277 + .../internal/InternalBytebufHandler.java | 234 + .../leaves/command/CommandArgument.java | 55 + .../leaves/command/CommandArgumentResult.java | 69 + .../leaves/command/CommandArgumentType.java | 55 + .../leaves/command/LeavesCommand.java | 121 + .../leaves/command/LeavesCommandUtil.java | 81 + .../leaves/command/LeavesSubcommand.java | 18 + .../leaves/command/NoBlockUpdateCommand.java | 52 + .../command/subcommands/ConfigCommand.java | 83 + .../command/subcommands/CounterCommand.java | 121 + .../PeacefulModeSwitchCommand.java | 87 + .../command/subcommands/ReloadCommand.java | 22 + .../command/subcommands/UpdateCommand.java | 21 + .../leaves/config/ConfigConverter.java | 20 + .../leaves/config/ConfigValidator.java | 15 + .../leaves/config/ConfigValidatorImpl.java | 108 + .../leavesmc/leaves/config/GlobalConfig.java | 16 + .../leaves/config/GlobalConfigCategory.java | 12 + .../leaves/config/GlobalConfigCreator.java | 84 + .../leaves/config/GlobalConfigManager.java | 232 + .../leavesmc/leaves/config/RemovedConfig.java | 26 + .../org/leavesmc/leaves/entity/CraftBot.java | 94 + .../leaves/entity/CraftBotManager.java | 80 + .../leaves/entity/CraftPhotographer.java | 73 + .../entity/CraftPhotographerManager.java | 84 + .../world/chunk/LithiumHashPalette.java | 197 + .../plugin/MinecraftInternalPlugin.java | 151 + .../provider/LeavesPluginProviderFactory.java | 57 + .../configuration/LeavesPluginMeta.java | 90 + .../profile/LeavesAuthenticationService.java | 18 + .../LeavesMinecraftSessionService.java | 102 + .../leaves/protocol/AppleSkinProtocol.java | 127 + .../leaves/protocol/BBORProtocol.java | 227 + .../CarpetAlternativeBlockPlacement.java | 167 + .../leaves/protocol/CarpetServerProtocol.java | 120 + .../leaves/protocol/LMSPasterProtocol.java | 150 + .../protocol/LitematicaEasyPlaceProtocol.java | 214 + .../leaves/protocol/PcaSyncProtocol.java | 432 + .../leaves/protocol/XaeroMapProtocol.java | 42 + .../protocol/bladeren/BladerenProtocol.java | 150 + .../protocol/bladeren/MsptSyncProtocol.java | 77 + .../protocol/core/LeavesCustomPayload.java | 29 + .../leaves/protocol/core/LeavesProtocol.java | 12 + .../protocol/core/LeavesProtocolManager.java | 432 + .../leaves/protocol/core/ProtocolHandler.java | 55 + .../leaves/protocol/core/ProtocolUtils.java | 52 + .../leaves/protocol/jade/JadeProtocol.java | 296 + .../protocol/jade/accessor/Accessor.java | 32 + .../protocol/jade/accessor/AccessorImpl.java | 82 + .../protocol/jade/accessor/BlockAccessor.java | 50 + .../jade/accessor/BlockAccessorImpl.java | 163 + .../jade/accessor/EntityAccessor.java | 44 + .../jade/accessor/EntityAccessorImpl.java | 123 + .../jade/payload/ClientHandshakePayload.java | 35 + .../jade/payload/ReceiveDataPayload.java | 28 + .../jade/payload/RequestBlockPayload.java | 51 + .../jade/payload/RequestEntityPayload.java | 53 + .../jade/payload/ServerHandshakePayload.java | 52 + .../protocol/jade/provider/IJadeProvider.java | 12 + .../jade/provider/IServerDataProvider.java | 8 + .../provider/IServerExtensionProvider.java | 10 + .../ItemStorageExtensionProvider.java | 138 + .../jade/provider/ItemStorageProvider.java | 88 + .../provider/StreamServerDataProvider.java | 26 + .../jade/provider/block/BeehiveProvider.java | 34 + .../provider/block/BrewingStandProvider.java | 43 + .../jade/provider/block/CampfireProvider.java | 55 + .../block/ChiseledBookshelfProvider.java | 39 + .../provider/block/CommandBlockProvider.java | 40 + .../jade/provider/block/FurnaceProvider.java | 60 + .../provider/block/HopperLockProvider.java | 32 + .../jade/provider/block/JukeboxProvider.java | 32 + .../jade/provider/block/LecternProvider.java | 33 + .../block/MobSpawnerCooldownProvider.java | 42 + .../provider/block/ObjectNameProvider.java | 63 + .../jade/provider/block/RedstoneProvider.java | 36 + .../provider/entity/AnimalOwnerProvider.java | 43 + .../provider/entity/MobBreedingProvider.java | 44 + .../provider/entity/MobGrowthProvider.java | 43 + .../entity/NextEntityDropProvider.java | 35 + .../provider/entity/PetArmorProvider.java | 35 + .../entity/StatusEffectsProvider.java | 45 + .../entity/ZombieVillagerProvider.java | 34 + .../protocol/jade/tool/ShearsToolHandler.java | 32 + .../protocol/jade/tool/SimpleToolHandler.java | 71 + .../protocol/jade/tool/ToolHandler.java | 17 + .../leaves/protocol/jade/util/CommonUtil.java | 79 + .../protocol/jade/util/HierarchyLookup.java | 139 + .../protocol/jade/util/IHierarchyLookup.java | 66 + .../protocol/jade/util/ItemCollector.java | 118 + .../protocol/jade/util/ItemIterator.java | 102 + .../leaves/protocol/jade/util/JadeCodec.java | 59 + .../jade/util/LootTableMineableCollector.java | 109 + .../jade/util/PairHierarchyLookup.java | 120 + .../protocol/jade/util/PriorityStore.java | 40 + .../leaves/protocol/jade/util/ViewGroup.java | 62 + .../jade/util/WrappedHierarchyLookup.java | 104 + .../protocol/rei/REIServerProtocol.java | 107 + .../rei/payload/CreateItemGrabPayload.java | 28 + .../rei/payload/CreateItemHotbarPayload.java | 28 + .../rei/payload/CreateItemMessagePayload.java | 29 + .../rei/payload/CreateItemPayload.java | 28 + .../protocol/rei/payload/MoveItemPayload.java | 30 + .../protocol/servux/PacketSplitter.java | 117 + .../servux/ServuxEntityDataProtocol.java | 278 + .../protocol/servux/ServuxProtocol.java | 16 + .../servux/ServuxStructuresProtocol.java | 452 ++ .../syncmatica/CommunicationManager.java | 396 + .../leaves/protocol/syncmatica/Feature.java | 23 + .../protocol/syncmatica/FeatureSet.java | 67 + .../protocol/syncmatica/FileStorage.java | 80 + .../syncmatica/LocalLitematicState.java | 24 + .../protocol/syncmatica/MessageType.java | 8 + .../protocol/syncmatica/PacketType.java | 30 + .../protocol/syncmatica/PlayerIdentifier.java | 37 + .../syncmatica/PlayerIdentifierProvider.java | 46 + .../protocol/syncmatica/ServerPlacement.java | 166 + .../protocol/syncmatica/ServerPosition.java | 51 + .../protocol/syncmatica/SubRegionData.java | 90 + .../SubRegionPlacementModification.java | 65 + .../protocol/syncmatica/SyncmaticManager.java | 108 + .../syncmatica/SyncmaticaPayload.java | 26 + .../syncmatica/SyncmaticaProtocol.java | 127 + .../syncmatica/exchange/AbstractExchange.java | 66 + .../syncmatica/exchange/DownloadExchange.java | 125 + .../syncmatica/exchange/Exchange.java | 20 + .../syncmatica/exchange/ExchangeTarget.java | 39 + .../syncmatica/exchange/FeatureExchange.java | 48 + .../exchange/ModifyExchangeServer.java | 81 + .../syncmatica/exchange/UploadExchange.java | 101 + .../exchange/VersionHandshakeServer.java | 66 + .../leavesmc/leaves/region/IRegionFile.java | 40 + .../leaves/region/IRegionFileFactory.java | 85 + .../leavesmc/leaves/region/LeavesHooks.java | 20 + .../leaves/region/RegionFileFormat.java | 16 + .../region/linear/LinearRegionFile.java | 651 ++ .../leaves/region/linear/LinearVersion.java | 5 + .../leaves/replay/DigestOutputStream.java | 46 + .../leaves/replay/RecordMetaData.java | 23 + .../org/leavesmc/leaves/replay/Recorder.java | 285 + .../leaves/replay/RecorderOption.java | 57 + .../leavesmc/leaves/replay/ReplayFile.java | 198 + .../leavesmc/leaves/replay/ReplayMarker.java | 43 + .../leaves/replay/ServerPhotographer.java | 222 + .../replay/ServerPhotographerGameMode.java | 35 + .../spark/LeavesServerConfigProvider.java | 137 + .../leaves/spark/LeavesSparkPlugin.java | 28 + .../leavesmc/leaves/util/ArrayConstants.java | 21 + .../leaves/util/BlockPatternHelper.java | 28 + .../leaves/util/BreakBedrockList.java | 114 + .../leaves/util/ElytraAeronauticsHelper.java | 40 + .../leaves/util/FertilizableCoral.java | 72 + .../leavesmc/leaves/util/HopperCounter.java | 337 + .../leaves/util/LeavesUpdateHelper.java | 249 + .../leaves/util/LeavesVersionFetcher.java | 128 + .../org/leavesmc/leaves/util/MathUtils.java | 96 + .../leaves/util/McTechnicalModeHelper.java | 27 + .../leaves/util/ReturnPortalManager.java | 98 + .../leavesmc/leaves/util/ShulkerBoxUtils.java | 39 + .../leavesmc/leaves/util/TicketHelper.java | 175 + .../leavesmc/leaves/util/UUIDSerializer.java | 17 + .../util/UpdateSuppressionException.java | 32 + .../util/VillagerInfiniteDiscountHelper.java | 18 + .../org/leavesmc/leaves/util/WoolUtils.java | 38 + .../assets/minecraft/lang/zh_cn.json | 7109 +++++++++++++++++ patches/api/0001-Leaves-Server-Config.patch | 25 - ...Hide-irrelevant-compilation-warnings.patch | 35 - patches/api/0006-SIMD-support.patch | 26 - ...ve-iterators-from-inventory-contains.patch | 0 patches/server/0002-Dev-Fix.patch | 19 - patches/server/0004-Leaves-Server-Utils.patch | 426 - .../0005-Update-version-fetcher-repo.patch | 175 - ...006-Leaves-Server-Config-And-Command.patch | 2051 ----- .../server/0007-Leaves-Protocol-Core.patch | 738 -- patches/server/0010-Fakeplayer-support.patch | 4736 ----------- ...hears-in-dispenser-can-unlimited-use.patch | 19 - .../server/0012-Redstone-Shears-Wrench.patch | 102 - ...Add-isShrink-to-EntityResurrectEvent.patch | 30 - ...ick-can-change-ArmorStand-arm-status.patch | 23 - .../server/0017-Configurable-MC-59471.patch | 42 - patches/server/0018-No-chat-sign.patch | 163 - .../server/0020-Optimize-suffocation.patch | 36 - ...22-Config-to-disable-method-profiler.patch | 32 - ...025-Remove-lambda-from-ticking-guard.patch | 36 - ...-Cache-climbing-check-for-activation.patch | 47 - .../0028-Reduce-chunk-loading-lookups.patch | 43 - ...ndom-flatten-triangular-distribution.patch | 27 - patches/server/0031-BBOR-Protocol.patch | 267 - patches/server/0032-PCA-sync-protocol.patch | 733 -- patches/server/0033-Jade-Protocol.patch | 3540 -------- ...Alternative-block-placement-Protocol.patch | 495 -- .../server/0037-Stackable-ShulkerBoxes.patch | 464 -- .../0038-MC-Technical-Survival-Mode.patch | 126 - .../0039-Return-nether-portal-fix.patch | 203 - patches/server/0040-Appleskin-Protocol.patch | 139 - patches/server/0041-Xaero-Map-Protocol.patch | 66 - .../0042-Leaves-Extra-Yggdrasil-Service.patch | 190 - .../0043-Use-vanilla-random-config.patch | 77 - .../0044-Fix-update-suppression-crash.patch | 123 - patches/server/0045-Bedrock-break-list.patch | 201 - .../server/0046-Fix-trapdoor-feature.patch | 29 - ...e-distance-check-for-UseItemOnPacket.patch | 19 - .../0048-No-feather-falling-trample.patch | 24 - patches/server/0049-Syncmatica-Protocol.patch | 2062 ----- ...one-wire-dont-connect-if-on-trapdoor.patch | 28 - .../server/0054-Leaves-carpet-support.patch | 132 - .../server/0055-Creative-fly-no-clip.patch | 121 - patches/server/0057-Shave-snow-layers.patch | 53 - ...058-Elytra-aeronautics-no-chunk-load.patch | 126 - patches/server/0059-Cache-ignite-odds.patch | 64 - patches/server/0060-Lava-riptide.patch | 28 - .../server/0061-No-block-update-command.patch | 151 - ...0062-Raider-die-skip-self-raid-check.patch | 19 - .../0063-Container-open-passthrough.patch | 55 - patches/server/0064-SIMD-support.patch | 27 - ...Dont-respond-ping-before-start-fully.patch | 24 - .../0066-Faster-chunk-serialization.patch | 501 -- patches/server/0067-Bladeren-Protocol.patch | 162 - .../0069-Bladeren-mspt-sync-protocol.patch | 89 - .../0071-Optimize-noise-generation.patch | 281 - .../0073-Reduce-array-allocations.patch | 445 -- ...ck-frozen-ticks-before-landing-block.patch | 25 - ...81-Fix-villagers-dont-release-memory.patch | 43 - patches/server/0084-Zero-tick-plants.patch | 94 - patches/server/0085-Leaves-Updater.patch | 303 - .../0086-Force-peaceful-mode-switch.patch | 227 - patches/server/0087-Replay-Mod-API.patch | 1599 ---- patches/server/0088-Leaves-I18n.patch | 6288 --------------- patches/server/0090-RNG-Fishing.patch | 19 - patches/server/0091-Wool-Hopper-Counter.patch | 576 -- .../server/0092-Leaves-Reload-Command.patch | 46 - .../0095-Villager-infinite-discounts.patch | 46 - .../server/0096-CCE-update-suppression.patch | 45 - ...or-stand-cant-kill-by-mob-projectile.patch | 25 - .../server/0099-Make-Item-tick-vanilla.patch | 29 - patches/server/0101-Crafter-1-gt-delay.patch | 28 - .../0102-Linear-region-file-format.patch | 887 -- patches/server/0103-No-TNT-place-update.patch | 19 - patches/server/0104-Servux-Protocol.patch | 917 --- patches/server/0106-Renewable-deepslate.patch | 32 - patches/server/0108-Renewable-coral.patch | 148 - patches/server/0109-Fast-resume.patch | 261 - ...2-Fix-falling-block-s-block-location.patch | 25 - patches/server/0113-Bytebuf-API.patch | 649 -- .../0114-Allow-grindstone-overstacking.patch | 28 - patches/server/0115-Configurable-MC-67.patch | 18 - ...le-end-gateway-portal-entity-ticking.patch | 22 - ...sable-crystal-portal-proximity-check.patch | 64 - patches/server/0119-Leaves-plugins.patch | 317 - ...121-Fix-FallingBlockEntity-Duplicate.patch | 19 - .../0122-Old-BlockEntity-behaviour.patch | 83 - patches/server/0123-Revert-raid-changes.patch | 98 - .../server/0126-Disable-vault-blacklist.patch | 39 - .../0127-TEMP-Fix-color-in-console.patch | 20 - ...0128-Fix-EntityPortalExitEvent-logic.patch | 44 - .../0129-Fix-CraftPortalEvent-logic.patch | 26 - scripts/GetReleaseInfo.sh | 2 +- scripts/RemoveOldVersionTags.sh | 19 - settings.gradle.kts | 4 +- 457 files changed, 34990 insertions(+), 33902 deletions(-) create mode 100644 leaves-api/build.gradle.kts.patch rename patches/api/0007-Delete-Timings.patch => leaves-api/paper-patches/features/0001-Delete-Timings.patch (76%) rename {patches/api => leaves-api/paper-patches/features}/0002-Add-isShrink-to-EntityResurrectEvent.patch (100%) rename {patches/api => leaves-api/paper-patches/features}/0003-Add-fakeplayer-api.patch (99%) rename {patches/api => leaves-api/paper-patches/features}/0004-Player-operation-limiter.patch (100%) rename patches/api/0008-Force-peaceful-mode-switch.patch => leaves-api/paper-patches/features/0005-Force-peaceful-mode-switch.patch (83%) rename patches/api/0009-Replay-Mod-API.patch => leaves-api/paper-patches/features/0006-Replay-Mod-API.patch (90%) rename patches/api/0010-Bytebuf-API.patch => leaves-api/paper-patches/features/0007-Bytebuf-API.patch (96%) create mode 100644 leaves-api/paper-patches/features/0008-Revert-raid-changes.patch create mode 100644 leaves-server/build.gradle.kts.patch create mode 100644 leaves-server/minecraft-patches/features/0001-Build-changes.patch create mode 100644 leaves-server/minecraft-patches/features/0002-Leaves-Server-Config-And-Command.patch create mode 100644 leaves-server/minecraft-patches/features/0003-Leaves-Protocol-Core.patch rename patches/server/0008-Fix-trading-with-the-void.patch => leaves-server/minecraft-patches/features/0004-Fix-trading-with-the-void.patch (75%) rename patches/server/0009-Make-snowball-and-egg-can-knockback-player.patch => leaves-server/minecraft-patches/features/0005-Make-snowball-and-egg-can-knockback-player.patch (53%) create mode 100644 leaves-server/minecraft-patches/features/0006-Leaves-Fakeplayer.patch create mode 100644 leaves-server/minecraft-patches/features/0007-Make-shears-in-dispenser-can-unlimited-use.patch create mode 100644 leaves-server/minecraft-patches/features/0008-Redstone-Shears-Wrench.patch create mode 100644 leaves-server/minecraft-patches/features/0009-Add-isShrink-to-EntityResurrectEvent.patch rename patches/server/0014-Budding-Amethyst-can-push-by-piston.patch => leaves-server/minecraft-patches/features/0010-Budding-Amethyst-can-push-by-piston.patch (52%) rename patches/server/0015-Spectator-dont-get-Advancement.patch => leaves-server/minecraft-patches/features/0011-Spectator-dont-get-Advancement.patch (64%) create mode 100644 leaves-server/minecraft-patches/features/0012-Stick-can-change-ArmorStand-arm-status.patch create mode 100644 leaves-server/minecraft-patches/features/0013-Configurable-MC-59471.patch create mode 100644 leaves-server/minecraft-patches/features/0014-No-chat-sign.patch rename patches/server/0019-Dont-send-useless-entity-packets.patch => leaves-server/minecraft-patches/features/0015-Dont-send-useless-entity-packets.patch (65%) create mode 100644 leaves-server/minecraft-patches/features/0016-Optimize-suffocation.patch rename patches/server/0021-Only-check-for-spooky-season-once-an-hour.patch => leaves-server/minecraft-patches/features/0017-Only-check-for-spooky-season-once-an-hour.patch (54%) create mode 100644 leaves-server/minecraft-patches/features/0018-Config-to-disable-method-profiler.patch rename patches/server/0023-Throttle-goal-selector-during-inactive-ticking.patch => leaves-server/minecraft-patches/features/0019-Throttle-goal-selector-during-inactive-ticking.patch (73%) rename patches/server/0024-Reduce-entity-allocations.patch => leaves-server/minecraft-patches/features/0020-Reduce-entity-allocations.patch (64%) create mode 100644 leaves-server/minecraft-patches/features/0021-Remove-lambda-from-ticking-guard.patch create mode 100644 leaves-server/minecraft-patches/features/0022-Cache-climbing-check-for-activation.patch create mode 100644 leaves-server/minecraft-patches/features/0023-Reduce-chunk-loading-lookups.patch rename patches/server/0029-InstantBlockUpdater-Reintroduced.patch => leaves-server/minecraft-patches/features/0024-InstantBlockUpdater-Reintroduced.patch (57%) create mode 100644 leaves-server/minecraft-patches/features/0025-Random-flatten-triangular-distribution.patch create mode 100644 leaves-server/minecraft-patches/features/0026-BBOR-Protocol.patch create mode 100644 leaves-server/minecraft-patches/features/0027-PCA-sync-protocol.patch create mode 100644 leaves-server/minecraft-patches/features/0028-Jade-Protocol.patch create mode 100644 leaves-server/minecraft-patches/features/0029-Alternative-block-placement-Protocol.patch rename patches/server/0035-Player-operation-limiter.patch => leaves-server/minecraft-patches/features/0030-Player-operation-limiter.patch (53%) rename patches/server/0036-Renewable-Elytra.patch => leaves-server/minecraft-patches/features/0031-Renewable-Elytra.patch (65%) create mode 100644 leaves-server/minecraft-patches/features/0032-Stackable-ShulkerBoxes.patch create mode 100644 leaves-server/minecraft-patches/features/0033-MC-Technical-Survival-Mode.patch create mode 100644 leaves-server/minecraft-patches/features/0034-Return-nether-portal-fix.patch create mode 100644 leaves-server/minecraft-patches/features/0035-Leaves-Extra-Yggdrasil-Service.patch create mode 100644 leaves-server/minecraft-patches/features/0036-Configurable-vanilla-random.patch create mode 100644 leaves-server/minecraft-patches/features/0037-Catch-update-suppression-crash.patch create mode 100644 leaves-server/minecraft-patches/features/0038-Bedrock-break-list.patch create mode 100644 leaves-server/minecraft-patches/features/0039-Fix-trapdoor-feature.patch create mode 100644 leaves-server/minecraft-patches/features/0040-Disable-distance-check-for-UseItemOnPacket.patch create mode 100644 leaves-server/minecraft-patches/features/0041-No-feather-falling-trample.patch create mode 100644 leaves-server/minecraft-patches/features/0042-Syncmatica-Protocol.patch rename patches/server/0050-Shared-villager-discounts.patch => leaves-server/minecraft-patches/features/0043-Shared-villager-discounts.patch (59%) create mode 100644 leaves-server/minecraft-patches/features/0044-Redstone-wire-dont-connect-if-on-trapdoor.patch rename patches/server/0052-Disable-check-out-of-order-command.patch => leaves-server/minecraft-patches/features/0045-Disable-check-out-of-order-command.patch (75%) rename patches/server/0053-Despawn-enderman-with-block.patch => leaves-server/minecraft-patches/features/0046-Despawn-enderman-with-block.patch (53%) create mode 100644 leaves-server/minecraft-patches/features/0047-Creative-fly-no-clip.patch rename patches/server/0056-Optimized-dragon-respawn.patch => leaves-server/minecraft-patches/features/0048-Optimized-dragon-respawn.patch (53%) create mode 100644 leaves-server/minecraft-patches/features/0049-Shave-snow-layers.patch create mode 100644 leaves-server/minecraft-patches/features/0050-Elytra-aeronautics-no-chunk-load.patch create mode 100644 leaves-server/minecraft-patches/features/0051-Cache-ignite-odds.patch create mode 100644 leaves-server/minecraft-patches/features/0052-Lava-riptide.patch create mode 100644 leaves-server/minecraft-patches/features/0053-No-block-update-command.patch create mode 100644 leaves-server/minecraft-patches/features/0054-Raider-die-skip-self-raid-check.patch create mode 100644 leaves-server/minecraft-patches/features/0055-Container-open-passthrough.patch create mode 100644 leaves-server/minecraft-patches/features/0056-Dont-respond-ping-before-start-fully.patch create mode 100644 leaves-server/minecraft-patches/features/0057-Faster-chunk-serialization.patch rename patches/server/0068-Skip-secondary-POI-sensor-if-absent.patch => leaves-server/minecraft-patches/features/0058-Skip-secondary-POI-sensor-if-absent.patch (63%) rename patches/server/0070-Store-mob-counts-in-an-array.patch => leaves-server/minecraft-patches/features/0059-Store-mob-counts-in-an-array.patch (51%) create mode 100644 leaves-server/minecraft-patches/features/0060-Optimize-noise-generation.patch create mode 100644 leaves-server/minecraft-patches/features/0061-Reduce-array-allocations.patch rename patches/server/0074-Optimize-sun-burn-tick.patch => leaves-server/minecraft-patches/features/0062-Optimize-sun-burn-tick.patch (68%) rename patches/server/0075-Reduce-lambda-and-Optional-allocation-in-EntityBased.patch => leaves-server/minecraft-patches/features/0063-Reduce-lambda-and-Optional-allocation-in-EntityBased.patch (52%) rename patches/server/0076-Avoid-Class-isAssignableFrom-call-in-ClassInstanceMu.patch => leaves-server/minecraft-patches/features/0064-Avoid-Class-isAssignableFrom-call-in-ClassInstanceMu.patch (75%) rename patches/server/0077-Optimized-CubePointRange.patch => leaves-server/minecraft-patches/features/0065-Optimized-CubePointRange.patch (57%) create mode 100644 leaves-server/minecraft-patches/features/0066-Check-frozen-ticks-before-landing-block.patch rename patches/server/0079-Skip-entity-move-if-movement-is-zero.patch => leaves-server/minecraft-patches/features/0067-Skip-entity-move-if-movement-is-zero.patch (63%) rename patches/server/0080-Skip-cloning-advancement-criteria.patch => leaves-server/minecraft-patches/features/0068-Skip-cloning-advancement-criteria.patch (72%) create mode 100644 leaves-server/minecraft-patches/features/0069-Fix-villagers-dont-release-memory.patch rename patches/server/0082-Avoid-anvil-too-expensive.patch => leaves-server/minecraft-patches/features/0070-Avoid-anvil-too-expensive.patch (59%) rename patches/server/0083-Bow-infinity-fix.patch => leaves-server/minecraft-patches/features/0071-Bow-infinity-fix.patch (52%) create mode 100644 leaves-server/minecraft-patches/features/0072-Zero-tick-plants.patch create mode 100644 leaves-server/minecraft-patches/features/0073-Force-peaceful-mode-switch.patch create mode 100644 leaves-server/minecraft-patches/features/0074-Replay-Mod-API.patch create mode 100644 leaves-server/minecraft-patches/features/0075-Leaves-I18n.patch rename patches/server/0089-Fix-minecraft-hopper-not-work-without-player.patch => leaves-server/minecraft-patches/features/0076-Fix-minecraft-hopper-not-work-without-player.patch (57%) create mode 100644 leaves-server/minecraft-patches/features/0077-RNG-Fishing.patch create mode 100644 leaves-server/minecraft-patches/features/0078-Wool-Hopper-Counter.patch rename patches/server/0093-Spider-jockeys-drop-gapples.patch => leaves-server/minecraft-patches/features/0079-Spider-jockeys-drop-gapples.patch (61%) rename patches/server/0094-Force-Void-Trade.patch => leaves-server/minecraft-patches/features/0080-Force-Void-Trade.patch (52%) create mode 100644 leaves-server/minecraft-patches/features/0081-Villager-infinite-discounts.patch create mode 100644 leaves-server/minecraft-patches/features/0082-CCE-update-suppression.patch rename patches/server/0097-Disable-offline-warn-if-use-proxy.patch => leaves-server/minecraft-patches/features/0083-Disable-offline-warn-if-use-proxy.patch (57%) create mode 100644 leaves-server/minecraft-patches/features/0084-Armor-stand-cant-kill-by-mob-projectile.patch create mode 100644 leaves-server/minecraft-patches/features/0085-Make-Item-tick-vanilla.patch rename patches/server/0100-Copper-Bulb-1-gt-delay.patch => leaves-server/minecraft-patches/features/0086-Copper-Bulb-1-gt-delay.patch (65%) create mode 100644 leaves-server/minecraft-patches/features/0087-Crafter-1-gt-delay.patch create mode 100644 leaves-server/minecraft-patches/features/0088-More-Region-Format-Support.patch create mode 100644 leaves-server/minecraft-patches/features/0089-No-TNT-place-update.patch create mode 100644 leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch rename patches/server/0105-Placing-locked-hopper-no-longer-send-NC-updates.patch => leaves-server/minecraft-patches/features/0091-Placing-locked-hopper-no-longer-send-NC-updates.patch (72%) create mode 100644 leaves-server/minecraft-patches/features/0092-Renewable-deepslate.patch rename patches/server/0107-Renewable-sponges.patch => leaves-server/minecraft-patches/features/0093-Renewable-sponges.patch (64%) create mode 100644 leaves-server/minecraft-patches/features/0094-Renewable-coral.patch create mode 100644 leaves-server/minecraft-patches/features/0095-Fast-resume.patch rename patches/server/0110-Vanilla-hopper.patch => leaves-server/minecraft-patches/features/0096-Vanilla-hopper.patch (71%) create mode 100644 leaves-server/minecraft-patches/features/0097-Old-hopper-suckin-behavior.patch create mode 100644 leaves-server/minecraft-patches/features/0098-Fix-falling-block-s-block-location.patch create mode 100644 leaves-server/minecraft-patches/features/0099-Bytebuf-API.patch create mode 100644 leaves-server/minecraft-patches/features/0100-Allow-grindstone-overstacking.patch create mode 100644 leaves-server/minecraft-patches/features/0101-Configurable-MC-67.patch create mode 100644 leaves-server/minecraft-patches/features/0102-Disable-end-gateway-portal-entity-ticking.patch create mode 100644 leaves-server/minecraft-patches/features/0103-Disable-crystal-portal-proximity-check.patch rename patches/server/0118-Can-disable-LivingEntity-aiStep-alive-check.patch => leaves-server/minecraft-patches/features/0104-Can-disable-LivingEntity-aiStep-alive-check.patch (55%) rename patches/server/0120-Fix-fortress-mob-spawn.patch => leaves-server/minecraft-patches/features/0105-Fix-fortress-mob-spawn.patch (62%) create mode 100644 leaves-server/minecraft-patches/features/0106-Fix-FallingBlockEntity-Duplicate.patch create mode 100644 leaves-server/minecraft-patches/features/0107-Old-BlockEntity-behaviour.patch create mode 100644 leaves-server/minecraft-patches/features/0108-Revert-raid-changes.patch rename patches/server/0124-Allow-anvil-destroy-item-entities.patch => leaves-server/minecraft-patches/features/0109-Allow-anvil-destroy-item-entities.patch (50%) rename patches/server/0125-Fix-Incorrect-Collision-Behavior-for-Block-Shape.patch => leaves-server/minecraft-patches/features/0110-Fix-Incorrect-Collision-Behavior-for-Block-Shape.patch (81%) create mode 100644 leaves-server/minecraft-patches/features/0111-Disable-vault-blacklist.patch create mode 100644 leaves-server/minecraft-patches/features/0112-Fix-EntityPortalExitEvent-logic.patch create mode 100644 leaves-server/minecraft-patches/features/0113-Fix-CraftPortalEvent-logic.patch create mode 100644 leaves-server/minecraft-patches/features/0114-Xaero-Map-Protocol.patch create mode 100644 leaves-server/minecraft-patches/features/0115-Dont-save-Entity-tickCount.patch create mode 100644 leaves-server/minecraft-patches/features/0116-Fix-Warden-GameEventListener-register-on-load.patch create mode 100644 leaves-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch rename {patches/server => leaves-server/paper-patches/features}/0001-Build-changes.patch (86%) rename patches/server/0003-Delete-Timings.patch => leaves-server/paper-patches/features/0002-Delete-Timings.patch (66%) create mode 100644 leaves-server/paper-patches/features/0003-Leaves-Server-Config-And-Command.patch create mode 100644 leaves-server/paper-patches/features/0004-Leaves-Protocol-Core.patch create mode 100644 leaves-server/paper-patches/features/0005-Leaves-Fakeplayer.patch create mode 100644 leaves-server/paper-patches/features/0006-No-chat-sign.patch create mode 100644 leaves-server/paper-patches/features/0007-MC-Technical-Survival-Mode.patch create mode 100644 leaves-server/paper-patches/features/0008-Leaves-Extra-Yggdrasil-Service.patch rename patches/server/0072-Disable-packet-limit.patch => leaves-server/paper-patches/features/0009-Disable-packet-limit.patch (84%) create mode 100644 leaves-server/paper-patches/features/0010-Reduce-array-allocations.patch create mode 100644 leaves-server/paper-patches/features/0011-Force-peaceful-mode-switch.patch create mode 100644 leaves-server/paper-patches/features/0012-Replay-Mod-API.patch rename patches/server/0111-Force-minecraft-command.patch => leaves-server/paper-patches/features/0013-Force-minecraft-command.patch (100%) create mode 100644 leaves-server/paper-patches/features/0014-Bytebuf-API.patch create mode 100644 leaves-server/paper-patches/features/0015-Leaves-plugins.patch create mode 100644 leaves-server/paper-patches/files/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java.patch create mode 100644 leaves-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java.patch create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/.editorconfig create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/LeavesLogger.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/BotCommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/BotList.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/BotUtil.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/SimpleBytebufManager.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/WrappedBytebuf.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/internal/InternalBytebufHandler.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgument.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/NoBlockUpdateCommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/CounterCommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/PeacefulModeSwitchCommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigConverter.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidatorImpl.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftBot.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftPhotographer.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftPhotographerManager.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/world/chunk/LithiumHashPalette.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/profile/LeavesAuthenticationService.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/profile/LeavesMinecraftSessionService.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/CarpetAlternativeBlockPlacement.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/LMSPasterProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/bladeren/BladerenProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/bladeren/MsptSyncProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemGrabPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemHotbarPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemMessagePayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/MoveItemPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/region/IRegionFile.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/region/IRegionFileFactory.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/region/LeavesHooks.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/region/RegionFileFormat.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/region/linear/LinearRegionFile.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/region/linear/LinearVersion.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/replay/DigestOutputStream.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/replay/RecordMetaData.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/replay/Recorder.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/replay/RecorderOption.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayMarker.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographerGameMode.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/spark/LeavesServerConfigProvider.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/spark/LeavesSparkPlugin.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/ArrayConstants.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/BreakBedrockList.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/ElytraAeronauticsHelper.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/FertilizableCoral.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/HopperCounter.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/LeavesVersionFetcher.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/MathUtils.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/McTechnicalModeHelper.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/ReturnPortalManager.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/ShulkerBoxUtils.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/TicketHelper.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/UUIDSerializer.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/UpdateSuppressionException.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/VillagerInfiniteDiscountHelper.java create mode 100644 leaves-server/src/main/java/org/leavesmc/leaves/util/WoolUtils.java create mode 100644 leaves-server/src/main/resources/assets/minecraft/lang/zh_cn.json delete mode 100644 patches/api/0001-Leaves-Server-Config.patch delete mode 100644 patches/api/0005-Hide-irrelevant-compilation-warnings.patch delete mode 100644 patches/api/0006-SIMD-support.patch rename patches/{ => removed}/server/0026-Remove-iterators-from-inventory-contains.patch (100%) delete mode 100644 patches/server/0002-Dev-Fix.patch delete mode 100644 patches/server/0004-Leaves-Server-Utils.patch delete mode 100644 patches/server/0005-Update-version-fetcher-repo.patch delete mode 100644 patches/server/0006-Leaves-Server-Config-And-Command.patch delete mode 100644 patches/server/0007-Leaves-Protocol-Core.patch delete mode 100644 patches/server/0010-Fakeplayer-support.patch delete mode 100644 patches/server/0011-Make-shears-in-dispenser-can-unlimited-use.patch delete mode 100644 patches/server/0012-Redstone-Shears-Wrench.patch delete mode 100644 patches/server/0013-Add-isShrink-to-EntityResurrectEvent.patch delete mode 100644 patches/server/0016-Stick-can-change-ArmorStand-arm-status.patch delete mode 100644 patches/server/0017-Configurable-MC-59471.patch delete mode 100644 patches/server/0018-No-chat-sign.patch delete mode 100644 patches/server/0020-Optimize-suffocation.patch delete mode 100644 patches/server/0022-Config-to-disable-method-profiler.patch delete mode 100644 patches/server/0025-Remove-lambda-from-ticking-guard.patch delete mode 100644 patches/server/0027-Cache-climbing-check-for-activation.patch delete mode 100644 patches/server/0028-Reduce-chunk-loading-lookups.patch delete mode 100644 patches/server/0030-Random-flatten-triangular-distribution.patch delete mode 100644 patches/server/0031-BBOR-Protocol.patch delete mode 100644 patches/server/0032-PCA-sync-protocol.patch delete mode 100644 patches/server/0033-Jade-Protocol.patch delete mode 100644 patches/server/0034-Alternative-block-placement-Protocol.patch delete mode 100644 patches/server/0037-Stackable-ShulkerBoxes.patch delete mode 100644 patches/server/0038-MC-Technical-Survival-Mode.patch delete mode 100644 patches/server/0039-Return-nether-portal-fix.patch delete mode 100644 patches/server/0040-Appleskin-Protocol.patch delete mode 100644 patches/server/0041-Xaero-Map-Protocol.patch delete mode 100644 patches/server/0042-Leaves-Extra-Yggdrasil-Service.patch delete mode 100644 patches/server/0043-Use-vanilla-random-config.patch delete mode 100644 patches/server/0044-Fix-update-suppression-crash.patch delete mode 100644 patches/server/0045-Bedrock-break-list.patch delete mode 100644 patches/server/0046-Fix-trapdoor-feature.patch delete mode 100644 patches/server/0047-Disable-distance-check-for-UseItemOnPacket.patch delete mode 100644 patches/server/0048-No-feather-falling-trample.patch delete mode 100644 patches/server/0049-Syncmatica-Protocol.patch delete mode 100644 patches/server/0051-Redstone-wire-dont-connect-if-on-trapdoor.patch delete mode 100644 patches/server/0054-Leaves-carpet-support.patch delete mode 100644 patches/server/0055-Creative-fly-no-clip.patch delete mode 100644 patches/server/0057-Shave-snow-layers.patch delete mode 100644 patches/server/0058-Elytra-aeronautics-no-chunk-load.patch delete mode 100644 patches/server/0059-Cache-ignite-odds.patch delete mode 100644 patches/server/0060-Lava-riptide.patch delete mode 100644 patches/server/0061-No-block-update-command.patch delete mode 100644 patches/server/0062-Raider-die-skip-self-raid-check.patch delete mode 100644 patches/server/0063-Container-open-passthrough.patch delete mode 100644 patches/server/0064-SIMD-support.patch delete mode 100644 patches/server/0065-Dont-respond-ping-before-start-fully.patch delete mode 100644 patches/server/0066-Faster-chunk-serialization.patch delete mode 100644 patches/server/0067-Bladeren-Protocol.patch delete mode 100644 patches/server/0069-Bladeren-mspt-sync-protocol.patch delete mode 100644 patches/server/0071-Optimize-noise-generation.patch delete mode 100644 patches/server/0073-Reduce-array-allocations.patch delete mode 100644 patches/server/0078-Check-frozen-ticks-before-landing-block.patch delete mode 100644 patches/server/0081-Fix-villagers-dont-release-memory.patch delete mode 100644 patches/server/0084-Zero-tick-plants.patch delete mode 100644 patches/server/0085-Leaves-Updater.patch delete mode 100644 patches/server/0086-Force-peaceful-mode-switch.patch delete mode 100644 patches/server/0087-Replay-Mod-API.patch delete mode 100644 patches/server/0088-Leaves-I18n.patch delete mode 100644 patches/server/0090-RNG-Fishing.patch delete mode 100644 patches/server/0091-Wool-Hopper-Counter.patch delete mode 100644 patches/server/0092-Leaves-Reload-Command.patch delete mode 100644 patches/server/0095-Villager-infinite-discounts.patch delete mode 100644 patches/server/0096-CCE-update-suppression.patch delete mode 100644 patches/server/0098-Armor-stand-cant-kill-by-mob-projectile.patch delete mode 100644 patches/server/0099-Make-Item-tick-vanilla.patch delete mode 100644 patches/server/0101-Crafter-1-gt-delay.patch delete mode 100644 patches/server/0102-Linear-region-file-format.patch delete mode 100644 patches/server/0103-No-TNT-place-update.patch delete mode 100644 patches/server/0104-Servux-Protocol.patch delete mode 100644 patches/server/0106-Renewable-deepslate.patch delete mode 100644 patches/server/0108-Renewable-coral.patch delete mode 100644 patches/server/0109-Fast-resume.patch delete mode 100644 patches/server/0112-Fix-falling-block-s-block-location.patch delete mode 100644 patches/server/0113-Bytebuf-API.patch delete mode 100644 patches/server/0114-Allow-grindstone-overstacking.patch delete mode 100644 patches/server/0115-Configurable-MC-67.patch delete mode 100644 patches/server/0116-Disable-end-gateway-portal-entity-ticking.patch delete mode 100644 patches/server/0117-Disable-crystal-portal-proximity-check.patch delete mode 100644 patches/server/0119-Leaves-plugins.patch delete mode 100644 patches/server/0121-Fix-FallingBlockEntity-Duplicate.patch delete mode 100644 patches/server/0122-Old-BlockEntity-behaviour.patch delete mode 100644 patches/server/0123-Revert-raid-changes.patch delete mode 100644 patches/server/0126-Disable-vault-blacklist.patch delete mode 100644 patches/server/0127-TEMP-Fix-color-in-console.patch delete mode 100644 patches/server/0128-Fix-EntityPortalExitEvent-logic.patch delete mode 100644 patches/server/0129-Fix-CraftPortalEvent-logic.patch delete mode 100644 scripts/RemoveOldVersionTags.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 78f0134c..01ddb0ce 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -31,7 +31,7 @@ jobs: - name: Get Build Number run: bash scripts/GetBuildNumber.sh - name: Apply Patches - run: ./gradlew applyPatches + run: ./gradlew applyAllPatches - name: Create Leavesclip Jar run: ./gradlew createMojmapLeavesclipJar env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 63bb000c..f49cc820 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,10 +25,10 @@ jobs: cache-read-only: false - name: Setup Git Config run: | - git config --global user.email "ci@leavesmc.org" - git config --global user.name "LeavesMC CI" + git config --global user.email "ci-test@leavesmc.org" + git config --global user.name "LeavesMC Test CI" - name: Apply Patches - run: ./gradlew applyPatches + run: ./gradlew applyAllPatches - name: Create Leavesclip Jar run: ./gradlew createMojmapLeavesclipJar - name: Move Jar @@ -38,7 +38,7 @@ jobs: } jarName="leaves-$(prop mcVersion).jar" - mv build/libs/Leaves-leavesclip-"$(prop version)"-mojmap.jar "$jarName" + mv leaves-server/build/libs/leaves-leavesclip-"$(prop version)"-mojmap.jar "$jarName" echo "jar=$jarName" >> "$GITHUB_ENV" - name: Upload Artifact uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 4d3c4b9e..ed2f3333 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ -.gradle/ -build/ +.gradle # Eclipse stuff .classpath @@ -24,9 +23,10 @@ dependency-reduced-pom.xml .*.sw[a-p] # various other potential build files -build/ -bin/ -dist/ +/build +*/build/ +bin +dist manifest.mf # Mac filesystem dust @@ -44,12 +44,13 @@ out/ *~ # other stuff -leaves-server -leaves-api -paper-api-generator -mc-dev -.idea -testserver +/leaves-api/build.gradle.kts +/leaves-server/build.gradle.kts +/leaves-server/src/minecraft + +/paper-server +/paper-api + run # other sh diff --git a/README.md b/README.md index 6d2e3e05..839809af 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ maven { } dependencies { - compileOnly("org.leavesmc.leaves:leaves-api:1.21.3-R0.1-SNAPSHOT") + compileOnly("org.leavesmc.leaves:leaves-api:1.21.4-R0.1-SNAPSHOT") } ``` @@ -40,7 +40,7 @@ Each time you want to update your dependency, you must re-build Leaves. Leaves-Server: ```kotlin dependencies { - compileOnly("org.leavesmc.leaves:leaves:1.21.3-R0.1-SNAPSHOT") + compileOnly("org.leavesmc.leaves:leaves:1.21.4-R0.1-SNAPSHOT") } ``` @@ -48,9 +48,9 @@ dependencies { You need JDK 21 and good Internet conditions -Clone this repo, run `./gradlew applyPatches`, then run `./gradlew createMojmapLeavesclipJar` in your terminal. +Clone this repo, run `./gradlew applyAllPatches`, then run `./gradlew createMojmapLeavesclipJar` in your terminal. -You can find the jars in the `build/libs` directory. +You can find the jars in the `leaves-server/build/libs` directory. ## Pull Requests diff --git a/README_cn.md b/README_cn.md index c9fba87e..d364489e 100644 --- a/README_cn.md +++ b/README_cn.md @@ -30,7 +30,7 @@ maven { } dependencies { - compileOnly("org.leavesmc.leaves:leaves-api:1.21.3-R0.1-SNAPSHOT") + compileOnly("org.leavesmc.leaves:leaves-api:1.21.4-R0.1-SNAPSHOT") } ``` @@ -39,7 +39,7 @@ dependencies { Leaves-Server: ```kotlin dependencies { - compileOnly("org.leavesmc.leaves:leaves:1.21.3-R0.1-SNAPSHOT") + compileOnly("org.leavesmc.leaves:leaves:1.21.4-R0.1-SNAPSHOT") } ``` @@ -47,9 +47,9 @@ dependencies { 你需要最低 JDK 21 和一个可以正常访问各种 git/maven 库的网络 -首先克隆此储存库,然后在你的终端里依次执行 `./gradlew applyPatches` 和 `./gradlew createMojmapLeavesclipJar` +首先克隆此储存库,然后在你的终端里依次执行 `./gradlew applyAllPatches` 和 `./gradlew createMojmapLeavesclipJar` -最后 你可以在 `build/libs` 文件夹里找到对应的jar文件 +最后 你可以在 `leaves-server/build/libs` 文件夹里找到对应的jar文件 ## 对于想要出一份力的开发者 diff --git a/build.gradle.kts b/build.gradle.kts index 2f5753ff..29c3f74f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,32 +1,35 @@ plugins { java - `maven-publish` - id("org.leavesmc.leavesweight.patcher") version "1.0.2-SNAPSHOT" + id("org.leavesmc.leavesweight.patcher") version "2.0.0-SNAPSHOT" } -allprojects { - apply(plugin = "java") +subprojects { + apply(plugin = "java-library") apply(plugin = "maven-publish") - java { + extensions.configure { toolchain { languageVersion = JavaLanguageVersion.of(21) } } -} -subprojects { - apply(plugin = "java") - - java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(21)) + repositories { + mavenLocal() + mavenCentral() + maven("https://repo.papermc.io/repository/maven-public/") + maven("https://repo.leavesmc.org/releases") { + content { onlyForConfigurations("leavesclip") } } } + tasks.withType().configureEach { + isPreserveFileTimestamps = false + isReproducibleFileOrder = true + } tasks.withType { options.encoding = Charsets.UTF_8.name() - options.release.set(21) + options.release = 21 + options.isFork = true } tasks.withType { options.encoding = Charsets.UTF_8.name() @@ -35,58 +38,8 @@ subprojects { filteringCharset = Charsets.UTF_8.name() } - repositories { - mavenLocal() - mavenCentral() - maven("https://oss.sonatype.org/content/groups/public/") - maven("https://repo.papermc.io/repository/maven-public/") - maven("https://ci.emc.gs/nexus/content/groups/aikar/") - maven("https://repo.aikar.co/content/groups/aikar") - maven("https://repo.md-5.net/content/repositories/releases/") - maven("https://hub.spigotmc.org/nexus/content/groups/public/") - maven("https://jitpack.io") - } -} -repositories { - mavenCentral() - maven("https://repo.leavesmc.org/releases") { - content { onlyForConfigurations("leavesclip") } - } -} - -dependencies { - remapper("net.fabricmc:tiny-remapper:0.10.3:fat") - decompiler("org.vineflower:vineflower:1.10.1") - leavesclip("org.leavesmc:leavesclip:2.0.1") -} - -paperweight { - serverProject.set(project(":leaves-server")) - - remapRepo.set("https://maven.fabricmc.net/") - decompileRepo.set("https://files.minecraftforge.net/maven/") - - usePaperArchiveUpstream(providers.gradleProperty("paperRef")) { - withPaperPatcher { - apiPatchDir.set(layout.projectDirectory.dir("patches/api")) - apiOutputDir.set(layout.projectDirectory.dir("leaves-api")) - - serverPatchDir.set(layout.projectDirectory.dir("patches/server")) - serverOutputDir.set(layout.projectDirectory.dir("leaves-server")) - } - - patchTasks.register("generatedApi") { - isBareDirectory = true - upstreamDirPath = "paper-api-generator/generated" - patchDir = layout.projectDirectory.dir("patches/generated-api") - outputDir = layout.projectDirectory.dir("paper-api-generator/generated") - } - } -} - -allprojects { - publishing { + extensions.configure { repositories { maven("https://repo.leavesmc.org/snapshots") { name = "leaves" @@ -98,3 +51,26 @@ allprojects { } } } + +paperweight { + upstreams.paper { + ref = providers.gradleProperty("paperRef") + + patchFile { + path = "paper-server/build.gradle.kts" + outputFile = file("leaves-server/build.gradle.kts") + patchFile = file("leaves-server/build.gradle.kts.patch") + } + patchFile { + path = "paper-api/build.gradle.kts" + outputFile = file("leaves-api/build.gradle.kts") + patchFile = file("leaves-api/build.gradle.kts.patch") + } + patchDir("paperApi") { + upstreamPath = "paper-api" + excludes = setOf("build.gradle.kts") + patchesDir = file("leaves-api/paper-patches") + outputDir = file("paper-api") + } + } +} \ No newline at end of file diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e24dff94..15ec2a4a 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -41,7 +41,7 @@ The patching system is based on git, and you can learn about it at here: ` -4. Run Gradle's task `rebuildPatches` to convert your commits to a new patch +4. Run Gradle's task `rebuildAllServerPatches` to convert your commits to a new patch 5. Push your patches to your repository After pushing, you can open a PR to submit your patches. @@ -70,5 +70,5 @@ You can modify a existing patch by following the steps below: 2. Run `git commit -a --fixup ` in your terminal to make a fix-up commit - If you want to edit the commit message, replace `--fixup` with `--squash`. 3. Run `git rebase -i --autosquash base` to rebase automatically, then just type `:q` to close the confirm page -4. Run Gradle's task `rebuildPatches` to modify existing patches +4. Run Gradle's task `rebuildAllServerPatches` to modify existing patches 5. Push and PR again diff --git a/docs/CONTRIBUTING_cn.md b/docs/CONTRIBUTING_cn.md index b2df2253..7463d598 100644 --- a/docs/CONTRIBUTING_cn.md +++ b/docs/CONTRIBUTING_cn.md @@ -38,7 +38,7 @@ Leaves 使用和 Paper 一样的补丁系统,并为了针对不同部分的修 如果你已经 Fork 了主储存库,那么下面你应该这么做: 1. 将你的仓库 clone 到本地; -2. 在你的 IDE 或 终端 内执行 Gradle 的 `applyPatches` 任务,如果是在终端内,你可以执行 `./gradlew applyPatches`; +2. 在你的 IDE 或 终端 内执行 Gradle 的 `applyAllPatches` 任务,如果是在终端内,你可以执行 `./gradlew applyAllPatches`; 3. 进入 `leaves-api` 或 `leaves-server` 文件夹进行修改。 By the way,`leaves-api` 和 `leaves-server` 并不是正常的 git 仓库。 @@ -54,7 +54,7 @@ By the way,`leaves-api` 和 `leaves-server` 并不是正常的 git 仓库。 1. 对 `leaves-api` 或者 / 和 `leaves-server` 进行修改; 2. 使用 git 添加你的修改,比如 `git add .`; 3. 使用 `git commit -m <提交信息>` 进行提交; -4. 运行 Gradle 任务 `rebuildPatches` 将你的提交转化为一个补丁; +4. 运行 Gradle 任务 `rebuildAllServerPatches` 将你的提交转化为一个补丁; 5. 将你生成的补丁文件进行推送。 这样做以后,你就可以将你的补丁文件进行 PR 提交。 @@ -67,5 +67,5 @@ By the way,`leaves-api` 和 `leaves-server` 并不是正常的 git 仓库。 2. 使用 `git commit -a --fixup ` 来进行一个更正提交; - 如果你想要更改提交信息,你也可以用 `--squash` 来代替 `--fixup`。 3. 使用 `git rebase -i --autosquash base` 来进行自动变基,你只需要输入 `:q` 来关闭确认页面即可; -4. 运行 Gradle 任务 `rebuildPatches` 来修改已被修改的补丁; +4. 运行 Gradle 任务 `rebuildAllServerPatches` 来修改已被修改的补丁; 5. 将修改后的补丁 PR 发回储存库。 diff --git a/gradle.properties b/gradle.properties index 1d7c8ada..f4df9a3d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,8 +1,8 @@ group=org.leavesmc.leaves -version=1.21.3-R0.1-SNAPSHOT -mcVersion=1.21.3 -paperRef=5a60ffb8b1cc88c1480a4c5f5c221291be97527f -preVersion=false +version=1.21.4-R0.1-SNAPSHOT +mcVersion=1.21.4 +paperRef=46f4fdaae3661941ac86f2157e625d907fdd8e81 +preVersion=true org.gradle.jvmargs=-Xmx2G org.gradle.caching=true org.gradle.parallel=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 5c40527d..18362b78 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/leaves-api/build.gradle.kts.patch b/leaves-api/build.gradle.kts.patch new file mode 100644 index 00000000..48fd6832 --- /dev/null +++ b/leaves-api/build.gradle.kts.patch @@ -0,0 +1,84 @@ +--- a/paper-api/build.gradle.kts ++++ b/paper-api/build.gradle.kts +@@ -93,19 +_,33 @@ + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + } + +-val generatedApiPath: java.nio.file.Path = layout.projectDirectory.dir("src/generated/java").asFile.toPath() ++val generatedApiPath: java.nio.file.Path = rootProject.layout.projectDirectory.dir("paper-api/src/generated/java").asFile.toPath() // Leaves - build change + idea { + module { + generatedSourceDirs.add(generatedApiPath.toFile()) + } + } ++// Leaves start - build change + sourceSets { + main { + java { + srcDir(generatedApiPath) ++ srcDir(file("../paper-api/src/main/java")) ++ } ++ resources { ++ srcDir(file("../paper-api/src/main/resources")) ++ } ++ } ++ test { ++ java { ++ srcDir(file("../paper-api/src/test/java")) ++ } ++ resources { ++ srcDir(file("../paper-api/src/test/resources")) + } + } + } ++// Leaves end - build change + + val outgoingVariants = arrayOf("runtimeElements", "apiElements", "sourcesElements", "javadocElements") + val mainCapability = "${project.group}:${project.name}:${project.version}" +@@ -150,6 +_,16 @@ + } + } + ++// Leaves start - hide irrelevant compilation warnings ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ compilerArgs.add("-Xlint:-module") ++ compilerArgs.add("-Xlint:-removal") ++ compilerArgs.add("-Xlint:-dep-ann") ++ compilerArgs.add("--add-modules=jdk.incubator.vector") // Leaves - SIMD support ++} ++// Leaves end - hide irrelevant compilation warnings ++ + tasks.jar { + from(generateApiVersioningFile.map { it.outputs.files.singleFile }) { + into("META-INF/maven/${project.group}/${project.name}") +@@ -169,7 +_,7 @@ + + tasks.withType { + val options = options as StandardJavadocDocletOptions +- options.overview = "src/main/javadoc/overview.html" ++ options.overview = "../paper-api/src/main/javadoc/overview.html" // Leaves - build change + options.use() + options.isDocFilesSubDirs = true + options.links( +@@ -202,16 +_,18 @@ + } + + // workaround for https://github.com/gradle/gradle/issues/4046 +- inputs.dir("src/main/javadoc").withPropertyName("javadoc-sourceset") ++ inputs.dir("../paper-api/src/main/javadoc").withPropertyName("javadoc-sourceset") // Leaves - build change + val fsOps = services.fileSystemOperations + doLast { + fsOps.copy { +- from("src/main/javadoc") { ++ from("../paper-api/src/main/javadoc") { // Leaves - build change + include("**/doc-files/**") + } + into("build/docs/javadoc") + } + } ++ options.addStringOption("Xdoclint:none", "-quiet") // Leaves - hide irrelevant compilation warnings ++ options.addStringOption("-add-modules", "jdk.incubator.vector") // Leaves - SIMD support + } + + tasks.test { diff --git a/patches/api/0007-Delete-Timings.patch b/leaves-api/paper-patches/features/0001-Delete-Timings.patch similarity index 76% rename from patches/api/0007-Delete-Timings.patch rename to leaves-api/paper-patches/features/0001-Delete-Timings.patch index 0f0a1916..0c42184f 100644 --- a/patches/api/0007-Delete-Timings.patch +++ b/leaves-api/paper-patches/features/0001-Delete-Timings.patch @@ -1,6 +1,6 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 24 Aug 2023 19:21:31 +0800 +Date: Sun, 26 Jan 2025 14:49:07 +0800 Subject: [PATCH] Delete Timings @@ -2094,8 +2094,776 @@ index 632c4961515f5052551f841cfa840e60bba7a257..00000000000000000000000000000000 - super.stopTiming(); - } -} +diff --git a/src/main/java/co/aikar/util/Counter.java b/src/main/java/co/aikar/util/Counter.java +deleted file mode 100644 +index dae84243804b4b076cafb3e1b29bdcf614efc93f..0000000000000000000000000000000000000000 +--- a/src/main/java/co/aikar/util/Counter.java ++++ /dev/null +@@ -1,39 +0,0 @@ +-package co.aikar.util; +- +-import com.google.common.collect.ForwardingMap; +- +-import java.util.HashMap; +-import java.util.Map; +-import org.jetbrains.annotations.NotNull; +-import org.jetbrains.annotations.Nullable; +- +-@Deprecated(forRemoval = true) +-public class Counter extends ForwardingMap { +- private final Map counts = new HashMap<>(); +- +- public long decrement(@Nullable T key) { +- return increment(key, -1); +- } +- public long increment(@Nullable T key) { +- return increment(key, 1); +- } +- public long decrement(@Nullable T key, long amount) { +- return increment(key, -amount); +- } +- public long increment(@Nullable T key, long amount) { +- Long count = this.getCount(key); +- count += amount; +- this.counts.put(key, count); +- return count; +- } +- +- public long getCount(@Nullable T key) { +- return this.counts.getOrDefault(key, 0L); +- } +- +- @NotNull +- @Override +- protected Map delegate() { +- return this.counts; +- } +-} +diff --git a/src/main/java/co/aikar/util/JSONUtil.java b/src/main/java/co/aikar/util/JSONUtil.java +deleted file mode 100644 +index c105a1429ca58b37be265708ec345e00f0d43ed8..0000000000000000000000000000000000000000 +--- a/src/main/java/co/aikar/util/JSONUtil.java ++++ /dev/null +@@ -1,141 +0,0 @@ +-package co.aikar.util; +- +-import com.google.common.base.Function; +-import com.google.common.collect.Lists; +-import com.google.common.collect.Maps; +-import org.jetbrains.annotations.NotNull; +-import org.jetbrains.annotations.Nullable; +-import org.json.simple.JSONArray; +-import org.json.simple.JSONObject; +- +-import java.util.ArrayList; +-import java.util.LinkedHashMap; +-import java.util.List; +-import java.util.Map; +- +-/** +- * Provides Utility methods that assist with generating JSON Objects +- */ +-@SuppressWarnings({"rawtypes", "SuppressionAnnotation"}) +-@Deprecated(forRemoval = true) +-public final class JSONUtil { +- private JSONUtil() {} +- +- /** +- * Creates a key/value "JSONPair" object +- * +- * @param key Key to use +- * @param obj Value to use +- * @return JSONPair +- */ +- @NotNull +- public static JSONPair pair(@NotNull String key, @Nullable Object obj) { +- return new JSONPair(key, obj); +- } +- +- @NotNull +- public static JSONPair pair(long key, @Nullable Object obj) { +- return new JSONPair(String.valueOf(key), obj); +- } +- +- /** +- * Creates a new JSON object from multiple JSONPair key/value pairs +- * +- * @param data JSONPairs +- * @return Map +- */ +- @NotNull +- public static Map createObject(@NotNull JSONPair... data) { +- return appendObjectData(new LinkedHashMap(), data); +- } +- +- /** +- * This appends multiple key/value Obj pairs into a JSON Object +- * +- * @param parent Map to be appended to +- * @param data Data to append +- * @return Map +- */ +- @NotNull +- public static Map appendObjectData(@NotNull Map parent, @NotNull JSONPair... data) { +- for (JSONPair JSONPair : data) { +- parent.put(JSONPair.key, JSONPair.val); +- } +- return parent; +- } +- +- /** +- * This builds a JSON array from a set of data +- * +- * @param data Data to build JSON array from +- * @return List +- */ +- @NotNull +- public static List toArray(@NotNull Object... data) { +- return Lists.newArrayList(data); +- } +- +- /** +- * These help build a single JSON array using a mapper function +- * +- * @param collection Collection to apply to +- * @param mapper Mapper to apply +- * @param Element Type +- * @return List +- */ +- @NotNull +- public static List toArrayMapper(@NotNull E[] collection, @NotNull Function mapper) { +- return toArrayMapper(Lists.newArrayList(collection), mapper); +- } +- +- @NotNull +- public static List toArrayMapper(@NotNull Iterable collection, @NotNull Function mapper) { +- List array = Lists.newArrayList(); +- for (E e : collection) { +- Object object = mapper.apply(e); +- if (object != null) { +- array.add(object); +- } +- } +- return array; +- } +- +- /** +- * These help build a single JSON Object from a collection, using a mapper function +- * +- * @param collection Collection to apply to +- * @param mapper Mapper to apply +- * @param Element Type +- * @return Map +- */ +- @NotNull +- public static Map toObjectMapper(@NotNull E[] collection, @NotNull Function mapper) { +- return toObjectMapper(Lists.newArrayList(collection), mapper); +- } +- +- @NotNull +- public static Map toObjectMapper(@NotNull Iterable collection, @NotNull Function mapper) { +- Map object = Maps.newLinkedHashMap(); +- for (E e : collection) { +- JSONPair JSONPair = mapper.apply(e); +- if (JSONPair != null) { +- object.put(JSONPair.key, JSONPair.val); +- } +- } +- return object; +- } +- +- /** +- * Simply stores a key and a value, used internally by many methods below. +- */ +- @SuppressWarnings("PublicInnerClass") +- public static class JSONPair { +- final String key; +- final Object val; +- +- JSONPair(@NotNull String key, @NotNull Object val) { +- this.key = key; +- this.val = val; +- } +- } +-} +diff --git a/src/main/java/co/aikar/util/LoadingIntMap.java b/src/main/java/co/aikar/util/LoadingIntMap.java +deleted file mode 100644 +index 5753b9bce89db2ac378ec41f1b61907cc2e23335..0000000000000000000000000000000000000000 +--- a/src/main/java/co/aikar/util/LoadingIntMap.java ++++ /dev/null +@@ -1,77 +0,0 @@ +-/* +- * Copyright (c) 2015. Starlis LLC / dba Empire Minecraft +- * +- * This source code is proprietary software and must not be redistributed without Starlis LLC's approval +- * +- */ +-package co.aikar.util; +- +- +-import com.google.common.base.Function; +-import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +-import org.jetbrains.annotations.NotNull; +-import org.jetbrains.annotations.Nullable; +- +-/** +- * Allows you to pass a Loader function that when a key is accessed that doesn't exist, +- * automatically loads the entry into the map by calling the loader Function. +- * +- * .get() Will only return null if the Loader can return null. +- * +- * You may pass any backing Map to use. +- * +- * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed. +- * +- * Do not wrap the backing map with Collections.synchronizedMap. +- * +- * @param Value +- */ +-@Deprecated(forRemoval = true) +-public class LoadingIntMap extends Int2ObjectOpenHashMap { +- private final Function loader; +- +- public LoadingIntMap(@NotNull Function loader) { +- super(); +- this.loader = loader; +- } +- +- public LoadingIntMap(int expectedSize, @NotNull Function loader) { +- super(expectedSize); +- this.loader = loader; +- } +- +- public LoadingIntMap(int expectedSize, float loadFactor, @NotNull Function loader) { +- super(expectedSize, loadFactor); +- this.loader = loader; +- } +- +- +- @Nullable +- @Override +- public V get(int key) { +- V res = super.get(key); +- if (res == null) { +- res = loader.apply(key); +- if (res != null) { +- put(key, res); +- } +- } +- return res; +- } +- +- /** +- * Due to java stuff, you will need to cast it to (Function) for some cases +- * +- * @param Type +- */ +- public abstract static class Feeder implements Function { +- @Nullable +- @Override +- public T apply(@Nullable Object input) { +- return apply(); +- } +- +- @Nullable +- public abstract T apply(); +- } +-} +diff --git a/src/main/java/co/aikar/util/LoadingMap.java b/src/main/java/co/aikar/util/LoadingMap.java +deleted file mode 100644 +index 1786eeb5cbeaad75602c9c5649bbcd9b2af5cf81..0000000000000000000000000000000000000000 +--- a/src/main/java/co/aikar/util/LoadingMap.java ++++ /dev/null +@@ -1,369 +0,0 @@ +-/* +- * This file is licensed under the MIT License (MIT). +- * +- * Copyright (c) 2014 Daniel Ennis +- * +- * Permission is hereby granted, free of charge, to any person obtaining a copy +- * of this software and associated documentation files (the "Software"), to deal +- * in the Software without restriction, including without limitation the rights +- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +- * copies of the Software, and to permit persons to whom the Software is +- * furnished to do so, subject to the following conditions: +- * +- * The above copyright notice and this permission notice shall be included in +- * all copies or substantial portions of the Software. +- * +- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +- * THE SOFTWARE. +- */ +-package co.aikar.util; +- +-import com.google.common.base.Preconditions; +-import java.lang.reflect.Constructor; +-import java.util.AbstractMap; +-import java.util.Collection; +-import java.util.HashMap; +-import java.util.IdentityHashMap; +-import java.util.Map; +-import java.util.Set; +-import java.util.function.Function; +-import org.jetbrains.annotations.NotNull; +-import org.jetbrains.annotations.Nullable; +- +-/** +- * Allows you to pass a Loader function that when a key is accessed that doesn't exists, +- * automatically loads the entry into the map by calling the loader Function. +- * +- * .get() Will only return null if the Loader can return null. +- * +- * You may pass any backing Map to use. +- * +- * This class is not thread safe and should be wrapped with Collections.synchronizedMap on the OUTSIDE of the LoadingMap if needed. +- * +- * Do not wrap the backing map with Collections.synchronizedMap. +- * +- * @param Key +- * @param Value +- */ +-@Deprecated(forRemoval = true) +-public class LoadingMap extends AbstractMap { +- private final Map backingMap; +- private final java.util.function.Function loader; +- +- /** +- * Initializes an auto loading map using specified loader and backing map +- * @param backingMap Map to wrap +- * @param loader Loader +- */ +- public LoadingMap(@NotNull Map backingMap, @NotNull java.util.function.Function loader) { +- this.backingMap = backingMap; +- this.loader = loader; +- } +- +- /** +- * Creates a new LoadingMap with the specified map and loader +- * +- * @param backingMap Actual map being used. +- * @param loader Loader to use +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map +- */ +- @NotNull +- public static Map of(@NotNull Map backingMap, @NotNull Function loader) { +- return new LoadingMap<>(backingMap, loader); +- } +- +- /** +- * Creates a LoadingMap with an auto instantiating loader. +- * +- * Will auto construct class of of Value when not found +- * +- * Since this uses Reflection, It is more effecient to define your own static loader +- * than using this helper, but if performance is not critical, this is easier. +- * +- * @param backingMap Actual map being used. +- * @param keyClass Class used for the K generic +- * @param valueClass Class used for the V generic +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map that auto instantiates on .get() +- */ +- @NotNull +- public static Map newAutoMap(@NotNull Map backingMap, @Nullable final Class keyClass, +- @NotNull final Class valueClass) { +- return new LoadingMap<>(backingMap, new AutoInstantiatingLoader<>(keyClass, valueClass)); +- } +- /** +- * Creates a LoadingMap with an auto instantiating loader. +- * +- * Will auto construct class of of Value when not found +- * +- * Since this uses Reflection, It is more effecient to define your own static loader +- * than using this helper, but if performance is not critical, this is easier. +- * +- * @param backingMap Actual map being used. +- * @param valueClass Class used for the V generic +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map that auto instantiates on .get() +- */ +- @NotNull +- public static Map newAutoMap(@NotNull Map backingMap, +- @NotNull final Class valueClass) { +- return newAutoMap(backingMap, null, valueClass); +- } +- +- /** +- * @see #newAutoMap +- * +- * new Auto initializing map using a HashMap. +- * +- * @param keyClass Class used for the K generic +- * @param valueClass Class used for the V generic +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map that auto instantiates on .get() +- */ +- @NotNull +- public static Map newHashAutoMap(@Nullable final Class keyClass, @NotNull final Class valueClass) { +- return newAutoMap(new HashMap<>(), keyClass, valueClass); +- } +- +- /** +- * @see #newAutoMap +- * +- * new Auto initializing map using a HashMap. +- * +- * @param valueClass Class used for the V generic +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map that auto instantiates on .get() +- */ +- @NotNull +- public static Map newHashAutoMap(@NotNull final Class valueClass) { +- return newHashAutoMap(null, valueClass); +- } +- +- /** +- * @see #newAutoMap +- * +- * new Auto initializing map using a HashMap. +- * +- * @param keyClass Class used for the K generic +- * @param valueClass Class used for the V generic +- * @param initialCapacity Initial capacity to use +- * @param loadFactor Load factor to use +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map that auto instantiates on .get() +- */ +- @NotNull +- public static Map newHashAutoMap(@Nullable final Class keyClass, @NotNull final Class valueClass, int initialCapacity, float loadFactor) { +- return newAutoMap(new HashMap<>(initialCapacity, loadFactor), keyClass, valueClass); +- } +- +- /** +- * @see #newAutoMap +- * +- * new Auto initializing map using a HashMap. +- * +- * @param valueClass Class used for the V generic +- * @param initialCapacity Initial capacity to use +- * @param loadFactor Load factor to use +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map that auto instantiates on .get() +- */ +- @NotNull +- public static Map newHashAutoMap(@NotNull final Class valueClass, int initialCapacity, float loadFactor) { +- return newHashAutoMap(null, valueClass, initialCapacity, loadFactor); +- } +- +- /** +- * Initializes an auto loading map using a HashMap +- * +- * @param loader Loader to use +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map +- */ +- @NotNull +- public static Map newHashMap(@NotNull Function loader) { +- return new LoadingMap<>(new HashMap<>(), loader); +- } +- +- /** +- * Initializes an auto loading map using a HashMap +- * +- * @param loader Loader to use +- * @param initialCapacity Initial capacity to use +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map +- */ +- @NotNull +- public static Map newHashMap(@NotNull Function loader, int initialCapacity) { +- return new LoadingMap<>(new HashMap<>(initialCapacity), loader); +- } +- /** +- * Initializes an auto loading map using a HashMap +- * +- * @param loader Loader to use +- * @param initialCapacity Initial capacity to use +- * @param loadFactor Load factor to use +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map +- */ +- @NotNull +- public static Map newHashMap(@NotNull Function loader, int initialCapacity, float loadFactor) { +- return new LoadingMap<>(new HashMap<>(initialCapacity, loadFactor), loader); +- } +- +- /** +- * Initializes an auto loading map using an Identity HashMap +- * +- * @param loader Loader to use +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map +- */ +- @NotNull +- public static Map newIdentityHashMap(@NotNull Function loader) { +- return new LoadingMap<>(new IdentityHashMap<>(), loader); +- } +- +- /** +- * Initializes an auto loading map using an Identity HashMap +- * +- * @param loader Loader to use +- * @param initialCapacity Initial capacity to use +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map +- */ +- @NotNull +- public static Map newIdentityHashMap(@NotNull Function loader, int initialCapacity) { +- return new LoadingMap<>(new IdentityHashMap<>(initialCapacity), loader); +- } +- +- @Override +- public int size() {return backingMap.size();} +- +- @Override +- public boolean isEmpty() {return backingMap.isEmpty();} +- +- @Override +- public boolean containsKey(@Nullable Object key) {return backingMap.containsKey(key);} +- +- @Override +- public boolean containsValue(@Nullable Object value) {return backingMap.containsValue(value);} +- +- @Nullable +- @Override +- public V get(@Nullable Object key) { +- V v = backingMap.get(key); +- if (v != null) { +- return v; +- } +- return backingMap.computeIfAbsent((K) key, loader); +- } +- +- @Nullable +- public V put(@Nullable K key, @Nullable V value) {return backingMap.put(key, value);} +- +- @Nullable +- @Override +- public V remove(@Nullable Object key) {return backingMap.remove(key);} +- +- public void putAll(@NotNull Map m) {backingMap.putAll(m);} +- +- @Override +- public void clear() {backingMap.clear();} +- +- @NotNull +- @Override +- public Set keySet() {return backingMap.keySet();} +- +- @NotNull +- @Override +- public Collection values() {return backingMap.values();} +- +- @Override +- public boolean equals(@Nullable Object o) {return backingMap.equals(o);} +- +- @Override +- public int hashCode() {return backingMap.hashCode();} +- +- @NotNull +- @Override +- public Set> entrySet() { +- return backingMap.entrySet(); +- } +- +- @NotNull +- public LoadingMap clone() { +- return new LoadingMap<>(backingMap, loader); +- } +- +- private static class AutoInstantiatingLoader implements Function { +- final Constructor constructor; +- private final Class valueClass; +- +- AutoInstantiatingLoader(@Nullable Class keyClass, @NotNull Class valueClass) { +- try { +- this.valueClass = valueClass; +- if (keyClass != null) { +- constructor = valueClass.getConstructor(keyClass); +- } else { +- constructor = null; +- } +- } catch (NoSuchMethodException e) { +- throw new IllegalStateException( +- valueClass.getName() + " does not have a constructor for " + (keyClass != null ? keyClass.getName() : null)); +- } +- } +- +- @NotNull +- @Override +- public V apply(@Nullable K input) { +- try { +- return (constructor != null ? constructor.newInstance(input) : valueClass.newInstance()); +- } catch (Exception e) { +- throw new ExceptionInInitializerError(e); +- } +- } +- +- @Override +- public int hashCode() { +- return super.hashCode(); +- } +- +- @Override +- public boolean equals(Object object) { +- return false; +- } +- } +- +- /** +- * Due to java stuff, you will need to cast it to (Function) for some cases +- * +- * @param Type +- */ +- public abstract static class Feeder implements Function { +- @Nullable +- @Override +- public T apply(@Nullable Object input) { +- return apply(); +- } +- +- @Nullable +- public abstract T apply(); +- } +-} +diff --git a/src/main/java/co/aikar/util/MRUMapCache.java b/src/main/java/co/aikar/util/MRUMapCache.java +deleted file mode 100644 +index 3e61a926620a67daec3af54b72a1b911eaef2ed4..0000000000000000000000000000000000000000 +--- a/src/main/java/co/aikar/util/MRUMapCache.java ++++ /dev/null +@@ -1,112 +0,0 @@ +-/* +- * This file is licensed under the MIT License (MIT). +- * +- * Copyright (c) 2014 Daniel Ennis +- * +- * Permission is hereby granted, free of charge, to any person obtaining a copy +- * of this software and associated documentation files (the "Software"), to deal +- * in the Software without restriction, including without limitation the rights +- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +- * copies of the Software, and to permit persons to whom the Software is +- * furnished to do so, subject to the following conditions: +- * +- * The above copyright notice and this permission notice shall be included in +- * all copies or substantial portions of the Software. +- * +- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +- * THE SOFTWARE. +- */ +-package co.aikar.util; +- +-import java.util.AbstractMap; +-import java.util.Collection; +-import java.util.Map; +-import java.util.Set; +-import org.jetbrains.annotations.NotNull; +-import org.jetbrains.annotations.Nullable; +- +-/** +- * Implements a Most Recently Used cache in front of a backing map, to quickly access the last accessed result. +- * +- * @param Key Type of the Map +- * @param Value Type of the Map +- */ +-@Deprecated(forRemoval = true) +-public class MRUMapCache extends AbstractMap { +- final Map backingMap; +- Object cacheKey; +- V cacheValue; +- public MRUMapCache(@NotNull final Map backingMap) { +- this.backingMap = backingMap; +- } +- +- public int size() {return backingMap.size();} +- +- public boolean isEmpty() {return backingMap.isEmpty();} +- +- public boolean containsKey(@Nullable Object key) { +- return key != null && key.equals(cacheKey) || backingMap.containsKey(key); +- } +- +- public boolean containsValue(@Nullable Object value) { +- return value != null && value == cacheValue || backingMap.containsValue(value); +- } +- +- @Nullable +- public V get(@Nullable Object key) { +- if (cacheKey != null && cacheKey.equals(key)) { +- return cacheValue; +- } +- cacheKey = key; +- return cacheValue = backingMap.get(key); +- } +- +- @Nullable +- public V put(@Nullable K key, @Nullable V value) { +- cacheKey = key; +- return cacheValue = backingMap.put(key, value); +- } +- +- @Nullable +- public V remove(@Nullable Object key) { +- if (key != null && key.equals(cacheKey)) { +- cacheKey = null; +- } +- return backingMap.remove(key); +- } +- +- public void putAll(@NotNull Map m) {backingMap.putAll(m);} +- +- public void clear() { +- cacheKey = null; +- cacheValue = null; +- backingMap.clear(); +- } +- +- @NotNull +- public Set keySet() {return backingMap.keySet();} +- +- @NotNull +- public Collection values() {return backingMap.values();} +- +- @NotNull +- public Set> entrySet() {return backingMap.entrySet();} +- +- /** +- * Wraps the specified map with a most recently used cache +- * +- * @param map Map to be wrapped +- * @param Key Type of the Map +- * @param Value Type of the Map +- * @return Map +- */ +- @NotNull +- public static Map of(@NotNull Map map) { +- return new MRUMapCache(map); +- } +-} diff --git a/src/main/java/org/bukkit/command/Command.java b/src/main/java/org/bukkit/command/Command.java -index 0a26fffe9b1e5080b5639767a03af11006108b4a..3b73c0e59788f5f49ca2423032550f11855d52ae 100644 +index 71eb845a4d3b8b6ec3b816a0f20ec807e0f9a86d..a43419c23aa0f6fd809caf5a841cb138f350b7ba 100644 --- a/src/main/java/org/bukkit/command/Command.java +++ b/src/main/java/org/bukkit/command/Command.java @@ -33,16 +33,6 @@ public abstract class Command { @@ -2177,43 +2945,39 @@ index 5df19bd701c67506689fc7f49d91f99ebfbc83f0..940565704d0e8914a76cf25daf7d1f5e server.getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerCommandException(ex, target, sender, args))); // Paper //target.timings.stopTiming(); // Spigot // Paper diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java -index b878e7167cfcdea0e224c182b40abeadd339d3b3..fc0239bca93e3dfb740af3239c1276b004607643 100644 +index 001465eedafa51ac027a4db51cba6223edfe1171..dd98b4886d21ac92d9f9139450258754e985fb85 100644 --- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java +++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java -@@ -720,12 +720,7 @@ public final class SimplePluginManager implements PluginManager { +@@ -720,7 +720,6 @@ public final class SimplePluginManager implements PluginManager { throw new IllegalPluginAccessException("Plugin attempted to register " + event + " while not enabled"); } - executor = new co.aikar.timings.TimedEventExecutor(executor, plugin, null, event); // Paper -- if (false) { // Spigot - RL handles useTimings check now // Paper -- getEventListeners(event).register(new TimedRegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); -- } else { -- getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); -- } -+ getEventListeners(event).register(new RegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); - } - - @NotNull -@@ -955,8 +950,7 @@ public final class SimplePluginManager implements PluginManager { + if (false) { // Spigot - RL handles useTimings check now // Paper + getEventListeners(event).register(new TimedRegisteredListener(listener, executor, priority, plugin, ignoreCancelled)); + } else { +@@ -955,18 +954,7 @@ public final class SimplePluginManager implements PluginManager { @Override public boolean useTimings() { - if (true) {return this.paperPluginManager.useTimings();} // Paper - return co.aikar.timings.Timings.isTimingsEnabled(); // Spigot -+ return false; - } - - /** -@@ -966,7 +960,6 @@ public final class SimplePluginManager implements PluginManager { - */ - @Deprecated(forRemoval = true) - public void useTimings(boolean use) { +- } +- +- /** +- * Sets whether or not per event timing code should be used +- * +- * @param use True if per event timing code should be used +- */ +- @Deprecated(forRemoval = true) +- public void useTimings(boolean use) { - co.aikar.timings.Timings.setTimingsEnabled(use); // Paper ++ return false; } // Paper start diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java -index eaefbb00e9993d54906cc8cf35cf753c0d6c7707..5c8004c307e1c7923e9d6b0126921127e44f23a2 100644 +index b412aaf08901d169ac9fc89b36f9d6ccb95c53d3..b2257f3e51a754b7d3d946b434745f22e3305b0a 100644 --- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java @@ -43,7 +43,6 @@ import org.bukkit.plugin.TimedRegisteredListener; @@ -2238,13 +3002,13 @@ index eaefbb00e9993d54906cc8cf35cf753c0d6c7707..5c8004c307e1c7923e9d6b0126921127 } } - }, plugin, method, eventClass); // Paper -+ }; ++ }; // Paper if (false) { // Spigot - RL handles useTimings check now eventSet.add(new TimedRegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); } else { diff --git a/src/main/java/org/spigotmc/CustomTimingsHandler.java b/src/main/java/org/spigotmc/CustomTimingsHandler.java deleted file mode 100644 -index 12946bd55fcf7c40d39081779a7fa30049ee6165..0000000000000000000000000000000000000000 +index 5fbacfcf108432c5187aa9a4092d00d7d5b0fd53..0000000000000000000000000000000000000000 --- a/src/main/java/org/spigotmc/CustomTimingsHandler.java +++ /dev/null @@ -1,67 +0,0 @@ @@ -2298,7 +3062,7 @@ index 12946bd55fcf7c40d39081779a7fa30049ee6165..00000000000000000000000000000000 - public CustomTimingsHandler(@NotNull String name) { - Timing timing; - -- new AuthorNagException("Deprecated use of CustomTimingsHandler. Please Switch to Timings.of ASAP").printStackTrace(); +- new AuthorNagException("Deprecated use of CustomTimingsHandler. Timings has been removed.").printStackTrace(); - try { - final Method ofSafe = TimingsManager.class.getDeclaredMethod("getHandler", String.class, String.class, Timing.class); - ofSafe.setAccessible(true); diff --git a/patches/api/0002-Add-isShrink-to-EntityResurrectEvent.patch b/leaves-api/paper-patches/features/0002-Add-isShrink-to-EntityResurrectEvent.patch similarity index 100% rename from patches/api/0002-Add-isShrink-to-EntityResurrectEvent.patch rename to leaves-api/paper-patches/features/0002-Add-isShrink-to-EntityResurrectEvent.patch diff --git a/patches/api/0003-Add-fakeplayer-api.patch b/leaves-api/paper-patches/features/0003-Add-fakeplayer-api.patch similarity index 99% rename from patches/api/0003-Add-fakeplayer-api.patch rename to leaves-api/paper-patches/features/0003-Add-fakeplayer-api.patch index 69a2ab0e..d7d4c110 100644 --- a/patches/api/0003-Add-fakeplayer-api.patch +++ b/leaves-api/paper-patches/features/0003-Add-fakeplayer-api.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Add fakeplayer api diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java -index 5ce98e09e9bfcae45896401c69578aa879222893..cb7e06ad207239a5626fc475c46d991fabb48293 100644 +index 9764489c0801eecb8bbfb06103607f0902bf23ab..8185c76052d7af4c2ea83805a9ee6c709148d3c3 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java -@@ -2964,6 +2964,17 @@ public final class Bukkit { +@@ -2980,6 +2980,17 @@ public final class Bukkit { } // Paper end - Folia region threading API @@ -27,10 +27,10 @@ index 5ce98e09e9bfcae45896401c69578aa879222893..cb7e06ad207239a5626fc475c46d991f public static Server.Spigot spigot() { return server.spigot(); diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index 0bae85d3489b628d2a2542f3c7b6f91f4a7c4af5..967bb7ba476f494ac8bfebb599f260017176f3e3 100644 +index fa6f9d50577059d99ee98662497f3fc049fa74ac..430fb104e3190ae0ef9a08627c7c0d0d636898c8 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java -@@ -62,6 +62,7 @@ import org.jetbrains.annotations.ApiStatus; +@@ -66,6 +66,7 @@ import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -38,7 +38,7 @@ index 0bae85d3489b628d2a2542f3c7b6f91f4a7c4af5..967bb7ba476f494ac8bfebb599f26001 /** * Represents a server implementation. -@@ -2615,4 +2616,13 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi +@@ -2658,4 +2659,13 @@ 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 diff --git a/patches/api/0004-Player-operation-limiter.patch b/leaves-api/paper-patches/features/0004-Player-operation-limiter.patch similarity index 100% rename from patches/api/0004-Player-operation-limiter.patch rename to leaves-api/paper-patches/features/0004-Player-operation-limiter.patch diff --git a/patches/api/0008-Force-peaceful-mode-switch.patch b/leaves-api/paper-patches/features/0005-Force-peaceful-mode-switch.patch similarity index 83% rename from patches/api/0008-Force-peaceful-mode-switch.patch rename to leaves-api/paper-patches/features/0005-Force-peaceful-mode-switch.patch index 3d80f0cc..71ccc163 100644 --- a/patches/api/0008-Force-peaceful-mode-switch.patch +++ b/leaves-api/paper-patches/features/0005-Force-peaceful-mode-switch.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Force peaceful mode switch diff --git a/src/main/java/org/bukkit/World.java b/src/main/java/org/bukkit/World.java -index 7a439c99fc4c5ee17d674460c8e58a9fe0c64e02..3d5893c4cb6fc914ff2dbbb2267420ec8653dac3 100644 +index 2729f71ac5525b7669fb7cc8719a75e5ce8d1dfc..f150caee08d5069e9e92bb09040522513097cff5 100644 --- a/src/main/java/org/bukkit/World.java +++ b/src/main/java/org/bukkit/World.java -@@ -4314,6 +4314,12 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient +@@ -4323,6 +4323,12 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient void setSendViewDistance(int viewDistance); // Paper end - view distance api diff --git a/patches/api/0009-Replay-Mod-API.patch b/leaves-api/paper-patches/features/0006-Replay-Mod-API.patch similarity index 90% rename from patches/api/0009-Replay-Mod-API.patch rename to leaves-api/paper-patches/features/0006-Replay-Mod-API.patch index 17236070..731ad9b9 100644 --- a/patches/api/0009-Replay-Mod-API.patch +++ b/leaves-api/paper-patches/features/0006-Replay-Mod-API.patch @@ -1,14 +1,14 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: MC_XiaoHei -Date: Sat, 5 Aug 2023 09:10:59 +0800 +From: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> +Date: Sun, 26 Jan 2025 01:39:16 -0500 Subject: [PATCH] Replay Mod API diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java -index cb7e06ad207239a5626fc475c46d991fabb48293..e37015328e3e53ae5f960c8235ef827b7abe329d 100644 +index 8185c76052d7af4c2ea83805a9ee6c709148d3c3..9564d2e3f3126104f73a1942ea5c45a69ebb158a 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java -@@ -2974,6 +2974,11 @@ public final class Bukkit { +@@ -2990,6 +2990,11 @@ public final class Bukkit { return server.getBotManager(); } // Leaves end - Bot API @@ -21,10 +21,10 @@ index cb7e06ad207239a5626fc475c46d991fabb48293..e37015328e3e53ae5f960c8235ef827b @NotNull public static Server.Spigot spigot() { diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index 967bb7ba476f494ac8bfebb599f260017176f3e3..cc86476f68e6934a3f8e6a5404876a39b943a926 100644 +index 430fb104e3190ae0ef9a08627c7c0d0d636898c8..991ec55218da08377bb71272fd6bca9f3bed0582 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java -@@ -63,6 +63,7 @@ import org.jetbrains.annotations.Contract; +@@ -67,6 +67,7 @@ import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.leavesmc.leaves.entity.BotManager; @@ -32,7 +32,7 @@ index 967bb7ba476f494ac8bfebb599f260017176f3e3..cc86476f68e6934a3f8e6a5404876a39 /** * Represents a server implementation. -@@ -2625,4 +2626,7 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi +@@ -2668,4 +2669,7 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi */ @NotNull BotManager getBotManager(); // Leaves end - Bot API diff --git a/patches/api/0010-Bytebuf-API.patch b/leaves-api/paper-patches/features/0007-Bytebuf-API.patch similarity index 96% rename from patches/api/0010-Bytebuf-API.patch rename to leaves-api/paper-patches/features/0007-Bytebuf-API.patch index 9a0d303c..85565435 100644 --- a/patches/api/0010-Bytebuf-API.patch +++ b/leaves-api/paper-patches/features/0007-Bytebuf-API.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Bytebuf API diff --git a/src/main/java/org/bukkit/Bukkit.java b/src/main/java/org/bukkit/Bukkit.java -index e37015328e3e53ae5f960c8235ef827b7abe329d..d4d30b4da5e05d1f60f9fbb41a7a11b98d7cf6a0 100644 +index 9564d2e3f3126104f73a1942ea5c45a69ebb158a..63c90d151f4a50e2da63f640587d7041dcc1a8b6 100644 --- a/src/main/java/org/bukkit/Bukkit.java +++ b/src/main/java/org/bukkit/Bukkit.java -@@ -2980,6 +2980,12 @@ public final class Bukkit { +@@ -2996,6 +2996,12 @@ public final class Bukkit { } // Leaves end - Photographer API @@ -22,10 +22,10 @@ index e37015328e3e53ae5f960c8235ef827b7abe329d..d4d30b4da5e05d1f60f9fbb41a7a11b9 public static Server.Spigot spigot() { return server.spigot(); diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index cc86476f68e6934a3f8e6a5404876a39b943a926..6a5afc818073989b6d3a108261aeff285409cfc0 100644 +index 991ec55218da08377bb71272fd6bca9f3bed0582..2590bc0813672ca566dfae734812de6a53519a1a 100644 --- a/src/main/java/org/bukkit/Server.java +++ b/src/main/java/org/bukkit/Server.java -@@ -2629,4 +2629,8 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi +@@ -2672,4 +2672,8 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi // Leaves start - Photographer API @NotNull PhotographerManager getPhotographerManager(); // Leaves end - Photographer API @@ -35,10 +35,10 @@ index cc86476f68e6934a3f8e6a5404876a39b943a926..6a5afc818073989b6d3a108261aeff28 + // Leaves end - Bytebuf API } diff --git a/src/main/java/org/bukkit/entity/Player.java b/src/main/java/org/bukkit/entity/Player.java -index 95f0b3186e313c7fbd5c8531d52b82a69e525f94..ec2a5f77c135706a544a870808241ea1d6b6e814 100644 +index 0081dd53b6a81ce7892e58d61f9f8a6718e30775..c706a782a1739b54af85d4b6a996bf1d5d571ba1 100644 --- a/src/main/java/org/bukkit/entity/Player.java +++ b/src/main/java/org/bukkit/entity/Player.java -@@ -3895,6 +3895,12 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM +@@ -3853,6 +3853,12 @@ public interface Player extends HumanEntity, Conversable, OfflinePlayer, PluginM boolean isChunkSent(long chunkKey); // Paper end @@ -48,9 +48,9 @@ index 95f0b3186e313c7fbd5c8531d52b82a69e525f94..ec2a5f77c135706a544a870808241ea1 + void sendPacket(org.leavesmc.leaves.bytebuf.Bytebuf buf, org.leavesmc.leaves.bytebuf.packet.PacketType type); + // Leaves end - Bytebuf API + - @NotNull @Override Spigot spigot(); + // Spigot end diff --git a/src/main/java/org/leavesmc/leaves/bytebuf/Bytebuf.java b/src/main/java/org/leavesmc/leaves/bytebuf/Bytebuf.java new file mode 100644 index 0000000000000000000000000000000000000000..7038b2a5090154fe8d75ba9c9413952d834bb609 diff --git a/leaves-api/paper-patches/features/0008-Revert-raid-changes.patch b/leaves-api/paper-patches/features/0008-Revert-raid-changes.patch new file mode 100644 index 00000000..df20ef16 --- /dev/null +++ b/leaves-api/paper-patches/features/0008-Revert-raid-changes.patch @@ -0,0 +1,20 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: huanli233 <392352840@qq.com> +Date: Thu, 6 Feb 2025 12:49:03 +0800 +Subject: [PATCH] Revert raid changes + + +diff --git a/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java b/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java +index 8fdfcbc7d20fe0af6b220ab94516247093637621..3eb836575307116ce0668eadd6a6ee0a9aafc7cd 100644 +--- a/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java ++++ b/src/main/java/org/bukkit/event/entity/EntityPotionEffectEvent.java +@@ -219,9 +219,7 @@ public class EntityPotionEffectEvent extends EntityEvent implements Cancellable + /** + * When a player gets bad omen after killing a patrol captain. + * +- * @deprecated no longer used, player now gets an ominous bottle instead + */ +- @Deprecated(since = "1.21") // Paper + PATROL_CAPTAIN, + /** + * When a potion effect is modified through the plugin methods. diff --git a/leaves-server/build.gradle.kts.patch b/leaves-server/build.gradle.kts.patch new file mode 100644 index 00000000..ae8e60e7 --- /dev/null +++ b/leaves-server/build.gradle.kts.patch @@ -0,0 +1,155 @@ +--- a/paper-server/build.gradle.kts ++++ b/paper-server/build.gradle.kts +@@ -5,14 +_,14 @@ + plugins { + `java-library` + `maven-publish` +- id("io.papermc.paperweight.core") ++ id("org.leavesmc.leavesweight.core") // Leaves - build change + } + + val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/" + + dependencies { + mache("io.papermc:mache:1.21.4+build.7") +- paperclip("io.papermc:paperclip:3.0.3") ++ leavesclip("org.leavesmc:leavesclip:2.0.1") // Leaves - build change + testRuntimeOnly("org.junit.platform:junit-platform-launcher") + } + +@@ -21,6 +_,18 @@ + // macheOldPath = file("F:\\Projects\\PaperTooling\\mache\\versions\\1.21.4\\src\\main\\java") + // gitFilePatches = true + ++ // Leaves start - build change ++ val leaves = forks.register("leaves") { ++ upstream.patchDir("paperServer") { ++ upstreamPath = "paper-server" ++ excludes = setOf("src/minecraft", "patches", "build.gradle.kts") ++ patchesDir = rootDirectory.dir("leaves-server/paper-patches") ++ outputDir = rootDirectory.dir("paper-server") ++ } ++ } ++ activeFork = leaves ++ // Leaves end - build change ++ + spigot { + buildDataRef = "3edaf46ec1eed4115ce1b18d2846cded42577e42" + packageVersion = "v1_21_R3" // also needs to be updated in MappingEnvironment +@@ -101,7 +_,22 @@ + } + } + +-val log4jPlugins = sourceSets.create("log4jPlugins") ++// Leaves start - build change ++sourceSets { ++ main { ++ java { srcDir("../paper-server/src/main/java") } ++ resources { srcDir("../paper-server/src/main/resources") } ++ } ++ test { ++ java { srcDir("../paper-server/src/test/java") } ++ resources { srcDir("../paper-server/src/test/resources") } ++ } ++} ++val log4jPlugins = sourceSets.create("log4jPlugins") { ++ java { srcDir("../paper-server/src/log4jPlugins/java") } ++} ++// Leaves end - build change ++ + configurations.named(log4jPlugins.compileClasspathConfigurationName) { + extendsFrom(configurations.compileClasspath.get()) + } +@@ -119,7 +_,13 @@ + } + + dependencies { +- implementation(project(":paper-api")) ++ implementation(project(":leaves-api")) // Leaves - build change ++ // Leaves start - linear ++ implementation("com.github.luben:zstd-jni:1.5.4-1") ++ implementation("org.lz4:lz4-java:1.8.0") ++ implementation("net.openhft:zero-allocation-hashing:0.16") ++ // Leaves end - linear ++ implementation("org.spongepowered:configurate-hocon:4.2.0-SNAPSHOT") // Leaves - leaves plugins + implementation("ca.spottedleaf:concurrentutil:0.0.3") + 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 +@@ -176,6 +_,16 @@ + implementation("me.lucko:spark-paper:1.10.119-SNAPSHOT") + } + ++// Leaves start - hide irrelevant compilation warnings ++tasks.withType { ++ val compilerArgs = options.compilerArgs ++ compilerArgs.add("-Xlint:-module") ++ compilerArgs.add("-Xlint:-removal") ++ compilerArgs.add("-Xlint:-dep-ann") ++ compilerArgs.add("--add-modules=jdk.incubator.vector") // SIMD ++} ++// Leaves end - hide irrelevant compilation warnings ++ + tasks.jar { + manifest { + val git = Git(rootProject.layout.projectDirectory.path) +@@ -188,14 +_,14 @@ + val gitBranch = git.exec(providers, "rev-parse", "--abbrev-ref", "HEAD").get().trim() + attributes( + "Main-Class" to "org.bukkit.craftbukkit.Main", +- "Implementation-Title" to "Paper", ++ "Implementation-Title" to "Leaves", // Leaves - build change + "Implementation-Version" to implementationVersion, + "Implementation-Vendor" to date, +- "Specification-Title" to "Paper", ++ "Specification-Title" to "Leaves", // Leaves - build change + "Specification-Version" to project.version, +- "Specification-Vendor" to "Paper Team", +- "Brand-Id" to "papermc:paper", +- "Brand-Name" to "Paper", ++ "Specification-Vendor" to "Leaves Team", // Leaves - build change ++ "Brand-Id" to "leavesmc:leaves", // Leaves - build change ++ "Brand-Name" to "Leaves", // Leaves - build change + "Build-Number" to (build ?: ""), + "Build-Time" to buildTime.toString(), + "Git-Branch" to gitBranch, +@@ -306,6 +_,14 @@ + classpath(sourceSets.main.map { it.runtimeClasspath }) + } + ++// Leaves start - create config file ++tasks.registerRunTask("createLeavesConfig") { ++ description = "Create a new leaves.yml" ++ mainClass = "org.leavesmc.leaves.config.GlobalConfigCreator" ++ classpath(sourceSets.main.map { it.runtimeClasspath }) ++} ++// Leaves end - create config file ++ + tasks.registerRunTask("runBundler") { + description = "Spin up a test server from the Mojang mapped bundler jar" + classpath(tasks.createMojmapBundlerJar.flatMap { it.outputZip }) +@@ -316,13 +_,15 @@ + classpath(tasks.createReobfBundlerJar.flatMap { it.outputZip }) + mainClass.set(null as String?) + } +-tasks.registerRunTask("runPaperclip") { +- description = "Spin up a test server from the Mojang mapped Paperclip jar" +- classpath(tasks.createMojmapPaperclipJar.flatMap { it.outputZip }) +- mainClass.set(null as String?) +-} +-tasks.registerRunTask("runReobfPaperclip") { +- description = "Spin up a test server from the reobf Paperclip jar" +- classpath(tasks.createReobfPaperclipJar.flatMap { it.outputZip }) +- mainClass.set(null as String?) +-} ++// Leaves start - build change ++tasks.registerRunTask("runLeavesclip") { ++ description = "Spin up a test server from the Mojang mapped Leavesclip jar" ++ classpath(tasks.createMojmapLeavesclipJar.flatMap { it.outputZip }) ++ mainClass.set(null as String?) ++} ++tasks.registerRunTask("runReobfLeavesclip") { ++ description = "Spin up a test server from the reobf Leavesclip jar" ++ classpath(tasks.createReobfLeavesclipJar.flatMap { it.outputZip }) ++ mainClass.set(null as String?) ++} ++// Leaves end - build change diff --git a/leaves-server/minecraft-patches/features/0001-Build-changes.patch b/leaves-server/minecraft-patches/features/0001-Build-changes.patch new file mode 100644 index 00000000..20426224 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0001-Build-changes.patch @@ -0,0 +1,70 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 26 Jan 2025 13:58:49 +0800 +Subject: [PATCH] Build changes + + +diff --git a/ca/spottedleaf/moonrise/paper/PaperHooks.java b/ca/spottedleaf/moonrise/paper/PaperHooks.java +index 4d344559a20a0c35c181e297e81788c747363ec9..9d25c263508da5b6a027132e6cc071b675e4af44 100644 +--- a/ca/spottedleaf/moonrise/paper/PaperHooks.java ++++ b/ca/spottedleaf/moonrise/paper/PaperHooks.java +@@ -28,7 +28,7 @@ import net.minecraft.world.phys.AABB; + import java.util.List; + import java.util.function.Predicate; + +-public final class PaperHooks extends BaseChunkSystemHooks implements PlatformHooks { ++public class PaperHooks extends BaseChunkSystemHooks implements PlatformHooks { // Leaves - not final + + @Override + public String getBrand() { +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index ae220a732c78ab076261f20b5a54c71d7fceb407..5dfdf76c8de5eda9a02f9a5fa1c3bada8ec7dcde 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1192,7 +1192,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Sat, 1 Feb 2025 14:21:05 +0800 +Subject: [PATCH] Leaves Server Config And Command + + +diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java +index fc9f1b1cde5516eee06d6365645386cd0e3d14ec..64708df57aeca5c1f656748da96c6dadfba67ed7 100644 +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -225,6 +225,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark + com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now + ++ org.leavesmc.leaves.LeavesConfig.init((java.io.File) options.valueOf("leaves-settings")); // Leaves - Server Config ++ + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics // Leaves - down + + this.setPvpAllowed(properties.pvp); diff --git a/leaves-server/minecraft-patches/features/0003-Leaves-Protocol-Core.patch b/leaves-server/minecraft-patches/features/0003-Leaves-Protocol-Core.patch new file mode 100644 index 00000000..fff7be88 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0003-Leaves-Protocol-Core.patch @@ -0,0 +1,94 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 2 Feb 2025 13:08:32 +0800 +Subject: [PATCH] Leaves Protocol Core + + +diff --git a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java +index fb263fa1f30a7dfcb7ec2656abfb38e5fe88eac9..7e19dfe90a63ff26f03b95891dacb7360bba5a3c 100644 +--- a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java ++++ b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java +@@ -40,13 +40,23 @@ public interface CustomPacketPayload { + + @Override + public void encode(B buffer, CustomPacketPayload value) { ++ // Leaves start - protocol core ++ if (value instanceof org.leavesmc.leaves.protocol.core.LeavesCustomPayload payload) { ++ buffer.writeResourceLocation(payload.id()); ++ payload.write(buffer); ++ return; ++ } ++ // Leaves end - protocol core + this.writeCap(buffer, value.type(), value); + } + + @Override + public CustomPacketPayload decode(B buffer) { + ResourceLocation resourceLocation = buffer.readResourceLocation(); +- return (CustomPacketPayload)this.findCodec(resourceLocation).decode(buffer); ++ // Leaves start - protocol core ++ var payload = org.leavesmc.leaves.protocol.core.LeavesProtocolManager.decode(resourceLocation, buffer); ++ return java.util.Objects.requireNonNullElseGet(payload, () -> this.findCodec(resourceLocation).decode(buffer)); ++ // Leaves end - protocol core + } + }; + } +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 5dfdf76c8de5eda9a02f9a5fa1c3bada8ec7dcde..bde8ddadd3930100d1e31e630f809e77d6a70dac 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1744,6 +1744,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop leavesPayload) { ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePayload(player, leavesPayload); ++ } ++ // Leaves end - protocol ++ + // CraftBukkit start + // Paper start - Brand support + if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.BrandPayload(String brand)) { +@@ -154,6 +160,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + String channels = payload.toString(com.google.common.base.Charsets.UTF_8); + for (String channel : channels.split("\0")) { + this.getCraftPlayer().addChannel(channel); ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleMinecraftRegister(channel, player); // Leaves - protocol + } + } catch (Exception ex) { + ServerGamePacketListenerImpl.LOGGER.error("Couldn't register custom payload", ex); +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 03feaf0adb8ee87e33744a4615dc2507a02f92d7..31d145e2368ee935235f8e88278dcaea0c72f2c0 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -330,6 +330,8 @@ public abstract class PlayerList { + return; + } + ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol ++ + final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); + + if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure +@@ -500,6 +502,7 @@ public abstract class PlayerList { + return this.remove(player, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? player.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(player.getDisplayName()))); + } + public net.kyori.adventure.text.Component remove(ServerPlayer player, net.kyori.adventure.text.Component leaveMessage) { ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerLeave(player); // Leaves - protocol + // Paper end - Fix kick event leave message not being sent + ServerLevel serverLevel = player.serverLevel(); + player.awardStat(Stats.LEAVE_GAME); diff --git a/patches/server/0008-Fix-trading-with-the-void.patch b/leaves-server/minecraft-patches/features/0004-Fix-trading-with-the-void.patch similarity index 75% rename from patches/server/0008-Fix-trading-with-the-void.patch rename to leaves-server/minecraft-patches/features/0004-Fix-trading-with-the-void.patch index 1f0e84ab..dca99681 100644 --- a/patches/server/0008-Fix-trading-with-the-void.patch +++ b/leaves-server/minecraft-patches/features/0004-Fix-trading-with-the-void.patch @@ -4,13 +4,13 @@ Date: Wed, 26 Jan 2022 17:20:54 +0800 Subject: [PATCH] Fix trading with the void -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 585e2b43a0326f0b81597fa1234d3c67c76af550..7d88402b024a5c11e1796d9bccdc1c352650ad07 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2761,11 +2761,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 67de67cf1e33672fe33fbb88aeb92e3a020a4ae5..a91136ca7b47b242f8e454f6472fd60712736754 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -2675,11 +2675,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe // Spigot end - // Spigot Start + // 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 - if (entity.getBukkitEntity() instanceof org.bukkit.inventory.Merchant merchant && merchant.getTrader() != null) { diff --git a/patches/server/0009-Make-snowball-and-egg-can-knockback-player.patch b/leaves-server/minecraft-patches/features/0005-Make-snowball-and-egg-can-knockback-player.patch similarity index 53% rename from patches/server/0009-Make-snowball-and-egg-can-knockback-player.patch rename to leaves-server/minecraft-patches/features/0005-Make-snowball-and-egg-can-knockback-player.patch index a91d1c82..ce8a514d 100644 --- a/patches/server/0009-Make-snowball-and-egg-can-knockback-player.patch +++ b/leaves-server/minecraft-patches/features/0005-Make-snowball-and-egg-can-knockback-player.patch @@ -4,15 +4,14 @@ Date: Fri, 28 Jan 2022 18:34:29 +0800 Subject: [PATCH] Make snowball and egg can knockback player -diff --git a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java -index 70961e151666a0ecf5b791853f4581eaebbdcc8b..5d9834ce6a8f892339438a4fe262c9f5d52484fb 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/Snowball.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/Snowball.java -@@ -61,6 +61,13 @@ public class Snowball extends ThrowableItemProjectile { +diff --git a/net/minecraft/world/entity/projectile/Snowball.java b/net/minecraft/world/entity/projectile/Snowball.java +index c57bbdc13221d2ce349f3f1d894193f80ff1e24b..51e412e814523c4acccee9caf39af729e1e950d8 100644 +--- a/net/minecraft/world/entity/projectile/Snowball.java ++++ b/net/minecraft/world/entity/projectile/Snowball.java +@@ -54,6 +54,12 @@ public class Snowball extends ThrowableItemProjectile { + Entity entity = result.getEntity(); int i = entity instanceof Blaze ? 3 : 0; - - entity.hurt(this.damageSources().thrown(this, this.getOwner()), (float) i); -+ + entity.hurt(this.damageSources().thrown(this, this.getOwner()), i); + // Leaves start - make snowball can knockback player + if (org.leavesmc.leaves.LeavesConfig.modify.snowballAndEggCanKnockback && entity instanceof net.minecraft.server.level.ServerPlayer player) { + player.hurt(this.damageSources().thrown(this, this.getOwner()), 0.0000001F); @@ -22,16 +21,16 @@ index 70961e151666a0ecf5b791853f4581eaebbdcc8b..5d9834ce6a8f892339438a4fe262c9f5 } @Override -diff --git a/src/main/java/net/minecraft/world/entity/projectile/ThrownEgg.java b/src/main/java/net/minecraft/world/entity/projectile/ThrownEgg.java -index 155c2bbd35adacb7c3668fbe81a7c454e5102c8b..26696c09a2ce5c483dc2f723689ed985445663b8 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/ThrownEgg.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/ThrownEgg.java -@@ -52,6 +52,12 @@ public class ThrownEgg extends ThrowableItemProjectile { - protected void onHitEntity(EntityHitResult entityHitResult) { - super.onHitEntity(entityHitResult); - entityHitResult.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F); +diff --git a/net/minecraft/world/entity/projectile/ThrownEgg.java b/net/minecraft/world/entity/projectile/ThrownEgg.java +index 76481c0e77fc3a2e4be8eeb9de8d1e6de5507c64..14aeb330627e6fa06834736ab747b0fe64666bec 100644 +--- a/net/minecraft/world/entity/projectile/ThrownEgg.java ++++ b/net/minecraft/world/entity/projectile/ThrownEgg.java +@@ -53,6 +53,12 @@ public class ThrownEgg extends ThrowableItemProjectile { + protected void onHitEntity(EntityHitResult result) { + super.onHitEntity(result); + result.getEntity().hurt(this.damageSources().thrown(this, this.getOwner()), 0.0F); + // Leaves start - make egg can knockback player -+ if (org.leavesmc.leaves.LeavesConfig.modify.snowballAndEggCanKnockback && entityHitResult.getEntity() instanceof ServerPlayer player) { ++ if (org.leavesmc.leaves.LeavesConfig.modify.snowballAndEggCanKnockback && result.getEntity() instanceof net.minecraft.server.level.ServerPlayer player) { + player.hurt(this.damageSources().thrown(this, this.getOwner()), 0.0000001F); + player.knockback(0.4000000059604645D, this.getX() - player.getX(), this.getZ() - player.getZ(), this, io.papermc.paper.event.entity.EntityKnockbackEvent.Cause.DAMAGE); + } diff --git a/leaves-server/minecraft-patches/features/0006-Leaves-Fakeplayer.patch b/leaves-server/minecraft-patches/features/0006-Leaves-Fakeplayer.patch new file mode 100644 index 00000000..6e77881e --- /dev/null +++ b/leaves-server/minecraft-patches/features/0006-Leaves-Fakeplayer.patch @@ -0,0 +1,479 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 2 Feb 2025 15:28:11 +0800 +Subject: [PATCH] Leaves Fakeplayer + + +diff --git a/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java b/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java +index 4b2ae046413146b11912e7aa4a9a3d643de6afd1..c5733fe17b4dd5dfe4bce461a305a13a188b2f77 100644 +--- a/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java ++++ b/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java +@@ -39,6 +39,7 @@ public abstract class SimpleCriterionTrigger testTrigger) { ++ if (player instanceof org.leavesmc.leaves.bot.ServerBot) return; // Leaves - bot skip + PlayerAdvancements advancements = player.getAdvancements(); + Set> set = (Set) advancements.criterionData.get(this); // Paper - fix PlayerAdvancements leak + if (set != null && !set.isEmpty()) { +diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java +index e1000d8ab5ae0034b56a3524d2caee8c299b50e7..f8a67220ebb72fd446006b7aaed87fc69bd0454d 100644 +--- a/net/minecraft/network/Connection.java ++++ b/net/minecraft/network/Connection.java +@@ -96,7 +96,7 @@ public class Connection extends SimpleChannelInboundHandler> { + @Nullable + private volatile PacketListener disconnectListener; + @Nullable +- private volatile PacketListener packetListener; ++ protected volatile PacketListener packetListener; // Leaves - private -> protected + @Nullable + private DisconnectionDetails disconnectionDetails; + private boolean encrypted; +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index bde8ddadd3930100d1e31e630f809e77d6a70dac..073d4a57f78dc18e4cf95bdb852877b8624ec3f8 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -304,6 +304,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping + public static final long SERVER_INIT = System.nanoTime(); // Paper - Lag compensation + ++ private org.leavesmc.leaves.bot.BotList botList; // Leaves - fakeplayer ++ + public static S spin(Function threadFunction) { + ca.spottedleaf.dataconverter.minecraft.datatypes.MCTypeRegistry.init(); // Paper - rewrite data converter system + AtomicReference atomicReference = new AtomicReference<>(); +@@ -740,6 +742,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop realPlayers; // Leaves - skip + + public LevelChunk getChunkIfLoaded(int x, int z) { + return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately +@@ -681,6 +682,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); + // Paper end - rewrite chunk system + this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit ++ this.realPlayers = Lists.newArrayList(); // Leaves - skip + } + + // Paper start +@@ -2204,6 +2206,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + return this.players; + } + ++ // Leaves start - fakeplayer skip ++ public List realPlayers() { ++ return this.realPlayers; ++ } ++ // Leaves end - fakeplayer skip ++ + @Override + public void onBlockStateChange(BlockPos pos, BlockState blockState, BlockState newState) { + Optional> optional = PoiTypes.forState(blockState); +@@ -2618,6 +2626,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + // 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 serverPlayer) { + ServerLevel.this.players.add(serverPlayer); ++ // Leaves start - skip ++ if (!(serverPlayer instanceof org.leavesmc.leaves.bot.ServerBot)) { ++ ServerLevel.this.realPlayers.add(serverPlayer); ++ } ++ // Leaves end - skip + ServerLevel.this.updateSleepingPlayerList(); + } + +@@ -2684,6 +2697,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + ServerLevel.this.getChunkSource().removeEntity(entity); + if (entity instanceof ServerPlayer serverPlayer) { + ServerLevel.this.players.remove(serverPlayer); ++ // Leaves start - skip ++ if (!(serverPlayer instanceof org.leavesmc.leaves.bot.ServerBot)) { ++ ServerLevel.this.realPlayers.remove(serverPlayer); ++ } ++ // Leaves end - skip + ServerLevel.this.updateSleepingPlayerList(); + } + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 57d432dc9e8d8e9a3e088e7c40b35178c30fe786..f8b80a6a9d98f69dcbb84629607ee55c8191b55f 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -197,7 +197,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + ); + public ServerGamePacketListenerImpl connection; + public final MinecraftServer server; +- public final ServerPlayerGameMode gameMode; ++ public ServerPlayerGameMode gameMode; // Leaves - not final + private final PlayerAdvancements advancements; + private final ServerStatsCounter stats; + private float lastRecordedHealthAndAbsorption = Float.MIN_VALUE; +@@ -804,16 +804,20 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + this.invulnerableTime--; + } + +- // Paper start - Configurable container update tick rate +- if (--this.containerUpdateDelay <= 0) { +- this.containerMenu.broadcastChanges(); +- this.containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate; +- } +- // Paper end - Configurable container update tick rate +- if (this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen +- this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason +- this.containerMenu = this.inventoryMenu; ++ // Leaves start - skip bot ++ if (!(this instanceof org.leavesmc.leaves.bot.ServerBot)) { ++ // Paper start - Configurable container update tick rate ++ if (--this.containerUpdateDelay <= 0) { ++ this.containerMenu.broadcastChanges(); ++ this.containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate; ++ } ++ // Paper end - Configurable container update tick rate ++ if (this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen ++ this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason ++ this.containerMenu = this.inventoryMenu; ++ } + } ++ // Leaves end - skip bot + + Entity camera = this.getCamera(); + if (camera != this) { +@@ -828,7 +832,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + } + } + +- CriteriaTriggers.TICK.trigger(this); ++ if (!(this instanceof org.leavesmc.leaves.bot.ServerBot)) CriteriaTriggers.TICK.trigger(this); // Leaves - skip bot + if (this.levitationStartPos != null) { + CriteriaTriggers.LEVITATION.trigger(this, this.levitationStartPos, this.tickCount - this.levitationStartTime); + } +@@ -1462,6 +1466,11 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + this.lastSentHealth = -1.0F; + this.lastSentFood = -1; + ++ // Leaves start - bot support ++ if (org.leavesmc.leaves.LeavesConfig.modify.fakeplayer.enable) { ++ this.server.getBotList().bots.forEach(bot -> bot.sendFakeDataIfNeed(this, true)); // Leaves - render bot ++ } ++ // Leaves end - bot support + + // CraftBukkit start + org.bukkit.event.player.PlayerChangedWorldEvent changeEvent = new org.bukkit.event.player.PlayerChangedWorldEvent(this.getBukkitEntity(), serverLevel.getWorld()); +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 99e82b35800ef5c1ed24967fe9194aa35d3e2f24..f7db21ff9ce35f4a3b80e1e36366a1878605931b 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -332,6 +332,19 @@ public abstract class PlayerList { + + org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol + ++ // Leaves start - bot support ++ if (org.leavesmc.leaves.LeavesConfig.modify.fakeplayer.enable) { ++ org.leavesmc.leaves.bot.ServerBot bot = this.server.getBotList().getBotByName(player.getScoreboardName()); ++ if (bot != null) { ++ this.server.getBotList().removeBot(bot, org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.INTERNAL, player.getBukkitEntity(), false); ++ } ++ this.server.getBotList().bots.forEach(bot1 -> { ++ bot1.sendPlayerInfo(player); ++ bot1.sendFakeDataIfNeed(player, true); ++ }); // Leaves - render bot ++ } ++ // Leaves end - bot support ++ + final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); + + if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure +@@ -852,6 +865,12 @@ public abstract class PlayerList { + } + // Paper end - Add PlayerPostRespawnEvent + ++ // Leaves start - bot support ++ if (org.leavesmc.leaves.LeavesConfig.modify.fakeplayer.enable) { ++ this.server.getBotList().bots.forEach(bot -> bot.sendFakeDataIfNeed(serverPlayer, true)); // Leaves - render bot ++ } ++ // Leaves end - bot support ++ + // CraftBukkit end + + return serverPlayer; +@@ -957,11 +976,16 @@ public abstract class PlayerList { + } + + public String[] getPlayerNamesArray() { +- String[] strings = new String[this.players.size()]; ++ String[] strings = new String[this.players.size() + this.server.getBotList().bots.size()]; // Leaves - fakeplayer support + + for (int i = 0; i < this.players.size(); i++) { + strings[i] = this.players.get(i).getGameProfile().getName(); + } ++ // Leaves start - fakeplayer support ++ for (int i = this.players.size(); i < strings.length; ++i) { ++ strings[i] = this.server.getBotList().bots.get(i - this.players.size()).getGameProfile().getName(); ++ } ++ // Leaves end - fakeplayer support + + return strings; + } +@@ -1045,7 +1069,14 @@ public abstract class PlayerList { + + @Nullable + public ServerPlayer getPlayerByName(String username) { +- return this.playersByName.get(username.toLowerCase(java.util.Locale.ROOT)); // Spigot ++ // Leaves start - fakeplayer support ++ username = username.toLowerCase(java.util.Locale.ROOT); ++ ServerPlayer player = this.playersByName.get(username); ++ if (player == null) { ++ player = this.server.getBotList().getBotByName(username); ++ } ++ return player; // Spigot ++ // Leaves end - fakeplayer support + } + + public void broadcast(@Nullable Player except, double x, double y, double z, double radius, ResourceKey dimension, Packet packet) { +@@ -1356,7 +1387,13 @@ public abstract class PlayerList { + + @Nullable + public ServerPlayer getPlayer(UUID playerUUID) { +- return this.playersByUUID.get(playerUUID); ++ // Leaves start - fakeplayer support ++ ServerPlayer player = this.playersByUUID.get(playerUUID); ++ if (player == null) { ++ player = this.server.getBotList().getBot(playerUUID); ++ } ++ return player; ++ // Leaves start - fakeplayer support + } + + public boolean canBypassPlayerLimit(GameProfile profile) { +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 711e3da3626807d264730c66dd392e27492721e5..83d8bd3e6008a5253ab225a324a393dcddd38789 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -1431,7 +1431,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + // Paper start - optimise collisions +- private Vec3 collide(Vec3 movement) { ++ public Vec3 collide(Vec3 movement) { // Leaves - private -> public + final boolean xZero = movement.x == 0.0; + final boolean yZero = movement.y == 0.0; + final boolean zZero = movement.z == 0.0; +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index c996a547d4448f7e21f8d5ccfe7e0021ccb3c44c..b1c4ac288b71f622b30e896323a8b986aba8d04b 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -186,7 +186,7 @@ public abstract class Player extends LivingEntity { + private int lastLevelUpTime; + public GameProfile gameProfile; + private boolean reducedDebugInfo; +- private ItemStack lastItemInMainHand = ItemStack.EMPTY; ++ protected ItemStack lastItemInMainHand = ItemStack.EMPTY; + private final ItemCooldowns cooldowns = this.createItemCooldowns(); + private Optional lastDeathLocation = Optional.empty(); + @Nullable +@@ -347,6 +347,12 @@ public abstract class Player extends LivingEntity { + } + } + ++ // Leaves start - fakeplayer ++ protected void livingEntityTick() { ++ super.tick(); ++ } ++ // Leaves end - fakeplayer ++ + @Override + protected float getMaxHeadRotationRelativeToBody() { + return this.isBlocking() ? 15.0F : super.getMaxHeadRotationRelativeToBody(); +@@ -658,7 +664,7 @@ public abstract class Player extends LivingEntity { + } + } + +- private void touch(Entity entity) { ++ public void touch(Entity entity) { // Leaves - private -> public + entity.playerTouch(this); + } + +diff --git a/net/minecraft/world/entity/projectile/FishingHook.java b/net/minecraft/world/entity/projectile/FishingHook.java +index 1e012c7ef699a64ff3f1b00f897bb893ab25ecbd..f2bf0cdbd29438ca51b74ae2fcdf49dba0d52804 100644 +--- a/net/minecraft/world/entity/projectile/FishingHook.java ++++ b/net/minecraft/world/entity/projectile/FishingHook.java +@@ -55,7 +55,7 @@ public class FishingHook extends Projectile { + public static final EntityDataAccessor DATA_HOOKED_ENTITY = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.INT); + private static final EntityDataAccessor DATA_BITING = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.BOOLEAN); + private int life; +- private int nibble; ++ public int nibble; // Leaves - private -> public + public int timeUntilLured; + public int timeUntilHooked; + public float fishAngle; +diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java +index 50af953a4698a3c6e16b840fab764dd733b3fbc9..b7285c67ce3f6db2fa23a6dc91fcfa6a3d19f9b4 100644 +--- a/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -365,6 +365,7 @@ public abstract class AbstractContainerMenu { + + private void doClick(int slotId, int button, ClickType clickType, Player player) { + Inventory inventory = player.getInventory(); ++ if (!doClickCheck(slotId, button, clickType, player)) return; // Leaves - doClick check + if (clickType == ClickType.QUICK_CRAFT) { + int i = this.quickcraftStatus; + this.quickcraftStatus = getQuickcraftHeader(button); +@@ -641,6 +642,22 @@ public abstract class AbstractContainerMenu { + } + } + ++ // Leaves start - doClick check ++ private boolean doClickCheck(int slotIndex, int button, ClickType actionType, Player player) { ++ if (slotIndex < 0) { ++ return true; ++ } ++ ++ Slot slot = getSlot(slotIndex); ++ ItemStack itemStack = slot.getItem(); ++ net.minecraft.world.item.component.CustomData customData = itemStack.get(net.minecraft.core.component.DataComponents.CUSTOM_DATA); ++ if (customData != null && customData.contains("Leaves.Gui.Placeholder")) { ++ return !customData.copyTag().getBoolean("Leaves.Gui.Placeholder"); ++ } ++ return true; ++ } ++ // Leaves end - doClick check ++ + private boolean tryItemClickBehaviourOverride(Player player, ClickAction action, Slot slot, ItemStack clickedItem, ItemStack carriedItem) { + FeatureFlagSet featureFlagSet = player.level().enabledFeatures(); + return carriedItem.isItemEnabled(featureFlagSet) && carriedItem.overrideStackedOnOther(slot, action, player) +diff --git a/net/minecraft/world/level/levelgen/PhantomSpawner.java b/net/minecraft/world/level/levelgen/PhantomSpawner.java +index 11d25e64349b27bf54dc1620e4cce444c79f581c..82d9d53ef0aa57342173af29d14d00e4039fb583 100644 +--- a/net/minecraft/world/level/levelgen/PhantomSpawner.java ++++ b/net/minecraft/world/level/levelgen/PhantomSpawner.java +@@ -57,6 +57,13 @@ public class PhantomSpawner implements CustomSpawner { + ServerStatsCounter stats = serverPlayer.getStats(); + int i1 = Mth.clamp(stats.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE); + int i2 = 24000; ++ ++ // Leaves start - fakeplayer spawn ++ if (serverPlayer instanceof org.leavesmc.leaves.bot.ServerBot bot && bot.getConfigValue(org.leavesmc.leaves.bot.agent.Configs.SPAWN_PHANTOM)) { ++ i1 = Math.max(bot.notSleepTicks, 1); ++ } ++ // Leaves end - fakeplayer spawn ++ + if (randomSource.nextInt(i1) >= level.paperConfig().entities.behavior.playerInsomniaStartTicks) { // Paper - Ability to control player's insomnia and phantoms + BlockPos blockPos1 = blockPos.above(20 + randomSource.nextInt(15)) + .east(-10 + randomSource.nextInt(21)) +diff --git a/net/minecraft/world/level/storage/LevelResource.java b/net/minecraft/world/level/storage/LevelResource.java +index bef794c3f58c41d910aa0bcc63fbdeea7225fddf..a601da588e6973cc5b87d3e3eeba49b53f6d9a6d 100644 +--- a/net/minecraft/world/level/storage/LevelResource.java ++++ b/net/minecraft/world/level/storage/LevelResource.java +@@ -15,7 +15,7 @@ public class LevelResource { + public static final LevelResource ROOT = new LevelResource("."); + private final String id; + +- private LevelResource(String id) { ++ public LevelResource(String id) { // Leaves - private -> public + this.id = id; + } + +diff --git a/net/minecraft/world/level/storage/PlayerDataStorage.java b/net/minecraft/world/level/storage/PlayerDataStorage.java +index c44110b123ba5912af18faf0065e9ded780da9b7..e9da7206f9508bb2597f1b6ba8e52fa81e993a10 100644 +--- a/net/minecraft/world/level/storage/PlayerDataStorage.java ++++ b/net/minecraft/world/level/storage/PlayerDataStorage.java +@@ -20,7 +20,7 @@ import net.minecraft.world.entity.player.Player; + import org.bukkit.craftbukkit.entity.CraftPlayer; + import org.slf4j.Logger; + +-public class PlayerDataStorage { ++public class PlayerDataStorage implements org.leavesmc.leaves.bot.IPlayerDataStorage { + private static final Logger LOGGER = LogUtils.getLogger(); + private final File playerDir; + protected final DataFixer fixerUpper; diff --git a/leaves-server/minecraft-patches/features/0007-Make-shears-in-dispenser-can-unlimited-use.patch b/leaves-server/minecraft-patches/features/0007-Make-shears-in-dispenser-can-unlimited-use.patch new file mode 100644 index 00000000..8427b759 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0007-Make-shears-in-dispenser-can-unlimited-use.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 27 Feb 2022 14:07:57 +0800 +Subject: [PATCH] Make shears in dispenser can unlimited use + + +diff --git a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +index 626e9feb6a6e7a2cbc7c63e30ba4fb6b923e85c7..e8622aed7c59c15ecf73901e610688778dee56fc 100644 +--- a/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java ++++ b/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java +@@ -46,7 +46,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { + if (!serverLevel.isClientSide()) { + BlockPos blockPos = blockSource.pos().relative(blockSource.state().getValue(DispenserBlock.FACING)); + this.setSuccess(tryShearBeehive(serverLevel, blockPos) || tryShearLivingEntity(serverLevel, blockPos, item, bukkitBlock, craftItem)); // CraftBukkit +- if (this.isSuccess()) { ++ if (this.isSuccess() && !org.leavesmc.leaves.LeavesConfig.modify.oldMC.shearsInDispenserCanZeroAmount) { // Leaves - Make shears in dispenser can unlimited use + item.hurtAndBreak(1, serverLevel, null, item1 -> {}); + } + } diff --git a/leaves-server/minecraft-patches/features/0008-Redstone-Shears-Wrench.patch b/leaves-server/minecraft-patches/features/0008-Redstone-Shears-Wrench.patch new file mode 100644 index 00000000..14887474 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0008-Redstone-Shears-Wrench.patch @@ -0,0 +1,102 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 27 Mar 2022 12:48:40 +0800 +Subject: [PATCH] Redstone Shears Wrench + + +diff --git a/net/minecraft/world/item/ShearsItem.java b/net/minecraft/world/item/ShearsItem.java +index 9140b41f2ffef897b74792c916e8320a9a7afb7c..0f4f970071a289209eb8131d289e23426d53e818 100644 +--- a/net/minecraft/world/item/ShearsItem.java ++++ b/net/minecraft/world/item/ShearsItem.java +@@ -23,6 +23,22 @@ import net.minecraft.world.level.block.GrowingPlantHeadBlock; + import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.gameevent.GameEvent; + ++// Leaves start - shears wrench ++import net.minecraft.Util; ++import net.minecraft.network.chat.Component; ++import net.minecraft.world.level.block.ComparatorBlock; ++import net.minecraft.world.level.block.DispenserBlock; ++import net.minecraft.world.level.block.HopperBlock; ++import net.minecraft.world.level.block.ObserverBlock; ++import net.minecraft.world.level.block.RepeaterBlock; ++import net.minecraft.world.level.block.CrafterBlock; ++import net.minecraft.world.level.block.LeverBlock; ++import net.minecraft.world.level.block.CocoaBlock; ++import net.minecraft.world.level.block.piston.PistonBaseBlock; ++import net.minecraft.world.level.block.state.StateDefinition; ++import net.minecraft.world.level.block.state.properties.Property; ++// Leaves end - shears wrench ++ + public class ShearsItem extends Item { + public ShearsItem(Item.Properties properties) { + super(properties); +@@ -81,7 +97,68 @@ public class ShearsItem extends Item { + + return InteractionResult.SUCCESS; + } else { ++ // Leaves start - shears wrench ++ Block block = blockState.getBlock(); ++ if (org.leavesmc.leaves.LeavesConfig.modify.redstoneShearsWrench && block instanceof ObserverBlock || block instanceof DispenserBlock || ++ block instanceof PistonBaseBlock || block instanceof HopperBlock || block instanceof RepeaterBlock || block instanceof ComparatorBlock || ++ block instanceof CrafterBlock || block instanceof LeverBlock || block instanceof CocoaBlock) { ++ StateDefinition blockstatelist = block.getStateDefinition(); ++ Property iblockstate = block instanceof CrafterBlock ? blockstatelist.getProperty("orientation") : blockstatelist.getProperty("facing"); ++ Player player = context.getPlayer(); ++ ++ if (iblockstate == null || player == null) { ++ return InteractionResult.FAIL; ++ } ++ ++ if (block instanceof PistonBaseBlock) { ++ if (getNameHelper(blockState, PistonBaseBlock.EXTENDED).equals("true")) { ++ return InteractionResult.FAIL; ++ } ++ } ++ ++ if (block instanceof RepeaterBlock || block instanceof ComparatorBlock) { ++ if (getNameHelper(blockState, ComparatorBlock.POWERED).equals("true")) { ++ return InteractionResult.FAIL; ++ } ++ if (block instanceof RepeaterBlock) { ++ if (getNameHelper(blockState, RepeaterBlock.LOCKED).equals("true")) { ++ return InteractionResult.FAIL; ++ } ++ } ++ } ++ ++ if (block instanceof CrafterBlock) { ++ if (getNameHelper(blockState, CrafterBlock.CRAFTING).equals("true")) { ++ return InteractionResult.FAIL; ++ } ++ } ++ ++ BlockState iblockdata1 = ShearsItem.cycleState(blockState, iblockstate, player.isSecondaryUseActive()); ++ level.setBlock(clickedPos, iblockdata1, 18); ++ ShearsItem.message(player, Component.translatable("item.minecraft.debug_stick.update", iblockstate.getName(), ShearsItem.getNameHelper(iblockdata1, iblockstate))); ++ return InteractionResult.CONSUME; ++ } ++ // Leaves end - shears wrench ++ + return super.useOn(context); + } + } ++ ++ // Leaves start - shears wrench ++ private static > BlockState cycleState(BlockState state, Property property, boolean inverse) { ++ return state.setValue(property, ShearsItem.getRelative(property.getPossibleValues(), state.getValue(property), inverse)); // CraftBukkit - decompile error ++ } ++ ++ private static T getRelative(Iterable elements, T current, boolean inverse) { ++ return inverse ? Util.findPreviousInIterable(elements, current) : Util.findNextInIterable(elements, current); ++ } ++ ++ private static void message(Player player, Component message) { ++ ((ServerPlayer) player).sendSystemMessage(message, true); ++ } ++ ++ private static > String getNameHelper(BlockState state, Property property) { ++ return property.getName(state.getValue(property)); ++ } ++ // Leaves end - shears wrench + } diff --git a/leaves-server/minecraft-patches/features/0009-Add-isShrink-to-EntityResurrectEvent.patch b/leaves-server/minecraft-patches/features/0009-Add-isShrink-to-EntityResurrectEvent.patch new file mode 100644 index 00000000..e711d54f --- /dev/null +++ b/leaves-server/minecraft-patches/features/0009-Add-isShrink-to-EntityResurrectEvent.patch @@ -0,0 +1,27 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 30 Mar 2022 08:58:45 +0000 +Subject: [PATCH] Add isShrink to EntityResurrectEvent + + +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index 239c443ddc9bacc08a39a8ef2ab17016a2480549..985a6428f91c5834f36b5cea632af83f50f49f2d 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -1595,14 +1595,14 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + final org.bukkit.inventory.EquipmentSlot handSlot = (hand != null) ? org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand) : null; +- final EntityResurrectEvent event = new EntityResurrectEvent((org.bukkit.entity.LivingEntity) this.getBukkitEntity(), handSlot); ++ final EntityResurrectEvent event = new EntityResurrectEvent((org.bukkit.entity.LivingEntity) this.getBukkitEntity(), handSlot, true); // Leaves - can dont shrink + event.setCancelled(itemStack == null); + this.level().getCraftServer().getPluginManager().callEvent(event); + if (event.isCancelled()) { + // Set death protection to null as the event was cancelled. Prevent any attempt at ressurection. + deathProtection = null; + } else { +- if (!itemInHand.isEmpty() && itemStack != null) { // Paper - only reduce item if actual totem was found ++ if (!itemInHand.isEmpty() && itemStack != null && event.isShrink()) { // Paper - only reduce item if actual totem was found // Leaves - can dont shrink + itemInHand.shrink(1); + } + // Paper start - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled diff --git a/patches/server/0014-Budding-Amethyst-can-push-by-piston.patch b/leaves-server/minecraft-patches/features/0010-Budding-Amethyst-can-push-by-piston.patch similarity index 52% rename from patches/server/0014-Budding-Amethyst-can-push-by-piston.patch rename to leaves-server/minecraft-patches/features/0010-Budding-Amethyst-can-push-by-piston.patch index 9d368d03..ac65330b 100644 --- a/patches/server/0014-Budding-Amethyst-can-push-by-piston.patch +++ b/leaves-server/minecraft-patches/features/0010-Budding-Amethyst-can-push-by-piston.patch @@ -4,11 +4,11 @@ Date: Sun, 3 Apr 2022 11:31:04 +0800 Subject: [PATCH] Budding Amethyst can push by piston -diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java -index b6d6c2cb9b227a17fb4ce42bc75f92206fbea043..b3a3388ef62b0622906b2470056cb41f0deb0391 100644 ---- a/src/main/java/net/minecraft/world/level/block/Block.java -+++ b/src/main/java/net/minecraft/world/level/block/Block.java -@@ -583,6 +583,13 @@ public class Block extends BlockBehaviour implements ItemLike { +diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java +index 976de81d65b6494cdad20f4ec5125fceec86f951..37f561ae2eca0d118c63f519fabdcfe9cb710826 100644 +--- a/net/minecraft/world/level/block/Block.java ++++ b/net/minecraft/world/level/block/Block.java +@@ -557,6 +557,13 @@ public class Block extends BlockBehaviour implements ItemLike { } // Spigot end @@ -19,31 +19,31 @@ index b6d6c2cb9b227a17fb4ce42bc75f92206fbea043..b3a3388ef62b0622906b2470056cb41f + } + // Leaves end - reset push reaction + - private static record ShapePairKey(VoxelShape first, VoxelShape second) { - - public boolean equals(Object object) { -diff --git a/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java b/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java -index 8920855b07a31715327b8102c7faafc9f916825d..e6cc94e9c1b2388b8e42821e6bfaa2e22d44f9d3 100644 ---- a/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/BuddingAmethystBlock.java -@@ -60,4 +60,12 @@ public class BuddingAmethystBlock extends AmethystBlock { + record ShapePairKey(VoxelShape first, VoxelShape second) { + @Override + public boolean equals(Object other) { +diff --git a/net/minecraft/world/level/block/BuddingAmethystBlock.java b/net/minecraft/world/level/block/BuddingAmethystBlock.java +index f6850d3ab54a6eb4ff718d861f39aac2facd3a88..5f51b4689b0ea92eb7e4e5e42e9eb5538930d60d 100644 +--- a/net/minecraft/world/level/block/BuddingAmethystBlock.java ++++ b/net/minecraft/world/level/block/BuddingAmethystBlock.java +@@ -58,4 +58,12 @@ public class BuddingAmethystBlock extends AmethystBlock { public static boolean canClusterGrowAtState(BlockState state) { return state.isAir() || state.is(Blocks.WATER) && state.getFluidState().getAmount() == 8; } + + // Leaves start - budding amethyst can push by piston -+ @org.jetbrains.annotations.Nullable + @Override ++ @org.jetbrains.annotations.Nullable + public net.minecraft.world.level.material.PushReaction getResetPushReaction() { + return org.leavesmc.leaves.LeavesConfig.modify.buddingAmethystCanPushByPiston ? net.minecraft.world.level.material.PushReaction.PUSH_ONLY : null; + } + // Leaves end - budding amethyst can push by piston } -diff --git a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -index 99fd67a78539133adf78d65e2c520ff3dd260301..4c1ec8de5aa2edde1dbcff350a65b7fc4a4b5e74 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/src/main/java/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -1159,7 +1159,7 @@ public abstract class BlockBehaviour implements FeatureElement { +diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java +index 5473b4006f7e0266ea11a7b05cef78a113c30d97..a37a5528b36ebade8b0e3fe570c6d4819ab1cfbf 100644 +--- a/net/minecraft/world/level/block/state/BlockBehaviour.java ++++ b/net/minecraft/world/level/block/state/BlockBehaviour.java +@@ -766,7 +766,7 @@ public abstract class BlockBehaviour implements FeatureElement { } public PushReaction getPistonPushReaction() { diff --git a/patches/server/0015-Spectator-dont-get-Advancement.patch b/leaves-server/minecraft-patches/features/0011-Spectator-dont-get-Advancement.patch similarity index 64% rename from patches/server/0015-Spectator-dont-get-Advancement.patch rename to leaves-server/minecraft-patches/features/0011-Spectator-dont-get-Advancement.patch index 59b916e7..2cec735d 100644 --- a/patches/server/0015-Spectator-dont-get-Advancement.patch +++ b/leaves-server/minecraft-patches/features/0011-Spectator-dont-get-Advancement.patch @@ -4,14 +4,14 @@ Date: Sat, 25 Jun 2022 18:04:35 +0800 Subject: [PATCH] Spectator dont get Advancement -diff --git a/src/main/java/net/minecraft/server/PlayerAdvancements.java b/src/main/java/net/minecraft/server/PlayerAdvancements.java -index b9ed92cd8a12f648eeaa02427d95d75468534420..7f8eaf4590a29b147aa8c05cec919fd7744e74ba 100644 ---- a/src/main/java/net/minecraft/server/PlayerAdvancements.java -+++ b/src/main/java/net/minecraft/server/PlayerAdvancements.java -@@ -221,6 +221,11 @@ public class PlayerAdvancements { +diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java +index 459e59c370a729dfeed3872f3a5984dd3da96abe..4e9ce0c8b459ef41a6945182401c47c61b16b1f7 100644 +--- a/net/minecraft/server/PlayerAdvancements.java ++++ b/net/minecraft/server/PlayerAdvancements.java +@@ -168,6 +168,11 @@ public class PlayerAdvancements { } - public boolean award(AdvancementHolder advancement, String criterionName) { + public boolean award(AdvancementHolder advancement, String criterionKey) { + // Leaves start - spectator don't get advancement + if (org.leavesmc.leaves.LeavesConfig.modify.spectatorDontGetAdvancement && player.gameMode.getGameModeForPlayer() == net.minecraft.world.level.GameType.SPECTATOR) { + return false; diff --git a/leaves-server/minecraft-patches/features/0012-Stick-can-change-ArmorStand-arm-status.patch b/leaves-server/minecraft-patches/features/0012-Stick-can-change-ArmorStand-arm-status.patch new file mode 100644 index 00000000..6c98dc39 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0012-Stick-can-change-ArmorStand-arm-status.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sat, 25 Jun 2022 19:54:23 +0800 +Subject: [PATCH] Stick can change ArmorStand arm status + + +diff --git a/net/minecraft/world/entity/decoration/ArmorStand.java b/net/minecraft/world/entity/decoration/ArmorStand.java +index a3cc0001a949597e345d7919c3f6109fa4a949ad..dfae614a67476d649be2a8c2cd8258abe12842e3 100644 +--- a/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -341,6 +341,12 @@ public class ArmorStand extends LivingEntity { + return InteractionResult.SUCCESS_SERVER; + } + } else { ++ // Leaves start - stick can change ArmorStand arm status ++ if (org.leavesmc.leaves.LeavesConfig.modify.stickChangeArmorStandArmStatus && itemInHand.is(Items.STICK) && player.isShiftKeyDown()) { ++ setShowArms(!showArms()); ++ } ++ // Leaves end - stick can change ArmorStand arm status ++ + if (this.isDisabled(equipmentSlotForItem)) { + return InteractionResult.FAIL; + } diff --git a/leaves-server/minecraft-patches/features/0013-Configurable-MC-59471.patch b/leaves-server/minecraft-patches/features/0013-Configurable-MC-59471.patch new file mode 100644 index 00000000..c5af57d1 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0013-Configurable-MC-59471.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sat, 30 Jul 2022 01:11:30 +0800 +Subject: [PATCH] Configurable MC-59471 + + +diff --git a/net/minecraft/world/level/block/TripWireHookBlock.java b/net/minecraft/world/level/block/TripWireHookBlock.java +index 30b97cdcd495490ef65c2ab9dfc39a39c93002ca..15454e9b3289a722c8d7e33ce421a544d03966fa 100644 +--- a/net/minecraft/world/level/block/TripWireHookBlock.java ++++ b/net/minecraft/world/level/block/TripWireHookBlock.java +@@ -201,7 +201,6 @@ public class TripWireHookBlock extends Block { + if (!cancelledEmitterHook) { // Paper - Call BlockRedstoneEvent + emitState(level, pos, flag2, flag3, flag, flag1); + if (!attaching) { +- if (io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.skipTripwireHookPlacementValidation || level.getBlockState(pos).is(Blocks.TRIPWIRE_HOOK)) // Paper - Validate tripwire hook placement before update + level.setBlock(pos, blockState1.setValue(FACING, direction), 3); + if (shouldNotifyNeighbours) { + notifyNeighbors(block, level, pos, direction); +@@ -214,10 +213,17 @@ public class TripWireHookBlock extends Block { + BlockPos blockPos1 = pos.relative(direction, i2); + BlockState blockState2 = blockStates[i2]; + if (blockState2 != null) { +- BlockState blockState3 = level.getBlockState(blockPos1); +- if (blockState3.is(Blocks.TRIPWIRE) || blockState3.is(Blocks.TRIPWIRE_HOOK)) { +- level.setBlock(blockPos1, blockState2.trySetValue(ATTACHED, Boolean.valueOf(flag2)), 3); ++ // Leaves start - MC-59471 ++ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.stringTripwireHookDuplicate) { ++ level.setBlock(blockPos1, blockState2.trySetValue(ATTACHED, flag2), 3); ++ level.getBlockState(blockPos1); ++ } else { ++ BlockState blockState3 = level.getBlockState(blockPos1); ++ if (blockState3.is(Blocks.TRIPWIRE) || blockState3.is(Blocks.TRIPWIRE_HOOK)) { ++ level.setBlock(blockPos1, blockState2.trySetValue(ATTACHED, Boolean.valueOf(flag2)), 3); ++ } + } ++ // Leaves end - MC-59471 + } + } + } diff --git a/leaves-server/minecraft-patches/features/0014-No-chat-sign.patch b/leaves-server/minecraft-patches/features/0014-No-chat-sign.patch new file mode 100644 index 00000000..40d6af71 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0014-No-chat-sign.patch @@ -0,0 +1,147 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 3 Aug 2022 11:20:51 +0800 +Subject: [PATCH] No chat sign + + +diff --git a/net/minecraft/commands/arguments/ArgumentSignatures.java b/net/minecraft/commands/arguments/ArgumentSignatures.java +index 47cb25aa9c37bd84d156288c397321009f1d9ae2..a94981882ac37ea215df3a71117d4a9b1ab79fcd 100644 +--- a/net/minecraft/commands/arguments/ArgumentSignatures.java ++++ b/net/minecraft/commands/arguments/ArgumentSignatures.java +@@ -13,10 +13,17 @@ public record ArgumentSignatures(List entries) { + private static final int MAX_ARGUMENT_COUNT = 8; + private static final int MAX_ARGUMENT_NAME_LENGTH = 16; + ++ // Leaves start - no chat sign + public ArgumentSignatures(FriendlyByteBuf buffer) { +- this(buffer.readCollection(FriendlyByteBuf.>limitValue(ArrayList::new, 8), ArgumentSignatures.Entry::new)); ++ this(readSign(buffer)); + } + ++ private static List readSign(FriendlyByteBuf buf) { ++ var entries = buf.readCollection(FriendlyByteBuf.>limitValue(ArrayList::new, 8), ArgumentSignatures.Entry::new); ++ return org.leavesmc.leaves.LeavesConfig.mics.noChatSign ? List.of() : entries; ++ } ++ // Leaves end - no chat sign ++ + public void write(FriendlyByteBuf buffer) { + buffer.writeCollection(this.entries, (buffer1, entry) -> entry.write(buffer1)); + } +diff --git a/net/minecraft/network/FriendlyByteBuf.java b/net/minecraft/network/FriendlyByteBuf.java +index e5e5d9bc095ccd9fbf1c8aaa09e5c4ebb1d1c920..2e4b1883b0338cbb2a8767c7eafcf41558a55ae9 100644 +--- a/net/minecraft/network/FriendlyByteBuf.java ++++ b/net/minecraft/network/FriendlyByteBuf.java +@@ -114,6 +114,14 @@ public class FriendlyByteBuf extends ByteBuf { + public void writeJsonWithCodec(Codec codec, T value, int maxLength) { + // Paper end - Adventure; add max length parameter + DataResult dataResult = codec.encodeStart(JsonOps.INSTANCE, value); ++ // Leaves start - no chat sign ++ if (codec == net.minecraft.network.protocol.status.ServerStatus.CODEC) { ++ JsonElement element = dataResult.getOrThrow(string -> new EncoderException("Failed to encode: " + string + " " + value)); ++ element.getAsJsonObject().addProperty("preventsChatReports", org.leavesmc.leaves.LeavesConfig.mics.noChatSign); ++ this.writeUtf(GSON.toJson(element)); ++ return; ++ } ++ // Leaves end - no chat sign + this.writeUtf(GSON.toJson(dataResult.getOrThrow(exception -> new EncoderException("Failed to encode: " + exception + " " + value))), maxLength); // Paper - Adventure; add max length parameter + } + +diff --git a/net/minecraft/network/protocol/game/ServerboundChatPacket.java b/net/minecraft/network/protocol/game/ServerboundChatPacket.java +index b5afc05924ae899e020c303c8b86398e1d4ab8a0..4479634e577913372faf87138b5ba26ba02ea4f7 100644 +--- a/net/minecraft/network/protocol/game/ServerboundChatPacket.java ++++ b/net/minecraft/network/protocol/game/ServerboundChatPacket.java +@@ -16,7 +16,7 @@ public record ServerboundChatPacket(String message, Instant timeStamp, long salt + ); + + private ServerboundChatPacket(FriendlyByteBuf buffer) { +- this(buffer.readUtf(256), buffer.readInstant(), buffer.readLong(), buffer.readNullable(MessageSignature::read), new LastSeenMessages.Update(buffer)); ++ this(buffer.readUtf(256), buffer.readInstant(), buffer.readLong(), buffer.readNullable(ServerboundChatPacket::readSign), new LastSeenMessages.Update(buffer)); // Leaves - no chat sign + } + + private void write(FriendlyByteBuf buffer) { +@@ -27,6 +27,14 @@ public record ServerboundChatPacket(String message, Instant timeStamp, long salt + this.lastSeenMessages.write(buffer); + } + ++ // Leaves start - no chat sign ++ private static MessageSignature readSign(FriendlyByteBuf buf) { ++ byte[] bs = new byte[256]; ++ buf.readBytes(bs); ++ return org.leavesmc.leaves.LeavesConfig.mics.noChatSign ? null : new MessageSignature(bs); ++ } ++ // Leaves end - no chat sign ++ + @Override + public PacketType type() { + return GamePacketTypes.SERVERBOUND_CHAT; +diff --git a/net/minecraft/network/protocol/game/ServerboundChatSessionUpdatePacket.java b/net/minecraft/network/protocol/game/ServerboundChatSessionUpdatePacket.java +index 1df628ac0b414511aaed6e09d78f884c4170f730..b92081d70ffeec47c304e553ce1aea0a8980752d 100644 +--- a/net/minecraft/network/protocol/game/ServerboundChatSessionUpdatePacket.java ++++ b/net/minecraft/network/protocol/game/ServerboundChatSessionUpdatePacket.java +@@ -26,6 +26,11 @@ public record ServerboundChatSessionUpdatePacket(RemoteChatSession.Data chatSess + + @Override + public void handle(ServerGamePacketListener handler) { ++ // Leaves start - no chat report ++ if (org.leavesmc.leaves.LeavesConfig.mics.noChatSign) { ++ return; ++ } ++ // Leaves end - no chat report + handler.handleChatSessionUpdate(this); + } + } +diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java +index 0c4e5aa382fed846a35badf903cab2756621a678..765521cae8bc1c65e3a390d018190646e39c4eb0 100644 +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -616,7 +616,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + // Paper start - Add setting for proxy online mode status + return properties.enforceSecureProfile + && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() +- && this.services.canValidateProfileKeys(); ++ && this.services.canValidateProfileKeys() && !org.leavesmc.leaves.LeavesConfig.mics.noChatSign; // Leaves - no chat sign + // Paper end - Add setting for proxy online mode status + } + +diff --git a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +index 46ffae71b5119c7c4527a21b19c8e822f4d87114..d3962942cfaf93e8a8f169f243a2cb3c6f4a0ed3 100644 +--- a/net/minecraft/server/network/ServerCommonPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerCommonPacketListenerImpl.java +@@ -284,10 +284,24 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + } + + public void send(Packet packet) { ++ // Leaves start - rebuild ClientboundPlayerChatPacket ++ if (org.leavesmc.leaves.LeavesConfig.mics.noChatSign) { ++ if (this instanceof ServerGamePacketListenerImpl && packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket chat) { ++ packet = new net.minecraft.network.protocol.game.ClientboundSystemChatPacket(chat.chatType().decorate(chat.unsignedContent() != null ? chat.unsignedContent() : Component.literal(chat.body().content())), false); ++ } ++ } ++ // Leaves end - rebuild ClientboundPlayerChatPacket + this.send(packet, null); + } + + public void send(Packet packet, @Nullable PacketSendListener listener) { ++ // Leaves start - no ClientboundPlayerChatHeaderPacket and rebuild ClientboundPlayerChatPacket ++ if (org.leavesmc.leaves.LeavesConfig.mics.noChatSign) { ++ if (packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket && listener != null) { ++ listener = null; ++ } ++ } ++ // Leaves end - no ClientboundPlayerChatHeaderPacket and rebuild ClientboundPlayerChatPacket + // CraftBukkit start + if (packet == null || this.processedDisconnect) { // Spigot + return; +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 43e1ee3c9e02a1ea33fbd41604c92fe12726ae04..07a2dad0480d030fe198785fab2faa6108a38214 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -1315,7 +1315,7 @@ public abstract class PlayerList { + } + + public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public +- return message.hasSignature() && !message.hasExpiredServer(Instant.now()); ++ return org.leavesmc.leaves.LeavesConfig.mics.noChatSign || (message.hasSignature() && !message.hasExpiredServer(Instant.now())); // Leaves - No Not Secure + } + + // CraftBukkit start diff --git a/patches/server/0019-Dont-send-useless-entity-packets.patch b/leaves-server/minecraft-patches/features/0015-Dont-send-useless-entity-packets.patch similarity index 65% rename from patches/server/0019-Dont-send-useless-entity-packets.patch rename to leaves-server/minecraft-patches/features/0015-Dont-send-useless-entity-packets.patch index 20873c6b..f5f77e39 100644 --- a/patches/server/0019-Dont-send-useless-entity-packets.patch +++ b/leaves-server/minecraft-patches/features/0015-Dont-send-useless-entity-packets.patch @@ -5,24 +5,24 @@ Subject: [PATCH] Dont send useless entity packets This patch is Powered by Purpur(https://github.com/PurpurMC/Purpur) -diff --git a/src/main/java/net/minecraft/server/level/ServerEntity.java b/src/main/java/net/minecraft/server/level/ServerEntity.java -index 90eb4927fa51ce3df86aa7b6c71f49150a03e337..1e1e5ade09d368f0fdc6533f26281bd7d6d31751 100644 ---- a/src/main/java/net/minecraft/server/level/ServerEntity.java -+++ b/src/main/java/net/minecraft/server/level/ServerEntity.java -@@ -226,6 +226,11 @@ public class ServerEntity { - packet1 = ClientboundEntityPositionSyncPacket.of(this.entity); - flag3 = true; +diff --git a/net/minecraft/server/level/ServerEntity.java b/net/minecraft/server/level/ServerEntity.java +index 0fb253aa55a24b56b17f524b3261c5b75c7d7e59..f2974a0f49711d17c349fdfa8f57d3a4706ff0ca 100644 +--- a/net/minecraft/server/level/ServerEntity.java ++++ b/net/minecraft/server/level/ServerEntity.java +@@ -186,6 +186,11 @@ public class ServerEntity { + } else if (flag) { + packet = new ClientboundMoveEntityPacket.Rot(this.entity.getId(), b, b1, this.entity.onGround()); flag4 = true; + // Leaves start - dont send useless entity packets -+ if (org.leavesmc.leaves.LeavesConfig.performance.dontSendUselessEntityPackets && isUselessPacket(packet1)) { -+ packet1 = null; ++ if (org.leavesmc.leaves.LeavesConfig.performance.dontSendUselessEntityPackets && isUselessPacket(packet)) { ++ packet = null; + } + // Leaves end - dont send useless entity packets } - - if ((this.trackDelta || this.entity.hasImpulse || this.entity instanceof LivingEntity && ((LivingEntity) this.entity).isFallFlying()) && this.tickCount > 0) { -@@ -332,6 +337,21 @@ public class ServerEntity { - }); + } else { + packet = new ClientboundMoveEntityPacket.PosRot(this.entity.getId(), (short)l, (short)l1, (short)l2, b, b1, this.entity.onGround()); +@@ -302,6 +307,21 @@ public class ServerEntity { + return currentPassengers.stream().filter(entity -> !initialPassengers.contains(entity)); } + // Leaves start - dont send useless entity packets @@ -42,4 +42,4 @@ index 90eb4927fa51ce3df86aa7b6c71f49150a03e337..1e1e5ade09d368f0fdc6533f26281bd7 + public void removePairing(ServerPlayer player) { this.entity.stopSeenByPlayer(player); - player.connection.send(new ClientboundRemoveEntitiesPacket(new int[]{this.entity.getId()})); + player.connection.send(new ClientboundRemoveEntitiesPacket(this.entity.getId())); diff --git a/leaves-server/minecraft-patches/features/0016-Optimize-suffocation.patch b/leaves-server/minecraft-patches/features/0016-Optimize-suffocation.patch new file mode 100644 index 00000000..6c27f4fa --- /dev/null +++ b/leaves-server/minecraft-patches/features/0016-Optimize-suffocation.patch @@ -0,0 +1,33 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 14 Aug 2022 08:25:24 +0800 +Subject: [PATCH] Optimize suffocation + +This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) + +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index 985a6428f91c5834f36b5cea632af83f50f49f2d..144b756fede8afcb42015a4b155801d6c3715a45 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -452,7 +452,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + if (this.isAlive()) { + boolean flag = this instanceof Player; + if (this.level() instanceof ServerLevel serverLevel1) { +- if (this.isInWall()) { ++ if ((!org.leavesmc.leaves.LeavesConfig.performance.enableSuffocationOptimization || this instanceof WitherBoss || (tickCount % 10 == 0 && couldPossiblyBeHurt(1.0F))) && this.isInWall()) { // Leaves - optimize suffocation + this.hurtServer(serverLevel1, this.damageSources().inWall(), 1.0F); + } else if (flag && !this.level().getWorldBorder().isWithinBounds(this.getBoundingBox())) { + double d = this.level().getWorldBorder().getDistanceToBorder(this) + this.level().getWorldBorder().getDamageSafeZone(); +@@ -1359,6 +1359,12 @@ public abstract class LivingEntity extends Entity implements Attackable { + return this.getHealth() <= 0.0F; + } + ++ // Leaves start - optimize suffocation ++ public boolean couldPossiblyBeHurt(float amount) { ++ return !((float) this.invulnerableTime > (float) this.invulnerableDuration / 2.0F) || !(amount <= this.lastHurt); ++ } ++ // Leaves end - optimize suffocation ++ + @Override + public boolean hurtServer(ServerLevel level, DamageSource damageSource, float amount) { + if (this.isInvulnerableTo(level, damageSource)) { diff --git a/patches/server/0021-Only-check-for-spooky-season-once-an-hour.patch b/leaves-server/minecraft-patches/features/0017-Only-check-for-spooky-season-once-an-hour.patch similarity index 54% rename from patches/server/0021-Only-check-for-spooky-season-once-an-hour.patch rename to leaves-server/minecraft-patches/features/0017-Only-check-for-spooky-season-once-an-hour.patch index 6cd6a8eb..e2bce914 100644 --- a/patches/server/0021-Only-check-for-spooky-season-once-an-hour.patch +++ b/leaves-server/minecraft-patches/features/0017-Only-check-for-spooky-season-once-an-hour.patch @@ -5,11 +5,11 @@ Subject: [PATCH] Only check for spooky season once an hour This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) -diff --git a/src/main/java/net/minecraft/world/entity/ambient/Bat.java b/src/main/java/net/minecraft/world/entity/ambient/Bat.java -index 60c2868f255d372226e0c1389caaa5477bbef41e..d432e00ec084d1c7a23f10be6c457681ae66db37 100644 ---- a/src/main/java/net/minecraft/world/entity/ambient/Bat.java -+++ b/src/main/java/net/minecraft/world/entity/ambient/Bat.java -@@ -242,13 +242,30 @@ public class Bat extends AmbientCreature { +diff --git a/net/minecraft/world/entity/ambient/Bat.java b/net/minecraft/world/entity/ambient/Bat.java +index 5ebe7b1dce367d5c5e1136b97b2b9f6737595201..54e59c1fdc9c189b9c6fb620444aeb3d08fde67a 100644 +--- a/net/minecraft/world/entity/ambient/Bat.java ++++ b/net/minecraft/world/entity/ambient/Bat.java +@@ -243,12 +243,30 @@ public class Bat extends AmbientCreature { } } @@ -17,27 +17,27 @@ index 60c2868f255d372226e0c1389caaa5477bbef41e..d432e00ec084d1c7a23f10be6c457681 + private static boolean isSpookySeason = false; + private static final int ONE_HOUR = 20 * 60 * 60; + private static int lastSpookyCheck = -ONE_HOUR; ++ private static boolean isHalloween() { -- LocalDate localdate = LocalDate.now(); -- int i = localdate.get(ChronoField.DAY_OF_MONTH); -- int j = localdate.get(ChronoField.MONTH_OF_YEAR); +- LocalDate localDate = LocalDate.now(); +- int i = localDate.get(ChronoField.DAY_OF_MONTH); +- int i1 = localDate.get(ChronoField.MONTH_OF_YEAR); +- return i1 == 10 && i >= 20 || i1 == 11 && i <= 3; + if (org.leavesmc.leaves.LeavesConfig.performance.checkSpookySeasonOnceAnHour) { + if (net.minecraft.server.MinecraftServer.currentTick - lastSpookyCheck > ONE_HOUR) { + LocalDate localdate = LocalDate.now(); -+ int i = localdate.get(ChronoField.DAY_OF_MONTH); -+ int j = localdate.get(ChronoField.MONTH_OF_YEAR); ++ int i = localdate.getDayOfMonth(); ++ int j = localdate.getMonth().getValue(); + + isSpookySeason = j == 10 && i >= 20 || j == 11 && i <= 3; + lastSpookyCheck = net.minecraft.server.MinecraftServer.currentTick; + } + return isSpookySeason; + } else { -+ LocalDate localdate = LocalDate.now(); -+ int i = localdate.get(ChronoField.DAY_OF_MONTH); -+ int j = localdate.get(ChronoField.MONTH_OF_YEAR); - -- return j == 10 && i >= 20 || j == 11 && i <= 3; -+ return j == 10 && i >= 20 || j == 11 && i <= 3; ++ LocalDate localDate = LocalDate.now(); ++ int i = localDate.get(ChronoField.DAY_OF_MONTH); ++ int i1 = localDate.get(ChronoField.MONTH_OF_YEAR); ++ return i1 == 10 && i >= 20 || i1 == 11 && i <= 3; + } } + // Leaves end - only check for spooky season once an hour diff --git a/leaves-server/minecraft-patches/features/0018-Config-to-disable-method-profiler.patch b/leaves-server/minecraft-patches/features/0018-Config-to-disable-method-profiler.patch new file mode 100644 index 00000000..cfdabb7b --- /dev/null +++ b/leaves-server/minecraft-patches/features/0018-Config-to-disable-method-profiler.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 15 Aug 2022 10:18:36 +0800 +Subject: [PATCH] Config to disable method profiler + +This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 073d4a57f78dc18e4cf95bdb852877b8624ec3f8..09fc1c1923f7cc4470cca3b703810fb165c4ca15 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1249,7 +1249,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop attributesToSync = new ObjectOpenHashSet<>(); private final Set attributesToUpdate = new ObjectOpenHashSet<>(); private final AttributeSupplier supplier; + private final java.util.function.Function, AttributeInstance> createInstance; // Leaves - reduce entity allocations - public AttributeMap(AttributeSupplier defaultAttributes) { - this.supplier = defaultAttributes; + public AttributeMap(AttributeSupplier supplier) { + this.supplier = supplier; + this.createInstance = attribute -> this.supplier.createInstance(this::onAttributeModified, attribute); } @@ -25,12 +25,12 @@ index 94d04a20f97405e02d7cccaabadc7a7e86e336f7..6b5fd2f97b0d4bdf14a9e9059153b1cc @Nullable public AttributeInstance getInstance(Holder attribute) { -- return this.attributes.computeIfAbsent(attribute, attributex -> this.supplier.createInstance(this::onAttributeModified, attributex)); +- return this.attributes.computeIfAbsent(attribute, holder -> this.supplier.createInstance(this::onAttributeModified, (Holder)holder)); + // Leaves start - cache lambda, as for some reason java allocates it anyways + if (org.leavesmc.leaves.LeavesConfig.performance.reduceEntityAllocations) { + return this.attributes.computeIfAbsent(attribute, this.createInstance); + } else { -+ return this.attributes.computeIfAbsent(attribute, attributex -> this.supplier.createInstance(this::onAttributeModified, attributex)); ++ return this.attributes.computeIfAbsent(attribute, holder -> this.supplier.createInstance(this::onAttributeModified, (Holder)holder)); + } + // Leaves end - cache lambda, as for some reason java allocates it anyways } diff --git a/leaves-server/minecraft-patches/features/0021-Remove-lambda-from-ticking-guard.patch b/leaves-server/minecraft-patches/features/0021-Remove-lambda-from-ticking-guard.patch new file mode 100644 index 00000000..f3e8c728 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0021-Remove-lambda-from-ticking-guard.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 17 Aug 2022 10:56:49 +0800 +Subject: [PATCH] Remove lambda from ticking guard + +This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) + +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index c91d6d24d44b3d718680771b2299f04724c15992..8c8b6c4c6fee9bb9279d02d6b7a47cb2e3f39d93 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -816,7 +816,23 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + profilerFiller.push("tick"); +- this.guardEntityTick(this::tickNonPassenger, entity); ++ // Leaves start - copied from this.guardEntityTick ++ if (org.leavesmc.leaves.LeavesConfig.performance.remove.tickGuardLambda) { ++ try { ++ this.tickNonPassenger(entity); // Leaves - changed ++ } catch (Throwable throwable) { ++ if (throwable instanceof ThreadDeath) throw throwable; // Paper ++ // Paper start - Prevent block entity and entity crashes ++ final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); ++ MinecraftServer.LOGGER.error(msg, throwable); ++ getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent ++ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); ++ // Paper end - Prevent block entity and entity crashes ++ } ++ } else { ++ this.guardEntityTick(this::tickNonPassenger, entity); ++ } ++ // Leaves end - copied from this.guardEntityTick + profilerFiller.pop(); + } + } diff --git a/leaves-server/minecraft-patches/features/0022-Cache-climbing-check-for-activation.patch b/leaves-server/minecraft-patches/features/0022-Cache-climbing-check-for-activation.patch new file mode 100644 index 00000000..8906559a --- /dev/null +++ b/leaves-server/minecraft-patches/features/0022-Cache-climbing-check-for-activation.patch @@ -0,0 +1,47 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 18 Aug 2022 16:31:08 +0800 +Subject: [PATCH] Cache climbing check for activation + +This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) + +diff --git a/io/papermc/paper/entity/activation/ActivationRange.java b/io/papermc/paper/entity/activation/ActivationRange.java +index bd888ef719b9bfc93bace0b1d0fb771ac659f515..4b552eca363396c7e4ccfbf25d7b826f1c8163bb 100644 +--- a/io/papermc/paper/entity/activation/ActivationRange.java ++++ b/io/papermc/paper/entity/activation/ActivationRange.java +@@ -215,7 +215,7 @@ public final class ActivationRange { + } + // special cases. + if (entity instanceof final LivingEntity living) { +- if (living.onClimbable() || living.jumping || living.hurtTime > 0 || !living.activeEffects.isEmpty() || living.isFreezing()) { ++ if (living.onClimableCached() || living.jumping || living.hurtTime > 0 || !living.activeEffects.isEmpty() || living.isFreezing()) { // Leaves - use cached + return 1; + } + if (entity instanceof final Mob mob && mob.getTarget() != null) { +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index 144b756fede8afcb42015a4b155801d6c3715a45..2dd370bf00b5ddf133c946b6e4d37b00be3ca5a5 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -2026,6 +2026,22 @@ public abstract class LivingEntity extends Entity implements Attackable { + return this.lastClimbablePos; + } + ++ // Leaves start - cache climbing check ++ private boolean cachedOnClimable = false; ++ private BlockPos lastClimbingPosition = null; ++ ++ public boolean onClimableCached() { ++ if (!org.leavesmc.leaves.LeavesConfig.performance.cacheClimbCheck) { ++ return this.onClimbable(); ++ } ++ if (!this.blockPosition().equals(this.lastClimbingPosition)) { ++ this.cachedOnClimable = this.onClimbable(); ++ this.lastClimbingPosition = this.blockPosition(); ++ } ++ return this.cachedOnClimable; ++ } ++ // Leaves end - cache climbing check ++ + public boolean onClimbable() { + if (this.isSpectator()) { + return false; diff --git a/leaves-server/minecraft-patches/features/0023-Reduce-chunk-loading-lookups.patch b/leaves-server/minecraft-patches/features/0023-Reduce-chunk-loading-lookups.patch new file mode 100644 index 00000000..43f7e419 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0023-Reduce-chunk-loading-lookups.patch @@ -0,0 +1,43 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 21 Aug 2022 08:29:15 +0800 +Subject: [PATCH] Reduce chunk loading & lookups + +This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) + +diff --git a/net/minecraft/world/entity/monster/EnderMan.java b/net/minecraft/world/entity/monster/EnderMan.java +index 4b5ffd278e0e9d47100e5452949e8d757bbfece4..e2f1623b977889d31407d060b8e0bf911a80049e 100644 +--- a/net/minecraft/world/entity/monster/EnderMan.java ++++ b/net/minecraft/world/entity/monster/EnderMan.java +@@ -309,11 +309,28 @@ public class EnderMan extends Monster implements NeutralMob { + private boolean teleport(double x, double y, double z) { + BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(x, y, z); + +- while (mutableBlockPos.getY() > this.level().getMinY() && !this.level().getBlockState(mutableBlockPos).blocksMotion()) { +- mutableBlockPos.move(Direction.DOWN); ++ // Leaves start - single chunk lookup ++ BlockState blockState; ++ if (org.leavesmc.leaves.LeavesConfig.performance.reduceChuckLoadAndLookup) { ++ net.minecraft.world.level.chunk.LevelChunk chunk = this.level().getChunkIfLoaded(mutableBlockPos); ++ if (chunk == null) { ++ return false; ++ } ++ ++ while (mutableBlockPos.getY() > this.level().getMinY() && !chunk.getBlockState(mutableBlockPos).blocksMotion()) { ++ mutableBlockPos.move(Direction.DOWN); ++ } ++ ++ blockState = chunk.getBlockState(mutableBlockPos); ++ } else { ++ while (mutableBlockPos.getY() > this.level().getMinY() && !this.level().getBlockState(mutableBlockPos).blocksMotion()) { ++ mutableBlockPos.move(Direction.DOWN); ++ } ++ ++ blockState = this.level().getBlockState(mutableBlockPos); + } ++ // Leaves end - single chunk lookup + +- BlockState blockState = this.level().getBlockState(mutableBlockPos); + boolean flag = blockState.blocksMotion(); + boolean isWater = blockState.getFluidState().is(FluidTags.WATER); + if (flag && !isWater) { diff --git a/patches/server/0029-InstantBlockUpdater-Reintroduced.patch b/leaves-server/minecraft-patches/features/0024-InstantBlockUpdater-Reintroduced.patch similarity index 57% rename from patches/server/0029-InstantBlockUpdater-Reintroduced.patch rename to leaves-server/minecraft-patches/features/0024-InstantBlockUpdater-Reintroduced.patch index 5eeceab9..f02a1443 100644 --- a/patches/server/0029-InstantBlockUpdater-Reintroduced.patch +++ b/leaves-server/minecraft-patches/features/0024-InstantBlockUpdater-Reintroduced.patch @@ -5,22 +5,22 @@ Subject: [PATCH] InstantBlockUpdater Reintroduced This patch is Powered by Carpet-TIS-Addition(https://github.com/plusls/Carpet-TIS-Addition) -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 022de445bbbb869c38be4972c98dcf1c665539ec..1187453b7d961a0605af636cb6b5ed0c1f6b4be3 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -878,7 +878,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 2bbebb4335d927f240abcac67a5b423e38dc33d7..1c7f7747725f95fe3cb92d26745ada7a9784b407 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -888,7 +888,13 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl this.thread = Thread.currentThread(); - this.biomeManager = new BiomeManager(this, i); - this.isDebug = flag1; -- this.neighborUpdater = new CollectingNeighborUpdater(this, j); + this.biomeManager = new BiomeManager(this, biomeZoomSeed); + this.isDebug = isDebug; +- this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates); + // Leaves start - instantBlockUpdaterReintroduced + if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.updater.instantBlockUpdaterReintroduced) { + this.neighborUpdater = new net.minecraft.world.level.redstone.InstantNeighborUpdater(this); + } else { -+ this.neighborUpdater = new CollectingNeighborUpdater(this, j); ++ this.neighborUpdater = new CollectingNeighborUpdater(this, maxChainedNeighborUpdates); + } + // Leaves end - instantBlockUpdaterReintroduced - this.registryAccess = iregistrycustom; - this.damageSources = new DamageSources(iregistrycustom); - // CraftBukkit start + this.registryAccess = registryAccess; + this.damageSources = new DamageSources(registryAccess); + diff --git a/leaves-server/minecraft-patches/features/0025-Random-flatten-triangular-distribution.patch b/leaves-server/minecraft-patches/features/0025-Random-flatten-triangular-distribution.patch new file mode 100644 index 00000000..4643919e --- /dev/null +++ b/leaves-server/minecraft-patches/features/0025-Random-flatten-triangular-distribution.patch @@ -0,0 +1,39 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 30 Nov 2022 07:32:05 +0800 +Subject: [PATCH] Random flatten triangular distribution + +This patch is Powered by Carpet-TIS-Addition(https://github.com/plusls/Carpet-TIS-Addition) + +diff --git a/net/minecraft/util/RandomSource.java b/net/minecraft/util/RandomSource.java +index 98a54bc4de251014342cda6d0951b7fea79ce553..17a79cef7dac1b407e4e62883b4a943d172fa6b9 100644 +--- a/net/minecraft/util/RandomSource.java ++++ b/net/minecraft/util/RandomSource.java +@@ -52,13 +52,25 @@ public interface RandomSource { + + double nextGaussian(); + ++ // Leaves start - flattenTriangularDistribution + default double triangle(double center, double maxDeviation) { +- return center + maxDeviation * (this.nextDouble() - this.nextDouble()); ++ if (org.leavesmc.leaves.LeavesConfig.modify.flattenTriangularDistribution) { ++ this.nextDouble(); ++ return center + maxDeviation * (-1 + this.nextDouble() * 2); ++ } else { ++ return center + maxDeviation * (this.nextDouble() - this.nextDouble()); ++ } + } + + default float triangle(float center, float maxDeviation) { +- return center + maxDeviation * (this.nextFloat() - this.nextFloat()); ++ if (org.leavesmc.leaves.LeavesConfig.modify.flattenTriangularDistribution) { ++ this.nextFloat(); ++ return center + maxDeviation * (-1 + this.nextFloat() * 2); ++ } else { ++ return center + maxDeviation * (this.nextFloat() - this.nextFloat()); ++ } + } ++ // Leaves end - flattenTriangularDistribution + + default void consumeCount(int count) { + for (int i = 0; i < count; i++) { diff --git a/leaves-server/minecraft-patches/features/0026-BBOR-Protocol.patch b/leaves-server/minecraft-patches/features/0026-BBOR-Protocol.patch new file mode 100644 index 00000000..86aaa71c --- /dev/null +++ b/leaves-server/minecraft-patches/features/0026-BBOR-Protocol.patch @@ -0,0 +1,34 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 13:03:42 +0800 +Subject: [PATCH] BBOR Protocol + + +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 07a2dad0480d030fe198785fab2faa6108a38214..7628d226960414f9d90d24dd2a9614d6c7e8ebca 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -1438,6 +1438,7 @@ public abstract class PlayerList { + serverPlayer.connection.send(clientboundUpdateRecipesPacket); + serverPlayer.getRecipeBook().sendInitialRecipeBook(serverPlayer); + } ++ org.leavesmc.leaves.protocol.BBORProtocol.onDataPackReload(); // Leaves - bbor + } + + public boolean isAllowCommandsForAllPlayers() { +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 761fdcd4a4e18f45547afd8edff44f61c6eeacb4..1776b79309ffd9a8a52f27a144606ed9a441251e 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -739,6 +739,11 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + + public void setLoaded(boolean loaded) { + this.loaded = loaded; ++ // Leaves start - bbor ++ if (loaded) { ++ org.leavesmc.leaves.protocol.BBORProtocol.onChunkLoaded(this); ++ } ++ // Leaves end - bbor + } + + public Level getLevel() { diff --git a/leaves-server/minecraft-patches/features/0027-PCA-sync-protocol.patch b/leaves-server/minecraft-patches/features/0027-PCA-sync-protocol.patch new file mode 100644 index 00000000..fec721af --- /dev/null +++ b/leaves-server/minecraft-patches/features/0027-PCA-sync-protocol.patch @@ -0,0 +1,289 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 13:17:03 +0800 +Subject: [PATCH] PCA sync protocol + +This patch is Powered by plusls-carpet-addition(https://github.com/plusls/plusls-carpet-addition) + +diff --git a/net/minecraft/world/entity/animal/horse/AbstractHorse.java b/net/minecraft/world/entity/animal/horse/AbstractHorse.java +index d52a8315f1e6876c26c732f4c4caa47bc6bebf6e..8f287d11ab9ce493d34bf70780964c0f9a7faaa0 100644 +--- a/net/minecraft/world/entity/animal/horse/AbstractHorse.java ++++ b/net/minecraft/world/entity/animal/horse/AbstractHorse.java +@@ -422,6 +422,11 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, + + @Override + public void containerChanged(Container invBasic) { ++ // Leaves start - pca ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(this); ++ } ++ // Leaves end - pca + boolean isSaddled = this.isSaddled(); + this.syncSaddleToClients(); + if (this.tickCount > 20 && !isSaddled && this.isSaddled()) { +diff --git a/net/minecraft/world/entity/npc/AbstractVillager.java b/net/minecraft/world/entity/npc/AbstractVillager.java +index a71d16d968bb90fd7aca6f01a3dd56df4f9a7ce6..18a087a52070b9bdee4d02ff3fc6a3c063e444d4 100644 +--- a/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -65,6 +65,15 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa + super(entityType, level); + this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F); + this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F); ++ // Leaves start - pca ++ if (!this.level().isClientSide()) { ++ this.inventory.addListener(inventory -> { ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(this); ++ } ++ }); ++ } ++ // Leaves end - pca + } + + @Override +diff --git a/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java b/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java +index 516b230769fb9ddaa49adca9b6aa64d4510810da..c279e2cdafbb710b799a730e5cf7056dd2142d3b 100644 +--- a/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java ++++ b/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java +@@ -64,6 +64,11 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme + + @Override + public void setChanged() { ++ // Leaves start - pca ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(this); ++ } ++ // Leaves end - pca + } + + @Override +diff --git a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index cb4b8567c029e3a53aafda2755e3773ea8b95af7..77e7188180cce9ef881de64b263c704b219b610a 100644 +--- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -434,6 +434,16 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + } + } + ++ // Leaves start - pca ++ @Override ++ public void setChanged() { ++ super.setChanged(); ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ } ++ // Leaves end - pca ++ + @Override + public void setRecipeUsed(@Nullable RecipeHolder recipe) { + if (recipe != null) { +diff --git a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +index 0f808855f58281578c2758513787f0f7330c9291..e31f55ad12160c8406c0ab719f593d4c35ac9100 100644 +--- a/net/minecraft/world/level/block/entity/BarrelBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BarrelBlockEntity.java +@@ -120,6 +120,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { + this.items = items; + } + ++ // Leaves start - pca ++ @Override ++ public void setChanged() { ++ super.setChanged(); ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ } ++ // Leaves end - pca ++ + @Override + protected Component getDefaultName() { + return Component.translatable("container.barrel"); +diff --git a/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java b/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +index 47582c2cbae227c47684b8451c7bac39bce7e0aa..56e65301adba14c0c8af161a7aa9049949ecaddc 100644 +--- a/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java +@@ -151,6 +151,11 @@ public class BeehiveBlockEntity extends BlockEntity { + super.setChanged(); + } + ++ // Leaves start - pca ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ // Leaves end - pca + return list; + } + +@@ -208,6 +213,11 @@ public class BeehiveBlockEntity extends BlockEntity { + this.level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(bee, this.getBlockState())); + } + ++ // Leaves start - pca ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ // Leaves end - pca + bee.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.ENTER_BLOCK); // CraftBukkit - add Bukkit remove cause + super.setChanged(); + } +@@ -329,6 +339,11 @@ public class BeehiveBlockEntity extends BlockEntity { + if (releaseOccupant(level, pos, state, beeData.toOccupant(), null, beeReleaseStatus, savedFlowerPos)) { + flag = true; + iterator.remove(); ++ // Leaves start - pca ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(java.util.Objects.requireNonNull(level.getBlockEntity(pos))); ++ } ++ // Leaves end - pca + } + // Paper start - Fix bees aging inside; use exitTickCounter to keep actual bee life + else { +@@ -372,6 +387,11 @@ public class BeehiveBlockEntity extends BlockEntity { + this.maxBees = tag.getInt("Bukkit.MaxEntities"); + } + // CraftBukkit end ++ // Leaves start - pca ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ // Leaves end - pca + } + + @Override +diff --git a/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +index 94f9477e78600eded6eecc4c961576501001d187..a9a85655aac78a0be91100e8b411a28eb066162d 100644 +--- a/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java +@@ -324,4 +324,14 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements + protected AbstractContainerMenu createMenu(int id, Inventory player) { + return new BrewingStandMenu(id, player, this, this.dataAccess); + } ++ ++ // Leaves start - pca ++ @Override ++ public void setChanged() { ++ super.setChanged(); ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ } ++ // Leaves end - pca + } +diff --git a/net/minecraft/world/level/block/entity/ChestBlockEntity.java b/net/minecraft/world/level/block/entity/ChestBlockEntity.java +index afd9d657e3dc5715f22a49acb100412d00df7b7a..c59bf8a56e61cfcae064fc8a92d2c9840f733b77 100644 +--- a/net/minecraft/world/level/block/entity/ChestBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ChestBlockEntity.java +@@ -198,6 +198,16 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement + otherChest.setItems(items); + } + ++ // Leaves start - pca ++ @Override ++ public void setChanged() { ++ super.setChanged(); ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ } ++ // Leaves end - pca ++ + @Override + protected AbstractContainerMenu createMenu(int id, Inventory player) { + return ChestMenu.threeRows(id, player, this); +diff --git a/net/minecraft/world/level/block/entity/ComparatorBlockEntity.java b/net/minecraft/world/level/block/entity/ComparatorBlockEntity.java +index d3bff18645f4d3c5b71a9ee232b8e51fa8aaaaa0..2922ce789b7ae5bcd4adb70ab153ecb19355e030 100644 +--- a/net/minecraft/world/level/block/entity/ComparatorBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ComparatorBlockEntity.java +@@ -24,6 +24,16 @@ public class ComparatorBlockEntity extends BlockEntity { + this.output = tag.getInt("OutputSignal"); + } + ++ // Leaves start - pca ++ @Override ++ public void setChanged() { ++ super.setChanged(); ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ } ++ // Leaves end - pca ++ + public int getOutputSignal() { + return this.output; + } +diff --git a/net/minecraft/world/level/block/entity/DispenserBlockEntity.java b/net/minecraft/world/level/block/entity/DispenserBlockEntity.java +index 33ca79c5713961a657d3a7af1f53b89e9449eba9..3c62662e626d3f0e309d2a973c73e6ac1055cae3 100644 +--- a/net/minecraft/world/level/block/entity/DispenserBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/DispenserBlockEntity.java +@@ -99,6 +99,16 @@ public class DispenserBlockEntity extends RandomizableContainerBlockEntity { + return stack; + } + ++ // Leaves start - pca ++ @Override ++ public void setChanged() { ++ super.setChanged(); ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ } ++ // Leaves end - pca ++ + @Override + protected Component getDefaultName() { + return Component.translatable("container.dispenser"); +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 5cd1326ad5d046c88b2b3449d610a78fa880b4cd..42a29e700d2549de7cd905c373212e9757bcfcf1 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -118,6 +118,16 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + this.facing = blockState.getValue(HopperBlock.FACING); + } + ++ // Leaves start - pca ++ @Override ++ public void setChanged() { ++ super.setChanged(); ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ } ++ // Leaves end - pca ++ + @Override + protected Component getDefaultName() { + return Component.translatable("container.hopper"); +@@ -194,6 +204,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + if (flag) { + blockEntity.setCooldown(level.spigotConfig.hopperTransfer); // Spigot + setChanged(level, pos, state); ++ // Leaves start - pca ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(blockEntity); ++ } ++ // Leaves end - pca + return true; + } + } +diff --git a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +index a2ae4b47d742e7fb9809cfc4575517c06400ec61..cc8781c0a357eebc2bff936b4e7be53cc316716d 100644 +--- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +@@ -258,6 +258,16 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + this.itemStacks = items; + } + ++ // Leaves start - pca ++ @Override ++ public void setChanged() { ++ super.setChanged(); ++ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { ++ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); ++ } ++ } ++ // Leaves end - pca ++ + @Override + public int[] getSlotsForFace(Direction side) { + return SLOTS; diff --git a/leaves-server/minecraft-patches/features/0028-Jade-Protocol.patch b/leaves-server/minecraft-patches/features/0028-Jade-Protocol.patch new file mode 100644 index 00000000..5f2caac8 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0028-Jade-Protocol.patch @@ -0,0 +1,111 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 13:33:19 +0800 +Subject: [PATCH] Jade Protocol + +This patch is Powered by Jade(https://github.com/Snownee/Jade) + +diff --git a/net/minecraft/world/entity/animal/armadillo/Armadillo.java b/net/minecraft/world/entity/animal/armadillo/Armadillo.java +index dfdbcb31458095a71c187efc2774ecc4945dd11b..cbcd0b7d2107f7ddd353b2bc2d51a0af32450c57 100644 +--- a/net/minecraft/world/entity/animal/armadillo/Armadillo.java ++++ b/net/minecraft/world/entity/animal/armadillo/Armadillo.java +@@ -61,7 +61,7 @@ public class Armadillo extends Animal { + public final AnimationState rollOutAnimationState = new AnimationState(); + public final AnimationState rollUpAnimationState = new AnimationState(); + public final AnimationState peekAnimationState = new AnimationState(); +- private int scuteTime; ++ public int scuteTime; // Leaves - private -> public + private boolean peekReceivedClient = false; + + public Armadillo(EntityType entityType, Level level) { +diff --git a/net/minecraft/world/entity/animal/frog/Tadpole.java b/net/minecraft/world/entity/animal/frog/Tadpole.java +index 97adf8142cdd322c4873c420ed760e9dee34da23..cd04199fa8861025e92884f9ec2d3c721c50ad75 100644 +--- a/net/minecraft/world/entity/animal/frog/Tadpole.java ++++ b/net/minecraft/world/entity/animal/frog/Tadpole.java +@@ -253,7 +253,7 @@ public class Tadpole extends AbstractFish { + } + } + +- private int getTicksLeftUntilAdult() { ++ public int getTicksLeftUntilAdult() { // Leaves - private -> public + return Math.max(0, ticksToBeFrog - this.age); + } + +diff --git a/net/minecraft/world/level/storage/loot/LootPool.java b/net/minecraft/world/level/storage/loot/LootPool.java +index 29ad43245a310756c4227acd7532e905f7f8b8ee..ad422817593449b8e914628b51d760e732e2d50c 100644 +--- a/net/minecraft/world/level/storage/loot/LootPool.java ++++ b/net/minecraft/world/level/storage/loot/LootPool.java +@@ -36,7 +36,7 @@ public class LootPool { + ) + .apply(instance, LootPool::new) + ); +- private final List entries; ++ public final List entries; // Leaves - private -> public + private final List conditions; + private final Predicate compositeCondition; + private final List functions; +diff --git a/net/minecraft/world/level/storage/loot/LootTable.java b/net/minecraft/world/level/storage/loot/LootTable.java +index f95d0f2da3d958519d28278079555c800aad02f8..bebffd07047e41c53b9e4f1ad5917680b8e8c796 100644 +--- a/net/minecraft/world/level/storage/loot/LootTable.java ++++ b/net/minecraft/world/level/storage/loot/LootTable.java +@@ -45,7 +45,7 @@ public class LootTable { + public static final Codec> CODEC = RegistryFileCodec.create(Registries.LOOT_TABLE, DIRECT_CODEC); + private final ContextKeySet paramSet; + private final Optional randomSequence; +- private final List pools; ++ public final List pools; // Leaves - private -> public + private final List functions; + private final BiFunction compositeFunction; + public org.bukkit.craftbukkit.CraftLootTable craftLootTable; // CraftBukkit +diff --git a/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java b/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java +index 8e91ddc6c0e492d165ad8322b4a3d5c3bad5409c..6e420bfb3c223b094157bdfec7dad20d8eab4968 100644 +--- a/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java ++++ b/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java +@@ -9,7 +9,7 @@ import net.minecraft.world.level.storage.loot.ValidationContext; + import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; + + public abstract class CompositeEntryBase extends LootPoolEntryContainer { +- protected final List children; ++ public final List children; // Leaves - private -> public + private final ComposableEntryContainer composedChildren; + + protected CompositeEntryBase(List children, List conditions) { +diff --git a/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java b/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java +index e0e933245e038b7229eeddbda272b081161ab603..c5e3834fa970ac909cefea43420378394153d781 100644 +--- a/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java ++++ b/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java +@@ -13,7 +13,7 @@ import net.minecraft.world.level.storage.loot.predicates.ConditionUserBuilder; + import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; + + public abstract class LootPoolEntryContainer implements ComposableEntryContainer { +- protected final List conditions; ++ public final List conditions; // Leaves - private -> public + private final Predicate compositeCondition; + + protected LootPoolEntryContainer(List conditions) { +diff --git a/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java b/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java +index d5e697a0cf6091a7f37c68e3c2a52851535735b1..a8a5a872a8647896e80f91cb5a89adead4005cf7 100644 +--- a/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java ++++ b/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java +@@ -25,7 +25,7 @@ public class NestedLootTable extends LootPoolSingletonContainer { + .and(singletonFields(instance)) + .apply(instance, NestedLootTable::new) + ); +- private final Either, LootTable> contents; ++ public final Either, LootTable> contents; // Leaves - private -> public + + private NestedLootTable( + Either, LootTable> contents, int weight, int quality, List conditions, List functions +diff --git a/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java b/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java +index 7134c54984a12949cd6a2e8dc35c2e1c0431e524..52f36fbb9bfcad81004e531efab85e9b87d3284d 100644 +--- a/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java ++++ b/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java +@@ -11,7 +11,7 @@ import net.minecraft.world.level.storage.loot.LootContext; + import net.minecraft.world.level.storage.loot.ValidationContext; + + public abstract class CompositeLootItemCondition implements LootItemCondition { +- protected final List terms; ++ public final List terms; // Leaves - private -> public + private final Predicate composedPredicate; + + protected CompositeLootItemCondition(List terms, Predicate composedPredicate) { diff --git a/leaves-server/minecraft-patches/features/0029-Alternative-block-placement-Protocol.patch b/leaves-server/minecraft-patches/features/0029-Alternative-block-placement-Protocol.patch new file mode 100644 index 00000000..979f9139 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0029-Alternative-block-placement-Protocol.patch @@ -0,0 +1,101 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 13:51:26 +0800 +Subject: [PATCH] Alternative block placement Protocol + +This patch is Powered by +carpet-extra(https://github.com/gnembon/carpet-extra) +MasaGadget(https://github.com/plusls/MasaGadget) +litematica(https://github.com/maruohon/litematica) + +diff --git a/net/minecraft/world/item/BlockItem.java b/net/minecraft/world/item/BlockItem.java +index 68e50c6ade879d263424f244070677cb81c34c33..e619a872d51bf0be64ff594c916c89a5bbf1d3fc 100644 +--- a/net/minecraft/world/item/BlockItem.java ++++ b/net/minecraft/world/item/BlockItem.java +@@ -158,6 +158,27 @@ public class BlockItem extends Item { + @Nullable + protected BlockState getPlacementState(BlockPlaceContext context) { + BlockState stateForPlacement = this.getBlock().getStateForPlacement(context); ++ // Leaves start - alternativeBlockPlacement ++ switch (org.leavesmc.leaves.LeavesConfig.protocol.alternativeBlockPlacement) { ++ case CARPET -> { ++ BlockState tryState = org.leavesmc.leaves.protocol.CarpetAlternativeBlockPlacement.alternativeBlockPlacement(getBlock(), context); ++ if (tryState != null) { ++ stateForPlacement = tryState; ++ } ++ } ++ case CARPET_FIX -> { ++ BlockState tryState = org.leavesmc.leaves.protocol.CarpetAlternativeBlockPlacement.alternativeBlockPlacementFix(getBlock(), context); ++ if (tryState != null) { ++ stateForPlacement = tryState; ++ } ++ } ++ case LITEMATICA -> { ++ if (stateForPlacement != null && this.canPlace(context, stateForPlacement)) { ++ return org.leavesmc.leaves.protocol.LitematicaEasyPlaceProtocol.applyPlacementProtocol(stateForPlacement, context); ++ } ++ } ++ } ++ // Leaves end - alternativeBlockPlacement + return stateForPlacement != null && this.canPlace(context, stateForPlacement) ? stateForPlacement : null; + } + +diff --git a/net/minecraft/world/item/StandingAndWallBlockItem.java b/net/minecraft/world/item/StandingAndWallBlockItem.java +index 12c6c8aeec89a0a55633c62fe98f5a3aa75fd476..1f0e7c391d02b18e2c89700025713ec3d759f2ea 100644 +--- a/net/minecraft/world/item/StandingAndWallBlockItem.java ++++ b/net/minecraft/world/item/StandingAndWallBlockItem.java +@@ -27,14 +27,14 @@ public class StandingAndWallBlockItem extends BlockItem { + @Nullable + @Override + protected BlockState getPlacementState(BlockPlaceContext context) { +- BlockState stateForPlacement = this.wallBlock.getStateForPlacement(context); ++ BlockState stateForPlacement = this.wallBlock.getRealStateForPlacement(context); // Leaves - alternativeBlockPlacement + BlockState blockState = null; + LevelReader level = context.getLevel(); + BlockPos clickedPos = context.getClickedPos(); + + for (Direction direction : context.getNearestLookingDirections()) { + if (direction != this.attachmentDirection.getOpposite()) { +- BlockState blockState1 = direction == this.attachmentDirection ? this.getBlock().getStateForPlacement(context) : stateForPlacement; ++ BlockState blockState1 = direction == this.attachmentDirection ? this.getBlock().getRealStateForPlacement(context) : stateForPlacement; // Leaves - alternativeBlockPlacement + if (blockState1 != null && this.canPlace(level, blockState1, clickedPos)) { + blockState = blockState1; + break; +diff --git a/net/minecraft/world/level/block/Block.java b/net/minecraft/world/level/block/Block.java +index 37f561ae2eca0d118c63f519fabdcfe9cb710826..43e338e75b7ded9f80e4ff2ce1a7dac043c93ea1 100644 +--- a/net/minecraft/world/level/block/Block.java ++++ b/net/minecraft/world/level/block/Block.java +@@ -392,6 +392,33 @@ public class Block extends BlockBehaviour implements ItemLike { + public void stepOn(Level level, BlockPos pos, BlockState state, Entity entity) { + } + ++ // Leaves start - alternativeBlockPlacement ++ @Nullable ++ public BlockState getRealStateForPlacement(BlockPlaceContext ctx) { ++ BlockState vanillaState = getStateForPlacement(ctx); ++ switch (org.leavesmc.leaves.LeavesConfig.protocol.alternativeBlockPlacement) { ++ case CARPET -> { ++ BlockState tryState = org.leavesmc.leaves.protocol.CarpetAlternativeBlockPlacement.alternativeBlockPlacement(this, ctx); ++ if (tryState != null) { ++ return tryState; ++ } ++ } ++ case CARPET_FIX -> { ++ BlockState tryState = org.leavesmc.leaves.protocol.CarpetAlternativeBlockPlacement.alternativeBlockPlacementFix(this, ctx); ++ if (tryState != null) { ++ return tryState; ++ } ++ } ++ case LITEMATICA -> { ++ if (vanillaState != null) { ++ return org.leavesmc.leaves.protocol.LitematicaEasyPlaceProtocol.applyPlacementProtocol(vanillaState, ctx); ++ } ++ } ++ } ++ return vanillaState; ++ } ++ // Leaves end - alternativeBlockPlacement ++ + @Nullable + public BlockState getStateForPlacement(BlockPlaceContext context) { + return this.defaultBlockState(); diff --git a/patches/server/0035-Player-operation-limiter.patch b/leaves-server/minecraft-patches/features/0030-Player-operation-limiter.patch similarity index 53% rename from patches/server/0035-Player-operation-limiter.patch rename to leaves-server/minecraft-patches/features/0030-Player-operation-limiter.patch index 8471c8ef..6b3951f2 100644 --- a/patches/server/0035-Player-operation-limiter.patch +++ b/leaves-server/minecraft-patches/features/0030-Player-operation-limiter.patch @@ -5,11 +5,11 @@ Subject: [PATCH] Player operation limiter This patch is Powered by plusls-carpet-addition(https://github.com/plusls/plusls-carpet-addition) -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index ebae6c704844755c75aa0c6f460603c5d909b5cf..d02f0123a830c77056e978bbef8d454d110d0589 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -329,6 +329,10 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index c9e7f3cfe5aff88424ced502be18e7bb0c20240f..68c934aec165690979f066eee73d4b2bc803ed14 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -393,6 +393,10 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc public com.destroystokyo.paper.event.entity.PlayerNaturallySpawnCreaturesEvent playerNaturallySpawnedEvent; // Paper - PlayerNaturallySpawnCreaturesEvent public @Nullable String clientBrandName = null; // Paper - Brand support public org.bukkit.event.player.PlayerQuitEvent.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 @@ -20,18 +20,18 @@ index ebae6c704844755c75aa0c6f460603c5d909b5cf..d02f0123a830c77056e978bbef8d454d // Paper start - rewrite chunk system private ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader.PlayerChunkLoaderData chunkLoader; -@@ -991,6 +995,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - this.joining = false; +@@ -798,6 +802,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc } // CraftBukkit end + this.tickClientLoadTimeout(); + this.resetOperationCountPerTick(); // Leaves - player operation limiter this.gameMode.tick(); this.wardenSpawnTracker.tick(); - --this.spawnInvulnerableTime; -@@ -3314,5 +3319,32 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - public CraftPlayer getBukkitEntity() { - return (CraftPlayer) super.getBukkitEntity(); + if (this.invulnerableTime > 0) { +@@ -3087,4 +3092,31 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + return (org.bukkit.craftbukkit.entity.CraftPlayer) super.getBukkitEntity(); } + // CraftBukkit end + + // Leaves start - player operation limiter + private void resetOperationCountPerTick() { @@ -59,54 +59,54 @@ index ebae6c704844755c75aa0c6f460603c5d909b5cf..d02f0123a830c77056e978bbef8d454d + return (instaBreakCountPerTick == 0 || placeBlockCountPerTick == 0) && (instaBreakCountPerTick <= 1 && placeBlockCountPerTick <= 2); + } + // Leaves end - player operation limiter - // CraftBukkit end } -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -index a96f859a5d0c6ec692d4627a69f3c9ee49199dbc..31ff81ca6a89fc3ce59c4b53a6a547eb4f2e812a 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayerGameMode.java -@@ -337,6 +337,19 @@ public class ServerPlayerGameMode { +diff --git a/net/minecraft/server/level/ServerPlayerGameMode.java b/net/minecraft/server/level/ServerPlayerGameMode.java +index 623c069f1fe079e020c6391a3db1a3d95cd3dbf5..d2b2290c73826278a89eb02ab81ee503ee41a4a2 100644 +--- a/net/minecraft/server/level/ServerPlayerGameMode.java ++++ b/net/minecraft/server/level/ServerPlayerGameMode.java +@@ -303,6 +303,19 @@ public class ServerPlayerGameMode { } - public void destroyAndAck(BlockPos pos, int sequence, String reason) { + public void destroyAndAck(BlockPos pos, int sequence, String message) { + // Leaves start - player operation limiter + if (org.leavesmc.leaves.LeavesConfig.modify.playerOperationLimiter) { -+ if (reason.equals("insta mine")) { ++ if (message.equals("insta mine")) { + player.addInstaBreakCountPerTick(); + if (!player.allowOperation()) { -+ MinecraftServer.getServer().server.getPluginManager().callEvent(new org.leavesmc.leaves.event.player.PlayerOperationLimitEvent(player.getBukkitEntity(), org.leavesmc.leaves.event.player.PlayerOperationLimitEvent.Operation.MINE, CraftBlock.at(level, pos))); ++ this.level.getCraftServer().getPluginManager().callEvent(new org.leavesmc.leaves.event.player.PlayerOperationLimitEvent(player.getBukkitEntity(), org.leavesmc.leaves.event.player.PlayerOperationLimitEvent.Operation.MINE, org.bukkit.craftbukkit.block.CraftBlock.at(level, pos))); + this.player.connection.send(new ClientboundBlockUpdatePacket(pos, this.level.getBlockState(pos))); -+ this.debugLogging(pos, false, sequence, reason); ++ this.debugLogging(pos, false, sequence, message); + return; + } + } + } + // Leaves end - player operation limiter if (this.destroyBlock(pos)) { - this.debugLogging(pos, true, sequence, reason); + this.debugLogging(pos, true, sequence, message); } else { -diff --git a/src/main/java/net/minecraft/world/item/BlockItem.java b/src/main/java/net/minecraft/world/item/BlockItem.java -index dba8ee20f9fed3adf26885471897ade154ec1d4d..77309808abd4ab476e815d60015ad828102a1f6b 100644 ---- a/src/main/java/net/minecraft/world/item/BlockItem.java -+++ b/src/main/java/net/minecraft/world/item/BlockItem.java -@@ -74,6 +74,20 @@ public class BlockItem extends Item { - final org.bukkit.block.BlockState oldBlockstate = blockstate != null ? blockstate : org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(blockactioncontext1.getLevel(), blockactioncontext1.getClickedPos()); // Paper - Reset placed block on exception +diff --git a/net/minecraft/world/item/BlockItem.java b/net/minecraft/world/item/BlockItem.java +index e619a872d51bf0be64ff594c916c89a5bbf1d3fc..1f4a3d2610abfa2ea2b1d5feba9606b806d6d416 100644 +--- a/net/minecraft/world/item/BlockItem.java ++++ b/net/minecraft/world/item/BlockItem.java +@@ -72,6 +72,21 @@ public class BlockItem extends Item { + final org.bukkit.block.BlockState oldBukkitState = bukkitState != null ? bukkitState : org.bukkit.craftbukkit.block.CraftBlockStates.getBlockState(blockPlaceContext.getLevel(), blockPlaceContext.getClickedPos()); // Paper - Reset placed block on exception // CraftBukkit end + // Leaves start - player operation limiter + if (org.leavesmc.leaves.LeavesConfig.modify.playerOperationLimiter && !context.getLevel().isClientSide()) { + ServerPlayer player = (ServerPlayer) context.getPlayer(); -+ if (player != null && iblockdata != null) { ++ if (player != null && placementState != null) { + player.addPlaceBlockCountPerTick(); + if (!player.allowOperation()) { -+ if (blockstate != null) { -+ MinecraftServer.getServer().server.getPluginManager().callEvent(new org.leavesmc.leaves.event.player.PlayerOperationLimitEvent(player.getBukkitEntity(), org.leavesmc.leaves.event.player.PlayerOperationLimitEvent.Operation.PLACE, blockstate.getBlock())); ++ if (bukkitState != null) { ++ context.getLevel().getCraftServer().getPluginManager().callEvent(new org.leavesmc.leaves.event.player.PlayerOperationLimitEvent(player.getBukkitEntity(), org.leavesmc.leaves.event.player.PlayerOperationLimitEvent.Operation.PLACE, bukkitState.getBlock())); + } + return InteractionResult.FAIL; + } + } + } + // Leaves end - player operation limiter - if (iblockdata == null) { ++ + if (placementState == null) { return InteractionResult.FAIL; - } else if (!this.placeBlock(blockactioncontext1, iblockdata)) { + } else if (!this.placeBlock(blockPlaceContext, placementState)) { diff --git a/patches/server/0036-Renewable-Elytra.patch b/leaves-server/minecraft-patches/features/0031-Renewable-Elytra.patch similarity index 65% rename from patches/server/0036-Renewable-Elytra.patch rename to leaves-server/minecraft-patches/features/0031-Renewable-Elytra.patch index 440ba374..2e7e5e7f 100644 --- a/patches/server/0036-Renewable-Elytra.patch +++ b/leaves-server/minecraft-patches/features/0031-Renewable-Elytra.patch @@ -5,12 +5,12 @@ Subject: [PATCH] Renewable Elytra This patch is Powered by Carpet-TIS-Addition(https://github.com/plusls/Carpet-TIS-Addition) -diff --git a/src/main/java/net/minecraft/world/entity/monster/Phantom.java b/src/main/java/net/minecraft/world/entity/monster/Phantom.java -index 150fd890ac65097b5434fd88e8d2b24a89dca79a..e47de71f7c120cf7f1a7cdb5e49b1d89163e5722 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Phantom.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Phantom.java -@@ -233,6 +233,20 @@ public class Phantom extends FlyingMob implements Enemy { - return predicate.test(world, this, target); +diff --git a/net/minecraft/world/entity/monster/Phantom.java b/net/minecraft/world/entity/monster/Phantom.java +index a91aba11ecda561d117c9d8db85c92cdcd81887e..d4123b93cec95daeb9991cf20b1d2c8701474606 100644 +--- a/net/minecraft/world/entity/monster/Phantom.java ++++ b/net/minecraft/world/entity/monster/Phantom.java +@@ -242,6 +242,20 @@ public class Phantom extends FlyingMob implements Enemy { + return targetingConditions.test(level, this, entity); } + // Leaves start - renewable elytra @@ -27,6 +27,6 @@ index 150fd890ac65097b5434fd88e8d2b24a89dca79a..e47de71f7c120cf7f1a7cdb5e49b1d89 + } + // Leaves end - renewable elytra + - // Paper start - @Nullable - java.util.UUID spawningEntity; + static enum AttackPhase { + CIRCLE, + SWOOP; diff --git a/leaves-server/minecraft-patches/features/0032-Stackable-ShulkerBoxes.patch b/leaves-server/minecraft-patches/features/0032-Stackable-ShulkerBoxes.patch new file mode 100644 index 00000000..7134ea64 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0032-Stackable-ShulkerBoxes.patch @@ -0,0 +1,393 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 14 Dec 2022 14:47:06 +0800 +Subject: [PATCH] Stackable ShulkerBoxes + +This patch is Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet) and plusls-carpet-addition(https://github.com/plusls/plusls-carpet-addition) + +diff --git a/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java b/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java +index 828fbe03e7beb860cd0816c7ac8adbffe196533b..513988c37de927ddf48f2bc9cf6905c70c41e75d 100644 +--- a/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java ++++ b/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java +@@ -23,7 +23,7 @@ public class ClientboundContainerSetContentPacket implements Packet= 1 && packet.slotNum() <= 45; +- boolean flag2 = itemStack.isEmpty() || itemStack.getCount() <= itemStack.getMaxStackSize(); ++ boolean flag2 = itemStack.isEmpty() || itemStack.getCount() <= org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemStack); // Leaves - stackable shulker boxes + if (flag || (flag1 && !ItemStack.matches(this.player.inventoryMenu.getSlot(packet.slotNum()).getItem(), packet.itemStack()))) { // Insist on valid slot + // CraftBukkit start - Call click event + InventoryView inventory = this.player.inventoryMenu.getBukkitView(); +diff --git a/net/minecraft/world/Container.java b/net/minecraft/world/Container.java +index 2d3721e311851c1801b090e99d4f9d0daf4e5f99..f779d27603e6d81435c061214a2db3a14a31c11e 100644 +--- a/net/minecraft/world/Container.java ++++ b/net/minecraft/world/Container.java +@@ -30,6 +30,12 @@ public interface Container extends Clearable { + return Math.min(this.getMaxStackSize(), stack.getMaxStackSize()); + } + ++ // Leaves start - stackable shulker boxes ++ default int getMaxStackLeaves(ItemStack stack) { ++ return Math.min(this.getMaxStackSize(), org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(stack)); ++ } ++ // Leaves end - stackable shulker boxes ++ + void setChanged(); + + boolean stillValid(Player player); +diff --git a/net/minecraft/world/SimpleContainer.java b/net/minecraft/world/SimpleContainer.java +index 190190463086f2fcbe28f00fa2f23795bf11f279..6ee5ecc89e360ca30ed5f7f355ae943c5a6337ca 100644 +--- a/net/minecraft/world/SimpleContainer.java ++++ b/net/minecraft/world/SimpleContainer.java +@@ -205,7 +205,7 @@ public class SimpleContainer implements Container, StackedContentsCompatible { + @Override + public void setItem(int index, ItemStack stack) { + this.items.set(index, stack); +- stack.limitSize(this.getMaxStackSize(stack)); ++ stack.limitSize(this.getMaxStackLeaves(stack)); + this.setChanged(); + } + +@@ -280,7 +280,7 @@ public class SimpleContainer implements Container, StackedContentsCompatible { + } + + private void moveItemsBetweenStacks(ItemStack stack, ItemStack other) { +- int maxStackSize = this.getMaxStackSize(other); ++ int maxStackSize = this.getMaxStackLeaves(other); // Leaves - stackable shulker boxes + int min = Math.min(stack.getCount(), maxStackSize - other.getCount()); + if (min > 0) { + other.grow(min); +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index 52a7ed0d991758bad0dcedcb7f97fb15ac6c6d04..d2d258246e7f7332c0420077e33c05e222bda46d 100644 +--- a/net/minecraft/world/entity/item/ItemEntity.java ++++ b/net/minecraft/world/entity/item/ItemEntity.java +@@ -274,10 +274,52 @@ public class ItemEntity extends Entity implements TraceableEntity { + + private boolean isMergable() { + ItemStack item = this.getItem(); +- return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < this.despawnRate && item.getCount() < item.getMaxStackSize(); // Paper - Alternative item-despawn-rate ++ return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < this.despawnRate && item.getCount() < org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(item); // Paper - Alternative item-despawn-rate // Leaves - stackable shulker boxes + } + ++ // Leaves end - stackable shulker boxes ++ private boolean tryStackShulkerBoxes(ItemEntity other) { ++ ItemStack selfStack = this.getItem(); ++ if (org.leavesmc.leaves.LeavesConfig.modify.shulkerBoxStackSize == 1 || ++ !(selfStack.getItem() instanceof net.minecraft.world.item.BlockItem blockItem) || ++ !(blockItem.getBlock() instanceof net.minecraft.world.level.block.ShulkerBoxBlock) ++ ) { ++ return false; ++ } ++ ++ ItemStack otherStack = other.getItem(); ++ if (selfStack.getItem() == otherStack.getItem() ++ && org.leavesmc.leaves.util.ShulkerBoxUtils.shulkerBoxNoItem(selfStack) ++ && org.leavesmc.leaves.util.ShulkerBoxUtils.shulkerBoxNoItem(otherStack) ++ && Objects.equals(selfStack.getComponents(), otherStack.getComponents()) // empty block entity tags are cleaned up when spawning ++ && selfStack.getCount() != org.leavesmc.leaves.LeavesConfig.modify.shulkerBoxStackSize) { ++ int amount = Math.min(otherStack.getCount(), org.leavesmc.leaves.LeavesConfig.modify.shulkerBoxStackSize - selfStack.getCount()); ++ ++ selfStack.grow(amount); ++ this.setItem(selfStack); ++ ++ this.pickupDelay = Math.max(other.pickupDelay, this.pickupDelay); ++ this.age = Math.min(other.getAge(), this.age); ++ ++ otherStack.shrink(amount); ++ if (otherStack.isEmpty()) { ++ other.discard(); ++ } ++ else { ++ other.setItem(otherStack); ++ } ++ return true; ++ } ++ return false; ++ } ++ // Leaves end - stackable shulker boxes ++ + private void tryToMerge(ItemEntity itemEntity) { ++ // Leaves start - stackable shulker boxes ++ if (tryStackShulkerBoxes(itemEntity)) { ++ return; ++ } ++ // Leaves end - stackable shulker boxes + ItemStack item = this.getItem(); + ItemStack item1 = itemEntity.getItem(); + if (Objects.equals(this.target, itemEntity.target) && areMergable(item, item1)) { +diff --git a/net/minecraft/world/entity/player/Inventory.java b/net/minecraft/world/entity/player/Inventory.java +index 839cbb67d3d38960d9114a4db5bab911b66a573c..9b628d553d3206a4881ed5e07fa6d2552b710cea 100644 +--- a/net/minecraft/world/entity/player/Inventory.java ++++ b/net/minecraft/world/entity/player/Inventory.java +@@ -97,10 +97,12 @@ public class Inventory implements Container, Nameable { + } + + private boolean hasRemainingSpaceForItem(ItemStack destination, ItemStack origin) { ++ // Leaves start - stackable shulker boxes + return !destination.isEmpty() +- && destination.isStackable() +- && destination.getCount() < this.getMaxStackSize(destination) ++ && org.leavesmc.leaves.util.ShulkerBoxUtils.isStackable(destination) ++ && destination.getCount() < org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(destination) + && ItemStack.isSameItemSameComponents(destination, origin); // Paper - check if itemstack is stackable first ++ // Leaves end - stackable shulker boxes + } + + // CraftBukkit start - Watch method above! :D +@@ -113,7 +115,7 @@ public class Inventory implements Container, Nameable { + } + + if (this.hasRemainingSpaceForItem(itemInSlot, itemStack)) { +- remains -= (itemInSlot.getMaxStackSize() < this.getMaxStackSize() ? itemInSlot.getMaxStackSize() : this.getMaxStackSize()) - itemInSlot.getCount(); ++ remains -= (org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemInSlot) < this.getMaxStackSize() ? org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemInSlot) : this.getMaxStackSize()) - itemInSlot.getCount(); // Leaves - stackable shulker boxes + } + if (remains <= 0) { + return itemStack.getCount(); +@@ -122,7 +124,7 @@ public class Inventory implements Container, Nameable { + + ItemStack itemInOffhand = this.getItem(this.items.size() + this.armor.size()); + if (this.hasRemainingSpaceForItem(itemInOffhand, itemStack)) { +- remains -= (itemInOffhand.getMaxStackSize() < this.getMaxStackSize() ? itemInOffhand.getMaxStackSize() : this.getMaxStackSize()) - itemInOffhand.getCount(); ++ remains -= (org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemInOffhand) < this.getMaxStackSize() ? org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemInOffhand) : this.getMaxStackSize()) - itemInOffhand.getCount(); // Leaves - stackable shulker boxes + } + if (remains <= 0) { + return itemStack.getCount(); +@@ -252,7 +254,7 @@ public class Inventory implements Container, Nameable { + this.setItem(slot, item); + } + +- int i = this.getMaxStackSize(item) - item.getCount(); ++ int i = this.getMaxStackLeaves(item) - item.getCount(); // Leaves - stackable shulker boxes + int min = Math.min(count, i); + if (min == 0) { + return count; +@@ -359,7 +361,7 @@ public class Inventory implements Container, Nameable { + break; + } + +- int i = stack.getMaxStackSize() - this.getItem(slotWithRemainingSpace).getCount(); ++ int i = org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(stack) - this.getItem(slotWithRemainingSpace).getCount(); // Leaves - stackable shulker boxes + if (this.add(slotWithRemainingSpace, stack.split(i)) && sendPacket && this.player instanceof ServerPlayer serverPlayer) { + serverPlayer.connection.send(this.createInventoryUpdatePacket(slotWithRemainingSpace)); + } +diff --git a/net/minecraft/world/entity/vehicle/ContainerEntity.java b/net/minecraft/world/entity/vehicle/ContainerEntity.java +index c18aea2bb5ddddadbc858b253ff4c08d82178a18..6a24b442a21298ef3bdbcb76de9bdcf006890c5d 100644 +--- a/net/minecraft/world/entity/vehicle/ContainerEntity.java ++++ b/net/minecraft/world/entity/vehicle/ContainerEntity.java +@@ -164,7 +164,7 @@ public interface ContainerEntity extends Container, MenuProvider { + default void setChestVehicleItem(int slot, ItemStack stack) { + this.unpackChestVehicleLootTable(null); + this.getItemStacks().set(slot, stack); +- stack.limitSize(this.getMaxStackSize(stack)); ++ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes + } + + default SlotAccess getChestVehicleSlot(final int index) { +diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java +index b7285c67ce3f6db2fa23a6dc91fcfa6a3d19f9b4..bca4a8528ec362471797c2d962cd3c903cae89f2 100644 +--- a/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -417,7 +417,7 @@ public abstract class AbstractContainerMenu { + && (this.quickcraftType == 2 || carried1.getCount() >= this.quickcraftSlots.size()) + && this.canDragTo(slot1)) { + int i2 = slot1.hasItem() ? slot1.getItem().getCount() : 0; +- int min = Math.min(itemStack.getMaxStackSize(), slot1.getMaxStackSize(itemStack)); ++ int min = Math.min(org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemStack), slot1.getMaxStackSize(itemStack)); // Leaves - stackable shulker boxes + int min1 = Math.min(getQuickCraftPlaceCount(this.quickcraftSlots, this.quickcraftType, itemStack) + i2, min); + count -= min1 - i2; + // slot1.setByPlayer(itemStack.copyWithCount(min1)); +@@ -531,7 +531,7 @@ public abstract class AbstractContainerMenu { + slot.setByPlayer(carried2); + } + } else if (ItemStack.isSameItemSameComponents(carried, carried2)) { +- Optional optional1 = slot.tryRemove(carried.getCount(), carried2.getMaxStackSize() - carried2.getCount(), player); ++ Optional optional1 = slot.tryRemove(carried.getCount(), org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(carried2) - carried2.getCount(), player); // Leaves - stackable shulker boxes + optional1.ifPresent(stack -> { + carried2.grow(stack.getCount()); + slot.onTake(player, stack); +@@ -593,7 +593,7 @@ public abstract class AbstractContainerMenu { + Slot slot2 = this.slots.get(slotId); + if (slot2.hasItem()) { + ItemStack itemStack = slot2.getItem(); +- this.setCarried(itemStack.copyWithCount(itemStack.getMaxStackSize())); ++ this.setCarried(itemStack.copyWithCount(org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemStack))); // Leaves - stackable shulker boxes + } + } else if (clickType == ClickType.THROW && this.getCarried().isEmpty() && slotId >= 0) { + Slot slot2 = this.slots.get(slotId); +@@ -624,15 +624,15 @@ public abstract class AbstractContainerMenu { + int maxStackSize = button == 0 ? 1 : -1; + + for (int i3 = 0; i3 < 2; i3++) { +- for (int i4 = count; i4 >= 0 && i4 < this.slots.size() && itemStack.getCount() < itemStack.getMaxStackSize(); i4 += maxStackSize) { ++ for (int i4 = count; i4 >= 0 && i4 < this.slots.size() && itemStack.getCount() < org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemStack); i4 += maxStackSize) { // Leaves - stackable shulker boxes + Slot slot3 = this.slots.get(i4); + if (slot3.hasItem() + && canItemQuickReplace(slot3, itemStack, true) + && slot3.mayPickup(player) + && this.canTakeItemForPickAll(itemStack, slot3)) { + ItemStack item1 = slot3.getItem(); +- if (i3 != 0 || item1.getCount() != item1.getMaxStackSize()) { +- ItemStack itemStack1 = slot3.safeTake(item1.getCount(), itemStack.getMaxStackSize() - itemStack.getCount(), player); ++ if (i3 != 0 || item1.getCount() != org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(item1)) { // Leaves - stackable shulker boxes ++ ItemStack itemStack1 = slot3.safeTake(item1.getCount(), org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemStack) - itemStack.getCount(), player); // Leaves - stackable shulker boxes + itemStack.grow(itemStack1.getCount()); + } + } +@@ -750,7 +750,7 @@ public abstract class AbstractContainerMenu { + i = endIndex - 1; + } + +- if (stack.isStackable()) { ++ if (org.leavesmc.leaves.util.ShulkerBoxUtils.isStackable(stack)) { // Leaves - stackable shulker boxes + while (!stack.isEmpty() && (reverseDirection ? i >= startIndex : i < endIndex)) { + Slot slot = this.slots.get(i); + ItemStack item = slot.getItem(); +diff --git a/net/minecraft/world/inventory/MerchantContainer.java b/net/minecraft/world/inventory/MerchantContainer.java +index a1576c3be00bdb19d02d52658c4b1818ea5c32db..be2f047bf1daab82473105865cd95955d4661a2a 100644 +--- a/net/minecraft/world/inventory/MerchantContainer.java ++++ b/net/minecraft/world/inventory/MerchantContainer.java +@@ -109,7 +109,7 @@ public class MerchantContainer implements Container { + @Override + public void setItem(int index, ItemStack stack) { + this.itemStacks.set(index, stack); +- stack.limitSize(this.getMaxStackSize(stack)); ++ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes + if (this.isPaymentSlot(index)) { + this.updateSellItem(); + } +diff --git a/net/minecraft/world/inventory/Slot.java b/net/minecraft/world/inventory/Slot.java +index 5ceb8964476b40db4511bec91ff13c4f522a1357..371bad86218971d6e031c6d74307b2ab0d460997 100644 +--- a/net/minecraft/world/inventory/Slot.java ++++ b/net/minecraft/world/inventory/Slot.java +@@ -75,7 +75,7 @@ public class Slot { + } + + public int getMaxStackSize(ItemStack stack) { +- return Math.min(this.getMaxStackSize(), stack.getMaxStackSize()); ++ return Math.min(this.getMaxStackSize(), org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(stack)); // Leaves - stackable shulker boxes + } + + @Nullable +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index 76f50437396f8f856381d0fbef52953ef7c263f6..6bc431cd0724de406c92830d618c26d69bcd1918 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -199,7 +199,7 @@ public final class ItemStack implements DataComponentHolder { + @Deprecated + @Nullable + private Item item; +- PatchedDataComponentMap components; ++ public PatchedDataComponentMap components; // Leaves - stackable shulker boxes + @Nullable + private Entity entityRepresentation; + +diff --git a/net/minecraft/world/level/block/AbstractCauldronBlock.java b/net/minecraft/world/level/block/AbstractCauldronBlock.java +index 648d22cdfcf261494038d589f5a5e32704083076..47e480515715376f568b7cb7468edf2bff3d4307 100644 +--- a/net/minecraft/world/level/block/AbstractCauldronBlock.java ++++ b/net/minecraft/world/level/block/AbstractCauldronBlock.java +@@ -58,9 +58,27 @@ public abstract class AbstractCauldronBlock extends Block { + ItemStack stack, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hitResult + ) { + CauldronInteraction cauldronInteraction = this.interactions.map().get(stack.getItem()); +- return cauldronInteraction.interact(state, level, pos, player, hand, stack, hitResult.getDirection()); // Paper - pass hit direction ++ return wrapInteractor(cauldronInteraction, state, level, pos, player, hand, stack, hitResult.getDirection()); // Paper - pass hit direction // Leaves - stackable shulker boxes + } + ++ // Leaves start - stackable shulker boxes ++ private InteractionResult wrapInteractor(CauldronInteraction cauldronBehavior, BlockState blockState, Level world, BlockPos blockPos, Player playerEntity, InteractionHand hand, ItemStack itemStack, net.minecraft.core.Direction hitDirection) { ++ int count = -1; ++ if (org.leavesmc.leaves.LeavesConfig.modify.shulkerBoxStackSize > 1 && itemStack.getItem() instanceof net.minecraft.world.item.BlockItem bi && ++ bi.getBlock() instanceof ShulkerBoxBlock) { ++ count = itemStack.getCount(); ++ } ++ InteractionResult result = cauldronBehavior.interact(blockState, world, blockPos, playerEntity, hand, itemStack, hitDirection); ++ if (count > 0 && result.consumesAction()) { ++ ItemStack current = playerEntity.getItemInHand(hand); ++ if (current.getItem() instanceof net.minecraft.world.item.BlockItem bi && bi.getBlock() instanceof ShulkerBoxBlock) { ++ current.setCount(count); ++ } ++ } ++ return result; ++ } ++ // Leaves end - stackable shulker boxes ++ + @Override + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; +diff --git a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +index 77e7188180cce9ef881de64b263c704b219b610a..2f5fa4310f475ecbb29e69c0461c7d3276f8536d 100644 +--- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -414,7 +414,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit + ItemStack itemStack = this.items.get(index); + boolean flag = !stack.isEmpty() && ItemStack.isSameItemSameComponents(itemStack, stack); + this.items.set(index, stack); +- stack.limitSize(this.getMaxStackSize(stack)); ++ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes + if (index == 0 && !flag && this.level instanceof ServerLevel serverLevel) { + this.cookingTotalTime = getTotalCookTime(serverLevel, this, this.recipeType, this.cookSpeedMultiplier); // Paper - cook speed multiplier API + this.cookingTimer = 0; +diff --git a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +index 26db603ed681a6c302596627d4dd5bf8a9bafc4e..9fcb70b5bf1912a5499aa7daeaa089adbe60cc55 100644 +--- a/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java +@@ -146,7 +146,7 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co + @Override + public void setItem(int slot, ItemStack stack) { + this.getItems().set(slot, stack); +- stack.limitSize(this.getMaxStackSize(stack)); ++ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes + this.setChanged(); + } + +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 42a29e700d2549de7cd905c373212e9757bcfcf1..3122753c96e98e57fa900cd15005ab4874e4b1db 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -109,7 +109,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + public void setItem(int index, ItemStack stack) { + this.unpackLootTable(null); + this.getItems().set(index, stack); +- stack.limitSize(this.getMaxStackSize(stack)); ++ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes + } + + @Override +@@ -688,9 +688,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + if (item.isEmpty()) { + // Spigot start - SPIGOT-6693, SimpleContainer#setItem + ItemStack leftover = ItemStack.EMPTY; // Paper - Make hoppers respect inventory max stack size +- if (!stack.isEmpty() && stack.getCount() > destination.getMaxStackSize()) { ++ if (!stack.isEmpty() && (stack.getCount() > destination.getMaxStackSize() || stack.getCount() > stack.getMaxStackSize())) { // Leaves - stackable shulker boxes + leftover = stack; // Paper - Make hoppers respect inventory max stack size +- stack = stack.split(destination.getMaxStackSize()); ++ stack = stack.split(Math.min(destination.getMaxStackSize(), stack.getMaxStackSize())); // Leaves - stackable shulker boxes + } + // Spigot end + ignoreBlockEntityUpdates = true; // Paper - Perf: Optimize Hoppers diff --git a/leaves-server/minecraft-patches/features/0033-MC-Technical-Survival-Mode.patch b/leaves-server/minecraft-patches/features/0033-MC-Technical-Survival-Mode.patch new file mode 100644 index 00000000..796d6e1a --- /dev/null +++ b/leaves-server/minecraft-patches/features/0033-MC-Technical-Survival-Mode.patch @@ -0,0 +1,81 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 19 Jan 2023 23:38:50 +0800 +Subject: [PATCH] MC Technical Survival Mode + +Will automatically overwrite some configuration after startup + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 68c934aec165690979f066eee73d4b2bc803ed14..5aa3f4270a4a8c11b3d785987bcfc1e06254ccab 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -1684,7 +1684,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + public boolean isInvulnerableTo(ServerLevel level, DamageSource damageSource) { + return (super.isInvulnerableTo(level, damageSource) // Paper - disable player cramming; + || this.isChangingDimension() && !damageSource.is(DamageTypes.ENDER_PEARL) +- || !this.hasClientLoaded()) || (!this.level().paperConfig().collisions.allowPlayerCrammingDamage && damageSource.is(DamageTypes.CRAMMING)); // Paper - disable player cramming; ++ || !this.hasClientLoaded()) || (!org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode && !this.level().paperConfig().collisions.allowPlayerCrammingDamage && damageSource.is(DamageTypes.CRAMMING)); // Paper - disable player cramming; // Leaves - mc technical survival mode + } + + @Override +diff --git a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +index ff1c84d37db48e1bd0283a900e199647c0e8eba1..cefa4c03b1d3722bb3a7872eb75a9f47517fe040 100644 +--- a/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java ++++ b/net/minecraft/world/entity/boss/enderdragon/EndCrystal.java +@@ -64,7 +64,7 @@ public class EndCrystal extends Entity { + } + + // Paper start - Fix invulnerable end crystals +- if (this.level().paperConfig().unsupportedSettings.fixInvulnerableEndCrystalExploit && this.generatedByDragonFight && this.isInvulnerable()) { ++ if (!org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode && this.level().paperConfig().unsupportedSettings.fixInvulnerableEndCrystalExploit && this.generatedByDragonFight && this.isInvulnerable()) { // Leaves - mc technical survival mode + if (!java.util.Objects.equals(((ServerLevel) this.level()).uuid, this.getOriginWorld()) + || ((ServerLevel) this.level()).getDragonFight() == null + || ((ServerLevel) this.level()).getDragonFight().respawnStage == null +diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java +index 40da052e7fea1306a007b3cb5c9daa33e0ef523e..583864d73999e4e25f6c26920c0bf9a7504d8463 100644 +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -98,7 +98,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + + @Override + public void tick() { +- if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot ++ if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > (org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode ? 2000 : this.level().spigotConfig.maxTntTicksPerTick)) { return; } // Spigot // Leaves - mc technical survival mode + this.handlePortal(); + this.applyGravity(); + this.move(MoverType.SELF, this.getDeltaMovement()); +diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java +index 17ce115e887cbbb06ad02ab7ddb488e27342c0e4..8fd9c191e5b14fc7dd90e8f7229acd6de97e0f9e 100644 +--- a/net/minecraft/world/level/NaturalSpawner.java ++++ b/net/minecraft/world/level/NaturalSpawner.java +@@ -88,7 +88,7 @@ public final class NaturalSpawner { + MobCategory category = entity.getType().getCategory(); + if (category != MobCategory.MISC) { + // Paper start - Only count natural spawns +- if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && ++ if (!org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode && !entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && // Leaves - mc technical survival mode + !(entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL || + entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) { + continue; +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 3122753c96e98e57fa900cd15005ab4874e4b1db..50bc72f736e9e7a9839a853254a81f9add03bacf 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -271,7 +271,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + origItemStack.setCount(originalItemCount); + } + } +- if (foundItem && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown ++ if (foundItem && level.paperConfig().hopper.cooldownWhenFull && !org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode) { // Inventory was full - cooldown // Leaves + hopper.setCooldown(level.spigotConfig.hopperTransfer); + } + return false; +@@ -312,7 +312,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + origItemStack.setCount(originalItemCount); + +- if (level.paperConfig().hopper.cooldownWhenFull) { ++ if (level.paperConfig().hopper.cooldownWhenFull && !org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode) { // Leaves + applyCooldown(hopper); + } + diff --git a/leaves-server/minecraft-patches/features/0034-Return-nether-portal-fix.patch b/leaves-server/minecraft-patches/features/0034-Return-nether-portal-fix.patch new file mode 100644 index 00000000..9487183e --- /dev/null +++ b/leaves-server/minecraft-patches/features/0034-Return-nether-portal-fix.patch @@ -0,0 +1,95 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 15:36:21 +0800 +Subject: [PATCH] Return nether portal fix + +This patch is powered by NetherPortalFix(https://github.com/TwelveIterationMods/NetherPortalFix) + +diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java +index 5aa3f4270a4a8c11b3d785987bcfc1e06254ccab..f881077a6cd6eeffac84d866941a400457085ff5 100644 +--- a/net/minecraft/server/level/ServerPlayer.java ++++ b/net/minecraft/server/level/ServerPlayer.java +@@ -1481,6 +1481,21 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc + org.bukkit.event.player.PlayerChangedWorldEvent changeEvent = new org.bukkit.event.player.PlayerChangedWorldEvent(this.getBukkitEntity(), serverLevel.getWorld()); + this.level().getCraftServer().getPluginManager().callEvent(changeEvent); + // CraftBukkit end ++ // Leaves start - nether portal fix ++ if (org.leavesmc.leaves.LeavesConfig.modify.netherPortalFix) { ++ final ResourceKey fromDim = serverLevel.dimension(); ++ final ResourceKey toDim = level().dimension(); ++ final ResourceKey OVERWORLD = Level.OVERWORLD; ++ final ResourceKey THE_NETHER = Level.NETHER; ++ if (!((fromDim != OVERWORLD || toDim != THE_NETHER) && (fromDim != THE_NETHER || toDim != OVERWORLD))) { ++ BlockPos fromPortal = org.leavesmc.leaves.util.ReturnPortalManager.findPortalAt(this, fromDim, lastPos); ++ BlockPos toPos = this.blockPosition(); ++ if (fromPortal != null) { ++ org.leavesmc.leaves.util.ReturnPortalManager.storeReturnPortal(this, toDim, toPos, fromPortal); ++ } ++ } ++ } ++ // Leaves end - nether portal fix + // Paper start - Reset shield blocking on dimension change + if (this.isBlocking()) { + this.stopUsingItem(); +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index de20a6fbbc9731a3b1cd30b3a0785637f6a33778..93eb9e2d4e44881181a07b12249c3812635fec14 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -852,6 +852,20 @@ public abstract class PlayerList { + if (fromWorld != level) { + org.bukkit.event.player.PlayerChangedWorldEvent event = new org.bukkit.event.player.PlayerChangedWorldEvent(player.getBukkitEntity(), fromWorld.getWorld()); + this.server.server.getPluginManager().callEvent(event); ++ // Leaves start - nether portal fix ++ if (org.leavesmc.leaves.LeavesConfig.modify.netherPortalFix) { ++ final ResourceKey fromDim = player.level().dimension(); ++ final ResourceKey toDim = serverPlayer.level().dimension(); ++ final ResourceKey OVERWORLD = Level.OVERWORLD; ++ final ResourceKey THE_NETHER = Level.NETHER; ++ if (!((fromDim != OVERWORLD || toDim != THE_NETHER) && (fromDim != THE_NETHER || toDim != OVERWORLD))) { ++ BlockPos fromPortal = org.leavesmc.leaves.util.ReturnPortalManager.findPortalAt(serverPlayer, fromDim, serverPlayer.lastPos); ++ if (fromPortal != null) { ++ org.leavesmc.leaves.util.ReturnPortalManager.storeReturnPortal(serverPlayer, toDim, serverPlayer.blockPosition(), fromPortal); ++ } ++ } ++ } ++ // Leaves end - nether portal fix + } + + // Save player file again if they were disconnected +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index 2dd370bf00b5ddf133c946b6e4d37b00be3ca5a5..5e7068bf9b5364c382fccfb110ed2a26a5ac1731 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -274,7 +274,7 @@ public abstract class LivingEntity extends Entity implements Attackable { + protected ItemStack useItem = ItemStack.EMPTY; + public int useItemRemaining; + protected int fallFlyTicks; +- private BlockPos lastPos; ++ public BlockPos lastPos; // Leaves - private -> public + private Optional lastClimbablePos = Optional.empty(); + @Nullable + private DamageSource lastDamageSource; +diff --git a/net/minecraft/world/level/block/NetherPortalBlock.java b/net/minecraft/world/level/block/NetherPortalBlock.java +index e2eb693b0130513115392cb0cb5a829ede5be8c5..fd6e228e5943c7004a595b5e154b7606ed330ea5 100644 +--- a/net/minecraft/world/level/block/NetherPortalBlock.java ++++ b/net/minecraft/world/level/block/NetherPortalBlock.java +@@ -183,7 +183,18 @@ public class NetherPortalBlock extends Block implements Portal { + + @Nullable + private TeleportTransition getExitPortal(ServerLevel level, Entity entity, BlockPos pos, BlockPos exitPos, boolean isNether, WorldBorder worldBorder, int searchRadius, boolean canCreatePortal, int createRadius) { // CraftBukkit +- Optional optional = level.getPortalForcer().findClosestPortalPosition(exitPos, worldBorder, searchRadius); // CraftBukkit ++ // Leaves start - fix return portal ++ Optional optional = Optional.empty(); ++ if (org.leavesmc.leaves.LeavesConfig.modify.netherPortalFix && entity instanceof net.minecraft.server.level.ServerPlayer player) { ++ org.leavesmc.leaves.util.ReturnPortalManager.ReturnPortal portal = org.leavesmc.leaves.util.ReturnPortalManager.findReturnPortal(player, entity.level().dimension(), entity.blockPosition()); ++ if (portal != null && level.getBlockState(portal.pos()).is(Blocks.NETHER_PORTAL)) { ++ optional = Optional.of(portal.pos()); ++ } ++ } ++ if (optional.isEmpty()) { ++ optional = level.getPortalForcer().findClosestPortalPosition(exitPos, worldBorder, searchRadius); // CraftBukkit ++ } ++ // Leaves end - fix return portal + BlockUtil.FoundRectangle largestRectangleAround; + TeleportTransition.PostTeleportTransition postTeleportTransition; + if (optional.isPresent()) { diff --git a/leaves-server/minecraft-patches/features/0035-Leaves-Extra-Yggdrasil-Service.patch b/leaves-server/minecraft-patches/features/0035-Leaves-Extra-Yggdrasil-Service.patch new file mode 100644 index 00000000..5d04a2d0 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0035-Leaves-Extra-Yggdrasil-Service.patch @@ -0,0 +1,45 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 15:48:19 +0800 +Subject: [PATCH] Leaves Extra Yggdrasil Service + + +diff --git a/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java b/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java +index 8c3151c25c172b846f0d028b5100718ada2d09d7..d59d05f8894481fb5ebe1aeb8ee7a162b3dc6f1c 100644 +--- a/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java ++++ b/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java +@@ -46,7 +46,7 @@ import java.util.stream.Collectors; + + public class YggdrasilMinecraftSessionService implements MinecraftSessionService { + private static final Logger LOGGER = LoggerFactory.getLogger(YggdrasilMinecraftSessionService.class); +- private final MinecraftClient client; ++ protected final MinecraftClient client; // Leaves - private -> protected + private final ServicesKeySet servicesKeySet; + private final String baseUrl; + private final URL joinUrl; +diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java +index 9aa664537cc37e44db46d5a2a64ae3116938c681..1a5a8ad60240c0864875f6314e01adab4a283ddf 100644 +--- a/net/minecraft/server/Main.java ++++ b/net/minecraft/server/Main.java +@@ -177,7 +177,7 @@ public class Main { + file = new File(bukkitConfiguration.getString("settings.world-container", ".")); + } + // Paper end - fix SPIGOT-5824 +- Services services = Services.create(new com.destroystokyo.paper.profile.PaperAuthenticationService(Proxy.NO_PROXY), file, userCacheFile, optionSet); // Paper - pass OptionSet to load paper config files; override authentication service; fix world-container ++ Services services = Services.create(new org.leavesmc.leaves.profile.LeavesAuthenticationService(Proxy.NO_PROXY), file, userCacheFile, optionSet); // Paper - pass OptionSet to load paper config files; override authentication service; fix world-container // Leaves - extra-yggdrasil-service + // CraftBukkit start + String string = Optional.ofNullable((String) optionSet.valueOf("world")).orElse(dedicatedServerSettings.getProperties().levelName); + LevelStorageSource levelStorageSource = LevelStorageSource.createDefault(file.toPath()); +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 09fc1c1923f7cc4470cca3b703810fb165c4ca15..1ce3afe6c3e3b7c0aad2706215d75a265d892070 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -245,7 +245,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public + private long lastServerStatus; + public final Thread serverThread; + private long lastTickNanos = Util.getNanos(); diff --git a/leaves-server/minecraft-patches/features/0036-Configurable-vanilla-random.patch b/leaves-server/minecraft-patches/features/0036-Configurable-vanilla-random.patch new file mode 100644 index 00000000..d37b505c --- /dev/null +++ b/leaves-server/minecraft-patches/features/0036-Configurable-vanilla-random.patch @@ -0,0 +1,86 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 16 Feb 2023 17:25:01 +0800 +Subject: [PATCH] Configurable vanilla random + + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index ea808042e8fc647603c213cc1bea2fa16d22e749..bf316830f199c3f83dc2fccccf51885431729389 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -255,7 +255,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + public double zOld; + public boolean noPhysics; + private boolean wasOnFire; +- public final RandomSource random = SHARED_RANDOM; // Paper - Share random for entities to make them more random ++ public final RandomSource random = org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom ? RandomSource.create() : SHARED_RANDOM; // Paper - Share random for entities to make them more random // Leaves - but mojang use it, optimize? no! + public int tickCount; + private int remainingFireTicks = -this.getFireImmuneTicks(); + public boolean wasTouchingWater; +diff --git a/net/minecraft/world/entity/animal/Bee.java b/net/minecraft/world/entity/animal/Bee.java +index 94244b148533ef026bf5c56abbc2bb5cfa83c938..91725b42a01790e0ac7d00bdc4a132ebb942a787 100644 +--- a/net/minecraft/world/entity/animal/Bee.java ++++ b/net/minecraft/world/entity/animal/Bee.java +@@ -801,7 +801,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + @VisibleForDebug + public class BeeGoToHiveGoal extends Bee.BaseBeeGoal { + public static final int MAX_TRAVELLING_TICKS = 2400; +- int travellingTicks = Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues ++ int travellingTicks = org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom ? Bee.this.level().random.nextInt(10) : Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues // Leaves - why no vanilla + private static final int MAX_BLACKLISTED_TARGETS = 3; + final List blacklistedTargets = Lists.newArrayList(); + @Nullable +@@ -917,7 +917,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { + + public class BeeGoToKnownFlowerGoal extends Bee.BaseBeeGoal { + private static final int MAX_TRAVELLING_TICKS = 2400; +- int travellingTicks = Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues ++ int travellingTicks = org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom ? Bee.this.level().random.nextInt(10) : Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues // Leaves - why no vanilla + + BeeGoToKnownFlowerGoal() { + this.setFlags(EnumSet.of(Goal.Flag.MOVE)); +diff --git a/net/minecraft/world/entity/animal/Squid.java b/net/minecraft/world/entity/animal/Squid.java +index 687ac3f50ed517a0b4df70c0c0a01215691ac718..dd3a0b30ab99d3a6680243c1dfb6dbddd7af2dbc 100644 +--- a/net/minecraft/world/entity/animal/Squid.java ++++ b/net/minecraft/world/entity/animal/Squid.java +@@ -46,7 +46,7 @@ public class Squid extends AgeableWaterCreature { + + public Squid(EntityType entityType, Level level) { + super(entityType, level); +- //this.random.setSeed(this.getId()); // Paper - Share random for entities to make them more random ++ if (org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom) this.random.setSeed(this.getId()); // Paper - Share random for entities to make them more random // Leaves - vanilla plz + this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F; + } + +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index d2d258246e7f7332c0420077e33c05e222bda46d..4f5db8203189a413f410fe7648e4995bae171918 100644 +--- a/net/minecraft/world/entity/item/ItemEntity.java ++++ b/net/minecraft/world/entity/item/ItemEntity.java +@@ -63,7 +63,13 @@ public class ItemEntity extends Entity implements TraceableEntity { + // Paper start - Don't use level random in entity constructors (to make them thread-safe) + this(EntityType.ITEM, level); + this.setPos(posX, posY, posZ); +- this.setDeltaMovement(this.random.nextDouble() * 0.2 - 0.1, 0.2, this.random.nextDouble() * 0.2 - 0.1); ++ // Leaves start - vanilla yes, safe no ++ if (org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom) { ++ this.setDeltaMovement(level.random.nextDouble() * 0.2 - 0.1, 0.2, level.random.nextDouble() * 0.2 - 0.1); ++ } else { ++ this.setDeltaMovement(this.random.nextDouble() * 0.2 - 0.1, 0.2, this.random.nextDouble() * 0.2 - 0.1); ++ } ++ // Leaves end - vanilla yes, safe no + this.setItem(itemStack); + // Paper end - Don't use level random in entity constructors + } +diff --git a/net/minecraft/world/entity/item/PrimedTnt.java b/net/minecraft/world/entity/item/PrimedTnt.java +index 583864d73999e4e25f6c26920c0bf9a7504d8463..717d96fa6459c77db9274c9cb3bedec4a4ff1a90 100644 +--- a/net/minecraft/world/entity/item/PrimedTnt.java ++++ b/net/minecraft/world/entity/item/PrimedTnt.java +@@ -66,7 +66,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { + public PrimedTnt(Level level, double x, double y, double z, @Nullable LivingEntity owner) { + this(EntityType.TNT, level); + this.setPos(x, y, z); +- double d = this.random.nextDouble() * (float) (Math.PI * 2); // Paper - Don't use level random in entity constructors ++ double d = (org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom ? level.random.nextDouble() : this.random.nextDouble()) * (float) (Math.PI * 2); // Paper - Don't use level random in entity constructors // Leaves - why? + this.setDeltaMovement(-Math.sin(d) * 0.02, 0.2F, -Math.cos(d) * 0.02); + this.setFuse(80); + this.xo = x; diff --git a/leaves-server/minecraft-patches/features/0037-Catch-update-suppression-crash.patch b/leaves-server/minecraft-patches/features/0037-Catch-update-suppression-crash.patch new file mode 100644 index 00000000..248eaafb --- /dev/null +++ b/leaves-server/minecraft-patches/features/0037-Catch-update-suppression-crash.patch @@ -0,0 +1,81 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Fri, 17 Mar 2023 15:57:08 +0800 +Subject: [PATCH] Catch update suppression crash + + +diff --git a/net/minecraft/network/protocol/PacketUtils.java b/net/minecraft/network/protocol/PacketUtils.java +index 4535858701b2bb232b9d2feb2af6551526232ddc..2a51acd97afc525170e8001b76f57ad13853aab0 100644 +--- a/net/minecraft/network/protocol/PacketUtils.java ++++ b/net/minecraft/network/protocol/PacketUtils.java +@@ -27,6 +27,10 @@ public class PacketUtils { + if (processor.shouldHandleMessage(packet)) { + try { + packet.handle(processor); ++ // Leaves start - update suppression crash fix ++ } catch (org.leavesmc.leaves.util.UpdateSuppressionException exception) { ++ org.leavesmc.leaves.LeavesLogger.LOGGER.info(exception.getMessage()); ++ // Leaves start - update suppression crash fix + } catch (Exception var4) { + if (var4 instanceof ReportedException reportedException && reportedException.getCause() instanceof OutOfMemoryError) { + throw makeReportedException(var4, packet, processor); +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 1ce3afe6c3e3b7c0aad2706215d75a265d892070..de9b2b92456466376186737914706df22e1578f5 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1727,6 +1727,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Wed, 8 May 2024 22:19:25 +0800 +Subject: [PATCH] Bedrock break list + + +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index de9b2b92456466376186737914706df22e1578f5..7822cecf362cd8d430d90c0daac6e97c8a8d124b 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1754,6 +1754,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Mon, 3 Feb 2025 16:42:23 +0800 +Subject: [PATCH] Fix trapdoor feature + + +diff --git a/net/minecraft/world/level/block/TrapDoorBlock.java b/net/minecraft/world/level/block/TrapDoorBlock.java +index 46c23990e78ce77b188052672778bdef560a0d13..c6aad768cad0f0eccbe5405f41c4fa34583970ba 100644 +--- a/net/minecraft/world/level/block/TrapDoorBlock.java ++++ b/net/minecraft/world/level/block/TrapDoorBlock.java +@@ -160,6 +160,8 @@ public class TrapDoorBlock extends HorizontalDirectionalBlock implements SimpleW + hasNeighborSignal = eventRedstone.getNewCurrent() > 0; + } + // CraftBukkit end ++ ++ /* Leaves - it is feature, not bug! + // Paper start - break redstone on trapdoors early + boolean open = state.getValue(TrapDoorBlock.OPEN) != hasNeighborSignal; + // note: this must run before any state for this block/its neighborus are written to the world +@@ -178,8 +180,9 @@ public class TrapDoorBlock extends HorizontalDirectionalBlock implements SimpleW + } + } + } +- if (open) { +- // Paper end - break redstone on trapdoors early ++ */ ++ ++ if (state.getValue(TrapDoorBlock.OPEN) != hasNeighborSignal) { + state = state.setValue(OPEN, Boolean.valueOf(hasNeighborSignal)); + this.playSound(null, level, pos, hasNeighborSignal); + } diff --git a/leaves-server/minecraft-patches/features/0040-Disable-distance-check-for-UseItemOnPacket.patch b/leaves-server/minecraft-patches/features/0040-Disable-distance-check-for-UseItemOnPacket.patch new file mode 100644 index 00000000..a911bb74 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0040-Disable-distance-check-for-UseItemOnPacket.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 23 May 2023 17:10:00 +0800 +Subject: [PATCH] Disable distance check for UseItemOnPacket + + +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 9955fc7bcf75dbe86c5fcbace2dd1f2ccf479207..29a6d220adeecafa8e8f19c90e392d809fcd7568 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -1921,7 +1921,7 @@ public class ServerGamePacketListenerImpl + if (this.player.canInteractWithBlock(blockPos, 1.0)) { + Vec3 vec3 = location.subtract(Vec3.atCenterOf(blockPos)); + double d = 1.0000001; +- if (Math.abs(vec3.x()) < 1.0000001 && Math.abs(vec3.y()) < 1.0000001 && Math.abs(vec3.z()) < 1.0000001) { ++ if (org.leavesmc.leaves.LeavesConfig.modify.disableDistanceCheckForUseItem ||Math.abs(vec3.x()) < 1.0000001 && Math.abs(vec3.y()) < 1.0000001 && Math.abs(vec3.z()) < 1.0000001) { // Leaves - disableDistanceCheckForUseItem + Direction direction = hitResult.getDirection(); + this.player.resetLastActionTime(); + int maxY = this.player.level().getMaxY(); diff --git a/leaves-server/minecraft-patches/features/0041-No-feather-falling-trample.patch b/leaves-server/minecraft-patches/features/0041-No-feather-falling-trample.patch new file mode 100644 index 00000000..0c682f35 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0041-No-feather-falling-trample.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 25 May 2023 16:37:06 +0800 +Subject: [PATCH] No feather falling trample + + +diff --git a/net/minecraft/world/level/block/FarmBlock.java b/net/minecraft/world/level/block/FarmBlock.java +index 47c9b32c89e7e6f84a279c2f6098ada77dc58b6b..9aafef742de9e833cc772da679127cd02cb8c1cb 100644 +--- a/net/minecraft/world/level/block/FarmBlock.java ++++ b/net/minecraft/world/level/block/FarmBlock.java +@@ -116,6 +116,13 @@ public class FarmBlock extends Block { + && entity instanceof LivingEntity + && (entity instanceof Player || serverLevel.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) + && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) { ++ // Leaves start - noFeatherFallingTrample ++ if (org.leavesmc.leaves.LeavesConfig.modify.noFeatherFallingTrample) { ++ if (net.minecraft.world.item.enchantment.EnchantmentHelper.getEnchantmentLevel(level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT).getOrThrow(net.minecraft.world.item.enchantment.Enchantments.FEATHER_FALLING), (LivingEntity) entity) > 0) { ++ return; ++ } ++ } ++ // Leaves end - noFeatherFallingTrample + // CraftBukkit start - Interact soil + org.bukkit.event.Cancellable cancellable; + if (entity instanceof Player) { diff --git a/leaves-server/minecraft-patches/features/0042-Syncmatica-Protocol.patch b/leaves-server/minecraft-patches/features/0042-Syncmatica-Protocol.patch new file mode 100644 index 00000000..67a9b5cc --- /dev/null +++ b/leaves-server/minecraft-patches/features/0042-Syncmatica-Protocol.patch @@ -0,0 +1,23 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 16:51:01 +0800 +Subject: [PATCH] Syncmatica Protocol + +This patch is Powered by Syncmatica(https://github.com/End-Tech/syncmatica) + +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index c0e3695e7f25b675aa74ff6125ee821209b5b667..57c68522f7800a8000f3702802a266e441a3dbf3 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -324,8 +324,11 @@ public class ServerGamePacketListenerImpl + player.getTextFilter().join(); + this.signedMessageDecoder = SignedMessageChain.Decoder.unsigned(player.getUUID(), server::enforceSecureProfile); + this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat ++ this.exchangeTarget = new org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget(this); // Leaves - Syncmatica Protocol + } + ++ public final org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget exchangeTarget; // Leaves - Syncmatica Protocol ++ + @Override + public void tick() { + if (this.ackBlockChangesUpTo > -1) { diff --git a/patches/server/0050-Shared-villager-discounts.patch b/leaves-server/minecraft-patches/features/0043-Shared-villager-discounts.patch similarity index 59% rename from patches/server/0050-Shared-villager-discounts.patch rename to leaves-server/minecraft-patches/features/0043-Shared-villager-discounts.patch index 7a058f64..43483dbc 100644 --- a/patches/server/0050-Shared-villager-discounts.patch +++ b/leaves-server/minecraft-patches/features/0043-Shared-villager-discounts.patch @@ -4,24 +4,24 @@ Date: Thu, 25 May 2023 17:15:22 +0800 Subject: [PATCH] Shared villager discounts -diff --git a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java -index c7f012674361a323c1efeca4660cd3f46d308ee1..c211b62fd753adad649c084fe791918c8c620c04 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java -+++ b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipContainer.java +diff --git a/net/minecraft/world/entity/ai/gossip/GossipContainer.java b/net/minecraft/world/entity/ai/gossip/GossipContainer.java +index b74a4ce1b629d440681a1f5c026997ccaf1d0373..4eb97c8265ed8346b208d06a37f8068004593944 100644 +--- a/net/minecraft/world/entity/ai/gossip/GossipContainer.java ++++ b/net/minecraft/world/entity/ai/gossip/GossipContainer.java @@ -115,6 +115,16 @@ public class GossipContainer { - public int getReputation(UUID target, Predicate gossipTypeFilter) { - GossipContainer.EntityGossips entityGossips = this.gossips.get(target); + public int getReputation(UUID identifier, Predicate gossip) { + GossipContainer.EntityGossips entityGossips = this.gossips.get(identifier); + // Leaves start - sharedVillagerDiscounts -+ if (org.leavesmc.leaves.LeavesConfig.modify.sharedVillagerDiscounts && gossipTypeFilter.test(GossipType.MAJOR_POSITIVE)) { ++ if (org.leavesmc.leaves.LeavesConfig.modify.sharedVillagerDiscounts && gossip.test(GossipType.MAJOR_POSITIVE)) { + int otherRep = 0; + if (entityGossips != null) { -+ otherRep = entityGossips.weightedValue(v -> gossipTypeFilter.test(v) && !v.equals(GossipType.MAJOR_POSITIVE)); ++ otherRep = entityGossips.weightedValue(v -> gossip.test(v) && !v.equals(GossipType.MAJOR_POSITIVE)); + } + int majorPositiveRep = this.gossips.values().stream().mapToInt(a -> a.weightedValue(v -> v.equals(GossipType.MAJOR_POSITIVE))).sum(); + return otherRep + Math.min(majorPositiveRep, GossipType.MAJOR_POSITIVE.max * GossipType.MAJOR_POSITIVE.weight); + } + // Leaves end - sharedVillagerDiscounts - return entityGossips != null ? entityGossips.weightedValue(gossipTypeFilter) : 0; + return entityGossips != null ? entityGossips.weightedValue(gossip) : 0; } diff --git a/leaves-server/minecraft-patches/features/0044-Redstone-wire-dont-connect-if-on-trapdoor.patch b/leaves-server/minecraft-patches/features/0044-Redstone-wire-dont-connect-if-on-trapdoor.patch new file mode 100644 index 00000000..d1841f13 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0044-Redstone-wire-dont-connect-if-on-trapdoor.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 14 Jun 2023 12:07:07 +0800 +Subject: [PATCH] Redstone wire dont connect if on trapdoor + + +diff --git a/net/minecraft/world/level/block/RedStoneWireBlock.java b/net/minecraft/world/level/block/RedStoneWireBlock.java +index 12c9d60314c99fb65e640d255a2d0c6b7790ad4d..ff29671f78a4e896e70daf89f440f3b9425b0760 100644 +--- a/net/minecraft/world/level/block/RedStoneWireBlock.java ++++ b/net/minecraft/world/level/block/RedStoneWireBlock.java +@@ -204,7 +204,7 @@ public class RedStoneWireBlock extends Block { + RandomSource random + ) { + if (direction == Direction.DOWN) { +- return !this.canSurviveOn(level, neighborPos, neighborState) ? Blocks.AIR.defaultBlockState() : state; ++ return org.leavesmc.leaves.LeavesConfig.modify.oldMC.updater.redstoneDontCantOnTrapDoor ? state : !this.canSurviveOn(level, neighborPos, neighborState) ? Blocks.AIR.defaultBlockState() : state; // Leaves - behavior to 1.19 + } else if (direction == Direction.UP) { + return this.getConnectionState(level, state, pos); + } else { +@@ -263,7 +263,7 @@ public class RedStoneWireBlock extends Block { + BlockPos blockPos = pos.relative(direction); + BlockState blockState = level.getBlockState(blockPos); + if (nonNormalCubeAbove) { +- boolean flag = blockState.getBlock() instanceof TrapDoorBlock || this.canSurviveOn(level, blockPos, blockState); ++ boolean flag = (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.updater.redstoneDontCantOnTrapDoor && blockState.getBlock() instanceof TrapDoorBlock) || this.canSurviveOn(level, blockPos, blockState); // Leaves - behavior to 1.19 + if (flag && shouldConnectTo(level.getBlockState(blockPos.above()))) { + if (blockState.isFaceSturdy(level, blockPos, direction.getOpposite())) { + return RedstoneSide.UP; diff --git a/patches/server/0052-Disable-check-out-of-order-command.patch b/leaves-server/minecraft-patches/features/0045-Disable-check-out-of-order-command.patch similarity index 75% rename from patches/server/0052-Disable-check-out-of-order-command.patch rename to leaves-server/minecraft-patches/features/0045-Disable-check-out-of-order-command.patch index 2290d0be..7560d4df 100644 --- a/patches/server/0052-Disable-check-out-of-order-command.patch +++ b/leaves-server/minecraft-patches/features/0045-Disable-check-out-of-order-command.patch @@ -4,10 +4,10 @@ Date: Sun, 10 Dec 2023 13:13:14 +0800 Subject: [PATCH] Disable check out-of-order command -diff --git a/src/main/java/net/minecraft/network/chat/SignedMessageChain.java b/src/main/java/net/minecraft/network/chat/SignedMessageChain.java -index 300929a406905f5ff1ede664d5b99fb0938d4d2e..48ffdcc799999f1d5f76e0bf124efc44c1aec3f8 100644 ---- a/src/main/java/net/minecraft/network/chat/SignedMessageChain.java -+++ b/src/main/java/net/minecraft/network/chat/SignedMessageChain.java +diff --git a/net/minecraft/network/chat/SignedMessageChain.java b/net/minecraft/network/chat/SignedMessageChain.java +index f6eed34b2fd72ab74cc9dc4b99ca184d512c0a66..a0c6b998bdf0477f524aa70e353f51e4fa37883c 100644 +--- a/net/minecraft/network/chat/SignedMessageChain.java ++++ b/net/minecraft/network/chat/SignedMessageChain.java @@ -45,7 +45,7 @@ public class SignedMessageChain { SignedMessageLink signedMessageLink = SignedMessageChain.this.nextLink; if (signedMessageLink == null) { diff --git a/patches/server/0053-Despawn-enderman-with-block.patch b/leaves-server/minecraft-patches/features/0046-Despawn-enderman-with-block.patch similarity index 53% rename from patches/server/0053-Despawn-enderman-with-block.patch rename to leaves-server/minecraft-patches/features/0046-Despawn-enderman-with-block.patch index 41a99ec0..540183f3 100644 --- a/patches/server/0053-Despawn-enderman-with-block.patch +++ b/leaves-server/minecraft-patches/features/0046-Despawn-enderman-with-block.patch @@ -4,11 +4,11 @@ Date: Sat, 17 Jun 2023 15:20:28 +0800 Subject: [PATCH] Despawn enderman with block -diff --git a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java -index 2b8e1a8e233071821411eb1f95c705efb4a6e816..e7a004ab26bd73a264bc7c911f99107c57661367 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java -+++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java -@@ -458,7 +458,7 @@ public class EnderMan extends Monster implements NeutralMob { +diff --git a/net/minecraft/world/entity/monster/EnderMan.java b/net/minecraft/world/entity/monster/EnderMan.java +index e2f1623b977889d31407d060b8e0bf911a80049e..a2437f181602cc334dc8b590c1b63565b2e67054 100644 +--- a/net/minecraft/world/entity/monster/EnderMan.java ++++ b/net/minecraft/world/entity/monster/EnderMan.java +@@ -457,7 +457,7 @@ public class EnderMan extends Monster implements NeutralMob { @Override public boolean requiresCustomPersistence() { @@ -16,4 +16,4 @@ index 2b8e1a8e233071821411eb1f95c705efb4a6e816..e7a004ab26bd73a264bc7c911f99107c + return super.requiresCustomPersistence() || (!org.leavesmc.leaves.LeavesConfig.modify.despawnEndermanWithBlock && this.getCarriedBlock() != null); // Leaves - despawn enderman with block } - private static class EndermanFreezeWhenLookedAt extends Goal { + static class EndermanFreezeWhenLookedAt extends Goal { diff --git a/leaves-server/minecraft-patches/features/0047-Creative-fly-no-clip.patch b/leaves-server/minecraft-patches/features/0047-Creative-fly-no-clip.patch new file mode 100644 index 00000000..1330c35c --- /dev/null +++ b/leaves-server/minecraft-patches/features/0047-Creative-fly-no-clip.patch @@ -0,0 +1,121 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 27 Jun 2023 09:26:58 +0800 +Subject: [PATCH] Creative fly no clip + + +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index f60336d17e4052551ee13d75c047f7b416b72ff4..7c6e0b0179f6c1381956915bd8993f8a75efd818 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -262,8 +262,8 @@ public abstract class Player extends LivingEntity { + + @Override + public void tick() { +- this.noPhysics = this.isSpectator(); +- if (this.isSpectator() || this.isPassenger()) { ++ this.noPhysics = this.isCreativeFlyOrSpectator(); // Leaves - creative no clip ++ if (this.isCreativeFlyOrSpectator() || this.isPassenger()) { // Leaves - creative no clip + this.setOnGround(false); + } + +@@ -470,7 +470,7 @@ public abstract class Player extends LivingEntity { + } + + Pose pose1; +- if (this.isSpectator() || this.isPassenger() || this.canPlayerFitWithinBlocksAndEntitiesWhen(pose)) { ++ if (this.isCreativeFlyOrSpectator() || this.isPassenger() || this.canPlayerFitWithinBlocksAndEntitiesWhen(pose)) { // Leaves - creative no clip + pose1 = pose; + } else if (this.canPlayerFitWithinBlocksAndEntitiesWhen(Pose.CROUCHING)) { + pose1 = Pose.CROUCHING; +@@ -604,7 +604,7 @@ public abstract class Player extends LivingEntity { + } + + this.bob = this.bob + (f - this.bob) * 0.4F; +- if (this.getHealth() > 0.0F && !this.isSpectator()) { ++ if (this.getHealth() > 0.0F && !this.isCreativeFlyOrSpectator()) { // Leaves - creative no clip + AABB aabb; + if (this.isPassenger() && !this.getVehicle().isRemoved()) { + aabb = this.getBoundingBox().minmax(this.getVehicle().getBoundingBox()).inflate(1.0, 0.0, 1.0); +@@ -2042,6 +2042,21 @@ public abstract class Player extends LivingEntity { + @Override + public abstract boolean isSpectator(); + ++ // Leaves start - creative no clip ++ public boolean isCreativeFlyOrSpectator() { ++ return isSpectator() || (org.leavesmc.leaves.LeavesConfig.modify.creativeNoClip && isCreative() && getAbilities().flying); ++ } ++ ++ public boolean canSpectatingPlace(net.minecraft.world.level.LevelReader world, BlockState state, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext context) { ++ if (this.isCreativeFlyOrSpectator()) { ++ net.minecraft.world.phys.shapes.VoxelShape voxelShape = state.getCollisionShape(world, pos, context); ++ return voxelShape.isEmpty() || world.isUnobstructed(this, voxelShape.move(pos.getX(), pos.getY(), pos.getZ())); ++ } else { ++ return world.isUnobstructed(state, pos, context); ++ } ++ } ++ // Leaves end - creative no clip ++ + @Override + public boolean canBeHitByProjectile() { + return !this.isSpectator() && super.canBeHitByProjectile(); +diff --git a/net/minecraft/world/item/BlockItem.java b/net/minecraft/world/item/BlockItem.java +index 1f4a3d2610abfa2ea2b1d5feba9606b806d6d416..c74e09eed6b8fe2852b3cb592e1a1e628ba9f22a 100644 +--- a/net/minecraft/world/item/BlockItem.java ++++ b/net/minecraft/world/item/BlockItem.java +@@ -216,7 +216,7 @@ public class BlockItem extends Item { + CollisionContext collisionContext = player == null ? CollisionContext.empty() : CollisionContext.of(player); + // CraftBukkit start + Level world = context.getLevel(); // Paper - Cancel hit for vanished players +- boolean canBuild = (!this.mustSurvive() || state.canSurvive(world, context.getClickedPos())) && world.checkEntityCollision(state, player, collisionContext, context.getClickedPos(), true); // Paper - Cancel hit for vanished players ++ boolean canBuild = (!this.mustSurvive() || state.canSurvive(world, context.getClickedPos())) && ((org.leavesmc.leaves.LeavesConfig.modify.creativeNoClip && context.getPlayer() != null) ? context.getPlayer().canSpectatingPlace(world, state, context.getClickedPos(), collisionContext) : world.checkEntityCollision(state, player, collisionContext, context.getClickedPos(), true)); // Paper - Cancel hit for vanished players // Leaves - creative no clip + org.bukkit.entity.Player bukkitPlayer = (context.getPlayer() instanceof ServerPlayer) ? (org.bukkit.entity.Player) context.getPlayer().getBukkitEntity() : null; + + BlockCanBuildEvent event = new BlockCanBuildEvent(CraftBlock.at(world, context.getClickedPos()), bukkitPlayer, CraftBlockData.fromData(state), canBuild, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent +diff --git a/net/minecraft/world/item/StandingAndWallBlockItem.java b/net/minecraft/world/item/StandingAndWallBlockItem.java +index 1f0e7c391d02b18e2c89700025713ec3d759f2ea..300ee12ca9584e53e9d72e3ebfd039beb3fab3b2 100644 +--- a/net/minecraft/world/item/StandingAndWallBlockItem.java ++++ b/net/minecraft/world/item/StandingAndWallBlockItem.java +@@ -45,7 +45,7 @@ public class StandingAndWallBlockItem extends BlockItem { + // return blockState != null && level.isUnobstructed(blockState, clickedPos, CollisionContext.empty()) ? blockState : null; + // CraftBukkit start + if (blockState != null) { +- boolean defaultReturn = level.isUnobstructed(blockState, clickedPos, CollisionContext.empty()); ++ boolean defaultReturn = (org.leavesmc.leaves.LeavesConfig.modify.creativeNoClip && context.getPlayer() != null) ? context.getPlayer().canSpectatingPlace(level, blockState, clickedPos, CollisionContext.empty()) : level.isUnobstructed(blockState, clickedPos, CollisionContext.empty()); // Leaves - creative no clip + org.bukkit.entity.Player player = (context.getPlayer() instanceof net.minecraft.server.level.ServerPlayer serverPlayer) ? serverPlayer.getBukkitEntity() : null; + + org.bukkit.event.block.BlockCanBuildEvent event = new org.bukkit.event.block.BlockCanBuildEvent(org.bukkit.craftbukkit.block.CraftBlock.at(context.getLevel(), clickedPos), player, org.bukkit.craftbukkit.block.data.CraftBlockData.fromData(blockState), defaultReturn, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent +diff --git a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +index cc8781c0a357eebc2bff936b4e7be53cc316716d..f5264831b2dc1843b09599086f2bb586d041535d 100644 +--- a/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java +@@ -147,7 +147,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl + List entities = level.getEntities(null, progressDeltaAabb); + if (!entities.isEmpty()) { + for (Entity entity : entities) { +- if (entity.getPistonPushReaction() != PushReaction.IGNORE) { ++ if (entity.getPistonPushReaction() != PushReaction.IGNORE && !(entity instanceof Player player && player.isCreativeFlyOrSpectator())) { // Leaves - creative no clip + entity.move( + MoverType.SHULKER_BOX, + new Vec3( +diff --git a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +index 1e6e940fca9d96ef410c7bf05524bd9b24db4a79..362007621cc03ccd4ce7b2c0f77680d25535d7b2 100644 +--- a/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java ++++ b/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java +@@ -148,7 +148,7 @@ public class PistonMovingBlockEntity extends BlockEntity { + d3 = movementDirection.getStepZ(); + } + +- entity.setDeltaMovement(d1, d2, d3); ++ if (!(entity instanceof net.minecraft.world.entity.player.Player player) || !player.isCreativeFlyOrSpectator()) entity.setDeltaMovement(d1, d2, d3); // Leaves - creative no clip + // Paper - EAR items stuck in slime pushed by a piston + entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); + entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); +@@ -184,6 +184,7 @@ public class PistonMovingBlockEntity extends BlockEntity { + } + + private static void moveEntityByPiston(Direction noClipDirection, Entity entity, double progress, Direction direction) { ++ if (entity instanceof net.minecraft.world.entity.player.Player player && player.isCreativeFlyOrSpectator()) return; // Leaves - creative no clip + NOCLIP.set(noClipDirection); + entity.move(MoverType.PISTON, new Vec3(progress * direction.getStepX(), progress * direction.getStepY(), progress * direction.getStepZ())); + entity.applyEffectsFromBlocks(); diff --git a/patches/server/0056-Optimized-dragon-respawn.patch b/leaves-server/minecraft-patches/features/0048-Optimized-dragon-respawn.patch similarity index 53% rename from patches/server/0056-Optimized-dragon-respawn.patch rename to leaves-server/minecraft-patches/features/0048-Optimized-dragon-respawn.patch index 5a2cb728..5ea3a0fb 100644 --- a/patches/server/0056-Optimized-dragon-respawn.patch +++ b/leaves-server/minecraft-patches/features/0048-Optimized-dragon-respawn.patch @@ -4,24 +4,24 @@ Date: Tue, 27 Jun 2023 13:22:34 +0800 Subject: [PATCH] Optimized dragon respawn -diff --git a/src/main/java/net/minecraft/world/level/block/state/pattern/BlockPattern.java b/src/main/java/net/minecraft/world/level/block/state/pattern/BlockPattern.java -index ee99519ebd46b1db3e76e7eb86e5cc121c867dc4..63f6f1328c4e39cc1f35480166ae5e22df943119 100644 ---- a/src/main/java/net/minecraft/world/level/block/state/pattern/BlockPattern.java -+++ b/src/main/java/net/minecraft/world/level/block/state/pattern/BlockPattern.java +diff --git a/net/minecraft/world/level/block/state/pattern/BlockPattern.java b/net/minecraft/world/level/block/state/pattern/BlockPattern.java +index f7bb979f08634a7e1b77c59040f59fb5e11aafa5..3cad7d6bca9af77bace18bfd7cc013ac22be4075 100644 +--- a/net/minecraft/world/level/block/state/pattern/BlockPattern.java ++++ b/net/minecraft/world/level/block/state/pattern/BlockPattern.java @@ -59,7 +59,7 @@ public class BlockPattern { } @Nullable -- private BlockPattern.BlockPatternMatch matches(BlockPos frontTopLeft, Direction forwards, Direction up, LoadingCache cache) { -+ public BlockPattern.BlockPatternMatch matches(BlockPos frontTopLeft, Direction forwards, Direction up, LoadingCache cache) { // Leaves - private -> public +- private BlockPattern.BlockPatternMatch matches(BlockPos pos, Direction finger, Direction thumb, LoadingCache cache) { ++ public BlockPattern.BlockPatternMatch matches(BlockPos pos, Direction finger, Direction thumb, LoadingCache cache) { // Leaves - private -> public for (int i = 0; i < this.width; i++) { - for (int j = 0; j < this.height; j++) { - for (int k = 0; k < this.depth; k++) { -diff --git a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java -index b331c93c82c27f9456fec208a0c008c5bedfa8c4..e944a730503a9c50bcde819515a1d7e7f1ec59fd 100644 ---- a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java -+++ b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java -@@ -292,8 +292,67 @@ public class EndDragonFight { + for (int i1 = 0; i1 < this.height; i1++) { + for (int i2 = 0; i2 < this.depth; i2++) { +diff --git a/net/minecraft/world/level/dimension/end/EndDragonFight.java b/net/minecraft/world/level/dimension/end/EndDragonFight.java +index 6e7e87c32734b3aae354bc34459e5f207da5c78f..5a28a821bb401f8f1465b085c9ffda52ba9a0a9f 100644 +--- a/net/minecraft/world/level/dimension/end/EndDragonFight.java ++++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java +@@ -274,8 +274,68 @@ public class EndDragonFight { return false; } @@ -85,53 +85,20 @@ index b331c93c82c27f9456fec208a0c008c5bedfa8c4..e944a730503a9c50bcde819515a1d7e7 + + return null; + } -+ // Leaves end - optimizedDragonRespawn - ChunkPos chunkcoordintpair = new ChunkPos(this.origin); ++ // Leaves end - optimizedDragonRespawn ++ + ChunkPos chunkPos = new ChunkPos(this.origin); - int i; -@@ -624,6 +683,11 @@ public class EndDragonFight { + for (int i = -8 + chunkPos.x; i <= 8 + chunkPos.x; i++) { +@@ -571,6 +631,11 @@ public class EndDragonFight { } - public boolean respawnDragon(List list) { // CraftBukkit - return boolean + public boolean respawnDragon(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 shapedetector_shapedetectorcollection = this.findExitPortal(); shapedetector_shapedetectorcollection != null; shapedetector_shapedetectorcollection = this.findExitPortal()) { - for (int i = 0; i < this.exitPortalPattern.getWidth(); ++i) { -diff --git a/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java b/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..567cd33afbd17d61ac1847692f5cb762270a3c40 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java -@@ -0,0 +1,28 @@ -+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; -+ } -+} + for (BlockPattern.BlockPatternMatch blockPatternMatch = this.findExitPortal(); blockPatternMatch != null; blockPatternMatch = this.findExitPortal()) { + for (int i = 0; i < this.exitPortalPattern.getWidth(); i++) { diff --git a/leaves-server/minecraft-patches/features/0049-Shave-snow-layers.patch b/leaves-server/minecraft-patches/features/0049-Shave-snow-layers.patch new file mode 100644 index 00000000..cc5ad723 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0049-Shave-snow-layers.patch @@ -0,0 +1,37 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 27 Jun 2023 14:07:00 +0800 +Subject: [PATCH] Shave snow layers + + +diff --git a/net/minecraft/world/item/ShovelItem.java b/net/minecraft/world/item/ShovelItem.java +index 75bbe901e79d9ba3250ed2426a36c1c3363c19c1..29849cd8f53daf2da0b9973c805d44376984013c 100644 +--- a/net/minecraft/world/item/ShovelItem.java ++++ b/net/minecraft/world/item/ShovelItem.java +@@ -44,6 +44,26 @@ public class ShovelItem extends DiggerItem { + return InteractionResult.PASS; + } else { + Player player = context.getPlayer(); ++ // Leaves start - shaveSnowLayers ++ if (org.leavesmc.leaves.LeavesConfig.modify.shaveSnowLayers && blockState.is(Blocks.SNOW)) { ++ int layers = blockState.getValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS); ++ ItemStack tool = context.getItemInHand(); ++ boolean hasSilkTouch = net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT).getOrThrow(net.minecraft.world.item.enchantment.Enchantments.SILK_TOUCH), tool) > 0; ++ BlockState shavedBlockState = layers > 1 ? blockState.setValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS, layers - 1) : Blocks.AIR.defaultBlockState(); ++ ++ level.setBlock(clickedPos, shavedBlockState, Block.UPDATE_ALL_IMMEDIATE); ++ level.gameEvent(GameEvent.BLOCK_CHANGE, clickedPos, GameEvent.Context.of(player, shavedBlockState)); ++ ++ Block.popResource(level, clickedPos, new ItemStack(hasSilkTouch ? Items.SNOW : Items.SNOWBALL)); ++ level.playSound(player, clickedPos, SoundEvents.SNOW_BREAK, SoundSource.BLOCKS, 1.0F, 1.0F); ++ ++ if (player != null) { ++ tool.hurtAndBreak(1, player, LivingEntity.getSlotForHand(context.getHand())); ++ } ++ ++ return InteractionResult.SUCCESS; ++ } ++ // Leaves end - shaveSnowLayers + BlockState blockState1 = FLATTENABLES.get(blockState.getBlock()); + BlockState blockState2 = null; + Runnable afterAction = null; // Paper diff --git a/leaves-server/minecraft-patches/features/0050-Elytra-aeronautics-no-chunk-load.patch b/leaves-server/minecraft-patches/features/0050-Elytra-aeronautics-no-chunk-load.patch new file mode 100644 index 00000000..e039dbe9 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0050-Elytra-aeronautics-no-chunk-load.patch @@ -0,0 +1,80 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 2 Jul 2023 09:25:00 +0800 +Subject: [PATCH] Elytra aeronautics no chunk load + + +diff --git a/net/minecraft/server/level/ChunkMap.java b/net/minecraft/server/level/ChunkMap.java +index 36c0a690e7e9b301c5a3d63fae2c7cbe36ba6cdf..6b625fbd538e5a4073e86e22adfcd382e1efde86 100644 +--- a/net/minecraft/server/level/ChunkMap.java ++++ b/net/minecraft/server/level/ChunkMap.java +@@ -792,7 +792,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + private boolean skipPlayer(ServerPlayer player) { +- return player.isSpectator() && !this.level.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); ++ return (player.isSpectator() && !this.level.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS)) ++ || (org.leavesmc.leaves.LeavesConfig.modify.elytraAeronautics.noChunk && player.elytraAeronauticsNoChunk); // Leaves - Elytra aeronautics + } + + void updatePlayerStatus(ServerPlayer player, boolean track) { +@@ -826,6 +827,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider + } + + public void move(ServerPlayer player) { ++ if (player.elytraAeronauticsNoChunk) return; // Leaves - no chunk + // Paper - optimise entity tracker + + SectionPos lastSectionPos = player.getLastSectionPos(); +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 1f8b1d6b72056ebd66bade417b10ce23b7e11b8a..24f2a1d295a02a8fdc1518f914549a980cd035c9 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -547,7 +547,7 @@ public class ServerGamePacketListenerImpl + speed *= 2f; // TODO: Get the speed of the vehicle instead of the player + + // Paper start - Prevent moving into unloaded chunks +- if (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && ( ++ if (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && !player.elytraAeronauticsNoChunk && ( // Leaves - no chunk load + !serverLevel.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position()))) || + !serverLevel.areChunksLoadedForMove(rootVehicle.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(rootVehicle.position()))) + )) { +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index c34e32549813af3a5d6e3edc2a606613ca2abb8f..92f363180ed4d2a91da996de9e8adfa4f3b17e69 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -1108,7 +1108,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return; + } + } +- ++ // Leaves start - elytra aeronautics ++ if (org.leavesmc.leaves.LeavesConfig.modify.elytraAeronautics.noChunk && this instanceof Player player) { ++ if (type == MoverType.PLAYER && player.isFallFlying()) { ++ org.leavesmc.leaves.util.ElytraAeronauticsHelper.flightBehaviour(player, movement); ++ } ++ } ++ // Leaves end - elytra aeronautics + ProfilerFiller profilerFiller = Profiler.get(); + profilerFiller.push("move"); + if (this.stuckSpeedMultiplier.lengthSqr() > 1.0E-7) { +@@ -2058,6 +2064,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + this.yo = y; + this.zo = d1; + this.setPos(d, y, d1); ++ if (this instanceof Player player && player.elytraAeronauticsNoChunk) return; // Leaves - elytra aeronautics + if (this.valid) this.level.getChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4); // CraftBukkit + } + +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index 1f72a78beee96d942b5f838c5de034c99c8fe98f..2cc72279fbe2b408ab3515da462701cc6ca6c257 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -200,6 +200,7 @@ public abstract class Player extends LivingEntity { + private int currentImpulseContextResetGraceTime; + public boolean affectsSpawning = true; // Paper - Affects Spawning API + public net.kyori.adventure.util.TriState flyingFallDamage = net.kyori.adventure.util.TriState.NOT_SET; // Paper - flying fall damage ++ public boolean elytraAeronauticsNoChunk = false; // Leaves - Elytra aeronautics + + // CraftBukkit start + public boolean fauxSleeping; diff --git a/leaves-server/minecraft-patches/features/0051-Cache-ignite-odds.patch b/leaves-server/minecraft-patches/features/0051-Cache-ignite-odds.patch new file mode 100644 index 00000000..550bbbac --- /dev/null +++ b/leaves-server/minecraft-patches/features/0051-Cache-ignite-odds.patch @@ -0,0 +1,63 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 5 Jul 2023 17:42:24 +0800 +Subject: [PATCH] Cache ignite odds + + +diff --git a/net/minecraft/world/level/block/FireBlock.java b/net/minecraft/world/level/block/FireBlock.java +index 7340c664fdcf991a2549c8f07f6ab093bbe6e4e8..d9796f683de7c0c12f45f62597792f80c66c60b7 100644 +--- a/net/minecraft/world/level/block/FireBlock.java ++++ b/net/minecraft/world/level/block/FireBlock.java +@@ -225,6 +225,7 @@ public class FireBlock extends BaseFireBlock { + this.checkBurnOut(level, pos.south(), 300 + i, random, ageValue, pos); + // CraftBukkit end + BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(); ++ Object2IntOpenHashMap blockPositionIgniteCache = new Object2IntOpenHashMap<>(); // Leaves - cache ignite odds + + for (int i1 = -1; i1 <= 1; i1++) { + for (int i2 = -1; i2 <= 1; i2++) { +@@ -236,7 +237,7 @@ public class FireBlock extends BaseFireBlock { + } + + mutableBlockPos.setWithOffset(pos, i1, i3, i2); +- int igniteOdds = this.getIgniteOdds(level, mutableBlockPos); ++ int igniteOdds = this.getIgniteOdds(level, mutableBlockPos, org.leavesmc.leaves.LeavesConfig.performance.cacheIgniteOdds ? blockPositionIgniteCache : null); // Leaves - cache ignite odds + if (igniteOdds > 0) { + int i5 = (igniteOdds + 40 + level.getDifficulty().getId() * 7) / (ageValue + 30); + if (isIncreasedFireBurnout) { +@@ -340,20 +341,33 @@ public class FireBlock extends BaseFireBlock { + return false; + } + ++ // Leaves start - cache ignite odds + private int getIgniteOdds(LevelReader level, BlockPos pos) { ++ return getIgniteOdds(level, pos, null); ++ } ++ ++ private int getIgniteOdds(LevelReader level, BlockPos pos, @org.jetbrains.annotations.Nullable Object2IntOpenHashMap cache) { + if (!level.isEmptyBlock(pos)) { + return 0; + } else { + int i = 0; + + for (Direction direction : Direction.values()) { +- BlockState blockState = level.getBlockState(pos.relative(direction)); +- i = Math.max(this.getIgniteOdds(blockState), i); ++ if (cache != null) { ++ i = Math.max(cache.computeIfAbsent(pos, key -> { ++ BlockState blockState = level.getBlockState(pos.relative(direction)); ++ return this.getIgniteOdds(blockState); ++ }), i); ++ } else { ++ BlockState blockState = level.getBlockState(pos.relative(direction)); ++ i = Math.max(this.getIgniteOdds(blockState), i); ++ } + } + + return i; + } + } ++ // Leaves end - cache ignite odds + + @Override + protected boolean canBurn(BlockState state) { diff --git a/leaves-server/minecraft-patches/features/0052-Lava-riptide.patch b/leaves-server/minecraft-patches/features/0052-Lava-riptide.patch new file mode 100644 index 00000000..264495b7 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0052-Lava-riptide.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Fri, 7 Jul 2023 16:53:32 +0800 +Subject: [PATCH] Lava riptide + + +diff --git a/net/minecraft/world/item/TridentItem.java b/net/minecraft/world/item/TridentItem.java +index 23284dbeff327d1b8dc89f3a0dc0ee549cec2daa..edf6ac62e068f46f68b76ea1e8a801cfe2611d3e 100644 +--- a/net/minecraft/world/item/TridentItem.java ++++ b/net/minecraft/world/item/TridentItem.java +@@ -78,7 +78,7 @@ public class TridentItem extends Item implements ProjectileItem { + return false; + } else { + float tridentSpinAttackStrength = EnchantmentHelper.getTridentSpinAttackStrength(stack, player); +- if (tridentSpinAttackStrength > 0.0F && !player.isInWaterOrRain()) { ++ if (tridentSpinAttackStrength > 0.0F && !player.isInWaterOrRain() || (org.leavesmc.leaves.LeavesConfig.modify.lavaRiptide && player.isInLava())) { // Leaves - lava riptide + return false; + } else if (stack.nextDamageWillBreak()) { + return false; +@@ -154,7 +154,7 @@ public class TridentItem extends Item implements ProjectileItem { + ItemStack itemInHand = player.getItemInHand(hand); + if (itemInHand.nextDamageWillBreak()) { + return InteractionResult.FAIL; +- } else if (EnchantmentHelper.getTridentSpinAttackStrength(itemInHand, player) > 0.0F && !player.isInWaterOrRain()) { ++ } else if (EnchantmentHelper.getTridentSpinAttackStrength(itemInHand, player) > 0.0F && !player.isInWaterOrRain() && !(org.leavesmc.leaves.LeavesConfig.modify.lavaRiptide && player.isInLava())) { // Leaves - lava riptide + return InteractionResult.FAIL; + } else { + player.startUsingItem(hand); diff --git a/leaves-server/minecraft-patches/features/0053-No-block-update-command.patch b/leaves-server/minecraft-patches/features/0053-No-block-update-command.patch new file mode 100644 index 00000000..818844d6 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0053-No-block-update-command.patch @@ -0,0 +1,93 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 19:16:16 +0800 +Subject: [PATCH] No block update command + + +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 8c8b6c4c6fee9bb9279d02d6b7a47cb2e3f39d93..ed37ba4ca1303a0ce97e7a1fce1b29682276292b 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -2383,6 +2383,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + + @Override + public void blockUpdated(BlockPos pos, Block block) { ++ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update + if (!this.isDebug()) { + // CraftBukkit start + if (this.populating) { +diff --git a/net/minecraft/world/item/ItemStack.java b/net/minecraft/world/item/ItemStack.java +index 6bc431cd0724de406c92830d618c26d69bcd1918..a212135f15701cb887b61081c16313105d952890 100644 +--- a/net/minecraft/world/item/ItemStack.java ++++ b/net/minecraft/world/item/ItemStack.java +@@ -484,7 +484,7 @@ public final class ItemStack implements DataComponentHolder { + net.minecraft.world.level.block.state.BlockState block = serverLevel.getBlockState(newPos); + + if (!(block.getBlock() instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // Containers get placed automatically +- block.onPlace(serverLevel, newPos, oldBlock, true, context); ++ if (!org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) block.onPlace(serverLevel, newPos, oldBlock, true, context); // Leaves - no block update + } + + serverLevel.notifyAndUpdatePhysics(newPos, null, oldBlock, block, serverLevel.getBlockState(newPos), updateFlag, net.minecraft.world.level.block.Block.UPDATE_LIMIT); // send null chunk as chunk.k() returns false by this point +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 1776b79309ffd9a8a52f27a144606ed9a441251e..1e4e1a3236fc6b9aa515f2025cdef5a901c64d58 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -405,7 +405,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + return null; + } else { + if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. +- state.onPlace(this.level, pos, blockState, isMoving); ++ if (!org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) state.onPlace(this.level, pos, blockState, isMoving); // Leaves - no block update + } + + if (state.hasBlockEntity()) { +diff --git a/net/minecraft/world/level/material/FlowingFluid.java b/net/minecraft/world/level/material/FlowingFluid.java +index e30499bdcd6600e5c9d4a755c1182fb6dff3735f..ce3a74860f4a6b65e6eb1cf422f71ec1f844ecd2 100644 +--- a/net/minecraft/world/level/material/FlowingFluid.java ++++ b/net/minecraft/world/level/material/FlowingFluid.java +@@ -475,6 +475,7 @@ public abstract class FlowingFluid extends Fluid { + + @Override + public void tick(ServerLevel level, BlockPos pos, BlockState blockState, FluidState fluidState) { ++ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update + if (!fluidState.isSource()) { + FluidState newLiquid = this.getNewLiquid(level, pos, level.getBlockState(pos)); + int spreadDelay = this.getSpreadDelay(level, pos, fluidState, newLiquid); +diff --git a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +index 028eae2f9a459b60e92f3344091083aa93b54485..63684402f7c89c7c5d71902db4bfb23132b1d28d 100644 +--- a/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java ++++ b/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java +@@ -47,6 +47,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { + } + + private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates updates) { ++ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update + boolean flag = this.count > 0; + boolean flag1 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; + this.count++; +diff --git a/net/minecraft/world/level/redstone/InstantNeighborUpdater.java b/net/minecraft/world/level/redstone/InstantNeighborUpdater.java +index d0da64e325f83ab073221a3fa7284e42b6a88534..32e10dccde3e80969641ba1fb67479b5a0bada06 100644 +--- a/net/minecraft/world/level/redstone/InstantNeighborUpdater.java ++++ b/net/minecraft/world/level/redstone/InstantNeighborUpdater.java +@@ -16,17 +16,20 @@ public class InstantNeighborUpdater implements NeighborUpdater { + + @Override + public void shapeUpdate(Direction direction, BlockState state, BlockPos pos, BlockPos neighborPos, int flags, int recursionLevel) { ++ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update + NeighborUpdater.executeShapeUpdate(this.level, direction, pos, neighborPos, state, flags, recursionLevel - 1); + } + + @Override + public void neighborChanged(BlockPos pos, Block neighborBlock, @Nullable Orientation orientation) { ++ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update + BlockState blockState = this.level.getBlockState(pos); + this.neighborChanged(blockState, pos, neighborBlock, orientation, false); + } + + @Override + public void neighborChanged(BlockState state, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) { ++ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update + NeighborUpdater.executeUpdate(this.level, state, pos, neighborBlock, orientation, movedByPiston); + } + } diff --git a/leaves-server/minecraft-patches/features/0054-Raider-die-skip-self-raid-check.patch b/leaves-server/minecraft-patches/features/0054-Raider-die-skip-self-raid-check.patch new file mode 100644 index 00000000..4e662eae --- /dev/null +++ b/leaves-server/minecraft-patches/features/0054-Raider-die-skip-self-raid-check.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 9 Jul 2023 16:51:47 +0800 +Subject: [PATCH] Raider die skip self raid check + + +diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java +index 8270d76a753bfd26a4c8ef6610bee5c24ee59cfe..72551960faaedbf43d1c65ad061f6d366028a3e9 100644 +--- a/net/minecraft/world/entity/raid/Raider.java ++++ b/net/minecraft/world/entity/raid/Raider.java +@@ -115,7 +115,7 @@ public abstract class Raider extends PatrollingMonster { + Entity entity = cause.getEntity(); + Raid currentRaid = this.getCurrentRaid(); + if (currentRaid != null) { +- if (this.isPatrolLeader()) { ++ if (!org.leavesmc.leaves.LeavesConfig.modify.skipSelfRaidCheck && this.isPatrolLeader()) { // Leaves - skip self raid check + currentRaid.removeLeader(this.getWave()); + } + diff --git a/leaves-server/minecraft-patches/features/0055-Container-open-passthrough.patch b/leaves-server/minecraft-patches/features/0055-Container-open-passthrough.patch new file mode 100644 index 00000000..6de08663 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0055-Container-open-passthrough.patch @@ -0,0 +1,80 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 21:31:03 +0800 +Subject: [PATCH] Container open passthrough + + +diff --git a/net/minecraft/world/entity/decoration/ItemFrame.java b/net/minecraft/world/entity/decoration/ItemFrame.java +index 65e1d7c5ac94b1cfb921fa009be59d3e5872f0b5..34a60bc540cd08da0ebec3a37ec0a01da3dd24de 100644 +--- a/net/minecraft/world/entity/decoration/ItemFrame.java ++++ b/net/minecraft/world/entity/decoration/ItemFrame.java +@@ -408,6 +408,20 @@ public class ItemFrame extends HangingEntity { + return InteractionResult.PASS; + } + } else { ++ // Leaves start - itemFrameContainerPassthrough ++ if (org.leavesmc.leaves.LeavesConfig.modify.containerPassthrough && !player.isShiftKeyDown()) { ++ BlockPos pos1 = this.pos.relative(this.direction.getOpposite()); ++ if (level().getBlockEntity(pos1) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity) { ++ BlockState blockState = level().getBlockState(pos1); ++ net.minecraft.world.phys.BlockHitResult hitResult = new net.minecraft.world.phys.BlockHitResult(Vec3.atCenterOf(pos1), this.direction, pos1, false); ++ if (flag1) { ++ return blockState.useItemOn(itemInHand, level(), player, hand, hitResult); ++ } else { ++ return blockState.useWithoutItem(level(), player, hitResult); ++ } ++ } ++ } ++ // Leaves end - itemFrameContainerPassthrough + // Paper start - Add PlayerItemFrameChangeEvent + io.papermc.paper.event.player.PlayerItemFrameChangeEvent event = new io.papermc.paper.event.player.PlayerItemFrameChangeEvent((org.bukkit.entity.Player) player.getBukkitEntity(), (org.bukkit.entity.ItemFrame) this.getBukkitEntity(), this.getItem().asBukkitCopy(), io.papermc.paper.event.player.PlayerItemFrameChangeEvent.ItemFrameChangeAction.ROTATE); + if (!event.callEvent()) { +diff --git a/net/minecraft/world/level/block/SignBlock.java b/net/minecraft/world/level/block/SignBlock.java +index f33a42b4888dbf46175e0be409aa6c8688d552b9..da815a9a5e447e9a1cf44f54e4fcbb9481d65d86 100644 +--- a/net/minecraft/world/level/block/SignBlock.java ++++ b/net/minecraft/world/level/block/SignBlock.java +@@ -106,6 +106,18 @@ public abstract class SignBlock extends BaseEntityBlock implements SimpleWaterlo + } else { + return InteractionResult.TRY_WITH_EMPTY_HAND; + } ++ // Leaves start - signContainerPassthrough ++ } else if (org.leavesmc.leaves.LeavesConfig.modify.containerPassthrough) { ++ BlockPos pos1 = pos.relative(hitResult.getDirection().getOpposite()); ++ if (this instanceof WallSignBlock || this instanceof WallHangingSignBlock) { ++ pos1 = pos.relative(state.getValue(HorizontalDirectionalBlock.FACING).getOpposite()); ++ } ++ if (level.getBlockEntity(pos1) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity) { ++ BlockState state1 = level.getBlockState(pos1); ++ return state1.useItemOn(stack, level, player, hand, hitResult.withPosition(pos1)); ++ } ++ return InteractionResult.PASS; ++ // Leaves end - signContainerPassthrough + } else { + return InteractionResult.TRY_WITH_EMPTY_HAND; + } +@@ -131,6 +143,25 @@ public abstract class SignBlock extends BaseEntityBlock implements SimpleWaterlo + return InteractionResult.SUCCESS_SERVER; + } else if (flag) { + return InteractionResult.SUCCESS_SERVER; ++ // Leaves start - signContainerPassthrough ++ } else if (org.leavesmc.leaves.LeavesConfig.modify.containerPassthrough) { ++ if (player.isShiftKeyDown()) { ++ if (!this.otherPlayerIsEditingSign(player, signBlockEntity) && player.mayBuild() && this.hasEditableText(player, signBlockEntity, isFacingFrontText)) { ++ this.openTextEdit(player, signBlockEntity, isFacingFrontText); ++ return InteractionResult.SUCCESS; ++ } ++ } ++ ++ BlockPos pos1 = pos.relative(hitResult.getDirection().getOpposite()); ++ if (this instanceof WallSignBlock || this instanceof WallHangingSignBlock) { ++ pos1 = pos.relative(state.getValue(HorizontalDirectionalBlock.FACING).getOpposite()); ++ } ++ if (level.getBlockEntity(pos1) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity) { ++ BlockState state1 = level.getBlockState(pos1); ++ return state1.useWithoutItem(level, player, hitResult.withPosition(pos1)); ++ } ++ return InteractionResult.PASS; ++ // Leaves end - signContainerPassthrough + } else if (!this.otherPlayerIsEditingSign(player, signBlockEntity) + && player.mayBuild() + && this.hasEditableText(player, signBlockEntity, isFacingFrontText)) { diff --git a/leaves-server/minecraft-patches/features/0056-Dont-respond-ping-before-start-fully.patch b/leaves-server/minecraft-patches/features/0056-Dont-respond-ping-before-start-fully.patch new file mode 100644 index 00000000..eb6e18ef --- /dev/null +++ b/leaves-server/minecraft-patches/features/0056-Dont-respond-ping-before-start-fully.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 17 Jul 2023 23:16:58 +0800 +Subject: [PATCH] Dont respond ping before start fully + +This patch is Powered by Gale(https://github.com/GaleMC/Gale) + +diff --git a/net/minecraft/server/network/ServerStatusPacketListenerImpl.java b/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +index 465559113071a47e706f77a5b0996597ee986b3d..f64b21ef2660a1e60e3d6cddef04950e983b3baa 100644 +--- a/net/minecraft/server/network/ServerStatusPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerStatusPacketListenerImpl.java +@@ -37,6 +37,12 @@ public class ServerStatusPacketListenerImpl implements ServerStatusPacketListene + } else { + this.hasRequestedStatus = true; + // this.connection.send(new ClientboundStatusResponsePacket(this.status)); // Paper ++ // Leaves start - dont respond it before start full ++ var status = net.minecraft.server.MinecraftServer.getServer().getStatus(); ++ if (org.leavesmc.leaves.LeavesConfig.mics.dontRespondPingBeforeStart && (status == null || status.version().isEmpty())) { ++ return; ++ } ++ // Leaves end - dont respond it before start full + com.destroystokyo.paper.network.StandardPaperServerListPingEventImpl.processRequest(net.minecraft.server.MinecraftServer.getServer(), this.connection); // Paper - handle status request + } + } diff --git a/leaves-server/minecraft-patches/features/0057-Faster-chunk-serialization.patch b/leaves-server/minecraft-patches/features/0057-Faster-chunk-serialization.patch new file mode 100644 index 00000000..d87f2639 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0057-Faster-chunk-serialization.patch @@ -0,0 +1,272 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 18 Jul 2023 13:14:15 +0800 +Subject: [PATCH] Faster chunk serialization + +This patch is Powered by Gale(https://github.com/GaleMC/Gale) + +diff --git a/net/minecraft/util/BitStorage.java b/net/minecraft/util/BitStorage.java +index 02502d50f0255f5bbcc0ecb965abb48cc1a112da..0abee1cd9d6a5a22d3136e3711de926c3a2d4d73 100644 +--- a/net/minecraft/util/BitStorage.java ++++ b/net/minecraft/util/BitStorage.java +@@ -21,6 +21,8 @@ public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counti + + BitStorage copy(); + ++ void compact(net.minecraft.world.level.chunk.Palette srcPalette, net.minecraft.world.level.chunk.Palette dstPalette, short[] out); // Leaves - faster chunk serialization ++ + // Paper start - block counting + // provide default impl in case mods implement this... + @Override +diff --git a/net/minecraft/util/SimpleBitStorage.java b/net/minecraft/util/SimpleBitStorage.java +index e6306a68c8652d4c5d22d5ecb1416f5f931f76ee..7b4b3b8a02be3ab5ecafdea1ab74c246daf37d98 100644 +--- a/net/minecraft/util/SimpleBitStorage.java ++++ b/net/minecraft/util/SimpleBitStorage.java +@@ -465,4 +465,44 @@ public class SimpleBitStorage implements BitStorage { + super(message); + } + } ++ ++ // Leaves start - faster chunk serialization ++ @Override ++ public void compact(net.minecraft.world.level.chunk.Palette srcPalette, net.minecraft.world.level.chunk.Palette dstPalette, short[] out) { ++ if (this.size >= Short.MAX_VALUE) { ++ throw new IllegalStateException("Array too large"); ++ } ++ ++ if (this.size != out.length) { ++ throw new IllegalStateException("Array size mismatch"); ++ } ++ ++ short[] mappings = new short[(int) (this.mask + 1)]; ++ ++ int idx = 0; ++ ++ for (long word : this.data) { ++ long bits = word; ++ ++ for (int elementIdx = 0; elementIdx < this.valuesPerLong; ++elementIdx) { ++ int value = (int) (bits & this.mask); ++ int remappedId = mappings[value]; ++ ++ if (remappedId == 0) { ++ remappedId = dstPalette.idFor(srcPalette.valueFor(value)) + 1; ++ mappings[value] = (short) remappedId; ++ } ++ ++ out[idx] = (short) (remappedId - 1); ++ bits >>= this.bits; ++ ++ ++idx; ++ ++ if (idx >= this.size) { ++ return; ++ } ++ } ++ } ++ } ++ // Leaves end - faster chunk serialization + } +diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java +index 09fd99c9cbd23b5f3c899bfb00c9b89651948ed8..50993ce7519a77c6a9d36cb925125adccda7037f 100644 +--- a/net/minecraft/util/ZeroBitStorage.java ++++ b/net/minecraft/util/ZeroBitStorage.java +@@ -63,6 +63,8 @@ public class ZeroBitStorage implements BitStorage { + return this; + } + ++ @Override public void compact(net.minecraft.world.level.chunk.Palette srcPalette, net.minecraft.world.level.chunk.Palette dstPalette, short[] out) {} // Leaves - faster chunk serialization ++ + // Paper start - block counting + @Override + public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap moonrise$countEntries() { +diff --git a/net/minecraft/world/level/chunk/PaletteResize.java b/net/minecraft/world/level/chunk/PaletteResize.java +index c723606fa0be811e580ba47de8c9c575583cc930..c768443c8c6a4b05018bbc70d54b6f41e53e7738 100644 +--- a/net/minecraft/world/level/chunk/PaletteResize.java ++++ b/net/minecraft/world/level/chunk/PaletteResize.java +@@ -1,5 +1,5 @@ + package net.minecraft.world.level.chunk; + +-interface PaletteResize { ++public interface PaletteResize { // Leaves - package -> public + int onResize(int bits, T objectAdded); + } +diff --git a/net/minecraft/world/level/chunk/PalettedContainer.java b/net/minecraft/world/level/chunk/PalettedContainer.java +index f5da433050fd3060e0335d4002d520ebe8cd691f..afd78a03fb29c633cc8dc811a14df43aff69ba98 100644 +--- a/net/minecraft/world/level/chunk/PalettedContainer.java ++++ b/net/minecraft/world/level/chunk/PalettedContainer.java +@@ -25,6 +25,24 @@ import net.minecraft.util.ThreadingDetector; + import net.minecraft.util.ZeroBitStorage; + + public class PalettedContainer implements PaletteResize, PalettedContainerRO { ++ ++ // Leaves start - faster chunk serialization ++ private static final ThreadLocal CACHED_ARRAY_4096 = ThreadLocal.withInitial(() -> new short[4096]); ++ private static final ThreadLocal CACHED_ARRAY_64 = ThreadLocal.withInitial(() -> new short[64]); ++ ++ private Optional asOptional(long[] data) { ++ return Optional.of(Arrays.stream(data)); ++ } ++ ++ private short[] getOrCreate(int size) { ++ return switch (size) { ++ case 64 -> CACHED_ARRAY_64.get(); ++ case 4096 -> CACHED_ARRAY_4096.get(); ++ default -> new short[size]; ++ }; ++ } ++ // Leaves end - faster chunk serialization ++ + private static final int MIN_PALETTE_BITS = 0; + private final PaletteResize dummyPaletteResize = (bits, objectAdded) -> 0; + public final IdMap registry; +@@ -344,28 +362,76 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + public synchronized PalettedContainerRO.PackedData pack(IdMap registry, PalettedContainer.Strategy strategy) { // Paper - synchronize + this.acquire(); + +- PalettedContainerRO.PackedData var12; +- try { +- HashMapPalette hashMapPalette = new HashMapPalette<>(registry, this.data.storage.getBits(), this.dummyPaletteResize); +- int size = strategy.size(); +- int[] ints = new int[size]; +- this.data.storage.unpack(ints); +- swapPalette(ints, id -> hashMapPalette.idFor(this.data.palette.valueFor(id))); +- int i = strategy.calculateBitsForSerialization(registry, hashMapPalette.getSize()); +- Optional optional; +- if (i != 0) { +- SimpleBitStorage simpleBitStorage = new SimpleBitStorage(i, size, ints); +- optional = Optional.of(Arrays.stream(simpleBitStorage.getRaw())); +- } else { +- optional = Optional.empty(); +- } ++ // Leaves start - faster chunk serialization ++ if (!org.leavesmc.leaves.LeavesConfig.performance.fasterChunkSerialization) { ++ PalettedContainerRO.PackedData var12; ++ try { ++ HashMapPalette hashMapPalette = new HashMapPalette<>(registry, this.data.storage.getBits(), this.dummyPaletteResize); ++ int i = strategy.size(); ++ int[] is = new int[i]; ++ this.data.storage.unpack(is); ++ swapPalette(is, (id) -> { ++ return hashMapPalette.idFor(this.data.palette.valueFor(id)); ++ }); ++ int j = strategy.calculateBitsForSerialization(registry, hashMapPalette.getSize()); ++ Optional optional; ++ if (j != 0) { ++ SimpleBitStorage simpleBitStorage = new SimpleBitStorage(j, i, is); ++ optional = Optional.of(Arrays.stream(simpleBitStorage.getRaw())); ++ } else { ++ optional = Optional.empty(); ++ } + +- var12 = new PalettedContainerRO.PackedData<>(hashMapPalette.getEntries(), optional); +- } finally { +- this.release(); ++ var12 = new PalettedContainerRO.PackedData<>(hashMapPalette.getEntries(), optional); ++ } finally { ++ this.release(); ++ } ++ return var12; ++ } else { ++ Optional data = Optional.empty(); ++ List elements = null; ++ try { ++ // The palette that will be serialized ++ org.leavesmc.leaves.lithium.common.world.chunk.LithiumHashPalette hashPalette = null; ++ ++ final Palette palette = this.data.palette(); ++ final BitStorage storage = this.data.storage(); ++ if (storage instanceof ZeroBitStorage || palette.getSize() == 1) { ++ // If the palette only contains one entry, don't attempt to repack it. ++ elements = List.of(palette.valueFor(0)); ++ } else if (palette instanceof org.leavesmc.leaves.lithium.common.world.chunk.LithiumHashPalette lithiumHashPalette) { ++ hashPalette = lithiumHashPalette; ++ } ++ if (elements == null) { ++ org.leavesmc.leaves.lithium.common.world.chunk.LithiumHashPalette compactedPalette = new org.leavesmc.leaves.lithium.common.world.chunk.LithiumHashPalette<>(registry, storage.getBits(), this.dummyPaletteResize); ++ short[] array = this.getOrCreate(strategy.size()); ++ ++ storage.compact(this.data.palette(), compactedPalette, array); ++ ++ // If the palette didn't change during compaction, do a simple copy of the data array ++ if (hashPalette != null && hashPalette.getSize() == compactedPalette.getSize() && storage.getBits() == strategy.calculateBitsForSerialization(registry, hashPalette.getSize())) { // paletteSize can de-sync from palette - see https://github.com/CaffeineMC/lithium-fabric/issues/279 ++ data = this.asOptional(storage.getRaw().clone()); ++ elements = hashPalette.getElements(); ++ } else { ++ int bits = strategy.calculateBitsForSerialization(registry, compactedPalette.getSize()); ++ if (bits != 0) { ++ // Re-pack the integer array as the palette has changed size ++ SimpleBitStorage copy = new SimpleBitStorage(bits, array.length); ++ for (int i = 0; i < array.length; ++i) { ++ copy.set(i, array[i]); ++ } ++ // We don't need to clone the data array as we are the sole owner of it ++ data = this.asOptional(copy.getRaw()); ++ } ++ } ++ elements = compactedPalette.getElements(); ++ } ++ } finally { ++ this.release(); ++ } ++ return new PalettedContainerRO.PackedData<>(elements, data); + } +- +- return var12; ++ // Leaves end - faster chunk serialization + } + + private static void swapPalette(int[] bits, IntUnaryOperator operator) { +@@ -405,13 +471,47 @@ public class PalettedContainer implements PaletteResize, PalettedContainer + + @Override + public void count(PalettedContainer.CountConsumer countConsumer) { +- if (this.data.palette.getSize() == 1) { +- countConsumer.accept(this.data.palette.valueFor(0), this.data.storage.getSize()); ++ // Leaves start - faster chunk serialization ++ if (!org.leavesmc.leaves.LeavesConfig.performance.fasterChunkSerialization) { ++ if (this.data.palette.getSize() == 1) { ++ countConsumer.accept(this.data.palette.valueFor(0), this.data.storage.getSize()); ++ } else { ++ Int2IntOpenHashMap int2IntOpenHashMap = new Int2IntOpenHashMap(); ++ this.data.storage.getAll((key) -> { ++ int2IntOpenHashMap.addTo(key, 1); ++ }); ++ int2IntOpenHashMap.int2IntEntrySet().forEach((entry) -> { ++ countConsumer.accept(this.data.palette.valueFor(entry.getIntKey()), entry.getIntValue()); ++ }); ++ } + } else { +- Int2IntOpenHashMap map = new Int2IntOpenHashMap(); +- this.data.storage.getAll(id -> map.addTo(id, 1)); +- map.int2IntEntrySet().forEach(idEntry -> countConsumer.accept(this.data.palette.valueFor(idEntry.getIntKey()), idEntry.getIntValue())); ++ int len = this.data.palette().getSize(); ++ ++ // Do not allocate huge arrays if we're using a large palette ++ if (len > 4096) { ++ if (this.data.palette.getSize() == 1) { ++ countConsumer.accept(this.data.palette.valueFor(0), this.data.storage.getSize()); ++ } else { ++ Int2IntOpenHashMap int2IntOpenHashMap = new Int2IntOpenHashMap(); ++ this.data.storage.getAll((key) -> { ++ int2IntOpenHashMap.addTo(key, 1); ++ }); ++ int2IntOpenHashMap.int2IntEntrySet().forEach((entry) -> { ++ countConsumer.accept(this.data.palette.valueFor(entry.getIntKey()), entry.getIntValue()); ++ }); ++ } ++ } ++ ++ short[] counts = new short[len]; ++ this.data.storage().getAll(i -> counts[i]++); ++ for (int i = 0; i < counts.length; i++) { ++ T obj = this.data.palette().valueFor(i); ++ if (obj != null) { ++ countConsumer.accept(obj, counts[i]); ++ } ++ } + } ++ // Leaves end - faster chunk serialization + } + + record Configuration(Palette.Factory factory, int bits) { diff --git a/patches/server/0068-Skip-secondary-POI-sensor-if-absent.patch b/leaves-server/minecraft-patches/features/0058-Skip-secondary-POI-sensor-if-absent.patch similarity index 63% rename from patches/server/0068-Skip-secondary-POI-sensor-if-absent.patch rename to leaves-server/minecraft-patches/features/0058-Skip-secondary-POI-sensor-if-absent.patch index 18645f3f..c110bbce 100644 --- a/patches/server/0068-Skip-secondary-POI-sensor-if-absent.patch +++ b/leaves-server/minecraft-patches/features/0058-Skip-secondary-POI-sensor-if-absent.patch @@ -5,14 +5,14 @@ Subject: [PATCH] Skip secondary POI sensor if absent This patch is Powered by Gale(https://github.com/GaleMC/Gale) -diff --git a/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java -index a0e0692d17760f440fe81d52887284c787e562db..c6f5f7f7c0821b707d9cfe239ccab748e6b8b9cb 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java -+++ b/src/main/java/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +diff --git a/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java b/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java +index 6b99afb4f237b5d6def98f3e03492975b795bc95..ef4413c0661800d1d712d89b6a8e95d034e90ae3 100644 +--- a/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java ++++ b/net/minecraft/world/entity/ai/sensing/SecondaryPoiSensor.java @@ -22,6 +22,15 @@ public class SecondaryPoiSensor extends Sensor { @Override - protected void doTick(ServerLevel world, Villager entity) { + protected void doTick(ServerLevel level, Villager entity) { + // Leaves start - skip secondary POI sensor if absent + if (org.leavesmc.leaves.LeavesConfig.performance.skipSecondaryPOISensorIfAbsent) { + var secondaryPoi = entity.getVillagerData().getProfession().secondaryPoi(); @@ -22,6 +22,6 @@ index a0e0692d17760f440fe81d52887284c787e562db..c6f5f7f7c0821b707d9cfe239ccab748 + } + } + // Leaves end - skip secondary POI sensor if absent - ResourceKey resourceKey = world.dimension(); + ResourceKey resourceKey = level.dimension(); BlockPos blockPos = entity.blockPosition(); List list = Lists.newArrayList(); diff --git a/patches/server/0070-Store-mob-counts-in-an-array.patch b/leaves-server/minecraft-patches/features/0059-Store-mob-counts-in-an-array.patch similarity index 51% rename from patches/server/0070-Store-mob-counts-in-an-array.patch rename to leaves-server/minecraft-patches/features/0059-Store-mob-counts-in-an-array.patch index 932876f6..fb0bb74b 100644 --- a/patches/server/0070-Store-mob-counts-in-an-array.patch +++ b/leaves-server/minecraft-patches/features/0059-Store-mob-counts-in-an-array.patch @@ -5,34 +5,34 @@ Subject: [PATCH] Store mob counts in an array This patch is Powered by Gale(https://github.com/GaleMC/Gale) -diff --git a/src/main/java/net/minecraft/world/level/LocalMobCapCalculator.java b/src/main/java/net/minecraft/world/level/LocalMobCapCalculator.java -index 2039b16e5e9bc0797b3f31081d221bb8b34a4dc7..973331b29f2fd672c18c2dea858f473009d59637 100644 ---- a/src/main/java/net/minecraft/world/level/LocalMobCapCalculator.java -+++ b/src/main/java/net/minecraft/world/level/LocalMobCapCalculator.java +diff --git a/net/minecraft/world/level/LocalMobCapCalculator.java b/net/minecraft/world/level/LocalMobCapCalculator.java +index 9641219c190261dea0db5f95f040a705ba0a3ff9..463a86eee6a75bdd8989b5bed46f118073a4fe25 100644 +--- a/net/minecraft/world/level/LocalMobCapCalculator.java ++++ b/net/minecraft/world/level/LocalMobCapCalculator.java @@ -43,13 +43,26 @@ public class LocalMobCapCalculator { static class MobCounts { private final Object2IntMap counts = new Object2IntOpenHashMap<>(MobCategory.values().length); + public final int[] arrCounts = new int[MobCategory.values().length]; // Leaves - store mob counts in an array - public void add(MobCategory spawnGroup) { -- this.counts.computeInt(spawnGroup, (group, density) -> density == null ? 1 : density + 1); + public void add(MobCategory category) { +- this.counts.computeInt(category, (key, value) -> value == null ? 1 : value + 1); + // Leaves start - store mob counts in an array + if (!org.leavesmc.leaves.LeavesConfig.performance.storeMobCountsInArray) { -+ this.counts.computeInt(spawnGroup, (group, density) -> density == null ? 1 : density + 1); ++ this.counts.computeInt(category, (group, density) -> density == null ? 1 : density + 1); + } else { -+ this.arrCounts[spawnGroup.ordinal()]++; ++ this.arrCounts[category.ordinal()]++; + } + // Leaves end - store mob counts in an array } - public boolean canSpawn(MobCategory spawnGroup) { -- return this.counts.getOrDefault(spawnGroup, 0) < spawnGroup.getMaxInstancesPerChunk(); + public boolean canSpawn(MobCategory category) { +- return this.counts.getOrDefault(category, 0) < category.getMaxInstancesPerChunk(); + // Leaves start - store mob counts in an array + if (!org.leavesmc.leaves.LeavesConfig.performance.storeMobCountsInArray) { -+ return this.counts.getOrDefault(spawnGroup, 0) < spawnGroup.getMaxInstancesPerChunk(); ++ return this.counts.getOrDefault(category, 0) < category.getMaxInstancesPerChunk(); + } else { -+ return this.arrCounts[spawnGroup.ordinal()] < spawnGroup.getMaxInstancesPerChunk(); ++ return this.arrCounts[category.ordinal()] < category.getMaxInstancesPerChunk(); + } + // Leaves end - store mob counts in an array } diff --git a/leaves-server/minecraft-patches/features/0060-Optimize-noise-generation.patch b/leaves-server/minecraft-patches/features/0060-Optimize-noise-generation.patch new file mode 100644 index 00000000..5ce740a3 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0060-Optimize-noise-generation.patch @@ -0,0 +1,281 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 18 Jul 2023 15:27:19 +0800 +Subject: [PATCH] Optimize noise generation + +This patch is Powered by Gale(https://github.com/GaleMC/Gale) + +diff --git a/net/minecraft/world/level/levelgen/synth/ImprovedNoise.java b/net/minecraft/world/level/levelgen/synth/ImprovedNoise.java +index fb11a2eea540d55e50eab59f9857ca5d99f556f8..47225646e98b5707af167cf8758f44c42c3cd38c 100644 +--- a/net/minecraft/world/level/levelgen/synth/ImprovedNoise.java ++++ b/net/minecraft/world/level/levelgen/synth/ImprovedNoise.java +@@ -11,6 +11,27 @@ public final class ImprovedNoise { + public final double yo; + public final double zo; + ++ // Leaves start - optimize noise generation ++ private static final double[] FLAT_SIMPLEX_GRAD = new double[]{ ++ 1, 1, 0, 0, ++ -1, 1, 0, 0, ++ 1, -1, 0, 0, ++ -1, -1, 0, 0, ++ 1, 0, 1, 0, ++ -1, 0, 1, 0, ++ 1, 0, -1, 0, ++ -1, 0, -1, 0, ++ 0, 1, 1, 0, ++ 0, -1, 1, 0, ++ 0, 1, -1, 0, ++ 0, -1, -1, 0, ++ 1, 1, 0, 0, ++ 0, -1, 1, 0, ++ -1, 1, 0, 0, ++ 0, -1, -1, 0, ++ }; ++ // Leaves end - optimize noise generation ++ + public ImprovedNoise(RandomSource random) { + this.xo = random.nextDouble() * 256.0; + this.yo = random.nextDouble() * 256.0; +@@ -41,9 +62,20 @@ public final class ImprovedNoise { + int floor = Mth.floor(d); + int floor1 = Mth.floor(d1); + int floor2 = Mth.floor(d2); +- double d3 = d - floor; +- double d4 = d1 - floor1; +- double d5 = d2 - floor2; ++ // Leaves start - optimize noise generation ++ double d3; ++ double d4; ++ double d5; ++ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { ++ d3 = d - (double)floor; ++ d4 = d1 - (double)floor1; ++ d5 = d2 - (double)floor2; ++ } else { ++ d3 = d - floor; ++ d4 = d1 - floor1; ++ d5 = d2 - floor2; ++ } ++ // Leaves end - optimize noise generation + double d7; + if (yScale != 0.0) { + double d6; +@@ -53,12 +85,24 @@ public final class ImprovedNoise { + d6 = d4; + } + +- d7 = Mth.floor(d6 / yScale + 1.0E-7F) * yScale; ++ // Leaves start - optimize noise generation ++ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { ++ d7 = (double)Mth.floor(d6 / yScale + 1.0E-7F) * yScale; ++ } else { ++ d7 = Math.floor(d6 / yScale + (double)1.0E-7F) * yScale; ++ } ++ // Leaves end - optimize noise generation + } else { + d7 = 0.0; + } + +- return this.sampleAndLerp(floor, floor1, floor2, d3, d4 - d7, d5, d4); ++ // Leaves start - optimize noise generation ++ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { ++ return this.sampleAndLerp(floor, floor1, floor2, d3, d4 - d7, d5, d4); ++ } else { ++ return this.sampleAndLerp((int) floor, (int) floor1, (int) floor2, d3, d4 - d7, d5, d4); ++ } ++ // Leaves end - optimize noise generation + } + + public double noiseWithDerivative(double x, double y, double z, double[] values) { +@@ -68,10 +112,19 @@ public final class ImprovedNoise { + int floor = Mth.floor(d); + int floor1 = Mth.floor(d1); + int floor2 = Mth.floor(d2); +- double d3 = d - floor; +- double d4 = d1 - floor1; +- double d5 = d2 - floor2; +- return this.sampleWithDerivative(floor, floor1, floor2, d3, d4, d5, values); ++ // Leaves start - optimize noise generation ++ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { ++ double d3 = d - (double)floor; ++ double d4 = d1 - (double)floor1; ++ double d5 = d2 - (double)floor2; ++ return this.sampleWithDerivative(floor, floor1, floor2, d3, d4, d5, values); ++ } else { ++ double d3 = d - floor; ++ double d4 = d1 - floor1; ++ double d5 = d2 - floor2; ++ return this.sampleWithDerivative((int) floor, (int) floor1, (int) floor2, d3, d4, d5, values); ++ } ++ // Leaves end - optimize noise generation + } + + private static double gradDot(int gradIndex, double xFactor, double yFactor, double zFactor) { +@@ -83,24 +136,90 @@ public final class ImprovedNoise { + } + + private double sampleAndLerp(int gridX, int gridY, int gridZ, double deltaX, double weirdDeltaY, double deltaZ, double deltaY) { +- int i = this.p(gridX); +- int i1 = this.p(gridX + 1); +- int i2 = this.p(i + gridY); +- int i3 = this.p(i + gridY + 1); +- int i4 = this.p(i1 + gridY); +- int i5 = this.p(i1 + gridY + 1); +- double d = gradDot(this.p(i2 + gridZ), deltaX, weirdDeltaY, deltaZ); +- double d1 = gradDot(this.p(i4 + gridZ), deltaX - 1.0, weirdDeltaY, deltaZ); +- double d2 = gradDot(this.p(i3 + gridZ), deltaX, weirdDeltaY - 1.0, deltaZ); +- double d3 = gradDot(this.p(i5 + gridZ), deltaX - 1.0, weirdDeltaY - 1.0, deltaZ); +- double d4 = gradDot(this.p(i2 + gridZ + 1), deltaX, weirdDeltaY, deltaZ - 1.0); +- double d5 = gradDot(this.p(i4 + gridZ + 1), deltaX - 1.0, weirdDeltaY, deltaZ - 1.0); +- double d6 = gradDot(this.p(i3 + gridZ + 1), deltaX, weirdDeltaY - 1.0, deltaZ - 1.0); +- double d7 = gradDot(this.p(i5 + gridZ + 1), deltaX - 1.0, weirdDeltaY - 1.0, deltaZ - 1.0); +- double d8 = Mth.smoothstep(deltaX); +- double d9 = Mth.smoothstep(deltaY); +- double d10 = Mth.smoothstep(deltaZ); +- return Mth.lerp3(d8, d9, d10, d, d1, d2, d3, d4, d5, d6, d7); ++ // Leaves start - optimize noise generation ++ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { ++ int i = this.p(gridX); ++ int i1 = this.p(gridX + 1); ++ int i2 = this.p(i + gridY); ++ int i3 = this.p(i + gridY + 1); ++ int i4 = this.p(i1 + gridY); ++ int i5 = this.p(i1 + gridY + 1); ++ double d = gradDot(this.p(i2 + gridZ), deltaX, weirdDeltaY, deltaZ); ++ double d1 = gradDot(this.p(i4 + gridZ), deltaX - 1.0, weirdDeltaY, deltaZ); ++ double d2 = gradDot(this.p(i3 + gridZ), deltaX, weirdDeltaY - 1.0, deltaZ); ++ double d3 = gradDot(this.p(i5 + gridZ), deltaX - 1.0, weirdDeltaY - 1.0, deltaZ); ++ double d4 = gradDot(this.p(i2 + gridZ + 1), deltaX, weirdDeltaY, deltaZ - 1.0); ++ double d5 = gradDot(this.p(i4 + gridZ + 1), deltaX - 1.0, weirdDeltaY, deltaZ - 1.0); ++ double d6 = gradDot(this.p(i3 + gridZ + 1), deltaX, weirdDeltaY - 1.0, deltaZ - 1.0); ++ double d7 = gradDot(this.p(i5 + gridZ + 1), deltaX - 1.0, weirdDeltaY - 1.0, deltaZ - 1.0); ++ double d8 = Mth.smoothstep(deltaX); ++ double d9 = Mth.smoothstep(deltaY); ++ double d10 = Mth.smoothstep(deltaZ); ++ return Mth.lerp3(d8, d9, d10, d, d1, d2, d3, d4, d5, d6, d7); ++ } else { ++ final int var0 = gridX & 0xFF; ++ final int var1 = (gridX + 1) & 0xFF; ++ final int var2 = this.p[var0] & 0xFF; ++ final int var3 = this.p[var1] & 0xFF; ++ final int var4 = (var2 + gridY) & 0xFF; ++ final int var5 = (var3 + gridY) & 0xFF; ++ final int var6 = (var2 + gridY + 1) & 0xFF; ++ final int var7 = (var3 + gridY + 1) & 0xFF; ++ final int var8 = this.p[var4] & 0xFF; ++ final int var9 = this.p[var5] & 0xFF; ++ final int var10 = this.p[var6] & 0xFF; ++ final int var11 = this.p[var7] & 0xFF; ++ ++ final int var12 = (var8 + gridZ) & 0xFF; ++ final int var13 = (var9 + gridZ) & 0xFF; ++ final int var14 = (var10 + gridZ) & 0xFF; ++ final int var15 = (var11 + gridZ) & 0xFF; ++ final int var16 = (var8 + gridZ + 1) & 0xFF; ++ final int var17 = (var9 + gridZ + 1) & 0xFF; ++ final int var18 = (var10 + gridZ + 1) & 0xFF; ++ final int var19 = (var11 + gridZ + 1) & 0xFF; ++ final int var20 = (this.p[var12] & 15) << 2; ++ final int var21 = (this.p[var13] & 15) << 2; ++ final int var22 = (this.p[var14] & 15) << 2; ++ final int var23 = (this.p[var15] & 15) << 2; ++ final int var24 = (this.p[var16] & 15) << 2; ++ final int var25 = (this.p[var17] & 15) << 2; ++ final int var26 = (this.p[var18] & 15) << 2; ++ final int var27 = (this.p[var19] & 15) << 2; ++ final double var60 = deltaX - 1.0; ++ final double var61 = deltaY - 1.0; ++ final double var62 = deltaZ - 1.0; ++ final double var87 = FLAT_SIMPLEX_GRAD[(var20) | 0] * deltaX + FLAT_SIMPLEX_GRAD[(var20) | 1] * weirdDeltaY + FLAT_SIMPLEX_GRAD[(var20) | 2] * deltaZ; ++ final double var88 = FLAT_SIMPLEX_GRAD[(var21) | 0] * var60 + FLAT_SIMPLEX_GRAD[(var21) | 1] * weirdDeltaY + FLAT_SIMPLEX_GRAD[(var21) | 2] * deltaZ; ++ final double var89 = FLAT_SIMPLEX_GRAD[(var22) | 0] * deltaX + FLAT_SIMPLEX_GRAD[(var22) | 1] * var61 + FLAT_SIMPLEX_GRAD[(var22) | 2] * deltaZ; ++ final double var90 = FLAT_SIMPLEX_GRAD[(var23) | 0] * var60 + FLAT_SIMPLEX_GRAD[(var23) | 1] * var61 + FLAT_SIMPLEX_GRAD[(var23) | 2] * deltaZ; ++ final double var91 = FLAT_SIMPLEX_GRAD[(var24) | 0] * deltaX + FLAT_SIMPLEX_GRAD[(var24) | 1] * weirdDeltaY + FLAT_SIMPLEX_GRAD[(var24) | 2] * var62; ++ final double var92 = FLAT_SIMPLEX_GRAD[(var25) | 0] * var60 + FLAT_SIMPLEX_GRAD[(var25) | 1] * weirdDeltaY + FLAT_SIMPLEX_GRAD[(var25) | 2] * var62; ++ final double var93 = FLAT_SIMPLEX_GRAD[(var26) | 0] * deltaX + FLAT_SIMPLEX_GRAD[(var26) | 1] * var61 + FLAT_SIMPLEX_GRAD[(var26) | 2] * var62; ++ final double var94 = FLAT_SIMPLEX_GRAD[(var27) | 0] * var60 + FLAT_SIMPLEX_GRAD[(var27) | 1] * var61 + FLAT_SIMPLEX_GRAD[(var27) | 2] * var62; ++ ++ final double var95 = deltaX * 6.0 - 15.0; ++ final double var96 = weirdDeltaY * 6.0 - 15.0; ++ final double var97 = deltaZ * 6.0 - 15.0; ++ final double var98 = deltaX * var95 + 10.0; ++ final double var99 = weirdDeltaY * var96 + 10.0; ++ final double var100 = deltaZ * var97 + 10.0; ++ final double var101 = deltaX * deltaX * deltaX * var98; ++ final double var102 = weirdDeltaY * weirdDeltaY * weirdDeltaY * var99; ++ final double var103 = deltaZ * deltaZ * deltaZ * var100; ++ ++ final double var113 = var87 + var101 * (var88 - var87); ++ final double var114 = var93 + var101 * (var94 - var93); ++ final double var115 = var91 + var101 * (var92 - var91); ++ final double var116 = var89 + var101 * (var90 - var89); ++ final double var117 = var114 - var115; ++ final double var118 = var102 * (var116 - var113); ++ final double var119 = var102 * var117; ++ final double var120 = var113 + var118; ++ final double var121 = var115 + var119; ++ return var120 + (var103 * (var121 - var120)); ++ } ++ // Leaves end - optimize noise generation + } + + private double sampleWithDerivative(int gridX, int gridY, int gridZ, double deltaX, double deltaY, double deltaZ, double[] noiseValues) { +diff --git a/net/minecraft/world/level/levelgen/synth/PerlinNoise.java b/net/minecraft/world/level/levelgen/synth/PerlinNoise.java +index da3c26fbad32d75d71f7e59c8c3341316a754756..82a2569139b088b30f9d8a13f50d556fefd78742 100644 +--- a/net/minecraft/world/level/levelgen/synth/PerlinNoise.java ++++ b/net/minecraft/world/level/levelgen/synth/PerlinNoise.java +@@ -26,6 +26,10 @@ public class PerlinNoise { + private final double lowestFreqValueFactor; + private final double lowestFreqInputFactor; + private final double maxValue; ++ // Leaves start - optimize noise generation ++ private final int octaveSamplersCount; ++ private final double [] amplitudesArray; ++ // Leaves end - optimize noise generation + + @Deprecated + public static PerlinNoise createLegacyForBlendedNoise(RandomSource random, IntStream octaves) { +@@ -127,6 +131,10 @@ public class PerlinNoise { + this.lowestFreqInputFactor = Math.pow(2.0, -i); + this.lowestFreqValueFactor = Math.pow(2.0, size - 1) / (Math.pow(2.0, size) - 1.0); + this.maxValue = this.edgeValue(2.0); ++ // Leaves start - optimize noise generation ++ this.octaveSamplersCount = this.noiseLevels.length; ++ this.amplitudesArray = this.amplitudes.toDoubleArray(); ++ // Leaves end - optimize noise generation + } + + protected double maxValue() { +@@ -138,7 +146,30 @@ public class PerlinNoise { + } + + public double getValue(double x, double y, double z) { +- return this.getValue(x, y, z, 0.0, 0.0, false); ++ // Leaves start - optimize noise generation ++ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { ++ return this.getValue(x, y, z, 0.0, 0.0, false); ++ } else { ++ double d = 0.0; ++ double e = this.lowestFreqInputFactor; ++ double f = this.lowestFreqValueFactor; ++ ++ for(int i = 0; i < this.octaveSamplersCount; ++i) { ++ ImprovedNoise perlinNoiseSampler = this.noiseLevels[i]; ++ if (perlinNoiseSampler != null) { ++ @SuppressWarnings("deprecation") ++ double g = perlinNoiseSampler.noise( ++ wrap(x * e), wrap(y * e), wrap(z * e), 0.0, 0.0 ++ ); ++ d += this.amplitudesArray[i] * g * f; ++ } ++ ++ e *= 2.0; ++ f /= 2.0; ++ } ++ return d; ++ } ++ // Leaves end - optimize noise generation + } + + @Deprecated diff --git a/leaves-server/minecraft-patches/features/0061-Reduce-array-allocations.patch b/leaves-server/minecraft-patches/features/0061-Reduce-array-allocations.patch new file mode 100644 index 00000000..3ed79193 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0061-Reduce-array-allocations.patch @@ -0,0 +1,281 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 20 Jul 2023 15:03:28 +0800 +Subject: [PATCH] Reduce array allocations + +This patch is Powered by Gale(https://github.com/GaleMC/Gale) + +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 d21ce54ebb5724c04eadf56a2cde701d5eeb5db2..fa4b8ea36acf031ceafc1812e657c2312eb49599 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java +@@ -29,6 +29,7 @@ import java.util.Arrays; + import java.util.Iterator; + import java.util.List; + import java.util.function.Predicate; ++import org.leavesmc.leaves.util.ArrayConstants; + + public final class ChunkEntitySlices { + +@@ -378,7 +379,7 @@ public final class ChunkEntitySlices { + + private static final class BasicEntityList { + +- private static final Entity[] EMPTY = new Entity[0]; ++ // protected static final Entity[] EMPTY = new Entity[0]; // Leaves - reduce array allocations + private static final int DEFAULT_CAPACITY = 4; + + private E[] storage; +@@ -389,7 +390,7 @@ public final class ChunkEntitySlices { + } + + public BasicEntityList(final int cap) { +- this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); ++ this.storage = (E[])(cap <= 0 ? ArrayConstants.emptyEntityArray : new Entity[cap]); // Leaves - reduce array allocations + } + + public boolean isEmpty() { +@@ -401,7 +402,7 @@ public final class ChunkEntitySlices { + } + + private void resize() { +- if (this.storage == EMPTY) { ++ if (this.storage == ArrayConstants.emptyEntityArray) { // Leaves - 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 6927124a4ea1f460158bf25679104b6f9e9ccee4..b0e8f47b9fe2b62d03c11a0b95253d7de7b8adac 100644 +--- a/net/minecraft/nbt/ByteArrayTag.java ++++ b/net/minecraft/nbt/ByteArrayTag.java +@@ -6,6 +6,7 @@ import java.io.IOException; + import java.util.Arrays; + import java.util.List; + import org.apache.commons.lang3.ArrayUtils; ++import org.leavesmc.leaves.util.ArrayConstants; + + public class ByteArrayTag extends CollectionTag { + private static final int SELF_SIZE_IN_BYTES = 24; +@@ -174,7 +175,7 @@ public class ByteArrayTag extends CollectionTag { + + @Override + public void clear() { +- this.data = new byte[0]; ++ this.data = ArrayConstants.emptyByteArray; // Leaves - reduce array allocations + } + + @Override +diff --git a/net/minecraft/nbt/CompoundTag.java b/net/minecraft/nbt/CompoundTag.java +index 361bc458e0bb590c43da60a1cd993a2785ee45e9..9d1e0ba4ecccfc8a251d440d63ed90796981808e 100644 +--- a/net/minecraft/nbt/CompoundTag.java ++++ b/net/minecraft/nbt/CompoundTag.java +@@ -18,6 +18,7 @@ import javax.annotation.Nullable; + import net.minecraft.CrashReport; + import net.minecraft.CrashReportCategory; + import net.minecraft.ReportedException; ++import org.leavesmc.leaves.util.ArrayConstants; + + public class CompoundTag implements Tag { + public static final Codec CODEC = Codec.PASSTHROUGH +@@ -409,7 +410,7 @@ public class CompoundTag implements Tag { + throw new ReportedException(this.createReport(key, ByteArrayTag.TYPE, var3)); + } + +- return new byte[0]; ++ return ArrayConstants.emptyByteArray; // Leaves - reduce array allocations + } + + public int[] getIntArray(String key) { +@@ -421,7 +422,7 @@ public class CompoundTag implements Tag { + throw new ReportedException(this.createReport(key, IntArrayTag.TYPE, var3)); + } + +- return new int[0]; ++ return ArrayConstants.emptyIntArray; // Leaves - reduce array allocations + } + + public long[] getLongArray(String key) { +@@ -433,7 +434,7 @@ public class CompoundTag implements Tag { + throw new ReportedException(this.createReport(key, LongArrayTag.TYPE, var3)); + } + +- return new long[0]; ++ return ArrayConstants.emptyLongArray; // Leaves - reduce array allocations + } + + public CompoundTag getCompound(String key) { +diff --git a/net/minecraft/nbt/IntArrayTag.java b/net/minecraft/nbt/IntArrayTag.java +index 7e27546bcb587d03b6de2ab43244e6c61fdb55f4..9e94f41e61687e2dacafec9c06646ec7f7980b62 100644 +--- a/net/minecraft/nbt/IntArrayTag.java ++++ b/net/minecraft/nbt/IntArrayTag.java +@@ -6,6 +6,7 @@ import java.io.IOException; + import java.util.Arrays; + import java.util.List; + import org.apache.commons.lang3.ArrayUtils; ++import org.leavesmc.leaves.util.ArrayConstants; + + public class IntArrayTag extends CollectionTag { + private static final int SELF_SIZE_IN_BYTES = 24; +@@ -181,7 +182,7 @@ public class IntArrayTag extends CollectionTag { + + @Override + public void clear() { +- this.data = new int[0]; ++ this.data = ArrayConstants.emptyIntArray; // Leaves - reduce array allocations + } + + @Override +diff --git a/net/minecraft/network/Connection.java b/net/minecraft/network/Connection.java +index f8a67220ebb72fd446006b7aaed87fc69bd0454d..f94b0148d90329e15801fbb893f02cdddbdb4779 100644 +--- a/net/minecraft/network/Connection.java ++++ b/net/minecraft/network/Connection.java +@@ -65,6 +65,7 @@ import org.apache.commons.lang3.Validate; + import org.slf4j.Logger; + import org.slf4j.Marker; + import org.slf4j.MarkerFactory; ++import org.leavesmc.leaves.util.ArrayConstants; + + public class Connection extends SimpleChannelInboundHandler> { + private static final float AVERAGE_PACKETS_SMOOTHING = 0.75F; +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index ed37ba4ca1303a0ce97e7a1fce1b29682276292b..7763d4c818606ed034f28e050166fe8cae16cfb8 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -169,6 +169,7 @@ import net.minecraft.world.phys.shapes.Shapes; + import net.minecraft.world.phys.shapes.VoxelShape; + import net.minecraft.world.ticks.LevelTicks; + import org.slf4j.Logger; ++import org.leavesmc.leaves.util.ArrayConstants; + + public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel { // Paper - rewrite chunk system // Paper - chunk tick iteration + public static final BlockPos END_SPAWN_POINT = new BlockPos(100, 50, 0); +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 24f2a1d295a02a8fdc1518f914549a980cd035c9..339be9915708171a5682af8193c09f96f7fba02c 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -245,6 +245,7 @@ import org.bukkit.inventory.CraftingInventory; + import org.bukkit.inventory.InventoryView; + import org.bukkit.inventory.SmithingInventory; + // CraftBukkit end ++import org.leavesmc.leaves.util.ArrayConstants; + + public class ServerGamePacketListenerImpl + extends ServerCommonPacketListenerImpl +@@ -801,7 +802,7 @@ public class ServerGamePacketListenerImpl + // Paper start + final int index; + if (packet.getCommand().length() > 64 && ((index = packet.getCommand().indexOf(' ')) == -1 || index >= 64)) { +- this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - add proper async disconnect ++ this.disconnectAsync(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - add proper async disconnect // Leaves - reduce array allocations + return; + } + // Paper end +diff --git a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +index 6689aeacf50d1498e8d23cce696fb4fecbd1cf39..66d74f720f60c85d3b0e0c04e45bbceedc4e314a 100644 +--- a/net/minecraft/server/network/ServerLoginPacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerLoginPacketListenerImpl.java +@@ -50,6 +50,7 @@ import org.bukkit.craftbukkit.entity.CraftPlayer; + import org.bukkit.craftbukkit.util.Waitable; + import org.bukkit.event.player.AsyncPlayerPreLoginEvent; + import org.bukkit.event.player.PlayerPreLoginEvent; ++import org.leavesmc.leaves.util.ArrayConstants; + + public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener, CraftPlayer.TransferCookieConnection { + // CraftBukkit end +diff --git a/net/minecraft/server/players/StoredUserList.java b/net/minecraft/server/players/StoredUserList.java +index d445e8f126f077d8419c52fa5436ea963a1a42a4..0c0f70b5c624e05811aef398a2e6c29c14a9e323 100644 +--- a/net/minecraft/server/players/StoredUserList.java ++++ b/net/minecraft/server/players/StoredUserList.java +@@ -21,6 +21,7 @@ import javax.annotation.Nullable; + import net.minecraft.Util; + import net.minecraft.util.GsonHelper; + import org.slf4j.Logger; ++import org.leavesmc.leaves.util.ArrayConstants; + + public abstract class StoredUserList> { + private static final Logger LOGGER = LogUtils.getLogger(); +@@ -70,7 +71,7 @@ public abstract class StoredUserList> { + } + + public String[] getUserList() { +- return this.map.keySet().toArray(new String[0]); ++ return (String[]) this.map.keySet().toArray(ArrayConstants.emptyStringArray); // Leaves - reduce array allocations + } + + public boolean isEmpty() { +diff --git a/net/minecraft/util/ZeroBitStorage.java b/net/minecraft/util/ZeroBitStorage.java +index 50993ce7519a77c6a9d36cb925125adccda7037f..e5124b566e791c1c011b301f910a892689cf3c65 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 = org.leavesmc.leaves.util.ArrayConstants.emptyLongArray; // Leaves - reduce array allocations + private final int size; + + public ZeroBitStorage(int size) { +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index e9ec19be9393ccd1e257247de86ba05e51471790..e10f4190393a934d617cd2c7afd05f3a4a6e89f9 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -102,6 +102,7 @@ import org.bukkit.craftbukkit.util.CraftSpawnCategory; + import org.bukkit.entity.SpawnCategory; + import org.bukkit.event.block.BlockPhysicsEvent; + // CraftBukkit end ++import org.leavesmc.leaves.util.ArrayConstants; + + public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel, ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system // Paper - optimise collisions + public static final Codec> RESOURCE_KEY_CODEC = ResourceKey.codec(Registries.DIMENSION); +diff --git a/net/minecraft/world/level/block/ComposterBlock.java b/net/minecraft/world/level/block/ComposterBlock.java +index 86f9c284f434a16888beb60b89f460de2c0450a8..e3177018adab3d87eb4af8ac158def96908b041a 100644 +--- a/net/minecraft/world/level/block/ComposterBlock.java ++++ b/net/minecraft/world/level/block/ComposterBlock.java +@@ -410,7 +410,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + + @Override + public int[] getSlotsForFace(Direction side) { +- return new int[0]; ++ return org.leavesmc.leaves.util.ArrayConstants.emptyIntArray; // Leaves - reduce array allocations + } + + @Override +@@ -445,7 +445,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + + @Override + public int[] getSlotsForFace(Direction side) { +- return side == Direction.UP ? new int[]{0} : new int[0]; ++ return side == Direction.UP ? org.leavesmc.leaves.util.ArrayConstants.zeroSingletonIntArray : org.leavesmc.leaves.util.ArrayConstants.emptyIntArray; // Leaves - reduce array allocations + } + + @Override +@@ -496,7 +496,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { + + @Override + public int[] getSlotsForFace(Direction side) { +- return side == Direction.DOWN ? new int[]{0} : new int[0]; ++ return side == Direction.DOWN ? org.leavesmc.leaves.util.ArrayConstants.zeroSingletonIntArray : org.leavesmc.leaves.util.ArrayConstants.emptyIntArray; // Leaves - 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 2f5fa4310f475ecbb29e69c0461c7d3276f8536d..304940047eee6fab5b763ce13fdd5d7e6f8e70f1 100644 +--- a/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java +@@ -38,13 +38,14 @@ import net.minecraft.world.level.block.AbstractFurnaceBlock; + import net.minecraft.world.level.block.Blocks; + import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.phys.Vec3; ++import org.leavesmc.leaves.util.ArrayConstants; + + public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, RecipeCraftingHolder, StackedContentsCompatible { + protected static final int SLOT_INPUT = 0; + 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 = ArrayConstants.zeroSingletonIntArray; // Leaves - 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/patches/server/0074-Optimize-sun-burn-tick.patch b/leaves-server/minecraft-patches/features/0062-Optimize-sun-burn-tick.patch similarity index 68% rename from patches/server/0074-Optimize-sun-burn-tick.patch rename to leaves-server/minecraft-patches/features/0062-Optimize-sun-burn-tick.patch index d1819053..ec753283 100644 --- a/patches/server/0074-Optimize-sun-burn-tick.patch +++ b/leaves-server/minecraft-patches/features/0062-Optimize-sun-burn-tick.patch @@ -5,15 +5,17 @@ Subject: [PATCH] Optimize sun burn tick This patch is Powered by Gale(https://github.com/GaleMC/Gale) -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 13956aa8210d0234176a9d7450afdf42a2311f11..6a19f5725387b7178720ba209ebc2a5c46241bf5 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -2166,8 +2166,22 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - /** @deprecated */ +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 92f363180ed4d2a91da996de9e8adfa4f3b17e69..cbb3062d1bfd9d9695e0d72df3f91ad084e043c2 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -2039,10 +2039,22 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + @Deprecated public float getLightLevelDependentMagicValue() { -- return this.level().hasChunkAt(this.getBlockX(), this.getBlockZ()) ? this.level().getLightLevelDependentMagicValue(BlockPos.containing(this.getX(), this.getEyeY(), this.getZ())) : 0.0F; +- return this.level().hasChunkAt(this.getBlockX(), this.getBlockZ()) +- ? this.level().getLightLevelDependentMagicValue(BlockPos.containing(this.getX(), this.getEyeY(), this.getZ())) +- : 0.0F; + // Leaves start - optimize sun burn tick + if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeSunBurnTick) { + return this.level().hasChunkAt(this.getBlockX(), this.getBlockZ()) ? this.level().getLightLevelDependentMagicValue(BlockPos.containing(this.getX(), this.getEyeY(), this.getZ())) : 0.0F; @@ -31,15 +33,15 @@ index 13956aa8210d0234176a9d7450afdf42a2311f11..6a19f5725387b7178720ba209ebc2a5c } + // Leaves end - optimize sun burn tick - public void absMoveTo(double x, double y, double z, float yaw, float pitch) { + public void absMoveTo(double x, double y, double z, float yRot, float xRot) { this.absMoveTo(x, y, z); -diff --git a/src/main/java/net/minecraft/world/entity/Mob.java b/src/main/java/net/minecraft/world/entity/Mob.java -index c80976958ad8f51aadf0dab5d49a3e11c77010d4..b3775c9c5986f460b0b65ce49a5e2ff064349394 100644 ---- a/src/main/java/net/minecraft/world/entity/Mob.java -+++ b/src/main/java/net/minecraft/world/entity/Mob.java -@@ -1742,15 +1742,41 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab - - protected void playAttackSound() {} +diff --git a/net/minecraft/world/entity/Mob.java b/net/minecraft/world/entity/Mob.java +index b694ea0a5fee852c31ff503a42e50a999c5a287b..6810037ab23e79986ef0f5d959bc317142df67cd 100644 +--- a/net/minecraft/world/entity/Mob.java ++++ b/net/minecraft/world/entity/Mob.java +@@ -1627,20 +1627,40 @@ public abstract class Mob extends LivingEntity implements EquipmentUser, Leashab + protected void playAttackSound() { + } + // Leaves start - optimize sun burn tick + private BlockPos cached_eye_blockpos; @@ -48,9 +50,14 @@ index c80976958ad8f51aadf0dab5d49a3e11c77010d4..b3775c9c5986f460b0b65ce49a5e2ff0 + public boolean isSunBurnTick() { if (this.level().isDay() && !this.level().isClientSide) { -- float f = this.getLightLevelDependentMagicValue(); -- BlockPos blockposition = BlockPos.containing(this.getX(), this.getEyeY(), this.getZ()); +- float lightLevelDependentMagicValue = this.getLightLevelDependentMagicValue(); +- BlockPos blockPos = BlockPos.containing(this.getX(), this.getEyeY(), this.getZ()); - boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; +- if (lightLevelDependentMagicValue > 0.5F +- && this.random.nextFloat() * 30.0F < (lightLevelDependentMagicValue - 0.4F) * 2.0F +- && !flag +- && this.level().canSeeSky(blockPos)) { +- return true; + // Leaves start - optimize sun burn tick + if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeSunBurnTick) { + float f = this.getLightLevelDependentMagicValue(); @@ -73,14 +80,12 @@ index c80976958ad8f51aadf0dab5d49a3e11c77010d4..b3775c9c5986f460b0b65ce49a5e2ff0 + if (f <= 0.5F) return false; + if (this.random.nextFloat() * 30.0F >= (f - 0.4F) * 2.0F) return false; + boolean flag = this.isInWaterRainOrBubble() || this.isInPowderSnow || this.wasInPowderSnow; - -- if (f > 0.5F && this.random.nextFloat() * 30.0F < (f - 0.4F) * 2.0F && !flag && this.level().canSeeSky(blockposition)) { -- return true; -+ if (!flag && this.level().canSeeSky(this.cached_eye_blockpos)) { -+ return true; -+ } ++ return !flag && this.level().canSeeSky(this.cached_eye_blockpos); } -+ // Leaves end - optimize sun burn tick } - +- return false; ++ // Leaves end - optimize sun burn tick + } + + @Override diff --git a/patches/server/0075-Reduce-lambda-and-Optional-allocation-in-EntityBased.patch b/leaves-server/minecraft-patches/features/0063-Reduce-lambda-and-Optional-allocation-in-EntityBased.patch similarity index 52% rename from patches/server/0075-Reduce-lambda-and-Optional-allocation-in-EntityBased.patch rename to leaves-server/minecraft-patches/features/0063-Reduce-lambda-and-Optional-allocation-in-EntityBased.patch index f24df5e3..ec7bd139 100644 --- a/patches/server/0075-Reduce-lambda-and-Optional-allocation-in-EntityBased.patch +++ b/leaves-server/minecraft-patches/features/0063-Reduce-lambda-and-Optional-allocation-in-EntityBased.patch @@ -6,24 +6,24 @@ Subject: [PATCH] Reduce lambda and Optional allocation in This patch is Powered by Gale(https://github.com/GaleMC/Gale) -diff --git a/src/main/java/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java b/src/main/java/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java -index 3527f1621ef9b4f3f8d8bbb93379f13ff141c3be..f6e51bce07849553c907c81ad141105f36f393cb 100644 ---- a/src/main/java/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java -+++ b/src/main/java/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java +diff --git a/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java b/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java +index 52cb2dcd714cf8c16f167466333eeb923e4ff183..8f2fbfb8d0a24d53697442addf940fa18ae4c69b 100644 +--- a/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java ++++ b/net/minecraft/world/level/EntityBasedExplosionDamageCalculator.java @@ -15,8 +15,20 @@ public class EntityBasedExplosionDamageCalculator extends ExplosionDamageCalcula @Override - public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter world, BlockPos pos, BlockState blockState, FluidState fluidState) { -- return super.getBlockExplosionResistance(explosion, world, pos, blockState, fluidState) -- .map(max -> this.source.getBlockExplosionResistance(explosion, world, pos, blockState, fluidState, max)); + public Optional getBlockExplosionResistance(Explosion explosion, BlockGetter reader, BlockPos pos, BlockState state, FluidState fluid) { +- return super.getBlockExplosionResistance(explosion, reader, pos, state, fluid) +- .map(resistance -> this.source.getBlockExplosionResistance(explosion, reader, pos, state, fluid, resistance)); + if (!org.leavesmc.leaves.LeavesConfig.performance.remove.damageLambda) { -+ return super.getBlockExplosionResistance(explosion, world, pos, blockState, fluidState) -+ .map(max -> this.source.getBlockExplosionResistance(explosion, world, pos, blockState, fluidState, max)); ++ return super.getBlockExplosionResistance(explosion, reader, pos, state, fluid) ++ .map(max -> this.source.getBlockExplosionResistance(explosion, reader, pos, state, fluid, max)); + } else { -+ Optional optionalBlastResistance = super.getBlockExplosionResistance(explosion, world, pos, blockState, fluidState); ++ Optional optionalBlastResistance = super.getBlockExplosionResistance(explosion, reader, pos, state, fluid); + if (optionalBlastResistance.isPresent()) { + float blastResistance = optionalBlastResistance.get(); -+ float effectiveExplosionResistance = this.source.getBlockExplosionResistance(explosion, world, pos, blockState, fluidState, blastResistance); ++ float effectiveExplosionResistance = this.source.getBlockExplosionResistance(explosion, reader, pos, state, fluid, blastResistance); + if (effectiveExplosionResistance != blastResistance) { + return Optional.of(effectiveExplosionResistance); + } diff --git a/patches/server/0076-Avoid-Class-isAssignableFrom-call-in-ClassInstanceMu.patch b/leaves-server/minecraft-patches/features/0064-Avoid-Class-isAssignableFrom-call-in-ClassInstanceMu.patch similarity index 75% rename from patches/server/0076-Avoid-Class-isAssignableFrom-call-in-ClassInstanceMu.patch rename to leaves-server/minecraft-patches/features/0064-Avoid-Class-isAssignableFrom-call-in-ClassInstanceMu.patch index 70a4d4fb..d81c4912 100644 --- a/patches/server/0076-Avoid-Class-isAssignableFrom-call-in-ClassInstanceMu.patch +++ b/leaves-server/minecraft-patches/features/0064-Avoid-Class-isAssignableFrom-call-in-ClassInstanceMu.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Avoid Class#isAssignableFrom call in ClassInstanceMultiMap This patch is Powered by Gale(https://github.com/GaleMC/Gale) -diff --git a/src/main/java/net/minecraft/util/ClassInstanceMultiMap.java b/src/main/java/net/minecraft/util/ClassInstanceMultiMap.java -index 038710ba934a9a57815dfe9f414b98223b848385..fd2176449a9ed8921032d6265c95dd5e7f5ae325 100644 ---- a/src/main/java/net/minecraft/util/ClassInstanceMultiMap.java -+++ b/src/main/java/net/minecraft/util/ClassInstanceMultiMap.java +diff --git a/net/minecraft/util/ClassInstanceMultiMap.java b/net/minecraft/util/ClassInstanceMultiMap.java +index 2a708ae0d5bb209650b525e3c56051f8b5655074..7f4b29fad0d70f0480b8b13d7a7d8d7a7f90b491 100644 +--- a/net/minecraft/util/ClassInstanceMultiMap.java ++++ b/net/minecraft/util/ClassInstanceMultiMap.java @@ -56,13 +56,24 @@ public class ClassInstanceMultiMap extends AbstractCollection { } @@ -17,7 +17,7 @@ index 038710ba934a9a57815dfe9f414b98223b848385..fd2176449a9ed8921032d6265c95dd5e - throw new IllegalArgumentException("Don't know how to search for " + type); - } else { - List list = this.byClass -- .computeIfAbsent(type, typeClass -> this.allInstances.stream().filter(typeClass::isInstance).collect(Util.toMutableList())); +- .computeIfAbsent(type, clazz -> this.allInstances.stream().filter(clazz::isInstance).collect(Util.toMutableList())); - return (Collection)Collections.unmodifiableCollection(list); + // Leaves start - avoid Class#isAssignableFrom call in ClassInstanceMultiMap + Collection collection = this.byClass.get(type); diff --git a/patches/server/0077-Optimized-CubePointRange.patch b/leaves-server/minecraft-patches/features/0065-Optimized-CubePointRange.patch similarity index 57% rename from patches/server/0077-Optimized-CubePointRange.patch rename to leaves-server/minecraft-patches/features/0065-Optimized-CubePointRange.patch index ea0a5d5b..270362b4 100644 --- a/patches/server/0077-Optimized-CubePointRange.patch +++ b/leaves-server/minecraft-patches/features/0065-Optimized-CubePointRange.patch @@ -5,11 +5,11 @@ Subject: [PATCH] Optimized CubePointRange This patch is Powered by Gale(https://github.com/GaleMC/Gale) -diff --git a/src/main/java/net/minecraft/world/phys/shapes/CubePointRange.java b/src/main/java/net/minecraft/world/phys/shapes/CubePointRange.java -index ad02cdb00360165f6405eb3044bd8320f01a7ef1..d628e696c3ddd10cc2a347a8dd720d9379cf894c 100644 ---- a/src/main/java/net/minecraft/world/phys/shapes/CubePointRange.java -+++ b/src/main/java/net/minecraft/world/phys/shapes/CubePointRange.java -@@ -3,21 +3,31 @@ package net.minecraft.world.phys.shapes; +diff --git a/net/minecraft/world/phys/shapes/CubePointRange.java b/net/minecraft/world/phys/shapes/CubePointRange.java +index 62aea61c5f240aa59b8489dd0bbb3d1b69b54b1c..b8e06a2d7f3a42a1cec1c87b950242acda54b214 100644 +--- a/net/minecraft/world/phys/shapes/CubePointRange.java ++++ b/net/minecraft/world/phys/shapes/CubePointRange.java +@@ -3,23 +3,33 @@ package net.minecraft.world.phys.shapes; import it.unimi.dsi.fastutil.doubles.AbstractDoubleList; public class CubePointRange extends AbstractDoubleList { @@ -17,27 +17,29 @@ index ad02cdb00360165f6405eb3044bd8320f01a7ef1..d628e696c3ddd10cc2a347a8dd720d93 private final int parts; + private final double scale; // Leaves - replace division by multiplication in CubePointRange - public CubePointRange(int sectionCount) { - if (sectionCount <= 0) { + public CubePointRange(int parts) { + if (parts <= 0) { throw new IllegalArgumentException("Need at least 1 part"); } else { - this.parts = sectionCount; -+ this.size = sectionCount + 1; + this.parts = parts; ++ this.size = parts + 1; } -+ this.scale = 1.0D / sectionCount; // Leaves - replace division by multiplication in CubePointRange ++ this.scale = 1.0D / parts; // Leaves - replace division by multiplication in CubePointRange } - public double getDouble(int i) { -- return (double)i / (double)this.parts; + @Override + public double getDouble(int value) { +- return (double)value / this.parts; + // Leaves start - replace division by multiplication in CubePointRange + if (!org.leavesmc.leaves.LeavesConfig.performance.optimizedCubePointRange) { -+ return (double)i / (double)this.parts; ++ return (double)value / (double)this.parts; + } else { -+ return i * this.scale; ++ return value * this.scale; + } + // Leaves start - replace division by multiplication in CubePointRange } + @Override public int size() { - return this.parts + 1; + return !org.leavesmc.leaves.LeavesConfig.performance.optimizedCubePointRange ? this.parts + 1 : size; // Leaves - replace parts by size in CubePointRange diff --git a/leaves-server/minecraft-patches/features/0066-Check-frozen-ticks-before-landing-block.patch b/leaves-server/minecraft-patches/features/0066-Check-frozen-ticks-before-landing-block.patch new file mode 100644 index 00000000..ec5e7b9a --- /dev/null +++ b/leaves-server/minecraft-patches/features/0066-Check-frozen-ticks-before-landing-block.patch @@ -0,0 +1,24 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 20 Jul 2023 20:33:52 +0800 +Subject: [PATCH] Check frozen ticks before landing block + +This patch is Powered by Gale(https://github.com/GaleMC/Gale) + +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index 5e7068bf9b5364c382fccfb110ed2a26a5ac1731..9b90d9f9a9317cdec1e2684e68d5ec03a143addc 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -572,10 +572,10 @@ public abstract class LivingEntity extends Entity implements Attackable { + } + + protected void tryAddFrost() { +- if (!this.getBlockStateOnLegacy().isAir()) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.checkFrozenTicksBeforeLandingBlock || !this.getBlockStateOnLegacy().isAir()) { // Leaves - check frozen ticks before landing block + int ticksFrozen = this.getTicksFrozen(); + if (ticksFrozen > 0) { +- AttributeInstance attribute = this.getAttribute(Attributes.MOVEMENT_SPEED); ++ AttributeInstance attribute = !org.leavesmc.leaves.LeavesConfig.performance.checkFrozenTicksBeforeLandingBlock || !this.getBlockStateOnLegacy().isAir() ? this.getAttribute(Attributes.MOVEMENT_SPEED) : null; // Leaves - check frozen ticks before landing block + if (attribute == null) { + return; + } diff --git a/patches/server/0079-Skip-entity-move-if-movement-is-zero.patch b/leaves-server/minecraft-patches/features/0067-Skip-entity-move-if-movement-is-zero.patch similarity index 63% rename from patches/server/0079-Skip-entity-move-if-movement-is-zero.patch rename to leaves-server/minecraft-patches/features/0067-Skip-entity-move-if-movement-is-zero.patch index bc5b009e..4b741183 100644 --- a/patches/server/0079-Skip-entity-move-if-movement-is-zero.patch +++ b/leaves-server/minecraft-patches/features/0067-Skip-entity-move-if-movement-is-zero.patch @@ -5,19 +5,19 @@ Subject: [PATCH] Skip entity move if movement is zero This patch is Powered by Gale(https://github.com/GaleMC/Gale) -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 6a19f5725387b7178720ba209ebc2a5c46241bf5..bab140f5489ab13bb67a3372cb1429e5ead1d93d 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -282,6 +282,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index cbb3062d1bfd9d9695e0d72df3f91ad084e043c2..12f37226992986fb633c36e651fc098e46937646 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -235,6 +235,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess public float yRotO; public float xRotO; - private AABB bb; + private AABB bb = INITIAL_AABB; + private boolean boundingBoxChanged = false; // Leaves - skip entity move if movement is zero public boolean onGround; public boolean horizontalCollision; public boolean verticalCollision; -@@ -1163,6 +1164,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -1085,6 +1086,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess // Paper end - detailed watchdog information public void move(MoverType type, Vec3 movement) { @@ -31,11 +31,11 @@ index 6a19f5725387b7178720ba209ebc2a5c46241bf5..bab140f5489ab13bb67a3372cb1429e5 final Vec3 originalMovement = movement; // Paper - Expose pre-collision velocity // Paper start - detailed watchdog information ca.spottedleaf.moonrise.common.util.TickThread.ensureTickThread("Cannot move an entity off-main"); -@@ -4421,6 +4429,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess +@@ -4208,6 +4216,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess } - public final void setBoundingBox(AABB boundingBox) { -+ if (!this.bb.equals(boundingBox)) this.boundingBoxChanged = true; // Leaves - skip entity move if movement is zero + public final void setBoundingBox(AABB bb) { ++ if (!this.bb.equals(bb)) this.boundingBoxChanged = true; // Leaves - skip entity move if movement is zero // CraftBukkit start - block invalid bounding boxes - double minX = boundingBox.minX, - minY = boundingBox.minY, + double minX = bb.minX, + minY = bb.minY, diff --git a/patches/server/0080-Skip-cloning-advancement-criteria.patch b/leaves-server/minecraft-patches/features/0068-Skip-cloning-advancement-criteria.patch similarity index 72% rename from patches/server/0080-Skip-cloning-advancement-criteria.patch rename to leaves-server/minecraft-patches/features/0068-Skip-cloning-advancement-criteria.patch index 90e6ffc5..dd901885 100644 --- a/patches/server/0080-Skip-cloning-advancement-criteria.patch +++ b/leaves-server/minecraft-patches/features/0068-Skip-cloning-advancement-criteria.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Skip cloning advancement criteria This patch is Powered by Gale(https://github.com/GaleMC/Gale) -diff --git a/src/main/java/net/minecraft/advancements/Advancement.java b/src/main/java/net/minecraft/advancements/Advancement.java -index fb4b7652c386fa10783d71899615723a958ffd56..dad1164397b112c687bcc13bd44e546e58d9d970 100644 ---- a/src/main/java/net/minecraft/advancements/Advancement.java -+++ b/src/main/java/net/minecraft/advancements/Advancement.java +diff --git a/net/minecraft/advancements/Advancement.java b/net/minecraft/advancements/Advancement.java +index d5c824eabf321a2c7600c7081b8d4f3057cfae2e..32a671ffc4c1970c0af918281b031344b07a4029 100644 +--- a/net/minecraft/advancements/Advancement.java ++++ b/net/minecraft/advancements/Advancement.java @@ -60,7 +60,7 @@ public record Advancement( AdvancementRequirements requirements, boolean sendsTelemetryEvent diff --git a/leaves-server/minecraft-patches/features/0069-Fix-villagers-dont-release-memory.patch b/leaves-server/minecraft-patches/features/0069-Fix-villagers-dont-release-memory.patch new file mode 100644 index 00000000..d550b4ef --- /dev/null +++ b/leaves-server/minecraft-patches/features/0069-Fix-villagers-dont-release-memory.patch @@ -0,0 +1,43 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sat, 22 Jul 2023 12:00:59 +0800 +Subject: [PATCH] Fix villagers dont release memory + + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 12f37226992986fb633c36e651fc098e46937646..ab63de942e3a34a84666877a202c4a191391a28b 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -3837,7 +3837,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + return this; + } + +- private Entity teleportCrossDimension(ServerLevel level, TeleportTransition teleportTransition) { ++ protected Entity teleportCrossDimension(ServerLevel level, TeleportTransition teleportTransition) { // Leaves - private -> protected + List passengers = this.getPassengers(); + List list = new ArrayList<>(passengers.size()); + this.ejectPassengers(); +diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java +index 2b83262e4a13eae86df82913ce4f3121e3631a43..6b565fcf91e1d94b649dac90bf3c923930d252f8 100644 +--- a/net/minecraft/world/entity/npc/Villager.java ++++ b/net/minecraft/world/entity/npc/Villager.java +@@ -1017,4 +1017,19 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + Optional memory = this.brain.getMemory(MemoryModuleType.LAST_SLEPT); + return memory.filter(_long -> gameTime - _long < 24000L).isPresent(); + } ++ ++ // Leaves start - fixes a memory leak when villagers get moved to another world ++ @Override ++ public Entity teleportCrossDimension(ServerLevel world, net.minecraft.world.level.portal.TeleportTransition transition) { ++ if (org.leavesmc.leaves.LeavesConfig.performance.villagersDontReleaseMemoryFix) { ++ this.releaseAllPois(); ++ this.getBrain().eraseMemory(MemoryModuleType.HOME); ++ this.getBrain().eraseMemory(MemoryModuleType.JOB_SITE); ++ this.getBrain().eraseMemory(MemoryModuleType.POTENTIAL_JOB_SITE); ++ this.getBrain().eraseMemory(MemoryModuleType.MEETING_POINT); ++ this.refreshBrain(transition.newLevel()); ++ } ++ return super.teleportCrossDimension(world, transition); ++ } ++ // Leaves end - fixes a memory leak when villagers get moved to another world + } diff --git a/patches/server/0082-Avoid-anvil-too-expensive.patch b/leaves-server/minecraft-patches/features/0070-Avoid-anvil-too-expensive.patch similarity index 59% rename from patches/server/0082-Avoid-anvil-too-expensive.patch rename to leaves-server/minecraft-patches/features/0070-Avoid-anvil-too-expensive.patch index 78ab8064..f8ef1e18 100644 --- a/patches/server/0082-Avoid-anvil-too-expensive.patch +++ b/leaves-server/minecraft-patches/features/0070-Avoid-anvil-too-expensive.patch @@ -4,16 +4,16 @@ Date: Sat, 22 Jul 2023 14:16:25 +0800 Subject: [PATCH] Avoid anvil too expensive -diff --git a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java -index 286ae002e1711ad9e800b7f2091988d66cd572a7..d27f1b159984032f7cbbd8ed59ea7ab34bdfc79d 100644 ---- a/src/main/java/net/minecraft/world/inventory/AnvilMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/AnvilMenu.java -@@ -287,7 +287,7 @@ public class AnvilMenu extends ItemCombinerMenu { +diff --git a/net/minecraft/world/inventory/AnvilMenu.java b/net/minecraft/world/inventory/AnvilMenu.java +index aaa022ac3656d68bad8dbd4c80a90b62fb6f9a16..e9ac7966fc49de16a444d0e991a4b736cfdb0a0d 100644 +--- a/net/minecraft/world/inventory/AnvilMenu.java ++++ b/net/minecraft/world/inventory/AnvilMenu.java +@@ -260,7 +260,7 @@ public class AnvilMenu extends ItemCombinerMenu { this.onlyRenaming = true; } - if (this.cost.get() >= this.maximumRepairCost && !this.player.getAbilities().instabuild) { // CraftBukkit + if (this.cost.get() >= this.maximumRepairCost && (!org.leavesmc.leaves.LeavesConfig.modify.avoidAnvilTooExpensive || this.cost.get() == DEFAULT_DENIED_COST) && !this.player.getAbilities().instabuild) { // CraftBukkit // Leaves - avoid anvil too expensive - itemstack1 = ItemStack.EMPTY; + itemStack = ItemStack.EMPTY; } diff --git a/patches/server/0083-Bow-infinity-fix.patch b/leaves-server/minecraft-patches/features/0071-Bow-infinity-fix.patch similarity index 52% rename from patches/server/0083-Bow-infinity-fix.patch rename to leaves-server/minecraft-patches/features/0071-Bow-infinity-fix.patch index 11c29b17..c15e40cb 100644 --- a/patches/server/0083-Bow-infinity-fix.patch +++ b/leaves-server/minecraft-patches/features/0071-Bow-infinity-fix.patch @@ -4,16 +4,16 @@ Date: Mon, 24 Jul 2023 15:54:18 +0800 Subject: [PATCH] Bow infinity fix -diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index b36d67774cf34cf95bcfbaa2fc8cb4f56b1e7557..fe9500f96709b9e15f63218de1b8a3f87007087c 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Player.java -+++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -2288,7 +2288,7 @@ public abstract class Player extends LivingEntity { +diff --git a/net/minecraft/world/entity/player/Player.java b/net/minecraft/world/entity/player/Player.java +index 94dab91807c828131d728374ae3c485d0f4272be..8736461dc3981419c63e0d738cc308d5913fa2c3 100644 +--- a/net/minecraft/world/entity/player/Player.java ++++ b/net/minecraft/world/entity/player/Player.java +@@ -2270,7 +2270,7 @@ public abstract class Player extends LivingEntity { } } - return this.abilities.instabuild ? new ItemStack(Items.ARROW) : ItemStack.EMPTY; -+ return this.abilities.instabuild || (org.leavesmc.leaves.LeavesConfig.modify.bowInfinityFix && net.minecraft.world.item.enchantment.EnchantmentHelper.processAmmoUse((ServerLevel) this.level(), stack, new ItemStack(Items.ARROW), 1) <= 0) ? new ItemStack(Items.ARROW) : ItemStack.EMPTY; ++ return this.abilities.instabuild || (org.leavesmc.leaves.LeavesConfig.modify.bowInfinityFix && net.minecraft.world.item.enchantment.EnchantmentHelper.processAmmoUse((ServerLevel) this.level(), shootable, new ItemStack(Items.ARROW), 1) <= 0) ? new ItemStack(Items.ARROW) : ItemStack.EMPTY; // Leaves - infinity fix } } } diff --git a/leaves-server/minecraft-patches/features/0072-Zero-tick-plants.patch b/leaves-server/minecraft-patches/features/0072-Zero-tick-plants.patch new file mode 100644 index 00000000..1b95bd55 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0072-Zero-tick-plants.patch @@ -0,0 +1,85 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 30 Jul 2023 12:20:16 +0800 +Subject: [PATCH] Zero tick plants + + +diff --git a/net/minecraft/world/level/block/BambooStalkBlock.java b/net/minecraft/world/level/block/BambooStalkBlock.java +index d824bb7d86fd9063ec8185f6bc74dc7450c2b73a..7598b403ed10559d271d302a7759f99e41d96dd5 100644 +--- a/net/minecraft/world/level/block/BambooStalkBlock.java ++++ b/net/minecraft/world/level/block/BambooStalkBlock.java +@@ -119,6 +119,10 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { + protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + if (!state.canSurvive(level, pos)) { + level.destroyBlock(pos, true); ++ // Leaves start - zero tick plants ++ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { ++ this.randomTick(state, level, pos, random); ++ // Leaves end - zero tick plants + } + } + +diff --git a/net/minecraft/world/level/block/CactusBlock.java b/net/minecraft/world/level/block/CactusBlock.java +index 913779c68b178dbbfac3b1dcee0c6342028e5570..590525a5de0119b8a9652a00ec99b10d6cae2c46 100644 +--- a/net/minecraft/world/level/block/CactusBlock.java ++++ b/net/minecraft/world/level/block/CactusBlock.java +@@ -43,6 +43,10 @@ public class CactusBlock extends Block { + protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + if (!state.canSurvive(level, pos)) { + level.destroyBlock(pos, true); ++ // Leaves start - zero tick plants ++ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { ++ this.randomTick(state, level, pos, random); ++ // Leaves end - zero tick plants + } + } + +diff --git a/net/minecraft/world/level/block/ChorusFlowerBlock.java b/net/minecraft/world/level/block/ChorusFlowerBlock.java +index 3af8f150474a8a461c5829850172196b7b9e7bfd..04e392cabf3b5c171d86cb47b29b34d05222d4a4 100644 +--- a/net/minecraft/world/level/block/ChorusFlowerBlock.java ++++ b/net/minecraft/world/level/block/ChorusFlowerBlock.java +@@ -49,6 +49,9 @@ public class ChorusFlowerBlock extends Block { + protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + if (!state.canSurvive(level, pos)) { + level.destroyBlock(pos, true); ++ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { ++ this.randomTick(state, level, pos, random); ++ // Leaves end - zero tick plants + } + } + +diff --git a/net/minecraft/world/level/block/GrowingPlantHeadBlock.java b/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +index 0994f7265322d1f33365a1df0faaffd9df05fcc0..90577291e4a2846a89c59bd5297a99e910378276 100644 +--- a/net/minecraft/world/level/block/GrowingPlantHeadBlock.java ++++ b/net/minecraft/world/level/block/GrowingPlantHeadBlock.java +@@ -148,4 +148,15 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements + protected GrowingPlantHeadBlock getHeadBlock() { + return this; + } ++ ++ // Leaves start - zero tick plants ++ @Override ++ public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { ++ if (!state.canSurvive(world, pos)) { ++ world.destroyBlock(pos, true); ++ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { ++ this.randomTick(state, world, pos, random); ++ } ++ } ++ // Leaves end - zero tick plants + } +diff --git a/net/minecraft/world/level/block/SugarCaneBlock.java b/net/minecraft/world/level/block/SugarCaneBlock.java +index 63d53f9090caca304c7f8c3f9910c57a6bdbb4d5..1d1e699926c2e244937b34d9f54c627bbd2ce5b4 100644 +--- a/net/minecraft/world/level/block/SugarCaneBlock.java ++++ b/net/minecraft/world/level/block/SugarCaneBlock.java +@@ -44,6 +44,10 @@ public class SugarCaneBlock extends Block { + protected void tick(BlockState state, ServerLevel level, BlockPos pos, RandomSource random) { + if (!state.canSurvive(level, pos)) { + level.destroyBlock(pos, true); ++ // Leaves start - zero tick plants ++ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { ++ this.randomTick(state, level, pos, random); ++ // Leaves end - zero tick plants + } + } + diff --git a/leaves-server/minecraft-patches/features/0073-Force-peaceful-mode-switch.patch b/leaves-server/minecraft-patches/features/0073-Force-peaceful-mode-switch.patch new file mode 100644 index 00000000..f0010f20 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0073-Force-peaceful-mode-switch.patch @@ -0,0 +1,100 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 3 Aug 2023 14:21:47 +0800 +Subject: [PATCH] Force peaceful mode switch + + +diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java +index 6540b2d6a1062d883811ce240c49d30d1925b291..f4f44bf7d67f021b115b5d53c7394fe5bb138024 100644 +--- a/net/minecraft/server/level/ServerChunkCache.java ++++ b/net/minecraft/server/level/ServerChunkCache.java +@@ -181,6 +181,12 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + // Paper end - chunk tick iteration optimisations + ++ // Leaves start - peaceful mode switch ++ public int peacefulModeSwitchTick = org.leavesmc.leaves.LeavesConfig.modify.forcePeacefulMode; ++ public int peacefulModeSwitchCount = -1; ++ private final List> peacefulModeSwitchEntityTypes = List.of(net.minecraft.world.entity.boss.wither.WitherBoss.class, net.minecraft.world.entity.monster.Shulker.class, net.minecraft.world.entity.monster.warden.Warden.class); ++ // Leaves end - peaceful mode switch ++ + + public ServerChunkCache( + ServerLevel level, +@@ -507,6 +513,20 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + this.lastInhabitedUpdate = gameTime; + if (!this.level.isDebug()) { + ProfilerFiller profilerFiller = Profiler.get(); ++ // Leaves start - peaceful mode switch ++ if (peacefulModeSwitchTick > 0) { ++ if (this.level.getLevelData().getGameTime() % peacefulModeSwitchTick == 0) { ++ peacefulModeSwitchCount = 0; ++ this.level.getAllEntities().forEach(entity -> { ++ if (peacefulModeSwitchEntityTypes.contains(entity.getClass())) { ++ peacefulModeSwitchCount++; ++ } ++ }); ++ } ++ } else { ++ peacefulModeSwitchCount = -1; ++ } ++ // Leaves end - peaceful mode switch + profilerFiller.push("pollingChunks"); + if (this.level.tickRateManager().runsNormally()) { + List list = this.tickingChunks; +@@ -598,6 +618,14 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + boolean _boolean = this.level.getGameRules().getBoolean(GameRules.RULE_DOMOBSPAWNING) && !this.level.players().isEmpty(); // CraftBukkit + int _int = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); + List filteredSpawningCategories; ++ // Leaves start - peaceful mode switch ++ boolean peacefulModeSwitch = false; ++ if (lastSpawnState != null && peacefulModeSwitchCount != -1) { ++ if (peacefulModeSwitchCount >= NaturalSpawner.globalLimitForCategory(level, net.minecraft.world.entity.MobCategory.MONSTER, lastSpawnState.getSpawnableChunkCount())) { ++ peacefulModeSwitch = true; ++ } ++ } ++ // Leaves end - peaceful mode switch + if (_boolean && (this.spawnEnemies || this.spawnFriendlies)) { + // Paper start - PlayerNaturallySpawnCreaturesEvent + for (ServerPlayer entityPlayer : this.level.players()) { +@@ -608,7 +636,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon + } + // Paper end - PlayerNaturallySpawnCreaturesEvent + boolean flag = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit +- filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level); // CraftBukkit ++ filteredSpawningCategories = NaturalSpawner.getFilteredSpawningCategories(spawnState, this.spawnFriendlies, this.spawnEnemies, flag, this.level, peacefulModeSwitch); // CraftBukkit Leaves start - peaceful mode switch + } else { + filteredSpawningCategories = List.of(); + } +diff --git a/net/minecraft/world/level/NaturalSpawner.java b/net/minecraft/world/level/NaturalSpawner.java +index 8fd9c191e5b14fc7dd90e8f7229acd6de97e0f9e..268a4489cc5f7d5358c6cd96e0acbaf65852f7d3 100644 +--- a/net/minecraft/world/level/NaturalSpawner.java ++++ b/net/minecraft/world/level/NaturalSpawner.java +@@ -122,16 +122,26 @@ public final class NaturalSpawner { + static Biome getRoughBiome(BlockPos pos, ChunkAccess chunk) { + return chunk.getNoiseBiome(QuartPos.fromBlock(pos.getX()), QuartPos.fromBlock(pos.getY()), QuartPos.fromBlock(pos.getZ())).value(); + } +- ++ // Leaves start - peaceful mode switch + // CraftBukkit start - add server + public static List getFilteredSpawningCategories( + NaturalSpawner.SpawnState spawnState, boolean spawnFriendlies, boolean spawnEnemies, boolean spawnPassives, ServerLevel level + ) { ++ return getFilteredSpawningCategories(spawnState, spawnFriendlies, spawnEnemies, spawnPassives, level, false); ++ } ++ ++ public static List getFilteredSpawningCategories(NaturalSpawner.SpawnState spawnState, boolean spawnFriendlies, boolean spawnEnemies, boolean spawnPassives, ServerLevel level, boolean peacefulModeSwitch) { ++ // Leaves end - peaceful mode switch + LevelData worlddata = level.getLevelData(); // CraftBukkit - Other mob type spawn tick rate + // CraftBukkit end + List list = new ArrayList<>(SPAWNING_CATEGORIES.length); + + for (MobCategory mobCategory : SPAWNING_CATEGORIES) { ++ // Leaves start - peaceful mode switch ++ if (mobCategory == MobCategory.MONSTER && peacefulModeSwitch) { ++ continue; ++ } ++ // Leaves end - peaceful mode switch + // CraftBukkit start - Use per-world spawn limits + boolean spawnThisTick = true; + int limit = mobCategory.getMaxInstancesPerChunk(); diff --git a/leaves-server/minecraft-patches/features/0074-Replay-Mod-API.patch b/leaves-server/minecraft-patches/features/0074-Replay-Mod-API.patch new file mode 100644 index 00000000..779398d5 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0074-Replay-Mod-API.patch @@ -0,0 +1,357 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 4 Feb 2025 19:45:19 +0800 +Subject: [PATCH] Replay Mod API + +This patch is Powered by ReplayMod(https://github.com/ReplayMod) + +diff --git a/net/minecraft/commands/CommandSourceStack.java b/net/minecraft/commands/CommandSourceStack.java +index c2b7164a1395842ab95428540782eeda4c7960b0..a00a59989c28429289fc4b65fb0605cc626de56c 100644 +--- a/net/minecraft/commands/CommandSourceStack.java ++++ b/net/minecraft/commands/CommandSourceStack.java +@@ -589,7 +589,7 @@ public class CommandSourceStack implements ExecutionCommandSource getOnlinePlayerNames() { +- return this.entity instanceof ServerPlayer sourcePlayer && !sourcePlayer.getBukkitEntity().hasPermission("paper.bypass-visibility.tab-completion") ? this.getServer().getPlayerList().getPlayers().stream().filter(serverPlayer -> sourcePlayer.getBukkitEntity().canSee(serverPlayer.getBukkitEntity())).map(serverPlayer -> serverPlayer.getGameProfile().getName()).toList() : Lists.newArrayList(this.server.getPlayerNames()); // Paper - Make CommandSourceStack respect hidden players ++ return this.entity instanceof ServerPlayer sourcePlayer && !(sourcePlayer instanceof org.leavesmc.leaves.replay.ServerPhotographer) && !sourcePlayer.getBukkitEntity().hasPermission("paper.bypass-visibility.tab-completion") ? this.getServer().getPlayerList().getPlayers().stream().filter(serverPlayer -> sourcePlayer.getBukkitEntity().canSee(serverPlayer.getBukkitEntity())).map(serverPlayer -> serverPlayer.getGameProfile().getName()).toList() : Lists.newArrayList(this.server.getPlayerNames()); // Paper - Make CommandSourceStack respect hidden players // Leaves - only real player + } + + @Override +diff --git a/net/minecraft/commands/arguments/selector/EntitySelector.java b/net/minecraft/commands/arguments/selector/EntitySelector.java +index 514f8fbdeb776087608665c35de95294aadf5cf0..2f78ca86f46ea08fdcf4b8047d3d0b04e2e29b0a 100644 +--- a/net/minecraft/commands/arguments/selector/EntitySelector.java ++++ b/net/minecraft/commands/arguments/selector/EntitySelector.java +@@ -128,11 +128,12 @@ public class EntitySelector { + return this.findPlayers(source); + } else if (this.playerName != null) { + ServerPlayer playerByName = source.getServer().getPlayerList().getPlayerByName(this.playerName); ++ playerByName = playerByName instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : playerByName; // Leaves - skip photographer + return playerByName == null ? List.of() : List.of(playerByName); + } else if (this.entityUUID != null) { + for (ServerLevel serverLevel : source.getServer().getAllLevels()) { + Entity entity = serverLevel.getEntity(this.entityUUID); +- if (entity != null) { ++ if (entity != null && !(entity instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { + if (entity.getType().isEnabled(source.enabledFeatures())) { + return List.of(entity); + } +@@ -146,7 +147,7 @@ public class EntitySelector { + AABB absoluteAabb = this.getAbsoluteAabb(vec3); + if (this.currentEntity) { + Predicate predicate = this.getPredicate(vec3, absoluteAabb, null); +- return source.getEntity() != null && predicate.test(source.getEntity()) ? List.of(source.getEntity()) : List.of(); ++ return source.getEntity() != null && !(source.getEntity() instanceof org.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(source.getEntity()) ? List.of(source.getEntity()) : List.of(); // Leaves - skip photographer + } else { + Predicate predicate = this.getPredicate(vec3, absoluteAabb, source.enabledFeatures()); + List list = new ObjectArrayList<>(); +@@ -157,6 +158,7 @@ public class EntitySelector { + this.addEntities(list, serverLevel1, absoluteAabb, predicate); + } + } ++ list.removeIf(entity -> entity instanceof org.leavesmc.leaves.replay.ServerPhotographer); // Leaves - skip photographer + + return this.sortAndLimit(vec3, list); + } +@@ -192,9 +194,11 @@ public class EntitySelector { + this.checkPermissions(source); + if (this.playerName != null) { + ServerPlayer playerByName = source.getServer().getPlayerList().getPlayerByName(this.playerName); ++ playerByName = playerByName instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : playerByName; // Leaves - skip photographer + return playerByName == null ? List.of() : List.of(playerByName); + } else if (this.entityUUID != null) { + ServerPlayer playerByName = source.getServer().getPlayerList().getPlayer(this.entityUUID); ++ playerByName = playerByName instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : playerByName; // Leaves - skip photographer + return playerByName == null ? List.of() : List.of(playerByName); + } else { + Vec3 vec3 = this.position.apply(source.getPosition()); +@@ -206,12 +210,12 @@ public class EntitySelector { + int resultLimit = this.getResultLimit(); + List players; + if (this.isWorldLimited()) { +- players = source.getLevel().getPlayers(predicate, resultLimit); ++ players = source.getLevel().getPlayers((player -> !(player instanceof org.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(player)), resultLimit); // Leaves - skip photographer + } else { + players = new ObjectArrayList<>(); + + for (ServerPlayer serverPlayer1 : source.getServer().getPlayerList().getPlayers()) { +- if (predicate.test(serverPlayer1)) { ++ if (predicate.test(serverPlayer1) && !(serverPlayer1 instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // Leaves - skip photographer + players.add(serverPlayer1); + if (players.size() >= resultLimit) { + return players; +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 7822cecf362cd8d430d90c0daac6e97c8a8d124b..438bbf36d4f80b38482b89e3ef59e9e48d0f62c2 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1639,7 +1639,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop players = this.playerList.getPlayers(); ++ List players = this.playerList.realPlayers; // Leaves - only real player + int maxPlayers = this.getMaxPlayers(); + if (this.hidesOnlinePlayers()) { + return new ServerStatus.Players(maxPlayers, players.size(), List.of()); +diff --git a/net/minecraft/server/PlayerAdvancements.java b/net/minecraft/server/PlayerAdvancements.java +index 4e9ce0c8b459ef41a6945182401c47c61b16b1f7..e9961c4fb7d1c808150171dca89b51eddf0c7518 100644 +--- a/net/minecraft/server/PlayerAdvancements.java ++++ b/net/minecraft/server/PlayerAdvancements.java +@@ -174,7 +174,7 @@ public class PlayerAdvancements { + } + // Leaves end - spectator don't get advancement + // Leaves start - bot can't get advancement +- if (player instanceof org.leavesmc.leaves.bot.ServerBot) { ++ if (player instanceof org.leavesmc.leaves.bot.ServerBot || player instanceof org.leavesmc.leaves.replay.ServerPhotographer) { // Leaves - and photographer + return false; + } + // Leaves end - bot can't get advancement +diff --git a/net/minecraft/server/commands/OpCommand.java b/net/minecraft/server/commands/OpCommand.java +index 5c0a04db38821dbb0cba2bb6f0787f113d167efd..cd153db93f709c3142942fac88ae3ca2226a65b3 100644 +--- a/net/minecraft/server/commands/OpCommand.java ++++ b/net/minecraft/server/commands/OpCommand.java +@@ -25,7 +25,7 @@ public class OpCommand { + (context, builder) -> { + PlayerList playerList = context.getSource().getServer().getPlayerList(); + return SharedSuggestionProvider.suggest( +- playerList.getPlayers() ++ playerList.realPlayers // Leaves - only real player + .stream() + .filter(player -> !playerList.isOp(player.getGameProfile())) + .map(player -> player.getGameProfile().getName()), +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 7763d4c818606ed034f28e050166fe8cae16cfb8..1deedd3fc28792fc368580973038c1824b15691d 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -2645,7 +2645,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + if (entity instanceof ServerPlayer serverPlayer) { + ServerLevel.this.players.add(serverPlayer); + // Leaves start - skip +- if (!(serverPlayer instanceof org.leavesmc.leaves.bot.ServerBot)) { ++ if (!(serverPlayer instanceof org.leavesmc.leaves.bot.ServerBot) && !(serverPlayer instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // and photographer + ServerLevel.this.realPlayers.add(serverPlayer); + } + // Leaves end - skip +@@ -2716,7 +2716,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + if (entity instanceof ServerPlayer serverPlayer) { + ServerLevel.this.players.remove(serverPlayer); + // Leaves start - skip +- if (!(serverPlayer instanceof org.leavesmc.leaves.bot.ServerBot)) { ++ if (!(serverPlayer instanceof org.leavesmc.leaves.bot.ServerBot) && !(serverPlayer instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // and photographer + ServerLevel.this.realPlayers.remove(serverPlayer); + } + // Leaves end - skip +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 93eb9e2d4e44881181a07b12249c3812635fec14..846a96de92c74cfd3091c21d692cb8bdac60c4eb 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -131,6 +131,7 @@ public abstract class PlayerList { + private boolean allowCommandsForAllPlayers; + private static final boolean ALLOW_LOGOUTIVATOR = false; + private int sendAllPlayerInfoIn; ++ public final List realPlayers = new java.util.concurrent.CopyOnWriteArrayList(); // Leaves - replay api + + // CraftBukkit start + private org.bukkit.craftbukkit.CraftServer cserver; +@@ -149,6 +150,119 @@ public abstract class PlayerList { + + abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor + ++ // Leaves start - replay mod api ++ public void placeNewPhotographer(Connection connection, org.leavesmc.leaves.replay.ServerPhotographer player, ServerLevel worldserver) { ++ player.isRealPlayer = true; // Paper ++ player.loginTime = System.currentTimeMillis(); // Paper ++ ++ ServerLevel worldserver1 = worldserver; ++ ++ player.setServerLevel(worldserver1); ++ player.spawnIn(worldserver1); ++ player.gameMode.setLevel((ServerLevel) player.level()); ++ ++ LevelData worlddata = worldserver1.getLevelData(); ++ ++ player.loadGameTypes(null); ++ ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, CommonListenerCookie.createInitial(player.gameProfile, false)); ++ GameRules gamerules = worldserver1.getGameRules(); ++ boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); ++ boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); ++ boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING); ++ ++ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); // Paper - replace old player chunk management ++ player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit ++ playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); ++ playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities())); ++ playerconnection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected)); ++ RecipeManager craftingmanager = this.server.getRecipeManager(); ++ playerconnection.send(new ClientboundUpdateRecipesPacket(craftingmanager.getSynchronizedItemProperties(), craftingmanager.getSynchronizedStonecutterRecipes())); ++ ++ this.sendPlayerPermissionLevel(player); ++ player.getStats().markAllDirty(); ++ player.getRecipeBook().sendInitialRecipeBook(player); ++ this.updateEntireScoreboard(worldserver1.getScoreboard(), player); ++ this.server.invalidateStatus(); ++ ++ playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); ++ ServerStatus serverping = this.server.getStatus(); ++ ++ if (serverping != null) { ++ player.sendServerStatus(serverping); ++ } ++ ++ this.players.add(player); ++ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot ++ this.playersByUUID.put(player.getUUID(), player); ++ ++ player.supressTrackerForLogin = true; ++ worldserver1.addNewPlayer(player); ++ this.server.getCustomBossEvents().onPlayerConnect(player); ++ org.bukkit.craftbukkit.entity.CraftPlayer bukkitPlayer = player.getBukkitEntity(); ++ ++ player.containerMenu.transferTo(player.containerMenu, bukkitPlayer); ++ if (!player.connection.isAcceptingMessages()) { ++ return; ++ } ++ ++ // org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol ++ ++ // Leaves start - bot support ++ if (org.leavesmc.leaves.LeavesConfig.modify.fakeplayer.enable) { ++ org.leavesmc.leaves.bot.ServerBot bot = this.server.getBotList().getBotByName(player.getScoreboardName()); ++ if (bot != null) { ++ this.server.getBotList().removeBot(bot, org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.INTERNAL, player.getBukkitEntity(), false); ++ } ++ this.server.getBotList().bots.forEach(bot1 -> { ++ bot1.sendPlayerInfo(player); ++ bot1.sendFakeDataIfNeed(player, true); ++ }); // Leaves - render bot ++ } ++ // Leaves end - bot support ++ ++ final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); ++ for (int i = 0; i < this.players.size(); ++i) { ++ ServerPlayer entityplayer1 = this.players.get(i); ++ ++ if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) { ++ continue; ++ } ++ ++ onlinePlayers.add(entityplayer1); ++ } ++ if (!onlinePlayers.isEmpty()) { ++ player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player)); ++ } ++ ++ player.sentListPacket = true; ++ player.supressTrackerForLogin = false; ++ ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); ++ ++ this.sendLevelInfo(player, worldserver1); ++ ++ if (player.level() == worldserver1 && !worldserver1.players().contains(player)) { ++ worldserver1.addNewPlayer(player); ++ this.server.getCustomBossEvents().onPlayerConnect(player); ++ } ++ ++ worldserver1 = player.serverLevel(); ++ java.util.Iterator iterator = player.getActiveEffects().iterator(); ++ while (iterator.hasNext()) { ++ MobEffectInstance mobeffect = iterator.next(); ++ playerconnection.send(new ClientboundUpdateMobEffectPacket(player.getId(), mobeffect, false)); ++ } ++ ++ if (player.isDeadOrDying()) { ++ net.minecraft.core.Holder plains = worldserver1.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME) ++ .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS); ++ player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket( ++ new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains), ++ worldserver1.getLightEngine(), null, null, false) ++ ); ++ } ++ } ++ // Leaves end - replay mod api ++ + public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie cookie) { + player.isRealPlayer = true; // Paper + player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed +@@ -307,6 +421,7 @@ public abstract class PlayerList { + + // player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below + this.players.add(player); ++ this.realPlayers.add(player); // Leaves - replay api + this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot + this.playersByUUID.put(player.getUUID(), player); + // this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(player))); // CraftBukkit - replaced with loop below +@@ -376,6 +491,12 @@ public abstract class PlayerList { + continue; + } + ++ // Leaves start - skip photographer ++ if (entityplayer1 instanceof org.leavesmc.leaves.replay.ServerPhotographer) { ++ continue; ++ } ++ // Leaves end - skip photographer ++ + onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join + } + // Paper start - Use single player info update packet on join +@@ -510,6 +631,43 @@ public abstract class PlayerList { + } + } + ++ // Leaves start - replay mod api ++ public void removePhotographer(org.leavesmc.leaves.replay.ServerPhotographer entityplayer) { ++ ServerLevel worldserver = entityplayer.serverLevel(); ++ ++ entityplayer.awardStat(Stats.LEAVE_GAME); ++ ++ if (entityplayer.containerMenu != entityplayer.inventoryMenu) { ++ entityplayer.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT); ++ } ++ ++ if (server.isSameThread()) entityplayer.doTick(); ++ ++ if (this.collideRuleTeamName != null) { ++ final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); ++ final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); ++ if (entityplayer.getTeam() == team && team != null) { ++ scoreBoard.removePlayerFromTeam(entityplayer.getScoreboardName(), team); ++ } ++ } ++ ++ worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); ++ entityplayer.retireScheduler(); ++ entityplayer.getAdvancements().stopListening(); ++ this.players.remove(entityplayer); ++ this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); ++ this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer); ++ UUID uuid = entityplayer.getUUID(); ++ ServerPlayer entityplayer1 = this.playersByUUID.get(uuid); ++ ++ if (entityplayer1 == entityplayer) { ++ this.playersByUUID.remove(uuid); ++ } ++ ++ this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity()); ++ } ++ // Leaves stop - replay mod api ++ + public net.kyori.adventure.text.Component remove(ServerPlayer player) { // CraftBukkit - return string // Paper - return Component + // Paper start - Fix kick event leave message not being sent + return this.remove(player, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? player.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(player.getDisplayName()))); +@@ -585,6 +743,7 @@ public abstract class PlayerList { + player.retireScheduler(); // Paper - Folia schedulers + player.getAdvancements().stopListening(); + this.players.remove(player); ++ this.realPlayers.remove(player); // Leaves - replay api + this.playersByName.remove(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot + this.server.getCustomBossEvents().onPlayerDisconnect(player); + UUID uuid = player.getUUID(); +@@ -681,7 +840,7 @@ public abstract class PlayerList { + // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile) + // ? Component.translatable("multiplayer.disconnect.server_full") + // : null; +- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) { ++ if (this.realPlayers.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameProfile)) { // Leaves - only real player + event.disallow(org.bukkit.event.player.PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure + } + } diff --git a/leaves-server/minecraft-patches/features/0075-Leaves-I18n.patch b/leaves-server/minecraft-patches/features/0075-Leaves-I18n.patch new file mode 100644 index 00000000..e097e17d --- /dev/null +++ b/leaves-server/minecraft-patches/features/0075-Leaves-I18n.patch @@ -0,0 +1,65 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 4 Feb 2025 20:01:19 +0800 +Subject: [PATCH] Leaves I18n + + +diff --git a/net/minecraft/locale/Language.java b/net/minecraft/locale/Language.java +index 7b9e2a1a208b46a69c16e6afd8b502259893574f..aa0db0cbbcfa079c43dbed6679abf99979e0abf0 100644 +--- a/net/minecraft/locale/Language.java ++++ b/net/minecraft/locale/Language.java +@@ -31,6 +31,42 @@ public abstract class Language { + public static final String DEFAULT = "en_us"; + private static volatile Language instance = loadDefault(); + ++ // Leaves start - i18n ++ public static void loadI18N(String lang) { ++ DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource(); ++ Map map = new HashMap<>(); ++ BiConsumer biConsumer = map::put; ++ parseTranslations(biConsumer, "/assets/minecraft/lang/" + lang + ".json"); ++ deprecatedTranslationsInfo.applyToMap(map); ++ final Map map2 = Map.copyOf(map); ++ Language language = new Language() { ++ @Override ++ public String getOrDefault(String key, String fallback) { ++ return map2.getOrDefault(key, fallback); ++ } ++ ++ @Override ++ public boolean has(String key) { ++ return map2.containsKey(key); ++ } ++ ++ @Override ++ public boolean isDefaultRightToLeft() { ++ return false; ++ } ++ ++ @Override ++ public FormattedCharSequence getVisualOrder(FormattedText text) { ++ return visitor -> text.visit( ++ (style, string) -> StringDecomposer.iterateFormatted(string, style, visitor) ? Optional.empty() : FormattedText.STOP_ITERATION, ++ Style.EMPTY ++ ).isPresent(); ++ } ++ }; ++ inject(language); ++ } ++ // Leaves end - i18n ++ + private static Language loadDefault() { + DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource(); + Map map = new HashMap<>(); +diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java +index 765521cae8bc1c65e3a390d018190646e39c4eb0..92f45558661007e761b48f59ff7f04d67f77f5ed 100644 +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -227,6 +227,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now + + org.leavesmc.leaves.LeavesConfig.init((java.io.File) options.valueOf("leaves-settings")); // Leaves - Server Config ++ net.minecraft.locale.Language.loadI18N(org.leavesmc.leaves.LeavesConfig.mics.serverLang); // Leaves - i18n + + com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics // Leaves - down + diff --git a/patches/server/0089-Fix-minecraft-hopper-not-work-without-player.patch b/leaves-server/minecraft-patches/features/0076-Fix-minecraft-hopper-not-work-without-player.patch similarity index 57% rename from patches/server/0089-Fix-minecraft-hopper-not-work-without-player.patch rename to leaves-server/minecraft-patches/features/0076-Fix-minecraft-hopper-not-work-without-player.patch index fdd01a25..c9eac88e 100644 --- a/patches/server/0089-Fix-minecraft-hopper-not-work-without-player.patch +++ b/leaves-server/minecraft-patches/features/0076-Fix-minecraft-hopper-not-work-without-player.patch @@ -4,10 +4,10 @@ Date: Wed, 16 Aug 2023 13:10:58 +0800 Subject: [PATCH] Fix minecraft hopper not work without player -diff --git a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java -index d81a6874e8b25f098df619f84c359e146c7f64de..6c2045844bb18767c0645c24e6629b59beb25f4e 100644 ---- a/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java -+++ b/src/main/java/net/minecraft/world/entity/vehicle/MinecartHopper.java +diff --git a/net/minecraft/world/entity/vehicle/MinecartHopper.java b/net/minecraft/world/entity/vehicle/MinecartHopper.java +index 8341e7f01606fca90e69384c16fc19bb9e20d1b7..deea1c6bcfbca04b0e9b45869eab5b8c5bbb17ca 100644 +--- a/net/minecraft/world/entity/vehicle/MinecartHopper.java ++++ b/net/minecraft/world/entity/vehicle/MinecartHopper.java @@ -99,6 +99,13 @@ public class MinecartHopper extends AbstractMinecartContainer implements Hopper } } @@ -21,4 +21,4 @@ index d81a6874e8b25f098df619f84c359e146c7f64de..6c2045844bb18767c0645c24e6629b59 + public boolean suckInItems() { if (HopperBlockEntity.suckInItems(this.level(), this)) { - this.immunize(); // Paper + this.immunize(); // Paper diff --git a/leaves-server/minecraft-patches/features/0077-RNG-Fishing.patch b/leaves-server/minecraft-patches/features/0077-RNG-Fishing.patch new file mode 100644 index 00000000..a6536b8c --- /dev/null +++ b/leaves-server/minecraft-patches/features/0077-RNG-Fishing.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 4 Sep 2023 22:09:10 +0800 +Subject: [PATCH] RNG Fishing + + +diff --git a/net/minecraft/world/entity/projectile/FishingHook.java b/net/minecraft/world/entity/projectile/FishingHook.java +index f2bf0cdbd29438ca51b74ae2fcdf49dba0d52804..1d3c2be401d9239b8e6cabfe016518ffcffcb83b 100644 +--- a/net/minecraft/world/entity/projectile/FishingHook.java ++++ b/net/minecraft/world/entity/projectile/FishingHook.java +@@ -525,7 +525,7 @@ public class FishingHook extends Projectile { + .withLuck(this.luck + playerOwner.getLuck()) + .create(LootContextParamSets.FISHING); + LootTable lootTable = this.level().getServer().reloadableRegistries().getLootTable(BuiltInLootTables.FISHING); +- List randomItems = lootTable.getRandomItems(lootParams); ++ List randomItems = org.leavesmc.leaves.LeavesConfig.modify.oldMC.rngFishing ? lootTable.getRandomItems(lootParams, this.random) : lootTable.getRandomItems(lootParams); // Leaves - world random + CriteriaTriggers.FISHING_ROD_HOOKED.trigger((ServerPlayer)playerOwner, stack, this, randomItems); + + for (ItemStack itemStack : randomItems) { diff --git a/leaves-server/minecraft-patches/features/0078-Wool-Hopper-Counter.patch b/leaves-server/minecraft-patches/features/0078-Wool-Hopper-Counter.patch new file mode 100644 index 00000000..a4123690 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0078-Wool-Hopper-Counter.patch @@ -0,0 +1,52 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 4 Feb 2025 20:18:35 +0800 +Subject: [PATCH] Wool Hopper Counter + +This patch is Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet) + +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 50bc72f736e9e7a9839a853254a81f9add03bacf..bd8d949ff0f3d0b6a893d3b3ae608da380e715f0 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -428,6 +428,13 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + // Paper end - Perf: Optimize Hoppers + + private static boolean ejectItems(Level level, BlockPos pos, HopperBlockEntity blockEntity) { ++ // Leaves start - hopper counter ++ if (org.leavesmc.leaves.util.HopperCounter.isEnabled()) { ++ if (woolHopperCounter(level, pos, level.getBlockState(pos), HopperBlockEntity.getContainerAt(level, pos))) { ++ return true; ++ } ++ } ++ // Leaves end - hopper counter + Container attachedContainer = getAttachedContainer(level, pos, blockEntity); + if (attachedContainer == null) { + return false; +@@ -494,6 +501,26 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + } + } + ++ // Leaves start - hopper counter ++ private static boolean woolHopperCounter(Level level, BlockPos blockPos, BlockState state, @Nullable Container container) { ++ if (container == null) { ++ return false; ++ } ++ net.minecraft.world.item.DyeColor woolColor = org.leavesmc.leaves.util.WoolUtils.getWoolColorAtPosition(level, blockPos.relative(state.getValue(HopperBlock.FACING))); ++ if (woolColor != null) { ++ for (int i = 0; i < container.getContainerSize(); ++i) { ++ if (!container.getItem(i).isEmpty()) { ++ ItemStack itemstack = container.getItem(i); ++ org.leavesmc.leaves.util.HopperCounter.getCounter(woolColor).add(level.getServer(), itemstack); ++ container.setItem(i, ItemStack.EMPTY); ++ } ++ } ++ return true; ++ } ++ return false; ++ } ++ // Leaves end - hopper counter ++ + private static int[] getSlots(Container container, Direction direction) { + if (container instanceof WorldlyContainer worldlyContainer) { + return worldlyContainer.getSlotsForFace(direction); diff --git a/patches/server/0093-Spider-jockeys-drop-gapples.patch b/leaves-server/minecraft-patches/features/0079-Spider-jockeys-drop-gapples.patch similarity index 61% rename from patches/server/0093-Spider-jockeys-drop-gapples.patch rename to leaves-server/minecraft-patches/features/0079-Spider-jockeys-drop-gapples.patch index 62925eae..f0a29487 100644 --- a/patches/server/0093-Spider-jockeys-drop-gapples.patch +++ b/leaves-server/minecraft-patches/features/0079-Spider-jockeys-drop-gapples.patch @@ -4,12 +4,12 @@ Date: Tue, 5 Sep 2023 08:49:01 +0800 Subject: [PATCH] Spider jockeys drop gapples -diff --git a/src/main/java/net/minecraft/world/entity/monster/Spider.java b/src/main/java/net/minecraft/world/entity/monster/Spider.java -index 91e521414c3ea5722aac7506b7589fbb399e9636..1b7c703464477d7c8b28f33edd5a971110e73908 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Spider.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Spider.java -@@ -145,6 +145,18 @@ public class Spider extends Monster { - this.entityData.set(Spider.DATA_FLAGS_ID, b0); +diff --git a/net/minecraft/world/entity/monster/Spider.java b/net/minecraft/world/entity/monster/Spider.java +index af0305079a367899708ee2bbac82aefaa9129d2f..e9a4734cddc33785ca6811f8f566904ace78fd78 100644 +--- a/net/minecraft/world/entity/monster/Spider.java ++++ b/net/minecraft/world/entity/monster/Spider.java +@@ -139,6 +139,18 @@ public class Spider extends Monster { + this.entityData.set(DATA_FLAGS_ID, b); } + // Leaves start - spider jockeys drop gapples @@ -26,4 +26,4 @@ index 91e521414c3ea5722aac7506b7589fbb399e9636..1b7c703464477d7c8b28f33edd5a9711 + @Nullable @Override - public SpawnGroupData finalizeSpawn(ServerLevelAccessor world, DifficultyInstance difficulty, EntitySpawnReason spawnReason, @Nullable SpawnGroupData entityData) { + public SpawnGroupData finalizeSpawn( diff --git a/patches/server/0094-Force-Void-Trade.patch b/leaves-server/minecraft-patches/features/0080-Force-Void-Trade.patch similarity index 52% rename from patches/server/0094-Force-Void-Trade.patch rename to leaves-server/minecraft-patches/features/0080-Force-Void-Trade.patch index 288b7c6b..7b391dc9 100644 --- a/patches/server/0094-Force-Void-Trade.patch +++ b/leaves-server/minecraft-patches/features/0080-Force-Void-Trade.patch @@ -4,33 +4,33 @@ Date: Thu, 14 Sep 2023 20:23:03 +0800 Subject: [PATCH] Force Void Trade -diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -index 6c0cece094d36ddb2ae8d67d3c847a2c8faa3da8..cb5524ed59a07693d1d7ff03ca4f27e07774e3e5 100644 ---- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -+++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -@@ -64,6 +64,7 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa +diff --git a/net/minecraft/world/entity/npc/AbstractVillager.java b/net/minecraft/world/entity/npc/AbstractVillager.java +index 18a087a52070b9bdee4d02ff3fc6a3c063e444d4..dba596ac7a26401a6f4d8726bcea0caf067af10e 100644 +--- a/net/minecraft/world/entity/npc/AbstractVillager.java ++++ b/net/minecraft/world/entity/npc/AbstractVillager.java +@@ -60,6 +60,7 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa @Nullable protected MerchantOffers offers; - private final SimpleContainer inventory = new SimpleContainer(8, (org.bukkit.craftbukkit.entity.CraftAbstractVillager) this.getBukkitEntity()); // CraftBukkit add argument + private final SimpleContainer inventory = new SimpleContainer(8, (org.bukkit.craftbukkit.entity.CraftAbstractVillager) this.getBukkitEntity()); // CraftBukkit - add argument + protected boolean voidTrade = false; // Leaves - force void trade - public AbstractVillager(EntityType type, Level world) { - super(type, world); + public AbstractVillager(EntityType entityType, Level level) { + super(entityType, level); @@ -154,7 +155,13 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa @Override - public void processTrade(MerchantOffer recipe, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent + public void processTrade(MerchantOffer offer, @Nullable io.papermc.paper.event.player.PlayerPurchaseEvent event) { // The MerchantRecipe passed in here is the one set by the PlayerPurchaseEvent if (event == null || event.willIncreaseTradeUses()) { -- recipe.increaseUses(); +- offer.increaseUses(); + // Leaves start - force void trade + if (voidTrade) { -+ recipe.increaseVoidUses(); ++ offer.increaseVoidUses(); + } else { -+ recipe.increaseUses(); ++ offer.increaseUses(); + } + // Leaves end - force void trade } if (event == null || event.isRewardingExp()) { - this.rewardTradeXp(recipe); + this.rewardTradeXp(offer); @@ -166,7 +173,7 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa @Override public void notifyTrade(MerchantOffer offer) { @@ -39,8 +39,8 @@ index 6c0cece094d36ddb2ae8d67d3c847a2c8faa3da8..cb5524ed59a07693d1d7ff03ca4f27e0 + if (!voidTrade) this.ambientSoundTime = -this.getAmbientSoundInterval(); // Leaves - force void trade // this.rewardTradeXp(offer); // Paper - Add PlayerTradeEvent and PlayerPurchaseEvent if (this.tradingPlayer instanceof ServerPlayer) { - CriteriaTriggers.TRADE.trigger((ServerPlayer) this.tradingPlayer, this, offer.getResult()); -@@ -184,7 +191,7 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa + CriteriaTriggers.TRADE.trigger((ServerPlayer)this.tradingPlayer, this, offer.getResult()); +@@ -183,7 +190,7 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa @Override public void notifyTradeUpdated(ItemStack stack) { if (!this.level().isClientSide && this.ambientSoundTime > -this.getAmbientSoundInterval() + 20) { @@ -48,8 +48,8 @@ index 6c0cece094d36ddb2ae8d67d3c847a2c8faa3da8..cb5524ed59a07693d1d7ff03ca4f27e0 + if (!voidTrade) this.ambientSoundTime = -this.getAmbientSoundInterval(); // Leaves - force void trade this.makeSound(this.getTradeUpdatedSound(!stack.isEmpty())); } - -@@ -241,6 +248,12 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa + } +@@ -235,6 +242,12 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa } protected void stopTrading() { @@ -59,37 +59,37 @@ index 6c0cece094d36ddb2ae8d67d3c847a2c8faa3da8..cb5524ed59a07693d1d7ff03ca4f27e0 + this.offers.forEach(MerchantOffer::resetVoidUses); + } + // Leaves end - force void trade - this.setTradingPlayer((Player) null); + this.setTradingPlayer(null); } -@@ -320,4 +333,10 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa - public boolean isClientSide() { - return this.level().isClientSide; +@@ -313,4 +326,10 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa + public boolean stillValid(Player player) { + return this.getTradingPlayer() == player && this.isAlive() && player.canInteractWithEntity(this, 4.0); } + + // Leaves start - force void trade + public void setVoidTrade() { -+ voidTrade = true; ++ this.voidTrade = true; + } + // Leaves end - force void trade } -diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java -index 8d3959e86ef1357e49aac6ef3c8285c770f2856a..d931e38924c2fbad84efdb2e9ac84061b1764870 100644 ---- a/src/main/java/net/minecraft/world/entity/npc/Villager.java -+++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java -@@ -348,6 +348,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler +diff --git a/net/minecraft/world/entity/npc/Villager.java b/net/minecraft/world/entity/npc/Villager.java +index 6b565fcf91e1d94b649dac90bf3c923930d252f8..31c2fb7780b822bf63543c4526739b9d3faad7d8 100644 +--- a/net/minecraft/world/entity/npc/Villager.java ++++ b/net/minecraft/world/entity/npc/Villager.java +@@ -380,6 +380,7 @@ public class Villager extends AbstractVillager implements ReputationEventHandler } - private void startTrading(Player customer) { + private void startTrading(Player player) { + this.voidTrade = false; // Leaves - force void trade - this.updateSpecialPrices(customer); - this.setTradingPlayer(customer); - this.openTradingScreen(customer, this.getDisplayName(), this.getVillagerData().getLevel()); -@@ -639,8 +640,12 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + this.updateSpecialPrices(player); + this.setTradingPlayer(player); + this.openTradingScreen(player, this.getDisplayName(), this.getVillagerData().getLevel()); +@@ -632,8 +633,12 @@ public class Villager extends AbstractVillager implements ReputationEventHandler + @Override protected void rewardTradeXp(MerchantOffer offer) { int i = 3 + this.random.nextInt(4); - -- this.villagerXp += offer.getXp(); +- this.villagerXp = this.villagerXp + offer.getXp(); - this.lastTradedPlayer = this.getTradingPlayer(); + // Leaves start - force void trade + if (!voidTrade) { @@ -100,11 +100,11 @@ index 8d3959e86ef1357e49aac6ef3c8285c770f2856a..d931e38924c2fbad84efdb2e9ac84061 if (this.shouldIncreaseLevel()) { this.updateMerchantTimer = 40; this.increaseProfessionLevelOnUpdate = true; -diff --git a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java -index 1e77cce428d9e53142aaa2cf780b7f862d536eca..2bac91f7f6943c7e1b5f63ea7dfeb3c3e390bcb7 100644 ---- a/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java -+++ b/src/main/java/net/minecraft/world/entity/npc/WanderingTrader.java -@@ -123,9 +123,10 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill +diff --git a/net/minecraft/world/entity/npc/WanderingTrader.java b/net/minecraft/world/entity/npc/WanderingTrader.java +index 6655d06e2011e20e7346dfe57527795269094d8a..8ebf885ceb13a8b5e5b3db74fc7ee859d10f3845 100644 +--- a/net/minecraft/world/entity/npc/WanderingTrader.java ++++ b/net/minecraft/world/entity/npc/WanderingTrader.java +@@ -133,9 +133,10 @@ public class WanderingTrader extends net.minecraft.world.entity.npc.AbstractVill return InteractionResult.CONSUME; } @@ -118,11 +118,11 @@ index 1e77cce428d9e53142aaa2cf780b7f862d536eca..2bac91f7f6943c7e1b5f63ea7dfeb3c3 return InteractionResult.SUCCESS; } else { -diff --git a/src/main/java/net/minecraft/world/inventory/MerchantMenu.java b/src/main/java/net/minecraft/world/inventory/MerchantMenu.java -index 6a529b5e289c416c0ebdc0260086ec039777aa40..f41e9303c6e118a426e779d4bbca450011ed7246 100644 ---- a/src/main/java/net/minecraft/world/inventory/MerchantMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/MerchantMenu.java -@@ -27,7 +27,7 @@ public class MerchantMenu extends AbstractContainerMenu { +diff --git a/net/minecraft/world/inventory/MerchantMenu.java b/net/minecraft/world/inventory/MerchantMenu.java +index 3019790d5eccef23c710e77615243551fcc676b0..9a87c729dd7289dc3cccfcf50087db7dc354f895 100644 +--- a/net/minecraft/world/inventory/MerchantMenu.java ++++ b/net/minecraft/world/inventory/MerchantMenu.java +@@ -25,7 +25,7 @@ public class MerchantMenu extends AbstractContainerMenu { private static final int SELLSLOT2_X = 162; private static final int BUYSLOT_X = 220; private static final int ROW_Y = 37; @@ -131,28 +131,28 @@ index 6a529b5e289c416c0ebdc0260086ec039777aa40..f41e9303c6e118a426e779d4bbca4500 private final MerchantContainer tradeContainer; private int merchantLevel; private boolean showProgressBar; -diff --git a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java -index 0efc8d997b34302c3e0a5d7ec73a11a940dbeefe..d246c9932f3f98972124089306d6ed740bb9a31b 100644 ---- a/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java -+++ b/src/main/java/net/minecraft/world/item/trading/MerchantOffer.java -@@ -51,6 +51,7 @@ public class MerchantOffer { +diff --git a/net/minecraft/world/item/trading/MerchantOffer.java b/net/minecraft/world/item/trading/MerchantOffer.java +index 6c06350751db7543d5bde7723121d9d9dbb79071..262cfdd05f013b2bbc2066ad4f0625ae291213b9 100644 +--- a/net/minecraft/world/item/trading/MerchantOffer.java ++++ b/net/minecraft/world/item/trading/MerchantOffer.java +@@ -39,6 +39,7 @@ public class MerchantOffer { public float priceMultiplier; public int xp; public boolean ignoreDiscounts; // Paper - Add ignore discounts API + public int voidTradeUses; // Leaves - force void trade - // CraftBukkit start - private CraftMerchantRecipe bukkitHandle; -@@ -77,6 +78,7 @@ public class MerchantOffer { + // CraftBukkit start + private org.bukkit.craftbukkit.inventory.@org.jspecify.annotations.Nullable CraftMerchantRecipe bukkitHandle; +@@ -78,6 +79,7 @@ public class MerchantOffer { this.priceMultiplier = priceMultiplier; - this.xp = merchantExperience; + this.xp = xp; this.ignoreDiscounts = ignoreDiscounts; // Paper + this.voidTradeUses = 0; // Leaves - force void trade } - public MerchantOffer(ItemCost buyItem, ItemStack sellItem, int maxUses, int merchantExperience, float priceMultiplier) { -@@ -154,6 +156,16 @@ public class MerchantOffer { - ++this.uses; + public MerchantOffer(ItemCost baseCostA, ItemStack result, int maxUses, int xp, float priceMultiplier) { +@@ -166,6 +168,16 @@ public class MerchantOffer { + this.uses++; } + // Leaves start - force void trade @@ -168,7 +168,7 @@ index 0efc8d997b34302c3e0a5d7ec73a11a940dbeefe..d246c9932f3f98972124089306d6ed74 public int getDemand() { return this.demand; } -@@ -183,7 +195,7 @@ public class MerchantOffer { +@@ -195,7 +207,7 @@ public class MerchantOffer { } public boolean isOutOfStock() { @@ -177,16 +177,17 @@ index 0efc8d997b34302c3e0a5d7ec73a11a940dbeefe..d246c9932f3f98972124089306d6ed74 } public void setToOutOfStock() { -diff --git a/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java b/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java -index a7a21f071161fb7e73a046717d2462f871ab653c..50d726874af316a264fa0c2589f5b04559dffb50 100644 ---- a/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java -@@ -120,6 +120,16 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal { - if (tileentity instanceof TheEndGatewayBlockEntity tileentityendgateway) { - Vec3 vec3d = tileentityendgateway.getPortalPosition(world, pos); - +diff --git a/net/minecraft/world/level/block/EndGatewayBlock.java b/net/minecraft/world/level/block/EndGatewayBlock.java +index 84a1bd5e40e635962d795506861447851e443eee..5c6bc04c241f13a8c09527a39f89fa52feb0eb76 100644 +--- a/net/minecraft/world/level/block/EndGatewayBlock.java ++++ b/net/minecraft/world/level/block/EndGatewayBlock.java +@@ -108,6 +108,17 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal { + public TeleportTransition getPortalDestination(ServerLevel level, Entity entity, BlockPos pos) { + if (level.getBlockEntity(pos) instanceof TheEndGatewayBlockEntity theEndGatewayBlockEntity) { + Vec3 portalPosition = theEndGatewayBlockEntity.getPortalPosition(level, pos); ++ + // Leaves start - force void trade -+ if (org.leavesmc.leaves.LeavesConfig.modify.forceVoidTrade && vec3d != null && entity instanceof net.minecraft.server.level.ServerPlayer player) { ++ if (org.leavesmc.leaves.LeavesConfig.modify.forceVoidTrade && portalPosition != null && entity instanceof net.minecraft.server.level.ServerPlayer player) { + if (player.containerMenu instanceof net.minecraft.world.inventory.MerchantMenu merchantMenu) { + if (merchantMenu.trader instanceof net.minecraft.world.entity.npc.AbstractVillager villager) { + villager.setVoidTrade(); @@ -195,6 +196,6 @@ index a7a21f071161fb7e73a046717d2462f871ab653c..50d726874af316a264fa0c2589f5b045 + } + // Leaves end - force void trade + - return vec3d == null ? null : (entity instanceof ThrownEnderpearl ? new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY) : new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY)); // CraftBukkit - } else { - return null; + if (portalPosition == null) { + return null; + } else { diff --git a/leaves-server/minecraft-patches/features/0081-Villager-infinite-discounts.patch b/leaves-server/minecraft-patches/features/0081-Villager-infinite-discounts.patch new file mode 100644 index 00000000..c7cba5a2 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0081-Villager-infinite-discounts.patch @@ -0,0 +1,22 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 4 Feb 2025 20:27:46 +0800 +Subject: [PATCH] Villager infinite discounts + + +diff --git a/net/minecraft/world/entity/ai/gossip/GossipType.java b/net/minecraft/world/entity/ai/gossip/GossipType.java +index 32548e375202452c5f5ec89fec8a82d62252e823..b8a05c113e8b228c904e952f6ab2b4c38694dc3e 100644 +--- a/net/minecraft/world/entity/ai/gossip/GossipType.java ++++ b/net/minecraft/world/entity/ai/gossip/GossipType.java +@@ -15,9 +15,9 @@ public enum GossipType implements StringRepresentable { + public static final int REPUTATION_CHANGE_PER_TRADE = 2; + public final String id; + public final int weight; +- public final int max; ++ public int max; // Leaves - not final + public final int decayPerDay; +- public final int decayPerTransfer; ++ public int decayPerTransfer; // Leaves - not final + public static final Codec CODEC = StringRepresentable.fromEnum(GossipType::values); + + private GossipType(final String id, final int weight, final int max, final int decayPerDay, final int decayPerTransfer) { diff --git a/leaves-server/minecraft-patches/features/0082-CCE-update-suppression.patch b/leaves-server/minecraft-patches/features/0082-CCE-update-suppression.patch new file mode 100644 index 00000000..1c5c1122 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0082-CCE-update-suppression.patch @@ -0,0 +1,29 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 28 Sep 2023 17:07:02 +0800 +Subject: [PATCH] CCE update suppression + + +diff --git a/net/minecraft/world/level/block/ShulkerBoxBlock.java b/net/minecraft/world/level/block/ShulkerBoxBlock.java +index 7794e66211af34ecca8524d3ca7e0d928f0ab0eb..fca50f68625050daabcae3a3b615cf88cce41111 100644 +--- a/net/minecraft/world/level/block/ShulkerBoxBlock.java ++++ b/net/minecraft/world/level/block/ShulkerBoxBlock.java +@@ -236,7 +236,9 @@ public class ShulkerBoxBlock extends BaseEntityBlock { + protected int getAnalogOutputSignal(BlockState blockState, Level level, BlockPos pos) { + // Leaves start - fix update suppression crash + try { +- return AbstractContainerMenu.getRedstoneSignalFromBlockEntity(level.getBlockEntity(pos)); ++ return org.leavesmc.leaves.LeavesConfig.modify.oldMC.updater.cceUpdateSuppression ? ++ AbstractContainerMenu.getRedstoneSignalFromContainer((net.minecraft.world.Container) level.getBlockEntity(pos)) : // Leaves - make cce happy(?) ++ AbstractContainerMenu.getRedstoneSignalFromBlockEntity(level.getBlockEntity(pos)); + } catch (ClassCastException ex) { + if (org.leavesmc.leaves.LeavesConfig.modify.updateSuppressionCrashFix) { + throw new org.leavesmc.leaves.util.UpdateSuppressionException(pos, this); +@@ -244,7 +246,6 @@ public class ShulkerBoxBlock extends BaseEntityBlock { + throw ex; + } + } +- // Leaves end - fix update suppression crash + } + + public static Block getBlockByColor(@Nullable DyeColor color) { diff --git a/patches/server/0097-Disable-offline-warn-if-use-proxy.patch b/leaves-server/minecraft-patches/features/0083-Disable-offline-warn-if-use-proxy.patch similarity index 57% rename from patches/server/0097-Disable-offline-warn-if-use-proxy.patch rename to leaves-server/minecraft-patches/features/0083-Disable-offline-warn-if-use-proxy.patch index 1fbd7853..fa58f1c8 100644 --- a/patches/server/0097-Disable-offline-warn-if-use-proxy.patch +++ b/leaves-server/minecraft-patches/features/0083-Disable-offline-warn-if-use-proxy.patch @@ -4,16 +4,16 @@ Date: Thu, 28 Sep 2023 20:25:55 +0800 Subject: [PATCH] Disable offline warn if use proxy -diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -index f6c081b3362d9b84d16874e8af30e2f865b3ef7c..9c2c477d2d402675838520fe4c726fb8ea8a90af 100644 ---- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -@@ -301,7 +301,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface +diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java +index 92f45558661007e761b48f59ff7f04d67f77f5ed..c301cc8b76295750b8dd0c6d7f8e5b11a275bb15 100644 +--- a/net/minecraft/server/dedicated/DedicatedServer.java ++++ b/net/minecraft/server/dedicated/DedicatedServer.java +@@ -288,7 +288,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 (!io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode()) { // Leaves - bungee or velocity - DedicatedServer.LOGGER.warn("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!"); - DedicatedServer.LOGGER.warn("The server will make no attempt to authenticate usernames. Beware."); + 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/leaves-server/minecraft-patches/features/0084-Armor-stand-cant-kill-by-mob-projectile.patch b/leaves-server/minecraft-patches/features/0084-Armor-stand-cant-kill-by-mob-projectile.patch new file mode 100644 index 00000000..88b63fcf --- /dev/null +++ b/leaves-server/minecraft-patches/features/0084-Armor-stand-cant-kill-by-mob-projectile.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Fri, 29 Sep 2023 10:39:36 +0800 +Subject: [PATCH] Armor stand cant kill by mob projectile + + +diff --git a/net/minecraft/world/entity/decoration/ArmorStand.java b/net/minecraft/world/entity/decoration/ArmorStand.java +index dfae614a67476d649be2a8c2cd8258abe12842e3..cb42b92dd66305be342b7bd7457fda504e6165e9 100644 +--- a/net/minecraft/world/entity/decoration/ArmorStand.java ++++ b/net/minecraft/world/entity/decoration/ArmorStand.java +@@ -484,6 +484,15 @@ public class ArmorStand extends LivingEntity { + // CraftBukkit end + boolean isCanBreakArmorStand = damageSource.is(DamageTypeTags.CAN_BREAK_ARMOR_STAND); + boolean isAlwaysKillsArmorStands = damageSource.is(DamageTypeTags.ALWAYS_KILLS_ARMOR_STANDS); ++ ++ // Leaves start - Armor stand cant kill by mob projectile ++ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.armorStandCantKillByMobProjectile) { ++ if (damageSource.getDirectEntity() instanceof net.minecraft.world.entity.projectile.Projectile projectile && !(projectile.getOwner() instanceof Player)) { ++ return false; ++ } ++ } ++ // Leaves end - Armor stand cant kill by mob projectile ++ + if (!isCanBreakArmorStand && !isAlwaysKillsArmorStands) { + return false; + } else if (damageSource.getEntity() instanceof Player player && !player.getAbilities().mayBuild) { diff --git a/leaves-server/minecraft-patches/features/0085-Make-Item-tick-vanilla.patch b/leaves-server/minecraft-patches/features/0085-Make-Item-tick-vanilla.patch new file mode 100644 index 00000000..e4e910de --- /dev/null +++ b/leaves-server/minecraft-patches/features/0085-Make-Item-tick-vanilla.patch @@ -0,0 +1,29 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 30 Oct 2023 10:43:44 +0800 +Subject: [PATCH] Make Item tick vanilla + + +diff --git a/net/minecraft/world/entity/item/ItemEntity.java b/net/minecraft/world/entity/item/ItemEntity.java +index 4f5db8203189a413f410fe7648e4995bae171918..e3cdb823b6d76e62725b6428651dacfe20fc4154 100644 +--- a/net/minecraft/world/entity/item/ItemEntity.java ++++ b/net/minecraft/world/entity/item/ItemEntity.java +@@ -133,6 +133,9 @@ public class ItemEntity extends Entity implements TraceableEntity { + // Paper start - EAR 2 + @Override + public void inactiveTick() { ++ // Leaves start - vanilla ++ this.tick(); ++ /* + super.inactiveTick(); + if (this.pickupDelay > 0 && this.pickupDelay != 32767) { + this.pickupDelay--; +@@ -150,6 +153,8 @@ public class ItemEntity extends Entity implements TraceableEntity { + // CraftBukkit end + this.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause + } ++ */ ++ // Leaves end - vanilla + } + // Paper end - EAR 2 + diff --git a/patches/server/0100-Copper-Bulb-1-gt-delay.patch b/leaves-server/minecraft-patches/features/0086-Copper-Bulb-1-gt-delay.patch similarity index 65% rename from patches/server/0100-Copper-Bulb-1-gt-delay.patch rename to leaves-server/minecraft-patches/features/0086-Copper-Bulb-1-gt-delay.patch index 26f6c426..7b91a05d 100644 --- a/patches/server/0100-Copper-Bulb-1-gt-delay.patch +++ b/leaves-server/minecraft-patches/features/0086-Copper-Bulb-1-gt-delay.patch @@ -4,34 +4,34 @@ Date: Tue, 12 Dec 2023 12:09:16 +0800 Subject: [PATCH] Copper Bulb 1 gt delay -diff --git a/src/main/java/net/minecraft/world/level/block/CopperBulbBlock.java b/src/main/java/net/minecraft/world/level/block/CopperBulbBlock.java -index 7c6481fc6e84f6477a4d113a222cae51cfa9053a..228fdf12ca2c85134efa527862f8901205a9064e 100644 ---- a/src/main/java/net/minecraft/world/level/block/CopperBulbBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/CopperBulbBlock.java +diff --git a/net/minecraft/world/level/block/CopperBulbBlock.java b/net/minecraft/world/level/block/CopperBulbBlock.java +index d3d3ad8bb33313e735eec559f854fb76bdf84cc2..4e346b1e77042a17bb28fb510196ff3bbde5bc92 100644 +--- a/net/minecraft/world/level/block/CopperBulbBlock.java ++++ b/net/minecraft/world/level/block/CopperBulbBlock.java @@ -32,14 +32,26 @@ public class CopperBulbBlock extends Block { @Override - protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { - if (oldState.getBlock() != state.getBlock() && world instanceof ServerLevel serverLevel) { + protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean movedByPiston) { + if (oldState.getBlock() != state.getBlock() && level instanceof ServerLevel serverLevel) { - this.checkAndFlip(state, serverLevel, pos); + // Leaves start - copper bulb 1 gt delay + if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.copperBulb1gt) { + this.checkAndFlip(state, serverLevel, pos); + } else { -+ world.scheduleTick(pos, this, 1); ++ level.scheduleTick(pos, this, 1); + } + // Leaves end - copper bulb 1 gt delay } } @Override - protected void neighborChanged(BlockState state, Level world, BlockPos pos, Block sourceBlock, @Nullable Orientation wireOrientation, boolean notify) { - if (world instanceof ServerLevel serverLevel) { + protected void neighborChanged(BlockState state, Level level, BlockPos pos, Block neighborBlock, @Nullable Orientation orientation, boolean movedByPiston) { + if (level instanceof ServerLevel serverLevel) { - this.checkAndFlip(state, serverLevel, pos); + // Leaves start - copper bulb 1 gt + if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.copperBulb1gt) { + this.checkAndFlip(state, serverLevel, pos); + } else { -+ world.scheduleTick(pos, this, 1); ++ level.scheduleTick(pos, this, 1); + } + // Leaves end - copper bulb 1 gt delay } diff --git a/leaves-server/minecraft-patches/features/0087-Crafter-1-gt-delay.patch b/leaves-server/minecraft-patches/features/0087-Crafter-1-gt-delay.patch new file mode 100644 index 00000000..f5b7ab2c --- /dev/null +++ b/leaves-server/minecraft-patches/features/0087-Crafter-1-gt-delay.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 12 Dec 2023 14:15:54 +0800 +Subject: [PATCH] Crafter 1 gt delay + + +diff --git a/net/minecraft/world/level/block/CrafterBlock.java b/net/minecraft/world/level/block/CrafterBlock.java +index 5f5966278faf86ed9b28955c80ba845c0cb75595..f5e3c1e4e13d14eac45622742391495ec6b3d925 100644 +--- a/net/minecraft/world/level/block/CrafterBlock.java ++++ b/net/minecraft/world/level/block/CrafterBlock.java +@@ -82,7 +82,7 @@ public class CrafterBlock extends BaseEntityBlock { + boolean triggeredValue = state.getValue(TRIGGERED); + BlockEntity blockEntity = level.getBlockEntity(pos); + if (hasNeighborSignal && !triggeredValue) { +- level.scheduleTick(pos, this, 4); ++ level.scheduleTick(pos, this, !org.leavesmc.leaves.LeavesConfig.modify.oldMC.crafter1gt ? 4 : 1); // Leaves - crafter 1 gt delay + level.setBlock(pos, state.setValue(TRIGGERED, Boolean.valueOf(true)), 2); + this.setBlockEntityTriggered(blockEntity, true); + } else if (!hasNeighborSignal && triggeredValue) { +@@ -132,7 +132,7 @@ public class CrafterBlock extends BaseEntityBlock { + @Override + public void setPlacedBy(Level level, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { + if (state.getValue(TRIGGERED)) { +- level.scheduleTick(pos, this, 4); ++ level.scheduleTick(pos, this, !org.leavesmc.leaves.LeavesConfig.modify.oldMC.crafter1gt ? 4 : 1); // Leaves - crafter 1 gt delay + } + } + diff --git a/leaves-server/minecraft-patches/features/0088-More-Region-Format-Support.patch b/leaves-server/minecraft-patches/features/0088-More-Region-Format-Support.patch new file mode 100644 index 00000000..615292dd --- /dev/null +++ b/leaves-server/minecraft-patches/features/0088-More-Region-Format-Support.patch @@ -0,0 +1,399 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 5 Feb 2025 22:04:41 +0800 +Subject: [PATCH] More Region Format Support + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java +index a814512fcfb85312474ae2c2c21443843bf57831..2b0349568f38321c893a8ffa16607350ac64a599 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 org.leavesmc.leaves.region.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); // Leaves - more region format + +- public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; ++ public org.leavesmc.leaves.region.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; // Leaves - more region 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 98fbc5c8044bd945d64569f13412a6e7e49a4e7f..01e9a0076632f3be8bd15fa6793ed6296fe4c2bb 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java +@@ -1260,7 +1260,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 org.leavesmc.leaves.region.IRegionFile regionFile = this.regionDataController.getCache().moonrise$getRegionFileIfLoaded(this.chunkX, this.chunkZ); // Leaves - more region format + if (regionFile != null) { + regionFile.flush(); + } // else: evicted from cache, which should have called flush +@@ -1470,7 +1470,7 @@ public final class MoonriseRegionFileIO { + + public static interface IORunnable { + +- public void run(final RegionFile regionFile) throws IOException; ++ public void run(final org.leavesmc.leaves.region.IRegionFile regionFile) throws IOException; // Leaves - more region 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..a6573e327ace16b7ea320eb1440ffcbc09c7a0fd 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 org.leavesmc.leaves.region.IRegionFile regionFile) throws IOException; // Leaves - more region format + } +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 438bbf36d4f80b38482b89e3ef59e9e48d0f62c2..164b8dc3f247aedd37bce24a7acb6ab1858e7d20 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -951,10 +951,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> progressMap = Reference2FloatMaps.synchronize(new Reference2FloatOpenHashMap<>()); + volatile Component status = Component.translatable("optimizeWorld.stage.counting"); +- static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mca$"); ++ static final Pattern REGEX = org.leavesmc.leaves.region.IRegionFileFactory.getRegionFileRegex(); // Leaves - more region format + final DimensionDataStorage overworldDataStorage; + + public WorldUpgrader( +@@ -261,7 +261,7 @@ public class WorldUpgrader implements AutoCloseable { + } + + private static List getAllChunkPositions(RegionStorageInfo regionStorageInfo, Path path) { +- File[] files = path.toFile().listFiles((directory, filename) -> filename.endsWith(".mca")); ++ File[] files = path.toFile().listFiles((directory, filename) -> org.leavesmc.leaves.region.IRegionFileFactory.isRegionFile(filename)); // Leaves - more region format + if (files == null) { + return List.of(); + } else { +@@ -274,7 +274,7 @@ public class WorldUpgrader implements AutoCloseable { + int i1 = Integer.parseInt(matcher.group(2)) << 5; + List list1 = Lists.newArrayList(); + +- try (RegionFile regionFile = new RegionFile(regionStorageInfo, file.toPath(), path, true)) { ++ try (org.leavesmc.leaves.region.IRegionFile regionFile = org.leavesmc.leaves.region.IRegionFileFactory.createRegionFile(regionStorageInfo, file.toPath(), path, true)) { // Leaves - more region format + for (int i2 = 0; i2 < 32; i2++) { + for (int i3 = 0; i3 < 32; i3++) { + ChunkPos chunkPos = new ChunkPos(i2 + i, i3 + i1); +@@ -322,7 +322,7 @@ public class WorldUpgrader implements AutoCloseable { + + protected abstract boolean tryProcessOnePosition(T chunkStorage, ChunkPos chunkPos, ResourceKey dimension); + +- private void onFileFinished(RegionFile regionFile) { ++ private void onFileFinished(org.leavesmc.leaves.region.IRegionFile regionFile) { // Leaves - more region format + if (WorldUpgrader.this.recreateRegionFiles) { + if (this.previousWriteFuture != null) { + this.previousWriteFuture.join(); +@@ -424,7 +424,7 @@ public class WorldUpgrader implements AutoCloseable { + } + } + +- record FileToUpgrade(RegionFile file, List chunksToUpgrade) { ++ record FileToUpgrade(org.leavesmc.leaves.region.IRegionFile file, List chunksToUpgrade) { // Leaves - more region format + } + + class PoiUpgrader extends WorldUpgrader.SimpleRegionStorageUpgrader { +diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java +index 0c41177462cca5c4bbab6490e323b9535fd6300f..bb6b482271da00586acb438fd7648f8f4cd5586b 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.util.profiling.jfr.JvmProfiler; + import net.minecraft.world.level.ChunkPos; + 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 org.leavesmc.leaves.region.IRegionFile { // Paper - rewrite chunk system // Leaves - more region 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; +@@ -124,7 +124,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + } + + // note: only call for CHUNK regionfiles +- boolean recalculateHeader() throws IOException { ++ public boolean recalculateHeader() throws IOException { // Leaves - more region format + if (!this.canRecalcHeader) { + return false; + } +@@ -786,7 +786,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche + } + } + +- protected synchronized void write(ChunkPos chunkPos, ByteBuffer chunkData) throws IOException { ++ public synchronized void write(ChunkPos chunkPos, ByteBuffer chunkData) throws IOException { // Leaves - more region format + int offsetIndex = getOffsetIndex(chunkPos); + int i = this.offsets.get(offsetIndex); + int sectorNumber = getSectorNumber(i); +@@ -904,7 +904,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 org.leavesmc.leaves.region.IRegionFile regionFile) throws IOException { // Leaves - more region format + regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count)); + } + // Paper end - rewrite chunk system +@@ -970,11 +970,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) { // Leaves - more region 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 { // Leaves - more region format + final int offset = getChunkIndex(x, z); + boolean previous = this.oversized[offset] == 1; + this.oversized[offset] = (byte) (oversized ? 1 : 0); +@@ -1013,7 +1013,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 { // Leaves - more region 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 dad7f94b611cf0fc68b1a3878c458233f6bb6d61..2689ba38ba9d2703078cd4f9f2c6d17610260bb3 100644 +--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java ++++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java +@@ -18,7 +18,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; +- public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); ++ public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap<>(); // Leaves - more region format + private final RegionStorageInfo info; + private final Path folder; + private final boolean sync; +@@ -33,7 +33,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.") || !org.leavesmc.leaves.region.IRegionFileFactory.isRegionFile(fileName)) { // Leaves - more region format + return null; + } + +@@ -54,7 +54,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + } + // Paper end + // Paper start - rewrite chunk system +- private static final int REGION_SHIFT = 5; ++ public static final int REGION_SHIFT = 5; // Leaves - private -> public + 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) { +@@ -93,15 +93,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 org.leavesmc.leaves.region.IRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { // Leaves - more region format + return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT)); + } + + @Override +- public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { ++ public synchronized final org.leavesmc.leaves.region.IRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { // Leaves - more region format + final long key = ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); + +- RegionFile ret = this.regionCache.getAndMoveToFirst(key); ++ org.leavesmc.leaves.region.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); + if (ret != null) { + return ret; + } +@@ -114,18 +114,28 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + this.regionCache.removeLast().close(); + } + +- final Path regionPath = this.folder.resolve(getRegionFileName(chunkX, chunkZ)); ++ // Leaves start - more region format ++ Path regionPath = null; ++ for (String fileName : org.leavesmc.leaves.region.IRegionFileFactory.getRegionFileName(chunkX, chunkZ)) { ++ regionPath = this.folder.resolve(fileName); ++ if (java.nio.file.Files.exists(regionPath)) { ++ break; ++ } else { ++ regionPath = null; ++ } ++ } + +- if (!java.nio.file.Files.exists(regionPath)) { ++ if (regionPath == null) { + this.markNonExisting(key); + return null; + } ++ // Leaves end - more region format + + this.createRegionFile(key); + + FileUtil.createDirectoriesSafe(this.folder); + +- ret = new RegionFile(this.info, regionPath, this.folder, this.sync); ++ ret = org.leavesmc.leaves.region.IRegionFileFactory.createRegionFile(this.info, regionPath, this.folder, this.sync); // Leaves - more region format + + this.regionCache.putAndMoveToFirst(key, ret); + +@@ -144,7 +154,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + } + + final ChunkPos pos = new ChunkPos(chunkX, chunkZ); +- final RegionFile regionFile = this.getRegionFile(pos); ++ final org.leavesmc.leaves.region.IRegionFile regionFile = this.getRegionFile(pos); // Leaves - more region 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) +@@ -178,7 +188,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 org.leavesmc.leaves.region.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Leaves - more region format + if (regionFile != null) { + regionFile.clear(pos); + } // else: didn't exist +@@ -193,7 +203,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 org.leavesmc.leaves.region.IRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Leaves - more region format + + final DataInputStream input = regionFile == null ? null : regionFile.getChunkDataInputStream(new ChunkPos(chunkX, chunkZ)); + +@@ -237,7 +247,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + } + // Paper end - rewrite chunk system + // Paper start - rewrite chunk system +- public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { ++ public org.leavesmc.leaves.region.IRegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { // Leaves - more region format + return this.getRegionFile(chunkcoordintpair, false); + } + // Paper end - rewrite chunk system +@@ -249,7 +259,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + this.isChunkData = isChunkDataFolder(this.folder); // Paper - recalculate region file headers + } + +- @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private RegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit ++ @org.jetbrains.annotations.Contract("_, false -> !null") @Nullable private org.leavesmc.leaves.region.IRegionFile getRegionFile(ChunkPos chunkPos, boolean existingOnly) throws IOException { // CraftBukkit // Leaves - more region format + // Paper start - rewrite chunk system + if (existingOnly) { + return this.moonrise$getRegionFileIfExists(chunkPos.x, chunkPos.z); +@@ -257,7 +267,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + synchronized (this) { + final long key = ChunkPos.asLong(chunkPos.x >> REGION_SHIFT, chunkPos.z >> REGION_SHIFT); + +- RegionFile ret = this.regionCache.getAndMoveToFirst(key); ++ org.leavesmc.leaves.region.IRegionFile ret = this.regionCache.getAndMoveToFirst(key); // Leaves - more region format + if (ret != null) { + return ret; + } +@@ -266,13 +276,22 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + this.regionCache.removeLast().close(); + } + +- final Path regionPath = this.folder.resolve(getRegionFileName(chunkPos.x, chunkPos.z)); ++ // Leaves start - more region format ++ Path regionPath = this.folder.resolve(org.leavesmc.leaves.region.IRegionFileFactory.getFirstRegionFileName(chunkPos.x, chunkPos.z)); ++ for (String fileName : org.leavesmc.leaves.region.IRegionFileFactory.getRegionFileName(chunkPos.x, chunkPos.z)) { ++ Path newRegionPath = this.folder.resolve(fileName); ++ if (java.nio.file.Files.exists(newRegionPath)) { ++ regionPath = newRegionPath; ++ break; ++ } ++ } ++ // Leaves end - more region format + + this.createRegionFile(key); + + FileUtil.createDirectoriesSafe(this.folder); + +- ret = new RegionFile(this.info, regionPath, this.folder, this.sync); ++ ret = org.leavesmc.leaves.region.IRegionFileFactory.createRegionFile(this.info, regionPath, this.folder, this.sync); // Leaves - more region format + + this.regionCache.putAndMoveToFirst(key, ret); + +@@ -286,7 +305,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + org.apache.logging.log4j.LogManager.getLogger().fatal(msg + " (" + file.toString().replaceAll(".+[\\\\/]", "") + " - " + x + "," + z + ") Go clean it up to remove this message. /minecraft:tp " + (x<<4)+" 128 "+(z<<4) + " - 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."); + } + +- private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { ++ private static CompoundTag readOversizedChunk(org.leavesmc.leaves.region.IRegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // Leaves - more region format + synchronized (regionfile) { + try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) { + CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z); +@@ -321,7 +340,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + @Nullable + public CompoundTag read(ChunkPos chunkPos) throws IOException { + // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing +- RegionFile regionFile = this.getRegionFile(chunkPos, true); ++ org.leavesmc.leaves.region.IRegionFile regionFile = this.getRegionFile(chunkPos, true); // Leaves - more region format + if (regionFile == null) { + return null; + } +@@ -360,7 +379,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + + public void scanChunk(ChunkPos chunkPos, StreamTagVisitor visitor) throws IOException { + // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing +- RegionFile regionFile = this.getRegionFile(chunkPos, true); ++ org.leavesmc.leaves.region.IRegionFile regionFile = this.getRegionFile(chunkPos, true); // Leaves - more region format + if (regionFile == null) { + return; + } +@@ -374,7 +393,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise + } + + public void write(ChunkPos chunkPos, @Nullable CompoundTag chunkData) throws IOException { // Paper - rewrite chunk system - public +- RegionFile regionFile = this.getRegionFile(chunkPos, chunkData == null); // CraftBukkit // Paper - rewrite chunk system ++ org.leavesmc.leaves.region.IRegionFile regionFile = this.getRegionFile(chunkPos, chunkData == null); // CraftBukkit // Paper - rewrite chunk system // Leaves - more region format + // Paper start - rewrite chunk system + if (regionFile == null) { + // if the RegionFile doesn't exist, no point in deleting from it +@@ -404,7 +423,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 org.leavesmc.leaves.region.IRegionFile regionFile : this.regionCache.values()) { // Leaves - more region format + try { + regionFile.close(); + } catch (final IOException ex) { +@@ -420,7 +439,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 org.leavesmc.leaves.region.IRegionFile regionFile : this.regionCache.values()) { // Leaves - more region format + try { + regionFile.flush(); + } catch (final IOException ex) { diff --git a/leaves-server/minecraft-patches/features/0089-No-TNT-place-update.patch b/leaves-server/minecraft-patches/features/0089-No-TNT-place-update.patch new file mode 100644 index 00000000..0b8e96f7 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0089-No-TNT-place-update.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 18 Jan 2024 12:31:31 +0800 +Subject: [PATCH] No TNT place update + + +diff --git a/net/minecraft/world/level/block/TntBlock.java b/net/minecraft/world/level/block/TntBlock.java +index 9dca05fb66ce3ff9e0c3e31978bf4d5d9e139b8e..bc960220f4d9854a73990ce06c598a86e56fa558 100644 +--- a/net/minecraft/world/level/block/TntBlock.java ++++ b/net/minecraft/world/level/block/TntBlock.java +@@ -45,7 +45,7 @@ public class TntBlock extends Block { + @Override + protected void onPlace(BlockState state, Level level, BlockPos pos, BlockState oldState, boolean isMoving) { + if (!oldState.is(state.getBlock())) { +- if (level.hasNeighborSignal(pos) && org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.REDSTONE, null, null)) { // CraftBukkit - TNTPrimeEvent ++ if (!org.leavesmc.leaves.LeavesConfig.modify.noTNTPlaceUpdate & level.hasNeighborSignal(pos) && org.bukkit.craftbukkit.event.CraftEventFactory.callTNTPrimeEvent(level, pos, org.bukkit.event.block.TNTPrimeEvent.PrimeCause.REDSTONE, null, null)) { // CraftBukkit - TNTPrimeEvent + // Paper start - TNTPrimeEvent + org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(level, pos); + if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.REDSTONE, null).callEvent()) { diff --git a/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch b/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch new file mode 100644 index 00000000..301a67b5 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 5 Feb 2025 23:11:32 +0800 +Subject: [PATCH] Servux Protocol + + +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 1deedd3fc28792fc368580973038c1824b15691d..8dc71b01b9e7970729a82c68402eec9375dbae04 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -2189,6 +2189,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + this.lastSpawnChunkRadius = i; ++ org.leavesmc.leaves.protocol.servux.ServuxStructuresProtocol.refreshSpawnMetadata = true; // Leaves - servux + } + + public LongSet getForcedChunks() { diff --git a/patches/server/0105-Placing-locked-hopper-no-longer-send-NC-updates.patch b/leaves-server/minecraft-patches/features/0091-Placing-locked-hopper-no-longer-send-NC-updates.patch similarity index 72% rename from patches/server/0105-Placing-locked-hopper-no-longer-send-NC-updates.patch rename to leaves-server/minecraft-patches/features/0091-Placing-locked-hopper-no-longer-send-NC-updates.patch index c9f07b69..972ca1f2 100644 --- a/patches/server/0105-Placing-locked-hopper-no-longer-send-NC-updates.patch +++ b/leaves-server/minecraft-patches/features/0091-Placing-locked-hopper-no-longer-send-NC-updates.patch @@ -4,11 +4,11 @@ Date: Sat, 20 Jan 2024 02:50:56 +0800 Subject: [PATCH] Placing locked hopper no longer send NC updates -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index bb5a19253fdb46fabb0da2faa2a7d608fa79e279..56031f83d19fec3a48a44ddd3146c2d02a154cb7 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -1202,7 +1202,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl +diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java +index 2d0b54c56f89f05a3babf257ca17e6ca32f54d96..9b5e0c569cc17475f6cb4a5c7bed3e39b9ae5b5e 100644 +--- a/net/minecraft/world/level/Level.java ++++ b/net/minecraft/world/level/Level.java +@@ -1213,7 +1213,11 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl } if ((i & 1) != 0) { diff --git a/leaves-server/minecraft-patches/features/0092-Renewable-deepslate.patch b/leaves-server/minecraft-patches/features/0092-Renewable-deepslate.patch new file mode 100644 index 00000000..ecc4cf38 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0092-Renewable-deepslate.patch @@ -0,0 +1,32 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 22 Jan 2024 11:15:53 +0800 +Subject: [PATCH] Renewable deepslate + + +diff --git a/net/minecraft/world/level/block/LiquidBlock.java b/net/minecraft/world/level/block/LiquidBlock.java +index 19d1906e9d4e92ff49a833bca03a7308ee8059e3..928b4196d4fe359dffa11be3b32a52761378a347 100644 +--- a/net/minecraft/world/level/block/LiquidBlock.java ++++ b/net/minecraft/world/level/block/LiquidBlock.java +@@ -190,7 +190,7 @@ public class LiquidBlock extends Block implements BucketPickup { + for (Direction direction : POSSIBLE_FLOW_DIRECTIONS) { + BlockPos blockPos = pos.relative(direction.getOpposite()); + if (level.getFluidState(blockPos).is(FluidTags.WATER)) { +- Block block = level.getFluidState(pos).isSource() ? Blocks.OBSIDIAN : Blocks.COBBLESTONE; ++ Block block = level.getFluidState(pos).isSource() ? Blocks.OBSIDIAN : (org.leavesmc.leaves.LeavesConfig.modify.renewableDeepslate && level.dimension() == Level.OVERWORLD && pos.getY() < 0 ? Blocks.COBBLED_DEEPSLATE : Blocks.COBBLESTONE); // Leaves - renewable deepslate + // CraftBukkit start + if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level, pos, block.defaultBlockState())) { + this.fizz(level, pos); +diff --git a/net/minecraft/world/level/material/LavaFluid.java b/net/minecraft/world/level/material/LavaFluid.java +index 6f135caffb7638c6156f00341aeac12b50cea99d..b4116b109e251e878b42e16edbaea07ac8256294 100644 +--- a/net/minecraft/world/level/material/LavaFluid.java ++++ b/net/minecraft/world/level/material/LavaFluid.java +@@ -211,7 +211,7 @@ public abstract class LavaFluid extends FlowingFluid { + if (this.is(FluidTags.LAVA) && fluidState1.is(FluidTags.WATER)) { + if (blockState.getBlock() instanceof LiquidBlock) { + // CraftBukkit start +- if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level.getMinecraftWorld(), pos, Blocks.STONE.defaultBlockState(), 3)) { ++ if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(level.getMinecraftWorld(), pos, org.leavesmc.leaves.LeavesConfig.modify.renewableDeepslate && level.getMinecraftWorld().dimension() == Level.OVERWORLD && pos.getY() < 0 ? Blocks.DEEPSLATE.defaultBlockState() : Blocks.STONE.defaultBlockState(), 3)) { // Leaves - renewable deepslate + return; + } + // CraftBukkit end diff --git a/patches/server/0107-Renewable-sponges.patch b/leaves-server/minecraft-patches/features/0093-Renewable-sponges.patch similarity index 64% rename from patches/server/0107-Renewable-sponges.patch rename to leaves-server/minecraft-patches/features/0093-Renewable-sponges.patch index 8792e3e4..e2f6f9c4 100644 --- a/patches/server/0107-Renewable-sponges.patch +++ b/leaves-server/minecraft-patches/features/0093-Renewable-sponges.patch @@ -4,21 +4,21 @@ Date: Mon, 22 Jan 2024 11:41:13 +0800 Subject: [PATCH] Renewable sponges -diff --git a/src/main/java/net/minecraft/world/entity/monster/Guardian.java b/src/main/java/net/minecraft/world/entity/monster/Guardian.java -index 951f46684623582980901c1ebc1870aa5bcf25a1..4c42999ae3f92ede0e7908e9bb4b4efa59b63cfb 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/Guardian.java -+++ b/src/main/java/net/minecraft/world/entity/monster/Guardian.java -@@ -342,6 +342,28 @@ public class Guardian extends Monster { - +diff --git a/net/minecraft/world/entity/monster/Guardian.java b/net/minecraft/world/entity/monster/Guardian.java +index c8e249b8f7ee8e9c075169ec988f5a0d459a3767..a4b9511114ff7422691962b7a01d3084dae1ecc1 100644 +--- a/net/minecraft/world/entity/monster/Guardian.java ++++ b/net/minecraft/world/entity/monster/Guardian.java +@@ -452,6 +452,28 @@ public class Guardian extends Monster { + } } + // Leaves start - renewable sponges + @Override -+ public void thunderHit(net.minecraft.server.level.ServerLevel world, net.minecraft.world.entity.LightningBolt lightning) { ++ public void thunderHit(net.minecraft.server.level.ServerLevel level, net.minecraft.world.entity.LightningBolt lightning) { + if (org.leavesmc.leaves.LeavesConfig.modify.renewableSponges && !this.isRemoved() && !(this instanceof ElderGuardian)) { + ElderGuardian elderGuardian = new ElderGuardian(EntityType.ELDER_GUARDIAN ,this.level()); + elderGuardian.moveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); -+ elderGuardian.finalizeSpawn(world ,this.level().getCurrentDifficultyAt(elderGuardian.blockPosition()), EntitySpawnReason.CONVERSION, null); ++ elderGuardian.finalizeSpawn(level ,this.level().getCurrentDifficultyAt(elderGuardian.blockPosition()), EntitySpawnReason.CONVERSION, null); + elderGuardian.setNoAi(this.isNoAi()); + + if (this.hasCustomName()) { @@ -29,11 +29,11 @@ index 951f46684623582980901c1ebc1870aa5bcf25a1..4c42999ae3f92ede0e7908e9bb4b4efa + this.level().addFreshEntity(elderGuardian); + this.discard(); + } else { -+ super.thunderHit(world, lightning); ++ super.thunderHit(level, lightning); + } + } + // Leaves end - renewable sponges + - private static class GuardianMoveControl extends MoveControl { - + static class GuardianMoveControl extends MoveControl { private final Guardian guardian; + diff --git a/leaves-server/minecraft-patches/features/0094-Renewable-coral.patch b/leaves-server/minecraft-patches/features/0094-Renewable-coral.patch new file mode 100644 index 00000000..fb0b1c8b --- /dev/null +++ b/leaves-server/minecraft-patches/features/0094-Renewable-coral.patch @@ -0,0 +1,109 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 5 Feb 2025 23:42:22 +0800 +Subject: [PATCH] Renewable coral + + +diff --git a/net/minecraft/world/level/block/CoralFanBlock.java b/net/minecraft/world/level/block/CoralFanBlock.java +index 9530fa0793ed0904b17f8972d09f98a97b2cecb0..0bc0c1f7f86f0893b571218c313c49ea90790a34 100644 +--- a/net/minecraft/world/level/block/CoralFanBlock.java ++++ b/net/minecraft/world/level/block/CoralFanBlock.java +@@ -13,7 +13,7 @@ import net.minecraft.world.level.block.state.BlockBehaviour; + import net.minecraft.world.level.block.state.BlockState; + import net.minecraft.world.level.material.Fluids; + +-public class CoralFanBlock extends BaseCoralFanBlock { ++public class CoralFanBlock extends BaseCoralFanBlock implements org.leavesmc.leaves.util.FertilizableCoral { // Leaves - renewable coral + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( + instance -> instance.group(CoralBlock.DEAD_CORAL_FIELD.forGetter(coralFanBlock -> coralFanBlock.deadBlock), propertiesCodec()) + .apply(instance, CoralFanBlock::new) +@@ -69,4 +69,11 @@ public class CoralFanBlock extends BaseCoralFanBlock { + return super.updateShape(state, level, scheduledTickAccess, pos, direction, neighborPos, neighborState, random); + } + } ++ ++ // Leaves start - renewable coral ++ @Override ++ public boolean isEnabled() { ++ return org.leavesmc.leaves.LeavesConfig.modify.renewableCoral == org.leavesmc.leaves.LeavesConfig.ModifyConfig.RenewableCoralType.EXPANDED; ++ } ++ // Leaves end - renewable coral + } +diff --git a/net/minecraft/world/level/block/CoralPlantBlock.java b/net/minecraft/world/level/block/CoralPlantBlock.java +index d1a4dd8ef1ad9d7fb8ae434fc096a9d5b9c5b69d..27df4f7ab514600aaa8dd65a3fee8c280a915a46 100644 +--- a/net/minecraft/world/level/block/CoralPlantBlock.java ++++ b/net/minecraft/world/level/block/CoralPlantBlock.java +@@ -16,7 +16,7 @@ import net.minecraft.world.level.material.Fluids; + import net.minecraft.world.phys.shapes.CollisionContext; + import net.minecraft.world.phys.shapes.VoxelShape; + +-public class CoralPlantBlock extends BaseCoralPlantTypeBlock { ++public class CoralPlantBlock extends BaseCoralPlantTypeBlock implements org.leavesmc.leaves.util.FertilizableCoral { // Leaves - renewable coral + public static final MapCodec CODEC = RecordCodecBuilder.mapCodec( + instance -> instance.group(CoralBlock.DEAD_CORAL_FIELD.forGetter(coralPlantBlock -> coralPlantBlock.deadBlock), propertiesCodec()) + .apply(instance, CoralPlantBlock::new) +@@ -79,4 +79,12 @@ public class CoralPlantBlock extends BaseCoralPlantTypeBlock { + protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { + return SHAPE; + } ++ ++ // Leaves start - renewable coral ++ @Override ++ public boolean isEnabled() { ++ return org.leavesmc.leaves.LeavesConfig.modify.renewableCoral == org.leavesmc.leaves.LeavesConfig.ModifyConfig.RenewableCoralType.EXPANDED ++ || org.leavesmc.leaves.LeavesConfig.modify.renewableCoral == org.leavesmc.leaves.LeavesConfig.ModifyConfig.RenewableCoralType.TRUE; ++ } ++ // Leaves end - renewable coral + } +diff --git a/net/minecraft/world/level/levelgen/feature/CoralClawFeature.java b/net/minecraft/world/level/levelgen/feature/CoralClawFeature.java +index abb8519ede1c2271c98497a8e3336f0ad89c340d..f5009d112b32ce91296ed84310bb9bf48bc0e408 100644 +--- a/net/minecraft/world/level/levelgen/feature/CoralClawFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/CoralClawFeature.java +@@ -17,7 +17,7 @@ public class CoralClawFeature extends CoralFeature { + } + + @Override +- protected boolean placeFeature(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state) { ++ public boolean placeFeature(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state) { // Leaves - protected -> public + if (!this.placeCoralBlock(level, random, pos, state)) { + return false; + } else { +diff --git a/net/minecraft/world/level/levelgen/feature/CoralFeature.java b/net/minecraft/world/level/levelgen/feature/CoralFeature.java +index d62e64a7663141a5e7223fb6c1358f3799ec1c3e..f0a9a461bfa9b128cc71537e58e0f1ade02a6698 100644 +--- a/net/minecraft/world/level/levelgen/feature/CoralFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/CoralFeature.java +@@ -31,7 +31,7 @@ public abstract class CoralFeature extends Feature { + return !optional.isEmpty() && this.placeFeature(worldGenLevel, randomSource, blockPos, optional.get().defaultBlockState()); + } + +- protected abstract boolean placeFeature(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state); ++ public abstract boolean placeFeature(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state); // Leaves - protected -> public + + protected boolean placeCoralBlock(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state) { + BlockPos blockPos = pos.above(); +diff --git a/net/minecraft/world/level/levelgen/feature/CoralMushroomFeature.java b/net/minecraft/world/level/levelgen/feature/CoralMushroomFeature.java +index cd550c704cbe46727caf965d7910d2a1d55f5697..29c5c8ed793834dd5bbafb89e48670a30dc8184d 100644 +--- a/net/minecraft/world/level/levelgen/feature/CoralMushroomFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/CoralMushroomFeature.java +@@ -14,7 +14,7 @@ public class CoralMushroomFeature extends CoralFeature { + } + + @Override +- protected boolean placeFeature(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state) { ++ public boolean placeFeature(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state) { // Leaves - protected -> public + int i = random.nextInt(3) + 3; + int i1 = random.nextInt(3) + 3; + int i2 = random.nextInt(3) + 3; +diff --git a/net/minecraft/world/level/levelgen/feature/CoralTreeFeature.java b/net/minecraft/world/level/levelgen/feature/CoralTreeFeature.java +index 521dbd30e9fb7a366d534fe6a952b0e3a8ec8449..13a0c047c86563d0f28866d513c52999bfce6fd1 100644 +--- a/net/minecraft/world/level/levelgen/feature/CoralTreeFeature.java ++++ b/net/minecraft/world/level/levelgen/feature/CoralTreeFeature.java +@@ -15,7 +15,7 @@ public class CoralTreeFeature extends CoralFeature { + } + + @Override +- protected boolean placeFeature(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state) { ++ public boolean placeFeature(LevelAccessor level, RandomSource random, BlockPos pos, BlockState state) { // Leaves - protected -> public + BlockPos.MutableBlockPos mutableBlockPos = pos.mutable(); + int i = random.nextInt(3) + 1; + diff --git a/leaves-server/minecraft-patches/features/0095-Fast-resume.patch b/leaves-server/minecraft-patches/features/0095-Fast-resume.patch new file mode 100644 index 00000000..fa2b30a9 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0095-Fast-resume.patch @@ -0,0 +1,80 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 5 Feb 2025 23:42:44 +0800 +Subject: [PATCH] Fast resume + + +diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +index b5817aa8f537593f6d9fc6b612c82ccccb250ac7..920d925159ad8088de3e751591d464d22ffd2c06 100644 +--- a/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java ++++ b/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java +@@ -603,6 +603,49 @@ public final class ChunkHolderManager { + } + } + ++ // Leaves start - add custom ticket ++ public void addTicketAtLevelCustom(final Ticket ticket, final long chunk, final boolean lock) { ++ final long removeDelay = ticket.moonrise$getRemoveDelay(); ++ ++ final int chunkX = CoordinateUtils.getChunkX(chunk); ++ final int chunkZ = CoordinateUtils.getChunkZ(chunk); ++ ++ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; ++ try { ++ final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> SortedArraySet.create(4)); ++ ++ final int levelBefore = getTicketLevelAt(ticketsAtChunk); ++ final Ticket current = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$replace(ticket); ++ final int levelAfter = getTicketLevelAt(ticketsAtChunk); ++ ++ if (current != ticket) { ++ final long oldRemoveDelay = ((ChunkSystemTicket) current).moonrise$getRemoveDelay(); ++ if (removeDelay != oldRemoveDelay) { ++ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) { ++ this.removeExpireCount(chunkX, chunkZ); ++ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) { ++ // since old != new, we have that NO_TIMEOUT_MARKER != new ++ this.addExpireCount(chunkX, chunkZ); ++ } ++ } ++ } else { ++ if (removeDelay != NO_TIMEOUT_MARKER) { ++ this.addExpireCount(chunkX, chunkZ); ++ } ++ } ++ ++ if (levelBefore != levelAfter) { ++ this.updateTicketLevel(chunk, levelAfter); ++ } ++ ++ } finally { ++ if (ticketLock != null) { ++ this.ticketLockArea.unlock(ticketLock); ++ } ++ } ++ } ++ // Leaves end - add custom ticket ++ + public boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { + return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); + } +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index 164b8dc3f247aedd37bce24a7acb6ab1858e7d20..26e6fc29125fb129cbeb4c736dff222d4fe648b9 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -744,6 +744,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Thu, 6 Feb 2025 00:05:23 +0800 +Subject: [PATCH] Old hopper suckin behavior + + +diff --git a/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +index 5c29416b677d6b19db6bba43f69b465feb2dcb3d..c9aa375b7e584f6fa300a9e11528237fa336519d 100644 +--- a/net/minecraft/world/level/block/entity/HopperBlockEntity.java ++++ b/net/minecraft/world/level/block/entity/HopperBlockEntity.java +@@ -593,7 +593,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen + + return false; + } else { +- boolean flag = hopper.isGridAligned() && blockState.isCollisionShapeFullBlock(level, blockPos) && !blockState.is(BlockTags.DOES_NOT_BLOCK_HOPPERS); ++ boolean flag = hopper.isGridAligned() && (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldHopperSuckInBehavior || blockState.isCollisionShapeFullBlock(level, blockPos)) && !blockState.is(BlockTags.DOES_NOT_BLOCK_HOPPERS); // Leaves - oldHopperSuckInBehavior + if (!flag) { + for (ItemEntity itemEntity : getItemsAtAndAbove(level, hopper)) { + if (addItem(hopper, itemEntity)) { diff --git a/leaves-server/minecraft-patches/features/0098-Fix-falling-block-s-block-location.patch b/leaves-server/minecraft-patches/features/0098-Fix-falling-block-s-block-location.patch new file mode 100644 index 00000000..9b565d7d --- /dev/null +++ b/leaves-server/minecraft-patches/features/0098-Fix-falling-block-s-block-location.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Fri, 16 Feb 2024 19:50:03 +0800 +Subject: [PATCH] Fix falling block's block location + + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index ab63de942e3a34a84666877a202c4a191391a28b..1afedf53e6159a08371c1e8ee41f1dbe84a043ba 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -4828,6 +4828,15 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + int floor = Mth.floor(x); + int floor1 = Mth.floor(y); + int floor2 = Mth.floor(z); ++ ++ // Leaves start - fix falling block location ++ if (this instanceof net.minecraft.world.entity.item.FallingBlockEntity) { ++ if (y < 0.0 && y + 1e-10 > 0.0) { ++ floor1 = 0; ++ } ++ } ++ // Leaves end - fix falling block location ++ + if (floor != this.blockPosition.getX() || floor1 != this.blockPosition.getY() || floor2 != this.blockPosition.getZ()) { + this.blockPosition = new BlockPos(floor, floor1, floor2); + this.inBlockState = null; diff --git a/leaves-server/minecraft-patches/features/0099-Bytebuf-API.patch b/leaves-server/minecraft-patches/features/0099-Bytebuf-API.patch new file mode 100644 index 00000000..55aec10d --- /dev/null +++ b/leaves-server/minecraft-patches/features/0099-Bytebuf-API.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> +Date: Thu, 6 Feb 2025 00:14:22 +0800 +Subject: [PATCH] Bytebuf API + + +diff --git a/net/minecraft/network/chat/Component.java b/net/minecraft/network/chat/Component.java +index c6e4f72825c868b416ce2e81fc6d9b5bfdbd85f2..5c4d0a9297387503f48cb4c1d6db6aedc6ad8258 100644 +--- a/net/minecraft/network/chat/Component.java ++++ b/net/minecraft/network/chat/Component.java +@@ -248,7 +248,7 @@ public interface Component extends Message, FormattedText, Iterable { + .getOrThrow(JsonParseException::new); + } + +- static JsonElement serialize(Component component, HolderLookup.Provider provider) { ++ public static JsonElement serialize(Component component, HolderLookup.Provider provider) { // Leaves - package -> public + return ComponentSerialization.CODEC + .encodeStart(provider.createSerializationContext(JsonOps.INSTANCE), component) + .getOrThrow(JsonParseException::new); +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 01bab5294ae27624d8941d55bbe676cb1ea73966..3a0a2df3511b23990ca402ca5b032c2ca7a7d1de 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -445,6 +445,12 @@ public abstract class PlayerList { + return; + } + ++ // Leaves start - Bytebuf API ++ if (!(player instanceof org.leavesmc.leaves.bot.ServerBot) && !(player instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { ++ this.cserver.getBytebufHandler().injectPlayer(player); ++ } ++ // Leaves end - Bytebuf API ++ + org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol + + // Leaves start - bot support diff --git a/leaves-server/minecraft-patches/features/0100-Allow-grindstone-overstacking.patch b/leaves-server/minecraft-patches/features/0100-Allow-grindstone-overstacking.patch new file mode 100644 index 00000000..1d483723 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0100-Allow-grindstone-overstacking.patch @@ -0,0 +1,63 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Lumine1909 <133463833+lumine1909@users.noreply.github.com> +Date: Wed, 26 Jun 2024 17:59:56 +0800 +Subject: [PATCH] Allow grindstone overstacking + + +diff --git a/net/minecraft/world/inventory/AbstractContainerMenu.java b/net/minecraft/world/inventory/AbstractContainerMenu.java +index bca4a8528ec362471797c2d962cd3c903cae89f2..07da1f42bd11236e037bd8cc25e79eae93fd20ae 100644 +--- a/net/minecraft/world/inventory/AbstractContainerMenu.java ++++ b/net/minecraft/world/inventory/AbstractContainerMenu.java +@@ -736,10 +736,15 @@ public abstract class AbstractContainerMenu { + public abstract boolean stillValid(Player player); + + protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection) { ++ // Leaves start - Add force move + // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent + return this.moveItemStackTo(stack, startIndex, endIndex, reverseDirection, false); + } + protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection, boolean isCheck) { ++ return this.moveItemStackTo(stack, startIndex, endIndex, reverseDirection, isCheck, false); ++ } ++ protected boolean moveItemStackTo(ItemStack stack, int startIndex, int endIndex, boolean reverseDirection, boolean isCheck, boolean forceMove) { ++ // Leaves end - Add force move + if (isCheck) { + stack = stack.copy(); + } +@@ -804,6 +809,14 @@ public abstract class AbstractContainerMenu { + // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent + if (itemx.isEmpty() && slotx.mayPlace(stack)) { + int i1 = slotx.getMaxStackSize(stack); ++ // Leaves start - Add force move ++ if (forceMove) { ++ slotx.setByPlayer(stack.split(stack.getCount())); ++ slotx.setChanged(); ++ flag = true; ++ break; ++ } ++ // Leaves end - Add force move + // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent + if (isCheck) { + stack.shrink(Math.min(stack.getCount(), i1)); +diff --git a/net/minecraft/world/inventory/GrindstoneMenu.java b/net/minecraft/world/inventory/GrindstoneMenu.java +index f85bd2a90c2694d96f67cc3701a9bbf081fe8475..5245ba03d101b51be33ca4952e8c8f788483d3cc 100644 +--- a/net/minecraft/world/inventory/GrindstoneMenu.java ++++ b/net/minecraft/world/inventory/GrindstoneMenu.java +@@ -178,7 +178,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + int i2 = i + i1 + max * 5 / 100; + int i3 = 1; + if (!inputItem.isDamageableItem()) { +- if (inputItem.getMaxStackSize() < 2 || !ItemStack.matches(inputItem, additionalItem)) { ++ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.allowGrindstoneOverstacking && inputItem.getMaxStackSize() < 2 || !ItemStack.matches(inputItem, additionalItem)) { // Leaves - allowGrindstoneOverstaking + return ItemStack.EMPTY; + } + +@@ -247,7 +247,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { + ItemStack item1 = this.repairSlots.getItem(0); + ItemStack item2 = this.repairSlots.getItem(1); + if (index == 2) { +- if (!this.moveItemStackTo(item, 3, 39, true)) { ++ if (!this.moveItemStackTo(item, 3, 39, true, false, org.leavesmc.leaves.LeavesConfig.modify.oldMC.allowGrindstoneOverstacking)) { // Leaves - allowGrindstoneOverstacking: Disable stack check + return ItemStack.EMPTY; + } + diff --git a/leaves-server/minecraft-patches/features/0101-Configurable-MC-67.patch b/leaves-server/minecraft-patches/features/0101-Configurable-MC-67.patch new file mode 100644 index 00000000..91969f26 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0101-Configurable-MC-67.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Lumine1909 <133463833+lumine1909@users.noreply.github.com> +Date: Wed, 26 Jun 2024 18:15:57 +0800 +Subject: [PATCH] Configurable MC-67 + + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 1afedf53e6159a08371c1e8ee41f1dbe84a043ba..6653e6b802785b040de1df686d93eef8ab6c0e6a 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -3987,6 +3987,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + + public boolean canTeleport(Level fromLevel, Level toLevel) { ++ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.allowEntityPortalWithPassenger && (this.isPassenger() || this.isVehicle())) return false; // Leaves - allowEntityPortalWithPassenger + if (!this.isAlive() || !this.valid) return false; // Paper - Fix item duplication and teleport issues + if (fromLevel.dimension() == Level.END && toLevel.dimension() == Level.OVERWORLD) { + for (Entity entity : this.getPassengers()) { diff --git a/leaves-server/minecraft-patches/features/0102-Disable-end-gateway-portal-entity-ticking.patch b/leaves-server/minecraft-patches/features/0102-Disable-end-gateway-portal-entity-ticking.patch new file mode 100644 index 00000000..cfec1a62 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0102-Disable-end-gateway-portal-entity-ticking.patch @@ -0,0 +1,27 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 1 Jul 2024 22:09:33 +0800 +Subject: [PATCH] Disable end gateway portal entity ticking + + +diff --git a/net/minecraft/world/level/block/EndGatewayBlock.java b/net/minecraft/world/level/block/EndGatewayBlock.java +index 5c6bc04c241f13a8c09527a39f89fa52feb0eb76..e686b2d9ef8bd7fe200226faef9f52f4feeeaed3 100644 +--- a/net/minecraft/world/level/block/EndGatewayBlock.java ++++ b/net/minecraft/world/level/block/EndGatewayBlock.java +@@ -122,11 +122,14 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal { + if (portalPosition == null) { + return null; + } else { ++ // Leaves start - Disable end gateway portal entity ticking ++ TeleportTransition.PostTeleportTransition postTeleportTransition = org.leavesmc.leaves.LeavesConfig.modify.oldMC.disableGatewayPortalEntityTicking ? TeleportTransition.DO_NOTHING : TeleportTransition.PLACE_PORTAL_TICKET; + return entity instanceof ThrownEnderpearl +- ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit ++ ? new TeleportTransition(level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Set.of(), postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY) // CraftBukkit + : new TeleportTransition( +- level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit ++ level, portalPosition, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), postTeleportTransition, org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_GATEWAY // CraftBukkit + ); ++ // Leaves end - Disable end gateway portal entity ticking + } + } else { + return null; diff --git a/leaves-server/minecraft-patches/features/0103-Disable-crystal-portal-proximity-check.patch b/leaves-server/minecraft-patches/features/0103-Disable-crystal-portal-proximity-check.patch new file mode 100644 index 00000000..7a2dbb42 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0103-Disable-crystal-portal-proximity-check.patch @@ -0,0 +1,64 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Bluemangoo +Date: Fri, 19 Jul 2024 15:04:03 +0800 +Subject: [PATCH] Disable crystal-portal proximity check + + +diff --git a/net/minecraft/world/item/EndCrystalItem.java b/net/minecraft/world/item/EndCrystalItem.java +index 1bc4c8be71b445f64134548a85fd81442298c0f1..a1d8b38b20efd7ef0fa6a8d92d218fada66d446e 100644 +--- a/net/minecraft/world/item/EndCrystalItem.java ++++ b/net/minecraft/world/item/EndCrystalItem.java +@@ -27,7 +27,7 @@ public class EndCrystalItem extends Item { + if (!blockState.is(Blocks.OBSIDIAN) && !blockState.is(Blocks.BEDROCK)) { + return InteractionResult.FAIL; + } else { +- BlockPos blockPos = clickedPos.above(); final BlockPos aboveBlockPosition = blockPos; // Paper - OBFHELPER ++ BlockPos blockPos = clickedPos.above(); // final BlockPos aboveBlockPosition = blockPos; // Paper - OBFHELPER // Leaves + if (!level.isEmptyBlock(blockPos)) { + return InteractionResult.FAIL; + } else { +@@ -51,7 +51,7 @@ public class EndCrystalItem extends Item { + level.gameEvent(context.getPlayer(), GameEvent.ENTITY_PLACE, blockPos); + EndDragonFight dragonFight = ((ServerLevel)level).getDragonFight(); + if (dragonFight != null) { +- dragonFight.tryRespawn(aboveBlockPosition); // Paper - Perf: Do crystal-portal proximity check before entity lookup ++ dragonFight.tryRespawn(); // dragonFight.tryRespawn(aboveBlockPosition); // Paper - Perf: Do crystal-portal proximity check before entity lookup // Leaves + } + } + +diff --git a/net/minecraft/world/level/dimension/end/EndDragonFight.java b/net/minecraft/world/level/dimension/end/EndDragonFight.java +index 5a28a821bb401f8f1465b085c9ffda52ba9a0a9f..b03c5fde5cc62a26f8bb7632b1a684755a6e5649 100644 +--- a/net/minecraft/world/level/dimension/end/EndDragonFight.java ++++ b/net/minecraft/world/level/dimension/end/EndDragonFight.java +@@ -575,12 +575,14 @@ public class EndDragonFight { + } + + public boolean tryRespawn() { // CraftBukkit - return boolean ++ /* Leaves + // Paper start - Perf: Do crystal-portal proximity check before entity lookup + return this.tryRespawn(null); + } + + public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal + // Paper end - Perf: Do crystal-portal proximity check before entity lookup ++ */ + if (this.dragonKilled && this.respawnStage == null) { + BlockPos blockPos = this.portalLocation; + if (blockPos == null) { +@@ -595,6 +597,7 @@ public class EndDragonFight { + + blockPos = this.portalLocation; + } ++ /* Leaves + // Paper start - Perf: Do crystal-portal proximity check before entity lookup + if (placedEndCrystalPos != null) { + // The end crystal must be 0 or 1 higher than the portal origin +@@ -610,7 +613,7 @@ public class EndDragonFight { + } + } + // Paper end - Perf: Do crystal-portal proximity check before entity lookup +- ++ */ + + List list = Lists.newArrayList(); + BlockPos blockPos1 = blockPos.above(1); diff --git a/patches/server/0118-Can-disable-LivingEntity-aiStep-alive-check.patch b/leaves-server/minecraft-patches/features/0104-Can-disable-LivingEntity-aiStep-alive-check.patch similarity index 55% rename from patches/server/0118-Can-disable-LivingEntity-aiStep-alive-check.patch rename to leaves-server/minecraft-patches/features/0104-Can-disable-LivingEntity-aiStep-alive-check.patch index cfd86593..b83a195f 100644 --- a/patches/server/0118-Can-disable-LivingEntity-aiStep-alive-check.patch +++ b/leaves-server/minecraft-patches/features/0104-Can-disable-LivingEntity-aiStep-alive-check.patch @@ -4,11 +4,11 @@ Date: Mon, 22 Jul 2024 18:30:42 +0800 Subject: [PATCH] Can disable LivingEntity aiStep alive check -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index b5a3606b0810290fbbd624a401977e24d5a6a178..c42367fd5971c124b3307738a3921f276c3ef570 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -3327,7 +3327,7 @@ public abstract class LivingEntity extends Entity implements Attackable { +diff --git a/net/minecraft/world/entity/LivingEntity.java b/net/minecraft/world/entity/LivingEntity.java +index 0cd9a4ff2c480774487bd82531996a35db81ae14..a007bfc5630739f4ea85d7e9ef0d67d2a027823e 100644 +--- a/net/minecraft/world/entity/LivingEntity.java ++++ b/net/minecraft/world/entity/LivingEntity.java +@@ -3166,7 +3166,7 @@ public abstract class LivingEntity extends Entity implements Attackable { } } diff --git a/patches/server/0120-Fix-fortress-mob-spawn.patch b/leaves-server/minecraft-patches/features/0105-Fix-fortress-mob-spawn.patch similarity index 62% rename from patches/server/0120-Fix-fortress-mob-spawn.patch rename to leaves-server/minecraft-patches/features/0105-Fix-fortress-mob-spawn.patch index d8d797b0..cd26b040 100644 --- a/patches/server/0120-Fix-fortress-mob-spawn.patch +++ b/leaves-server/minecraft-patches/features/0105-Fix-fortress-mob-spawn.patch @@ -4,12 +4,12 @@ Date: Mon, 29 Jul 2024 08:20:31 +0800 Subject: [PATCH] Fix fortress mob spawn -diff --git a/src/main/java/net/minecraft/world/level/biome/MobSpawnSettings.java b/src/main/java/net/minecraft/world/level/biome/MobSpawnSettings.java -index cb7465ed9bdebe1b31f02d11725e75ff8b44ca66..bc4b7bc3fe55b8c9c34a8bc22a035a3c4891a2af 100644 ---- a/src/main/java/net/minecraft/world/level/biome/MobSpawnSettings.java -+++ b/src/main/java/net/minecraft/world/level/biome/MobSpawnSettings.java -@@ -176,6 +176,14 @@ public class MobSpawnSettings { - this.maxCount = maxGroupSize; +diff --git a/net/minecraft/world/level/biome/MobSpawnSettings.java b/net/minecraft/world/level/biome/MobSpawnSettings.java +index 2d256b201df3afa1cf5879fdc931a1aa684110e4..6b8903bc4ec86302d6b5444742db22518fe91a8e 100644 +--- a/net/minecraft/world/level/biome/MobSpawnSettings.java ++++ b/net/minecraft/world/level/biome/MobSpawnSettings.java +@@ -175,6 +175,14 @@ public class MobSpawnSettings { + this.maxCount = maxCount; } + // Leaves start - fix fortress mob spawn diff --git a/leaves-server/minecraft-patches/features/0106-Fix-FallingBlockEntity-Duplicate.patch b/leaves-server/minecraft-patches/features/0106-Fix-FallingBlockEntity-Duplicate.patch new file mode 100644 index 00000000..302be77b --- /dev/null +++ b/leaves-server/minecraft-patches/features/0106-Fix-FallingBlockEntity-Duplicate.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 31 Jul 2024 12:51:44 +0800 +Subject: [PATCH] Fix FallingBlockEntity Duplicate + + +diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java +index 5746587666c7cb788764aab2f6ccf0f3ac5c282f..35d5dd7ca192ae384ecfd858e7c898c9f4b841b4 100644 +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -401,7 +401,7 @@ public class FallingBlockEntity extends Entity { + ResourceKey resourceKey1 = this.level().dimension(); + boolean flag = (resourceKey1 == Level.END || resourceKey == Level.END) && resourceKey1 != resourceKey; + Entity entity = super.teleport(teleportTransition); +- this.forceTickAfterTeleportToDuplicate = entity != null && flag && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation; // Paper ++ this.forceTickAfterTeleportToDuplicate = entity != null && flag; // Paper // Leaves + return entity; + } + } diff --git a/leaves-server/minecraft-patches/features/0107-Old-BlockEntity-behaviour.patch b/leaves-server/minecraft-patches/features/0107-Old-BlockEntity-behaviour.patch new file mode 100644 index 00000000..8e6855fe --- /dev/null +++ b/leaves-server/minecraft-patches/features/0107-Old-BlockEntity-behaviour.patch @@ -0,0 +1,90 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 14 Aug 2024 01:48:14 +0800 +Subject: [PATCH] Old BlockEntity behaviour + + +diff --git a/net/minecraft/world/level/block/ChiseledBookShelfBlock.java b/net/minecraft/world/level/block/ChiseledBookShelfBlock.java +index 1411c6b0471281827b9d21958c0c7d962809898f..14da3105d094e7c701e20e57e49bdb4b1791d3fd 100644 +--- a/net/minecraft/world/level/block/ChiseledBookShelfBlock.java ++++ b/net/minecraft/world/level/block/ChiseledBookShelfBlock.java +@@ -192,7 +192,14 @@ public class ChiseledBookShelfBlock extends BaseEntityBlock { + } + + chiseledBookShelfBlockEntity.clearContent(); +- flag = true; ++ // Leaves start - behaviour 1.21.1- ++ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) { ++ flag = false; ++ level.updateNeighbourForOutputSignal(pos, this); ++ } else { ++ flag = true; ++ } ++ // Leaves end - behaviour 1.21.1- + } else { + flag = false; + } +diff --git a/net/minecraft/world/level/block/LecternBlock.java b/net/minecraft/world/level/block/LecternBlock.java +index 82fcae1f8b4149f13adf5118287718812518f8bf..8d5f555421c5856ef8af36136b275c03e4b0a25f 100644 +--- a/net/minecraft/world/level/block/LecternBlock.java ++++ b/net/minecraft/world/level/block/LecternBlock.java +@@ -237,10 +237,11 @@ public class LecternBlock extends BaseEntityBlock { + this.popBook(state, level, pos); + } + +- super.onRemove(state, level, pos, newState, isMoving); ++ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, level, pos, newState, isMoving); // Leaves - behaviour 1.21.1- + if (state.getValue(POWERED)) { + updateBelow(level, pos, state); + } ++ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, level, pos, newState, isMoving); // Leaves - behaviour 1.21.1- + } + } + +diff --git a/net/minecraft/world/level/block/SculkSensorBlock.java b/net/minecraft/world/level/block/SculkSensorBlock.java +index 2fd1bae800b7dca0a58b572095e2c58012412481..bec8053c0f0cb5dad19b1d1e3a03dff7fd76f5eb 100644 +--- a/net/minecraft/world/level/block/SculkSensorBlock.java ++++ b/net/minecraft/world/level/block/SculkSensorBlock.java +@@ -138,10 +138,11 @@ public class SculkSensorBlock extends BaseEntityBlock implements SimpleWaterlogg + @Override + protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean movedByPiston) { + if (!state.is(newState.getBlock())) { +- super.onRemove(state, level, pos, newState, movedByPiston); ++ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, level, pos, newState, movedByPiston); // Leaves - behaviour 1.21.1- + if (getPhase(state) == SculkSensorPhase.ACTIVE) { + updateNeighbours(level, pos, state); + } ++ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, level, pos, newState, movedByPiston); // Leaves - behaviour 1.21.1- + } + } + +diff --git a/net/minecraft/world/level/block/ShulkerBoxBlock.java b/net/minecraft/world/level/block/ShulkerBoxBlock.java +index fca50f68625050daabcae3a3b615cf88cce41111..64a93d68418fbc2228e4de09e2b288e0db503e5c 100644 +--- a/net/minecraft/world/level/block/ShulkerBoxBlock.java ++++ b/net/minecraft/world/level/block/ShulkerBoxBlock.java +@@ -178,10 +178,11 @@ public class ShulkerBoxBlock extends BaseEntityBlock { + protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { + if (!state.is(newState.getBlock())) { + BlockEntity blockEntity = level.getBlockEntity(pos); +- super.onRemove(state, level, pos, newState, isMoving); ++ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, level, pos, newState, isMoving); // Leaves - behaviour 1.21.1- + if (blockEntity instanceof ShulkerBoxBlockEntity) { + level.updateNeighbourForOutputSignal(pos, state.getBlock()); + } ++ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, level, pos, newState, isMoving); // Leaves - behaviour 1.21.1- + } + } + +diff --git a/net/minecraft/world/level/block/entity/BlockEntity.java b/net/minecraft/world/level/block/entity/BlockEntity.java +index 77618757c0e678532dbab814aceed83f7f1cd892..6c6fad160cf206bca2b37a7bbe2a69b508ecac7d 100644 +--- a/net/minecraft/world/level/block/entity/BlockEntity.java ++++ b/net/minecraft/world/level/block/entity/BlockEntity.java +@@ -55,7 +55,7 @@ public abstract class BlockEntity { + } + + public boolean isValidBlockState(BlockState state) { +- return this.type.isValid(state); ++ return org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour || this.type.isValid(state); // Leaves - behaviour 1.21.1- + } + + public static BlockPos getPosFromTag(CompoundTag tag) { diff --git a/leaves-server/minecraft-patches/features/0108-Revert-raid-changes.patch b/leaves-server/minecraft-patches/features/0108-Revert-raid-changes.patch new file mode 100644 index 00000000..4b100301 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0108-Revert-raid-changes.patch @@ -0,0 +1,70 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: huanli233 <392352840@qq.com> +Date: Wed, 23 Oct 2024 23:10:48 +0800 +Subject: [PATCH] Revert raid changes + + +diff --git a/net/minecraft/world/effect/BadOmenMobEffect.java b/net/minecraft/world/effect/BadOmenMobEffect.java +index 80f17f33f670018240c854df589cf90cdeab6e70..4dea5d3898c82c54e7c40a6946038fcef3366289 100644 +--- a/net/minecraft/world/effect/BadOmenMobEffect.java ++++ b/net/minecraft/world/effect/BadOmenMobEffect.java +@@ -22,6 +22,11 @@ class BadOmenMobEffect extends MobEffect { + && !serverPlayer.isSpectator() + && level.getDifficulty() != Difficulty.PEACEFUL + && level.isVillage(serverPlayer.blockPosition())) { ++ // Leaves start - Revert raid changes ++ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.raid.allowBadOmenTriggerRaid) { ++ return level.getRaids().createOrExtendRaid(serverPlayer, serverPlayer.blockPosition()) != null; ++ } ++ // Leaves end - Revert raid changes + Raid raidAt = level.getRaidAt(serverPlayer.blockPosition()); + if (raidAt == null || raidAt.getRaidOmenLevel() < raidAt.getMaxRaidOmenLevel()) { + serverPlayer.addEffect(new MobEffectInstance(MobEffects.RAID_OMEN, 600, amplifier)); +diff --git a/net/minecraft/world/entity/raid/Raider.java b/net/minecraft/world/entity/raid/Raider.java +index 72551960faaedbf43d1c65ad061f6d366028a3e9..494623f69693af2833104fb3ec48acf5c31bedb0 100644 +--- a/net/minecraft/world/entity/raid/Raider.java ++++ b/net/minecraft/world/entity/raid/Raider.java +@@ -125,6 +125,43 @@ public abstract class Raider extends PatrollingMonster { + + currentRaid.removeFromRaid(this, false); + } ++ ++ // Leaves start - Revert raid changes ++ if (this.level() instanceof ServerLevel serverLevel) { ++ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.raid.giveBadOmenWhenKillPatrolLeader && raid == null && serverLevel.getRaidAt(this.blockPosition()) == null) { ++ ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); ++ net.minecraft.world.entity.player.Player entityhuman = null; ++ if (entity instanceof net.minecraft.world.entity.player.Player player) { ++ entityhuman = player; ++ } else if (entity instanceof net.minecraft.world.entity.animal.Wolf wolf) { ++ LivingEntity entityliving = wolf.getOwner(); ++ if (wolf.isTame() && entityliving instanceof net.minecraft.world.entity.player.Player player) { ++ entityhuman = player; ++ } ++ } ++ ++ if (entityhuman != null && !itemstack.isEmpty() && this.isCaptain()) { ++ net.minecraft.world.effect.MobEffectInstance mobeffect = entityhuman.getEffect(net.minecraft.world.effect.MobEffects.BAD_OMEN); ++ int i = 1; ++ ++ if (mobeffect != null) { ++ i += mobeffect.getAmplifier(); ++ entityhuman.removeEffectNoUpdate(net.minecraft.world.effect.MobEffects.BAD_OMEN); ++ } else { ++ --i; ++ } ++ ++ i = net.minecraft.util.Mth.clamp(i, 0, 4); ++ net.minecraft.world.effect.MobEffectInstance mobeffect1 = new net.minecraft.world.effect.MobEffectInstance(net.minecraft.world.effect.MobEffects.BAD_OMEN, 120000, i, false, false, true); ++ ++ if (!serverLevel.getGameRules().getBoolean(net.minecraft.world.level.GameRules.RULE_DISABLE_RAIDS)) { ++ entityhuman.addEffect(mobeffect1, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN); // CraftBukkit ++ } ++ this.setPatrolLeader(false); ++ } ++ } ++ } ++ // Leaves end - Revert raid changes + } + + super.die(cause); diff --git a/patches/server/0124-Allow-anvil-destroy-item-entities.patch b/leaves-server/minecraft-patches/features/0109-Allow-anvil-destroy-item-entities.patch similarity index 50% rename from patches/server/0124-Allow-anvil-destroy-item-entities.patch rename to leaves-server/minecraft-patches/features/0109-Allow-anvil-destroy-item-entities.patch index 826d6c0b..e50f564d 100644 --- a/patches/server/0124-Allow-anvil-destroy-item-entities.patch +++ b/leaves-server/minecraft-patches/features/0109-Allow-anvil-destroy-item-entities.patch @@ -4,16 +4,16 @@ Date: Sat, 26 Oct 2024 19:53:17 +0800 Subject: [PATCH] Allow anvil destroy item entities -diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java -index e067e826aae963e30dd9e12e57e9a63912165b0d..8b34cbe4a1bbabbfb281e758e65f3dc03ba8db78 100644 ---- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java -+++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java -@@ -291,7 +291,7 @@ public class FallingBlockEntity extends Entity { - if (i < 0) { +diff --git a/net/minecraft/world/entity/item/FallingBlockEntity.java b/net/minecraft/world/entity/item/FallingBlockEntity.java +index 35d5dd7ca192ae384ecfd858e7c898c9f4b841b4..ce4995fe70e885aa7ad1cc191ccd4cdd5e5b9681 100644 +--- a/net/minecraft/world/entity/item/FallingBlockEntity.java ++++ b/net/minecraft/world/entity/item/FallingBlockEntity.java +@@ -278,7 +278,7 @@ public class FallingBlockEntity extends Entity { + if (ceil < 0) { return false; } else { - Predicate predicate = EntitySelector.NO_CREATIVE_OR_SPECTATOR.and(EntitySelector.LIVING_ENTITY_STILL_ALIVE); + Predicate predicate = org.leavesmc.leaves.LeavesConfig.modify.oldMC.allowAnvilDestroyItemEntities ? EntitySelector.NO_CREATIVE_OR_SPECTATOR : EntitySelector.NO_CREATIVE_OR_SPECTATOR.and(EntitySelector.LIVING_ENTITY_STILL_ALIVE); // Leaves - Allow anvil destroy item entities - Block block = this.blockState.getBlock(); - DamageSource damagesource1; - + DamageSource damageSource = this.blockState.getBlock() instanceof Fallable fallable + ? fallable.getFallDamageSource(this) + : this.damageSources().fallingBlock(this); diff --git a/patches/server/0125-Fix-Incorrect-Collision-Behavior-for-Block-Shape.patch b/leaves-server/minecraft-patches/features/0110-Fix-Incorrect-Collision-Behavior-for-Block-Shape.patch similarity index 81% rename from patches/server/0125-Fix-Incorrect-Collision-Behavior-for-Block-Shape.patch rename to leaves-server/minecraft-patches/features/0110-Fix-Incorrect-Collision-Behavior-for-Block-Shape.patch index b7ae5158..be3df604 100644 --- a/patches/server/0125-Fix-Incorrect-Collision-Behavior-for-Block-Shape.patch +++ b/leaves-server/minecraft-patches/features/0110-Fix-Incorrect-Collision-Behavior-for-Block-Shape.patch @@ -4,10 +4,10 @@ Date: Thu, 24 Oct 2024 23:10:34 +0800 Subject: [PATCH] Fix Incorrect Collision Behavior for Block Shape -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java -index fb251665cdbafab90c6ff5e1bcb34fc17124d4d9..1d67244809da1b178448478e4b92727660af4a64 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java -+++ b/src/main/java/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +diff --git a/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java b/ca/spottedleaf/moonrise/patches/collisions/CollisionUtil.java +index e04bd54744335fb5398c6e4f7ce8b981f35bfb7d..b3c9d4f5f2ebf8fe981999ea9465e0f5754c5d6f 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; } @@ -23,7 +23,7 @@ index fb251665cdbafab90c6ff5e1bcb34fc17124d4d9..1d67244809da1b178448478e4b927276 // 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 && -@@ -2042,7 +2050,10 @@ public final class CollisionUtil { +@@ -2015,7 +2023,10 @@ public final class CollisionUtil { AABB singleAABB = ((CollisionVoxelShape)blockCollision).moonrise$getSingleAABBRepresentation(); if (singleAABB != null) { singleAABB = singleAABB.move((double)blockX, (double)blockY, (double)blockZ); diff --git a/leaves-server/minecraft-patches/features/0111-Disable-vault-blacklist.patch b/leaves-server/minecraft-patches/features/0111-Disable-vault-blacklist.patch new file mode 100644 index 00000000..94bdd3f8 --- /dev/null +++ b/leaves-server/minecraft-patches/features/0111-Disable-vault-blacklist.patch @@ -0,0 +1,42 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: huanli233 <392352840@qq.com> +Date: Sun, 27 Oct 2024 15:57:37 +0800 +Subject: [PATCH] Disable vault blacklist + + +diff --git a/net/minecraft/world/level/block/entity/vault/VaultServerData.java b/net/minecraft/world/level/block/entity/vault/VaultServerData.java +index 9bf95455f93a17c53bf457d90fd30a3595227569..f580b2431edf8768a60c57de84da64682d017d8f 100644 +--- a/net/minecraft/world/level/block/entity/vault/VaultServerData.java ++++ b/net/minecraft/world/level/block/entity/vault/VaultServerData.java +@@ -61,7 +61,7 @@ public class VaultServerData { + } + + boolean hasRewardedPlayer(Player player) { +- return this.rewardedPlayers.contains(player.getUUID()); ++ return !org.leavesmc.leaves.LeavesConfig.modify.disableVaultBlacklist && this.rewardedPlayers.contains(player.getUUID()); // Leaves - disable vault blacklist + } + + @VisibleForTesting +@@ -70,6 +70,7 @@ public class VaultServerData { + addToRewardedPlayers(player.getUUID()); + } + public boolean addToRewardedPlayers(final java.util.UUID player) { ++ if (org.leavesmc.leaves.LeavesConfig.modify.disableVaultBlacklist) return false; // Leaves - disable vault blacklist + final boolean removed = this.rewardedPlayers.add(player); + // Paper end - Vault API + if (this.rewardedPlayers.size() > 128) { +diff --git a/net/minecraft/world/level/block/entity/vault/VaultSharedData.java b/net/minecraft/world/level/block/entity/vault/VaultSharedData.java +index 50d6ff126a35ce8613175b550dac50cd82c43f9d..13ba4407b639ca64224348dd898c30d1256b13fa 100644 +--- a/net/minecraft/world/level/block/entity/vault/VaultSharedData.java ++++ b/net/minecraft/world/level/block/entity/vault/VaultSharedData.java +@@ -68,8 +68,8 @@ public class VaultSharedData { + Set set = config.playerDetector() + .detect(level, config.entitySelector(), pos, deactivationRange, false) + .stream() +- .filter(uuid -> !serverData.getRewardedPlayers().contains(uuid)) +- .collect(Collectors.toSet()); ++ .filter(uuid -> org.leavesmc.leaves.LeavesConfig.modify.disableVaultBlacklist || !serverData.getRewardedPlayers().contains(uuid)) ++ .collect(Collectors.toSet()); // Leaves - disable vault blacklist + if (!this.connectedPlayers.equals(set)) { + this.connectedPlayers = set; + this.markDirty(); diff --git a/leaves-server/minecraft-patches/features/0112-Fix-EntityPortalExitEvent-logic.patch b/leaves-server/minecraft-patches/features/0112-Fix-EntityPortalExitEvent-logic.patch new file mode 100644 index 00000000..af3fc5cf --- /dev/null +++ b/leaves-server/minecraft-patches/features/0112-Fix-EntityPortalExitEvent-logic.patch @@ -0,0 +1,44 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 2 Jan 2025 22:01:17 +0800 +Subject: [PATCH] Fix EntityPortalExitEvent logic + + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index 6653e6b802785b040de1df686d93eef8ab6c0e6a..caddf2c1dcaca7884dbdb6af8311c6bfb9a7aa60 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -3789,19 +3789,29 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + } + if (this.portalProcess != null) { // if in a portal + org.bukkit.craftbukkit.entity.CraftEntity bukkitEntity = this.getBukkitEntity(); ++ org.bukkit.util.Vector after = org.bukkit.craftbukkit.util.CraftVector.toBukkit(velocity); // Leaves - fix + org.bukkit.event.entity.EntityPortalExitEvent event = new org.bukkit.event.entity.EntityPortalExitEvent( + bukkitEntity, + bukkitEntity.getLocation(), to.clone(), +- bukkitEntity.getVelocity(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(velocity) ++ bukkitEntity.getVelocity(), after // Leaves - fix + ); + event.callEvent(); + ++ // Leaves start - fix + // Only change the target if actually needed, since we reset relative flags +- if (!event.isCancelled() && event.getTo() != null && (!event.getTo().equals(event.getFrom()) || !event.getAfter().equals(event.getBefore()))) { +- to = event.getTo().clone(); +- velocity = org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getAfter()); ++ if (event.isCancelled() || (!to.equals(event.getTo()) || !after.equals(event.getAfter()))) { ++ if (!event.isCancelled()) { ++ if (event.getTo() != null) { ++ to = event.getTo().clone(); ++ } ++ velocity = org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getAfter()); ++ } else { ++ to = event.getFrom().clone(); ++ velocity = Vec3.ZERO; ++ } + teleportTransition = new TeleportTransition(((org.bukkit.craftbukkit.CraftWorld) to.getWorld()).getHandle(), org.bukkit.craftbukkit.util.CraftLocation.toVec3D(to), velocity, to.getYaw(), to.getPitch(), teleportTransition.missingRespawnBlock(), teleportTransition.asPassenger(), Set.of(), teleportTransition.postTeleportTransition(), teleportTransition.cause()); + } ++ // Leaves end - fix + } + if (this.isRemoved()) { + return null; diff --git a/leaves-server/minecraft-patches/features/0113-Fix-CraftPortalEvent-logic.patch b/leaves-server/minecraft-patches/features/0113-Fix-CraftPortalEvent-logic.patch new file mode 100644 index 00000000..53e3cf8e --- /dev/null +++ b/leaves-server/minecraft-patches/features/0113-Fix-CraftPortalEvent-logic.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 2 Jan 2025 22:01:36 +0800 +Subject: [PATCH] Fix CraftPortalEvent logic + + +diff --git a/net/minecraft/world/level/block/EndPortalBlock.java b/net/minecraft/world/level/block/EndPortalBlock.java +index 01cddd7001b4a7f99c1b1d147fac904d3064d733..e0999bda26b5a6b17076760f5b124c061a873b74 100644 +--- a/net/minecraft/world/level/block/EndPortalBlock.java ++++ b/net/minecraft/world/level/block/EndPortalBlock.java +@@ -102,13 +102,13 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { + } + + // CraftBukkit start +- org.bukkit.craftbukkit.event.CraftPortalEvent event = entity.callPortalEvent(entity, org.bukkit.craftbukkit.util.CraftLocation.toBukkit(bottomCenter, level1.getWorld(), f, entity.getXRot()), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL, 0, 0); ++ org.bukkit.craftbukkit.event.CraftPortalEvent event = entity.callPortalEvent(entity, org.bukkit.craftbukkit.util.CraftLocation.toBukkit(bottomCenter, level1.getWorld(), f, 0.0F), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL, 0, 0); // Leaves - fix + if (event == null) { + return null; + } + org.bukkit.Location to = event.getTo(); + +- return new TeleportTransition(((org.bukkit.craftbukkit.CraftWorld) to.getWorld()).getHandle(), org.bukkit.craftbukkit.util.CraftLocation.toVec3D(to), entity.getDeltaMovement(), to.getYaw(), to.getPitch(), set, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL); ++ return new TeleportTransition(((org.bukkit.craftbukkit.CraftWorld) to.getWorld()).getHandle(), org.bukkit.craftbukkit.util.CraftLocation.toVec3D(to), Vec3.ZERO, to.getYaw(), to.getPitch(), set, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), org.bukkit.event.player.PlayerTeleportEvent.TeleportCause.END_PORTAL); // Leaves - fix + // CraftBukkit end + } + } diff --git a/leaves-server/minecraft-patches/features/0114-Xaero-Map-Protocol.patch b/leaves-server/minecraft-patches/features/0114-Xaero-Map-Protocol.patch new file mode 100644 index 00000000..12a417ab --- /dev/null +++ b/leaves-server/minecraft-patches/features/0114-Xaero-Map-Protocol.patch @@ -0,0 +1,18 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Fri, 7 Feb 2025 14:23:43 +0800 +Subject: [PATCH] Xaero Map Protocol + + +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 9187e876a3e226a9f28af32630122f2ead72b6f3..2aafa4a3e5314b69bbb8deedb5df3a8979efc5d9 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -1323,6 +1323,7 @@ public abstract class PlayerList { + player.connection.send(new ClientboundInitializeBorderPacket(worldBorder)); + player.connection.send(new ClientboundSetTimePacket(level.getGameTime(), level.getDayTime(), level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT))); + player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(level.getSharedSpawnPos(), level.getSharedSpawnAngle())); ++ org.leavesmc.leaves.protocol.XaeroMapProtocol.onSendWorldInfo(player); // Leaves - xaero map protocol + if (level.isRaining()) { + // CraftBukkit start - handle player weather + // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F)); diff --git a/leaves-server/minecraft-patches/features/0115-Dont-save-Entity-tickCount.patch b/leaves-server/minecraft-patches/features/0115-Dont-save-Entity-tickCount.patch new file mode 100644 index 00000000..1d6b3b4e --- /dev/null +++ b/leaves-server/minecraft-patches/features/0115-Dont-save-Entity-tickCount.patch @@ -0,0 +1,31 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Fri, 7 Feb 2025 22:32:29 +0800 +Subject: [PATCH] Dont save Entity tickCount + +Storing just tickCount will only lead to incorrect entity behavior and will not bring any benefits. + +diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java +index caddf2c1dcaca7884dbdb6af8311c6bfb9a7aa60..14b09ee5d605ec8ff86165fdc0b7aeba310f833e 100644 +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -2464,7 +2464,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + if (this.maxAirTicks != this.getDefaultMaxAirSupply()) { + compound.putInt("Bukkit.MaxAirSupply", this.getMaxAirSupply()); + } +- compound.putInt("Spigot.ticksLived", this.tickCount); + // CraftBukkit end + Component customName = this.getCustomName(); + if (customName != null) { +@@ -2637,11 +2636,6 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess + throw new IllegalStateException("Entity has invalid rotation"); + } + // CraftBukkit start +- // Spigot start +- if (this instanceof net.minecraft.world.entity.LivingEntity) { +- this.tickCount = compound.getInt("Spigot.ticksLived"); +- } +- // Spigot end + this.persist = !compound.contains("Bukkit.persist") || compound.getBoolean("Bukkit.persist"); + this.visibleByDefault = !compound.contains("Bukkit.visibleByDefault") || compound.getBoolean("Bukkit.visibleByDefault"); + // SPIGOT-6907: re-implement LivingEntity#setMaximumAir() diff --git a/leaves-server/minecraft-patches/features/0116-Fix-Warden-GameEventListener-register-on-load.patch b/leaves-server/minecraft-patches/features/0116-Fix-Warden-GameEventListener-register-on-load.patch new file mode 100644 index 00000000..c7d6387f --- /dev/null +++ b/leaves-server/minecraft-patches/features/0116-Fix-Warden-GameEventListener-register-on-load.patch @@ -0,0 +1,20 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sat, 8 Feb 2025 17:43:55 +0800 +Subject: [PATCH] Fix Warden GameEventListener register on load + +I completely don't understand why it can be fixed, but it fixed + +diff --git a/net/minecraft/world/level/gameevent/DynamicGameEventListener.java b/net/minecraft/world/level/gameevent/DynamicGameEventListener.java +index 2b98932e69271571e6e9350c55c82edc858d76f6..5a0297759836de779c5230d3d19497cd7ac4b37d 100644 +--- a/net/minecraft/world/level/gameevent/DynamicGameEventListener.java ++++ b/net/minecraft/world/level/gameevent/DynamicGameEventListener.java +@@ -41,7 +41,7 @@ public class DynamicGameEventListener { + + private static void ifChunkExists(LevelReader level, @Nullable SectionPos sectionPos, Consumer dispatcherConsumer) { + if (sectionPos != null) { +- ChunkAccess chunk = level.getChunkIfLoadedImmediately(sectionPos.getX(), sectionPos.getZ()); // Paper - Perf: can cause sync loads while completing a chunk, resulting in deadlock ++ ChunkAccess chunk = level.getChunk(sectionPos.x(), sectionPos.z(), ChunkStatus.FULL, false); // Leaves - disable perf, to fix listener load + if (chunk != null) { + dispatcherConsumer.accept(chunk.getListenerRegistry(sectionPos.y())); + } diff --git a/leaves-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch b/leaves-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch new file mode 100644 index 00000000..4cb63e9a --- /dev/null +++ b/leaves-server/minecraft-patches/sources/net/minecraft/world/entity/Entity.java.patch @@ -0,0 +1,40 @@ +--- a/net/minecraft/world/entity/Entity.java ++++ b/net/minecraft/world/entity/Entity.java +@@ -346,6 +_,7 @@ + public void inactiveTick() { + } + // Paper end - EAR 2 ++ private CompoundTag leavesData = new CompoundTag(); // Leaves - Leaves ex data + + public void setOrigin(@javax.annotation.Nonnull org.bukkit.Location location) { + this.origin = location.toVector(); +@@ -2521,6 +_,7 @@ + compound.putBoolean("Paper.FreezeLock", true); + } + // Paper end ++ compound.put("Leaves.Data", leavesData); // Leaves - leaves ex data + return compound; + } catch (Throwable var9) { + CrashReport crashReport = CrashReport.forThrowable(var9, "Saving entity NBT"); +@@ -2670,6 +_,11 @@ + freezeLocked = compound.getBoolean("Paper.FreezeLock"); + } + // Paper end ++ // Leaves start - leaves ex data ++ if (compound.contains("Leaves.Data")) { ++ leavesData = compound.getCompound("Leaves.Data"); ++ } ++ // Leaves end - leaves ex data + } catch (Throwable var17) { + CrashReport crashReport = CrashReport.forThrowable(var17, "Loading entity NBT"); + CrashReportCategory crashReportCategory = crashReport.addCategory("Entity being loaded"); +@@ -5077,4 +_,9 @@ + return ((ServerLevel) this.level).isPositionEntityTicking(this.blockPosition()); + } + // Paper end - Expose entity id counter ++ // Leaves start - leaves ex data ++ public CompoundTag getLeavesData() { ++ return leavesData; ++ } ++ // Leaves end - leaves ex data + } diff --git a/patches/server/0001-Build-changes.patch b/leaves-server/paper-patches/features/0001-Build-changes.patch similarity index 86% rename from patches/server/0001-Build-changes.patch rename to leaves-server/paper-patches/features/0001-Build-changes.patch index 51fe86a5..af0f07a3 100644 --- a/patches/server/0001-Build-changes.patch +++ b/leaves-server/paper-patches/features/0001-Build-changes.patch @@ -1,105 +1,14 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 13 Sep 2022 16:58:47 +0800 +Date: Sun, 26 Jan 2025 13:58:50 +0800 Subject: [PATCH] Build changes -diff --git a/build.gradle.kts b/build.gradle.kts -index faf3e3fd72e8c915e7a4803dacbe1bb576c6663e..d88a9d1908373ba44143013cda1ae51477a835cf 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -25,7 +25,7 @@ abstract class MockitoAgentProvider : CommandLineArgumentProvider { - // Paper end - configure mockito agent that is needed in newer java versions - - dependencies { -- implementation(project(":paper-api")) -+ implementation(project(":leaves-api")) // Leaves - build change - // Paper start - 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 -@@ -86,6 +86,15 @@ paperweight { - craftBukkitPackageVersion.set("v1_21_R2") // also needs to be updated in MappingEnvironment - } - -+// Leaves start - hide irrelevant compilation warnings -+tasks.withType { -+ val compilerArgs = options.compilerArgs -+ compilerArgs.add("-Xlint:-module") -+ compilerArgs.add("-Xlint:-removal") -+ compilerArgs.add("-Xlint:-dep-ann") -+} -+// Leaves end - hide irrelevant compilation warnings -+ - tasks.jar { - archiveClassifier.set("dev") - -@@ -99,14 +108,14 @@ tasks.jar { - val gitBranch = git("rev-parse", "--abbrev-ref", "HEAD").getText().trim() // Paper - attributes( - "Main-Class" to "org.bukkit.craftbukkit.Main", -- "Implementation-Title" to "Paper", -+ "Implementation-Title" to "Leaves", // Leaves - "Implementation-Version" to implementationVersion, - "Implementation-Vendor" to date, // Paper -- "Specification-Title" to "Paper", -+ "Specification-Title" to "Leaves", // Leaves - "Specification-Version" to project.version, -- "Specification-Vendor" to "Paper Team", -- "Brand-Id" to "papermc:paper", -- "Brand-Name" to "Paper", -+ "Specification-Vendor" to "Leaves Team", // Leaves -+ "Brand-Id" to "leavesmc:leaves", // Leaves -+ "Brand-Name" to "Leaves", // Leaves - "Build-Number" to (build ?: ""), - "Build-Time" to Instant.now().toString(), - "Git-Branch" to gitBranch, // Paper -@@ -172,7 +181,7 @@ fun TaskContainer.registerRunTask( - name: String, - block: JavaExec.() -> Unit - ): TaskProvider = register(name) { -- group = "paper" -+ group = "paperweight" - mainClass.set("org.bukkit.craftbukkit.Main") - standardInput = System.`in` - workingDir = rootProject.layout.projectDirectory -@@ -241,13 +250,13 @@ tasks.registerRunTask("runReobfBundler") { - classpath(rootProject.tasks.named("createReobfBundlerJar").flatMap { it.outputZip }) - mainClass.set(null as String?) - } --tasks.registerRunTask("runPaperclip") { -- description = "Spin up a test server from the Mojang mapped Paperclip jar" -- classpath(rootProject.tasks.named("createMojmapPaperclipJar").flatMap { it.outputZip }) -+tasks.registerRunTask("runLeavesclip") { -+ description = "Spin up a test server from the Mojang mapped Leavesclip jar" -+ classpath(rootProject.tasks.named("createMojmapLeavesclipJar").flatMap { it.outputZip }) - mainClass.set(null as String?) - } --tasks.registerRunTask("runReobfPaperclip") { -- description = "Spin up a test server from the reobf Paperclip jar" -- classpath(rootProject.tasks.named("createReobfPaperclipJar").flatMap { it.outputZip }) -+tasks.registerRunTask("runReobfLeavesclip") { -+ description = "Spin up a test server from the reobf Leavesclip jar" -+ classpath(rootProject.tasks.named("createReobfLeavesclipJar").flatMap { it.outputZip }) - mainClass.set(null as String?) - } -diff --git a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java -index ca8b6a926dfff3fdd6b04228809a4480366120b2..f77f04f7312a528d3f898c6c4b8603ab295c14fd 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java -+++ b/src/main/java/ca/spottedleaf/moonrise/paper/PaperHooks.java -@@ -25,7 +25,7 @@ import net.minecraft.world.phys.AABB; - import java.util.List; - import java.util.function.Predicate; - --public final class PaperHooks implements PlatformHooks { -+public class PaperHooks implements PlatformHooks { // Leaves - not final - - @Override - public String getBrand() { diff --git a/src/main/java/com/destroystokyo/paper/Metrics.java b/src/main/java/com/destroystokyo/paper/Metrics.java -index 4b002e8b75d117b726b0de274a76d3596fce015b..f6b362894bbd0f0f09f0f51a931529d708ca9b9e 100644 +index 8f62879582195d8ae4f64bd23f752fa133b1c973..783fe9b3b2ca3ef6de91086a33264368164530eb 100644 --- a/src/main/java/com/destroystokyo/paper/Metrics.java +++ b/src/main/java/com/destroystokyo/paper/Metrics.java -@@ -593,7 +593,8 @@ public class Metrics { +@@ -592,7 +592,8 @@ public class Metrics { boolean logFailedRequests = config.getBoolean("logFailedRequests", false); // Only start Metrics, if it's enabled in the config if (config.getBoolean("enabled", true)) { @@ -109,15 +18,15 @@ index 4b002e8b75d117b726b0de274a76d3596fce015b..f6b362894bbd0f0f09f0f51a931529d7 metrics.addCustomChart(new Metrics.SimplePie("minecraft_version", () -> { String minecraftVersion = Bukkit.getVersion(); -@@ -601,17 +602,21 @@ public class Metrics { +@@ -600,17 +601,21 @@ public class Metrics { return minecraftVersion; })); + metrics.addCustomChart(new Metrics.SimplePie("leaves_version", () -> { -+ String serverVersion = Bukkit.getVersion(); -+ if (!serverVersion.startsWith("null")) { -+ String gitHash = serverVersion.substring("git-Leaves-".length()).split("[-\\s]")[0].replaceAll("\"", ""); -+ return "git-Leaves-" + Bukkit.getMinecraftVersion() + "-" + gitHash; ++ final String implVersion = org.bukkit.craftbukkit.Main.class.getPackage().getImplementationVersion(); ++ if (implVersion != null) { ++ final String buildOrHash = implVersion.substring(implVersion.lastIndexOf('-') + 1); ++ return "git-Leaves-" + Bukkit.getServer().getMinecraftVersion() + "-" + buildOrHash; + } + return "unknown"; + })); @@ -140,7 +49,7 @@ index 4b002e8b75d117b726b0de274a76d3596fce015b..f6b362894bbd0f0f09f0f51a931529d7 metrics.addCustomChart(new Metrics.DrilldownPie("java_version", () -> { Map> map = new HashMap<>(); -@@ -643,41 +648,8 @@ public class Metrics { +@@ -642,41 +647,8 @@ public class Metrics { return map; })); @@ -196,67 +105,16 @@ index 790bad0494454ca12ee152e3de6da3da634d9b20..c060857cb0551fff8f5033553b887f3a private static final String BUILD_DEV = "DEV"; -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 4158473fd553a16fec23bcbcf9a278d413120600..9af88810bc722d7cb6d00db5b239ad3e55df53c1 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1264,7 +1264,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop -Date: Thu, 24 Aug 2023 19:21:29 +0800 +Date: Sun, 26 Jan 2025 14:49:06 +0800 Subject: [PATCH] Delete Timings diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java -index 7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b..a1c9726d25479b5326fe2fa2b0f5a98d6b2da4c5 100644 +index 7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b..a589689e3a9de1fffef62e0e3dcd79bb2e848c5b 100644 --- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java @@ -1,6 +1,5 @@ @@ -28,12 +28,12 @@ index 7ce9ebba8ce304d1f3f21d4f15ee5f3560d7700b..a1c9726d25479b5326fe2fa2b0f5a98d } - EventExecutor executor = new TimedEventExecutor(EventExecutor.create(method, eventClass), plugin, method, eventClass); -+ EventExecutor executor = EventExecutor.create(method, eventClass); ++ EventExecutor executor = EventExecutor.create(method, eventClass); // Leaves - disable timing eventSet.add(new RegisteredListener(listener, executor, eh.priority(), plugin, eh.ignoreCancelled())); } return ret; diff --git a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java -index 097500a59336db1bbfffcd1aa4cff7a8586e46ec..35b00c139864dd7925d46a2d6a317d7e3aae9638 100644 +index 097500a59336db1bbfffcd1aa4cff7a8586e46ec..a40398e68e2239a182f36f5b886ac00deb7af082 100644 --- a/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java +++ b/src/main/java/io/papermc/paper/plugin/manager/PaperPluginManagerImpl.java @@ -232,7 +232,7 @@ public class PaperPluginManagerImpl implements PluginManager, DependencyContext @@ -41,21 +41,7 @@ index 097500a59336db1bbfffcd1aa4cff7a8586e46ec..35b00c139864dd7925d46a2d6a317d7e @Override public boolean useTimings() { - return co.aikar.timings.Timings.isTimingsEnabled(); -+ return false; ++ return false; // Leaves - disable timing } @Override -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 7f0d05c242c649b3f0482334306898dedd1d5c43..aed5d75b170612960997dc34463fafd3a6283d92 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -3,9 +3,6 @@ package net.minecraft.server; - import com.google.common.base.Preconditions; - import com.google.common.base.Splitter; - import com.google.common.collect.ImmutableList; --import co.aikar.timings.Timings; --import com.destroystokyo.paper.event.server.PaperServerListPingEvent; --import com.google.common.base.Stopwatch; - import com.google.common.collect.Lists; - import com.google.common.collect.Maps; - import com.google.common.collect.Sets; diff --git a/leaves-server/paper-patches/features/0003-Leaves-Server-Config-And-Command.patch b/leaves-server/paper-patches/features/0003-Leaves-Server-Config-And-Command.patch new file mode 100644 index 00000000..04b1d2d5 --- /dev/null +++ b/leaves-server/paper-patches/features/0003-Leaves-Server-Config-And-Command.patch @@ -0,0 +1,50 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sat, 1 Feb 2025 14:21:06 +0800 +Subject: [PATCH] Leaves Server Config And Command + + +diff --git a/src/main/java/io/papermc/paper/SparksFly.java b/src/main/java/io/papermc/paper/SparksFly.java +index 62e2d5704c348955bc8284dc2d54c933b7bcdd06..7ef20f0138fad39a1d23edd7b26ddc8816427ec8 100644 +--- a/src/main/java/io/papermc/paper/SparksFly.java ++++ b/src/main/java/io/papermc/paper/SparksFly.java +@@ -42,7 +42,7 @@ public final class SparksFly { + this.mainThreadTaskQueue = new ConcurrentLinkedQueue<>(); + this.logger = Logger.getLogger(ID); + this.logger.log(Level.INFO, "This server bundles the spark profiler. For more information please visit https://docs.papermc.io/paper/profiling"); +- this.spark = PaperSparkModule.create(Compatibility.VERSION_1_0, server, this.logger, new PaperScheduler() { ++ this.spark = org.leavesmc.leaves.spark.LeavesSparkPlugin.create(Compatibility.VERSION_1_0, server, this.logger, new PaperScheduler() { // Leaves - make leaves + @Override + public void executeAsync(final Runnable runnable) { + MCUtil.scheduleAsyncTask(this.catching(runnable, "asynchronous")); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index cf5bcb0ebc79c318d106695e39ad2883a5734aa1..c6fbb091c513fb9e5527ac9cb08af607f14468b4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1116,6 +1116,7 @@ public final class CraftServer implements Server { + playerMetadata.removeAll(plugin); + } + // Paper end ++ org.leavesmc.leaves.LeavesConfig.init((File) console.options.valueOf("leaves-settings")); // Leaves - Server Config + this.reloadData(); + org.spigotmc.SpigotConfig.registerCommands(); // Spigot + io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper +diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java +index c07481501358cd55c610cebd4891c804003795fa..b6a60aac952c1275da203215119d65934602c113 100644 +--- a/src/main/java/org/bukkit/craftbukkit/Main.java ++++ b/src/main/java/org/bukkit/craftbukkit/Main.java +@@ -183,6 +183,14 @@ public class Main { + .defaultsTo("Unknown Server") + .describedAs("Name"); + // Paper end ++ ++ // Leaves start - Server Config ++ acceptsAll(asList("leaves", "leaves-settings"), "File for leaves settings") ++ .withRequiredArg() ++ .ofType(File.class) ++ .defaultsTo(new File("leaves.yml")) ++ .describedAs("Yml file"); ++ // Leaves end - Server Config + } + }; + diff --git a/leaves-server/paper-patches/features/0004-Leaves-Protocol-Core.patch b/leaves-server/paper-patches/features/0004-Leaves-Protocol-Core.patch new file mode 100644 index 00000000..2fdcaa53 --- /dev/null +++ b/leaves-server/paper-patches/features/0004-Leaves-Protocol-Core.patch @@ -0,0 +1,26 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 2 Feb 2025 13:08:33 +0800 +Subject: [PATCH] Leaves Protocol Core + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index c6fbb091c513fb9e5527ac9cb08af607f14468b4..16fdeed9bb0d088cd811c83c93517c0e51a7e9c3 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -493,6 +493,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 ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.init(); // Leaves - protocol + } + + public boolean getCommandBlockOverride(String command) { +@@ -1123,6 +1124,7 @@ public final class CraftServer implements Server { + this.spark.registerCommandBeforePlugins(this); // Paper - spark + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleServerReload(); // Leaves - protocol + + int pollCount = 0; + diff --git a/leaves-server/paper-patches/features/0005-Leaves-Fakeplayer.patch b/leaves-server/paper-patches/features/0005-Leaves-Fakeplayer.patch new file mode 100644 index 00000000..af50a4a2 --- /dev/null +++ b/leaves-server/paper-patches/features/0005-Leaves-Fakeplayer.patch @@ -0,0 +1,158 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Sun, 2 Feb 2025 15:28:12 +0800 +Subject: [PATCH] Leaves Fakeplayer + + +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 a589689e3a9de1fffef62e0e3dcd79bb2e848c5b..a0a6cde96322df8e455b26b32b1c593f332d4db6 100644 +--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +@@ -41,6 +41,12 @@ class PaperEventManager { + throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously."); + } + ++ // Leaves start - skip bot ++ if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Bot) { ++ return; ++ } ++ // Leaves end - skip bot ++ + HandlerList handlers = event.getHandlers(); + RegisteredListener[] listeners = handlers.getRegisteredListeners(); + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java +index adc6741e0e017660fbd39a62b69be1e67e0e143f..9bc40b07b8eebded4f748fd053b45571df6286a5 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java +@@ -434,6 +434,7 @@ public abstract class CraftRegionAccessor implements RegionAccessor { + @SuppressWarnings("unchecked") + public T addEntity(T entity) { + Preconditions.checkArgument(!entity.isInWorld(), "Entity has already been added to a world"); ++ Preconditions.checkState(!(entity instanceof org.leavesmc.leaves.entity.CraftBot), "[Leaves] Fakeplayers do not support changing world, Please use leaves fakeplayer-api instead!"); + net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle(); + if (nmsEntity.level() != this.getHandle().getLevel()) { + nmsEntity = nmsEntity.teleport(new TeleportTransition(this.getHandle().getLevel(), nmsEntity, TeleportTransition.DO_NOTHING)); +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 16fdeed9bb0d088cd811c83c93517c0e51a7e9c3..16ebf43a97bcf325d101e8d99cce35a21c7ccdeb 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -315,6 +315,7 @@ public final class CraftServer implements Server { + private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); // Paper + private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes + public final io.papermc.paper.SparksFly spark; // Paper - spark ++ private final org.leavesmc.leaves.entity.CraftBotManager botManager; // Leaves + + // Paper start - Folia region threading API + private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); +@@ -494,6 +495,7 @@ public final class CraftServer implements Server { + datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper + this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark + org.leavesmc.leaves.protocol.core.LeavesProtocolManager.init(); // Leaves - protocol ++ this.botManager = new org.leavesmc.leaves.entity.CraftBotManager(); // Leaves + } + + public boolean getCommandBlockOverride(String command) { +@@ -1500,7 +1502,7 @@ public final class CraftServer implements Server { + return false; + } + +- if (handle.players().size() > 0) { ++ if (handle.realPlayers().size() > 0) { // Leaves - skip + return false; + } + +@@ -3293,4 +3295,11 @@ public final class CraftServer implements Server { + this.console.addPluginAllowingSleep(plugin.getName(), value); + } + // Paper end - API to check if the server is sleeping ++ ++ // Leaves start - Bot API ++ @Override ++ public org.leavesmc.leaves.entity.CraftBotManager getBotManager() { ++ return botManager; ++ } ++ // Leaves end - Bot API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 600a0f423a83d9ecb0394faffc6b5ab79c69d6c6..2487ad1f7dd612c0e16d3229038fe58ce33d1a6a 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -237,7 +237,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public int getPlayerCount() { +- return world.players().size(); ++ return world.realPlayers().size(); // Leaves - skip + } + + @Override +@@ -1268,9 +1268,9 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + @Override + public List getPlayers() { +- List list = new ArrayList(this.world.players().size()); ++ List list = new ArrayList(this.world.realPlayers().size()); // Leaves - skip + +- for (net.minecraft.world.entity.player.Player human : this.world.players()) { ++ for (net.minecraft.world.entity.player.Player human : this.world.realPlayers()) { // Leaves - skip + HumanEntity bukkitEntity = human.getBukkitEntity(); + + if ((bukkitEntity != null) && (bukkitEntity instanceof Player)) { +@@ -1947,7 +1947,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); +- for (ServerPlayer player : this.getHandle().players()) { ++ for (ServerPlayer player : this.getHandle().realPlayers()) { // Leaves - skip + player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player.getX(), player.getY(), player.getZ(), seed, null)); + } + } +@@ -1975,7 +1975,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()) { +- for (ServerPlayer player : this.getHandle().players()) { ++ for (ServerPlayer player : this.getHandle().realPlayers()) { // Leaves - skip + player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player, seed, null)); + } + } else if (emitter instanceof CraftEntity craftEntity) { +@@ -2205,7 +2205,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( +- receivers == null ? this.getHandle().players() : receivers.stream().map(player -> ((CraftPlayer) player).getHandle()).collect(java.util.stream.Collectors.toList()), // Paper - Particle API ++ receivers == null ? this.getHandle().realPlayers() : receivers.stream().map(player -> ((CraftPlayer) player).getHandle()).collect(java.util.stream.Collectors.toList()), // Paper - Particle API // Leaves - skip + sender != null ? ((CraftPlayer) sender).getHandle() : null, // Sender // Paper - Particle API + CraftParticle.createParticleParam(particle, data), // Particle + force, +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index f8762064e0f377740688932c62145f7c789ea7ed..9131f13456e879bb2fee3fd5d77450df855011f2 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -98,6 +98,8 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + return new CraftHumanEntity(server, (net.minecraft.world.entity.player.Player) entity); + } + ++ if (entity instanceof org.leavesmc.leaves.bot.ServerBot bot) { return new org.leavesmc.leaves.entity.CraftBot(server, bot); } ++ + // Special case complex part, since there is no extra entity type for them + if (entity instanceof EnderDragonPart complexPart) { + if (complexPart.parentMob instanceof EnderDragon) { +diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +index fd4c1d67c134caf818f10bfd54831863c485f6a9..b09d208c62eb753c1b0598ccdfa080fa6f640c15 100644 +--- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java ++++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +@@ -1025,7 +1025,11 @@ public class CraftEventFactory { + event.setKeepInventory(keepInventory); + event.setKeepLevel(victim.keepLevel); // SPIGOT-2222: pre-set keepLevel + populateFields(victim, event); // Paper - make cancellable +- Bukkit.getServer().getPluginManager().callEvent(event); ++ // Leaves start - disable bot death event ++ if (!(victim instanceof org.leavesmc.leaves.bot.ServerBot)) { ++ Bukkit.getServer().getPluginManager().callEvent(event); ++ } ++ // Leaves end - disable bot death event + // Paper start - make cancellable + if (event.isCancelled()) { + return event; diff --git a/leaves-server/paper-patches/features/0006-No-chat-sign.patch b/leaves-server/paper-patches/features/0006-No-chat-sign.patch new file mode 100644 index 00000000..69406958 --- /dev/null +++ b/leaves-server/paper-patches/features/0006-No-chat-sign.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Wed, 3 Aug 2022 11:20:51 +0800 +Subject: [PATCH] No chat sign + + +diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +index 14e412ebf75b0e06ab53a1c8f9dd1be6ad1e2680..73d239536b373e292ee2883edac0049541b59ba0 100644 +--- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java ++++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java +@@ -317,7 +317,7 @@ public final class ChatProcessor { + + private void sendToServer(final ChatType.Bound chatType, final @Nullable Function msgFunction) { + final PlayerChatMessage toConsoleMessage = msgFunction == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(msgFunction.apply(ChatProcessor.this.server.console)); +- ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure"); ++ ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) || org.leavesmc.leaves.LeavesConfig.mics.noChatSign ? null : "Not Secure"); // Leaves - No Not Secure + } + } + diff --git a/leaves-server/paper-patches/features/0007-MC-Technical-Survival-Mode.patch b/leaves-server/paper-patches/features/0007-MC-Technical-Survival-Mode.patch new file mode 100644 index 00000000..a16af288 --- /dev/null +++ b/leaves-server/paper-patches/features/0007-MC-Technical-Survival-Mode.patch @@ -0,0 +1,19 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 19 Jan 2023 23:38:50 +0800 +Subject: [PATCH] MC Technical Survival Mode + +Will automatically overwrite some configuration after startup + +diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +index e48fa405d92fab221fa8331b65c8f324e801d439..4697237ede3ec3960c57eec549b73b0f785f6377 100644 +--- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java ++++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java +@@ -325,6 +325,7 @@ public class PaperConfigurations extends Configurations +Date: Mon, 3 Feb 2025 15:48:19 +0800 +Subject: [PATCH] Leaves Extra Yggdrasil Service + + +diff --git a/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java b/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java +index 48e774677edf17d4a99ae9ed23d1b371dab41abb..21409ff86db65c00d92bff9eae8bdeb3a872a361 100644 +--- a/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java ++++ b/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java +@@ -11,7 +11,7 @@ import java.net.Proxy; + + public class PaperAuthenticationService extends YggdrasilAuthenticationService { + +- private final Environment environment; ++ protected final Environment environment; // Leaves - private -> protected + + public PaperAuthenticationService(Proxy proxy) { + super(proxy); diff --git a/patches/server/0072-Disable-packet-limit.patch b/leaves-server/paper-patches/features/0009-Disable-packet-limit.patch similarity index 84% rename from patches/server/0072-Disable-packet-limit.patch rename to leaves-server/paper-patches/features/0009-Disable-packet-limit.patch index bb590b10..39d6396a 100644 --- a/patches/server/0072-Disable-packet-limit.patch +++ b/leaves-server/paper-patches/features/0009-Disable-packet-limit.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Disable packet limit diff --git a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java -index 17e23ca4dd2bbfba49ea00aa2b719a95feb931be..d4e2e93689e831e56e7500d6d14f1704e0c66874 100644 +index 42777adb028fe282c1619aeb5431c442ad5df0d0..90d448f9b183d0dd31db8eaf43bd039eeace5c65 100644 --- a/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java +++ b/src/main/java/io/papermc/paper/configuration/GlobalConfiguration.java -@@ -275,7 +275,7 @@ public class GlobalConfiguration extends ConfigurationPart { +@@ -282,7 +282,7 @@ public class GlobalConfiguration extends ConfigurationPart { } public boolean isEnabled() { diff --git a/leaves-server/paper-patches/features/0010-Reduce-array-allocations.patch b/leaves-server/paper-patches/features/0010-Reduce-array-allocations.patch new file mode 100644 index 00000000..52ca14d1 --- /dev/null +++ b/leaves-server/paper-patches/features/0010-Reduce-array-allocations.patch @@ -0,0 +1,62 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 20 Jul 2023 15:03:28 +0800 +Subject: [PATCH] Reduce array allocations + +This patch is Powered by Gale(https://github.com/GaleMC/Gale) + +diff --git a/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java b/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java +index ae60bd96b5284d54676d8e7e4dd5d170b526ec1e..0c474b1eb4dbef547890b7db5fcf9c13c86092a2 100644 +--- a/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java ++++ b/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java +@@ -7,6 +7,7 @@ import org.bukkit.command.CommandSender; + import org.checkerframework.checker.nullness.qual.NonNull; + import org.checkerframework.checker.nullness.qual.Nullable; + import org.checkerframework.framework.qual.DefaultQualifier; ++import org.leavesmc.leaves.util.ArrayConstants; + + @DefaultQualifier(NonNull.class) + public final class VersionCommand implements PaperSubcommand { +@@ -14,7 +15,7 @@ public final class VersionCommand implements PaperSubcommand { + public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { + final @Nullable Command ver = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); + if (ver != null) { +- ver.execute(sender, "paper", new String[0]); ++ ver.execute(sender, "paper", ArrayConstants.emptyStringArray); // Leaves - reduce array allocations + } + return true; + } +diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftEntityEquipment.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftEntityEquipment.java +index fdcc414f4fa246082ad0732133c870d915ae3084..556247696cde0d31cbb70907648d2970acf81153 100644 +--- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftEntityEquipment.java ++++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftEntityEquipment.java +@@ -165,7 +165,7 @@ public class CraftEntityEquipment implements EntityEquipment { + + @Override + public void clear() { +- for (net.minecraft.world.entity.EquipmentSlot slot : net.minecraft.world.entity.EquipmentSlot.values()) { ++ for (net.minecraft.world.entity.EquipmentSlot slot : net.minecraft.world.entity.EquipmentSlot.VALUES) { // Leaves - reduce array allocations + this.setEquipment(slot, null, false); + } + } +diff --git a/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java b/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java +index b25dc23b81687dd4d4e70b3615ffb91f8c03c68b..8fdadf58054b2475f2023f76824af7bbe1303383 100644 +--- a/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java +@@ -6,6 +6,7 @@ import java.util.ArrayList; + import java.util.Collection; + import java.util.Iterator; + import java.util.NoSuchElementException; ++import org.leavesmc.leaves.util.ArrayConstants; + + public final class WeakCollection implements Collection { + static final Object NO_VALUE = new Object(); +@@ -164,7 +165,7 @@ public final class WeakCollection implements Collection { + + @Override + public Object[] toArray() { +- return this.toArray(new Object[0]); ++ return this.toArray(ArrayConstants.emptyObjectArray); // Leaves - reduce array allocations + } + + @Override diff --git a/leaves-server/paper-patches/features/0011-Force-peaceful-mode-switch.patch b/leaves-server/paper-patches/features/0011-Force-peaceful-mode-switch.patch new file mode 100644 index 00000000..4d1cb954 --- /dev/null +++ b/leaves-server/paper-patches/features/0011-Force-peaceful-mode-switch.patch @@ -0,0 +1,29 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 3 Aug 2023 14:21:47 +0800 +Subject: [PATCH] Force peaceful mode switch + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +index 2487ad1f7dd612c0e16d3229038fe58ce33d1a6a..0667154dfec972961c679b8a963ec8a3b65c05a1 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java +@@ -2404,6 +2404,18 @@ public class CraftWorld extends CraftRegionAccessor implements World { + + // Paper - replace feature flag API + ++ // Leaves start - unsupported settings ++ @Override ++ public void setPeacefulModeSwitchTick(int tick) { ++ this.getHandle().chunkSource.peacefulModeSwitchTick = tick; ++ } ++ ++ @Override ++ public int getPeacefulModeSwitchTick() { ++ return this.getHandle().chunkSource.peacefulModeSwitchTick; ++ } ++ // Leaves end - unsupported settings ++ + public void storeBukkitValues(CompoundTag c) { + if (!this.persistentDataContainer.isEmpty()) { + c.put("BukkitValues", this.persistentDataContainer.toTagCompound()); diff --git a/leaves-server/paper-patches/features/0012-Replay-Mod-API.patch b/leaves-server/paper-patches/features/0012-Replay-Mod-API.patch new file mode 100644 index 00000000..90635923 --- /dev/null +++ b/leaves-server/paper-patches/features/0012-Replay-Mod-API.patch @@ -0,0 +1,78 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Tue, 4 Feb 2025 19:45:21 +0800 +Subject: [PATCH] Replay Mod API + +This patch is Powered by ReplayMod(https://github.com/ReplayMod) + +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 a0a6cde96322df8e455b26b32b1c593f332d4db6..b5031ba5c48c7d007a7c05766a2beff422504c3e 100644 +--- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java ++++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java +@@ -42,7 +42,7 @@ class PaperEventManager { + } + + // Leaves start - skip bot +- if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Bot) { ++ if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && (playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Bot || playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Photographer)) { // Leaves - and photographer + return; + } + // Leaves end - skip bot +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 16ebf43a97bcf325d101e8d99cce35a21c7ccdeb..99258ad5c0af91f24525bac9442cead9541c2e83 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -316,6 +316,7 @@ public final class CraftServer implements Server { + private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes + public final io.papermc.paper.SparksFly spark; // Paper - spark + private final org.leavesmc.leaves.entity.CraftBotManager botManager; // Leaves ++ private final org.leavesmc.leaves.entity.CraftPhotographerManager photographerManager = new org.leavesmc.leaves.entity.CraftPhotographerManager(); // Leaves + + // Paper start - Folia region threading API + private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); +@@ -410,7 +411,7 @@ public final class CraftServer implements Server { + public CraftServer(DedicatedServer console, PlayerList playerList) { + this.console = console; + this.playerList = (DedicatedPlayerList) playerList; +- this.playerView = Collections.unmodifiableList(Lists.transform(playerList.players, new Function() { ++ this.playerView = Collections.unmodifiableList(Lists.transform(playerList.realPlayers, new Function() { // Leaves - replay api + @Override + public CraftPlayer apply(ServerPlayer player) { + return player.getBukkitEntity(); +@@ -3302,4 +3303,11 @@ public final class CraftServer implements Server { + return botManager; + } + // Leaves end - Bot API ++ ++ // Leaves start - replay mod api ++ @Override ++ public org.leavesmc.leaves.entity.CraftPhotographerManager getPhotographerManager() { ++ return photographerManager; ++ } ++ // Leaves end - replay mod api + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +index 9131f13456e879bb2fee3fd5d77450df855011f2..f1c79b238b00f77cf2be1360d5a43429d3470896 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java +@@ -99,6 +99,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { + } + + if (entity instanceof org.leavesmc.leaves.bot.ServerBot bot) { return new org.leavesmc.leaves.entity.CraftBot(server, bot); } ++ if (entity instanceof org.leavesmc.leaves.replay.ServerPhotographer photographer) { return new org.leavesmc.leaves.entity.CraftPhotographer(server, photographer); } + + // 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 3260f20b667918dd7cd641d5d96688721fce2f9c..121560c873200390e261aef0f639f78a7a668883 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -2258,7 +2258,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + + @Override + public boolean canSee(Player player) { +- return this.canSee((org.bukkit.entity.Entity) player); ++ return !(player instanceof org.leavesmc.leaves.entity.Photographer) && this.canSee((org.bukkit.entity.Entity) player); // Leaves - skip photographer + } + + @Override diff --git a/patches/server/0111-Force-minecraft-command.patch b/leaves-server/paper-patches/features/0013-Force-minecraft-command.patch similarity index 100% rename from patches/server/0111-Force-minecraft-command.patch rename to leaves-server/paper-patches/features/0013-Force-minecraft-command.patch diff --git a/leaves-server/paper-patches/features/0014-Bytebuf-API.patch b/leaves-server/paper-patches/features/0014-Bytebuf-API.patch new file mode 100644 index 00000000..7e8a2a9e --- /dev/null +++ b/leaves-server/paper-patches/features/0014-Bytebuf-API.patch @@ -0,0 +1,55 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> +Date: Thu, 6 Feb 2025 00:14:23 +0800 +Subject: [PATCH] Bytebuf API + + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 99258ad5c0af91f24525bac9442cead9541c2e83..5420aa63b7888ba7ba4ed05aff43e77540c97f04 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -317,6 +317,7 @@ public final class CraftServer implements Server { + public final io.papermc.paper.SparksFly spark; // Paper - spark + private final org.leavesmc.leaves.entity.CraftBotManager botManager; // Leaves + private final org.leavesmc.leaves.entity.CraftPhotographerManager photographerManager = new org.leavesmc.leaves.entity.CraftPhotographerManager(); // Leaves ++ private final org.leavesmc.leaves.bytebuf.internal.InternalBytebufHandler internalBytebufHandler = new org.leavesmc.leaves.bytebuf.internal.InternalBytebufHandler(); // Leaves + + // Paper start - Folia region threading API + private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); +@@ -3310,4 +3311,15 @@ public final class CraftServer implements Server { + return photographerManager; + } + // Leaves end - replay mod api ++ ++ // Leaves start - Bytebuf API ++ @Override ++ public org.leavesmc.leaves.bytebuf.BytebufManager getBytebufManager() { ++ return internalBytebufHandler.getManager(); ++ } ++ ++ public org.leavesmc.leaves.bytebuf.internal.InternalBytebufHandler getBytebufHandler() { ++ return internalBytebufHandler; ++ } ++ // Leaves end - Bytebuf API + } +diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +index 121560c873200390e261aef0f639f78a7a668883..9fb92d7103f004cca7639ccac1eeda9d490262f4 100644 +--- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java ++++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java +@@ -3583,4 +3583,16 @@ public class CraftPlayer extends CraftHumanEntity implements Player { + handle.containerMenu.broadcastChanges(); + return new PaperPlayerGiveResult(leftovers.build(), drops.build()); + } ++ ++ // Leaves start - Bytebuf API ++ @Override ++ public void sendPacket(org.leavesmc.leaves.bytebuf.packet.Packet packet) { ++ this.server.getBytebufHandler().applyPacketToPlayer(this.getHandle(), packet); ++ } ++ ++ @Override ++ public void sendPacket(org.leavesmc.leaves.bytebuf.Bytebuf bytebuf, org.leavesmc.leaves.bytebuf.packet.PacketType type) { ++ this.server.getBytebufHandler().applyPacketToPlayer(this.getHandle(), new org.leavesmc.leaves.bytebuf.packet.Packet(type, bytebuf)); ++ } ++ // Leaves end - Bytebuf API + } diff --git a/leaves-server/paper-patches/features/0015-Leaves-plugins.patch b/leaves-server/paper-patches/features/0015-Leaves-plugins.patch new file mode 100644 index 00000000..e6ea8584 --- /dev/null +++ b/leaves-server/paper-patches/features/0015-Leaves-plugins.patch @@ -0,0 +1,145 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: MC_XiaoHei +Date: Mon, 22 Jul 2024 09:05:56 +0000 +Subject: [PATCH] Leaves plugins + + +diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +index f0fce4113fb07c64adbec029d177c236cbdcbae8..f3cb913f29e1aff46233af2f086d205a51ac582d 100644 +--- a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java ++++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java +@@ -63,6 +63,7 @@ public class PaperPluginsCommand extends BukkitCommand { + + private static final Component LEGACY_PLUGIN_STAR = Component.text('*', TextColor.color(255, 212, 42)).hoverEvent(LEGACY_PLUGIN_INFO); + private static final Component INFO_ICON_START = Component.text("ℹ ", INFO_COLOR); ++ private static final Component LEAVES_HEADER = Component.text("Leaves Plugins:", TextColor.color(55, 209, 171)); // Leaves - leaves plugin + private static final Component PAPER_HEADER = Component.text("Paper Plugins:", TextColor.color(2, 136, 209)); + private static final Component BUKKIT_HEADER = Component.text("Bukkit Plugins:", TextColor.color(237, 129, 6)); + private static final Component PLUGIN_TICK = Component.text("- ", NamedTextColor.DARK_GRAY); +@@ -170,6 +171,8 @@ public class PaperPluginsCommand extends BukkitCommand { + + TreeMap> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + TreeMap> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); ++ // Leaves start - leaves plugin ++ TreeMap> leavesPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + + for (PluginProvider provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) { +@@ -178,14 +181,23 @@ public class PaperPluginsCommand extends BukkitCommand { + if (provider instanceof SpigotPluginProvider) { + spigotPlugins.put(configuration.getDisplayName(), provider); + } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { +- paperPlugins.put(configuration.getDisplayName(), provider); ++ if(provider.getMeta() instanceof org.leavesmc.leaves.plugin.provider.configuration.LeavesPluginMeta) leavesPlugins.put(configuration.getDisplayName(), provider); ++ else paperPlugins.put(configuration.getDisplayName(), provider); + } + } + +- Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); ++ Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size() + leavesPlugins.size()), NamedTextColor.WHITE); + //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs + + sender.sendMessage(infoMessage); ++ if (!leavesPlugins.isEmpty()) { ++ sender.sendMessage(LEAVES_HEADER); ++ } ++ ++ for (Component component : formatProviders(leavesPlugins)) { ++ sender.sendMessage(component); ++ } ++ // Leaves end - leaves plugin + + if (!paperPlugins.isEmpty()) { + sender.sendMessage(PAPER_HEADER); +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..390625fbf54139b205a23b94d89a860fbb2c92d9 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 { // Leaves - leaves plugins + + + private static final TypeToken>> TYPE_TOKEN = new TypeToken<>() { +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 d3b3a8baca013909fa9c6204d964d7d7efeb2719..cdde16a4999fbf56c334c65e23d995b7a3604518 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 plugins + 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 8d0da6e46d4eb5eb05c3144510c4ef083559d0ec..72a69ed1d4cdeecd25bfa4fddc3ecc2b21550bad 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_CONF = "leaves-plugin.conf"; // Leaves - leaves plugins + 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 plugins ++ public static final PluginFileType LEAVES = new PluginFileType<>(LEAVES_PLUGIN_CONF, PaperPluginParent.LEAVES_FACTORY) { ++ @Override ++ protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) { ++ PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null; ++ if (parent.shouldCreateBootstrap()) { ++ bootstrapPluginProvider = parent.createBootstrapProvider(); ++ entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider); ++ } ++ entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider)); ++ } ++ }; + +- private static final List> VALUES = List.of(PAPER, SPIGOT); ++ private static final List> VALUES = List.of(LEAVES, PAPER, SPIGOT); ++ // Leaves end - leaves plugins + + 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..ebde8a79143a5e314d5054f2d125d276eaa1e734 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 plugins + private final Path path; + private final JarFile jarFile; + private final PaperPluginMeta description; +diff --git a/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java b/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java +index 28857d0c9b53f2068d51b8f09ef40df7a2b97502..b4d2d7280237a9ad7df095e26773e01211201b84 100644 +--- a/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java ++++ b/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java +@@ -333,7 +333,13 @@ public final class PluginRemapper { + } + index.skip(inputFile); + return CompletableFuture.completedFuture(inputFile); +- } ++ } else if (ns == null && Files.exists(fs.getPath(PluginFileType.LEAVES_PLUGIN_CONF))) { // Leaves start - leaves plugins ++ if (DEBUG_LOGGING) { ++ LOGGER.info("Plugin '{}' is a Leaves plugin with no namespace specified.", inputFile); ++ } ++ index.skip(inputFile); ++ return CompletableFuture.completedFuture(inputFile); ++ } // Leaves end - leaves plugins + } + } catch (final IOException ex) { + return CompletableFuture.failedFuture(new RuntimeException("Failed to open plugin jar " + inputFile, ex)); diff --git a/leaves-server/paper-patches/files/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java.patch b/leaves-server/paper-patches/files/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java.patch new file mode 100644 index 00000000..0fe03411 --- /dev/null +++ b/leaves-server/paper-patches/files/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java.patch @@ -0,0 +1,20 @@ +--- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java ++++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java +@@ -107,7 +_,7 @@ + } + + // Contributed by Techcable in GH-65 +- private static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) { ++ protected static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) { // Leaves - private -> protected + try { + final HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.github.com/repos/%s/compare/%s...%s".formatted(repo, branch, hash)).toURL().openConnection(); + connection.connect(); +@@ -130,7 +_,7 @@ + } + } + +- private @Nullable Component getHistory() { ++ protected @Nullable Component getHistory() { // Leaves - private -> protected + final VersionHistoryManager.@Nullable VersionData data = VersionHistoryManager.INSTANCE.getVersionData(); + if (data == null) { + return null; diff --git a/leaves-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java.patch b/leaves-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java.patch new file mode 100644 index 00000000..5b262f98 --- /dev/null +++ b/leaves-server/paper-patches/files/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java.patch @@ -0,0 +1,11 @@ +--- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java ++++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java +@@ -508,7 +_,7 @@ + // Paper start + @Override + public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() { +- return new com.destroystokyo.paper.PaperVersionFetcher(); ++ return new org.leavesmc.leaves.util.LeavesVersionFetcher(); // Leaves - Leaves version fetcher + } + + @Override diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/.editorconfig b/leaves-server/src/main/java/org/leavesmc/leaves/.editorconfig new file mode 100644 index 00000000..6f086600 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/.editorconfig @@ -0,0 +1,2 @@ +[*.java] +ij_java_use_fq_class_names = false \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java new file mode 100644 index 00000000..855e0d8d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java @@ -0,0 +1,980 @@ +package org.leavesmc.leaves; + +import com.destroystokyo.paper.util.SneakyThrow; +import io.papermc.paper.configuration.GlobalConfiguration; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.bukkit.command.Command; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.command.LeavesCommand; +import org.leavesmc.leaves.config.GlobalConfig; +import org.leavesmc.leaves.config.GlobalConfigCategory; +import org.leavesmc.leaves.config.RemovedConfig; +import org.leavesmc.leaves.config.GlobalConfigManager; +import org.leavesmc.leaves.region.RegionFileFormat; +import org.leavesmc.leaves.util.MathUtils; + +import org.leavesmc.leaves.config.ConfigValidatorImpl.BooleanConfigValidator; +import org.leavesmc.leaves.config.ConfigValidatorImpl.IntConfigValidator; +import org.leavesmc.leaves.config.ConfigValidatorImpl.StringConfigValidator; +import org.leavesmc.leaves.config.ConfigValidatorImpl.DoubleConfigValidator; +import org.leavesmc.leaves.config.ConfigValidatorImpl.ListConfigValidator; +import org.leavesmc.leaves.config.ConfigValidatorImpl.EnumConfigValidator; + +import java.io.File; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +import org.leavesmc.leaves.protocol.CarpetServerProtocol.CarpetRule; +import org.leavesmc.leaves.protocol.CarpetServerProtocol.CarpetRules; + +import org.leavesmc.leaves.protocol.bladeren.BladerenProtocol.LeavesFeatureSet; +import org.leavesmc.leaves.protocol.bladeren.BladerenProtocol.LeavesFeature; + +public final class LeavesConfig { + + public static final String CONFIG_HEADER = "Configuration file for Leaves."; + public static final int CURRENT_CONFIG_VERSION = 6; + + private static File configFile; + public static YamlConfiguration config; + + public static void init(final @NotNull File file) { + LeavesConfig.configFile = file; + config = new YamlConfiguration(); + config.options().setHeader(Collections.singletonList(CONFIG_HEADER)); + config.options().copyDefaults(true); + + if (!file.exists()) { + try { + boolean is = file.createNewFile(); + if (!is) { + throw new IOException("Can't create file"); + } + } catch (final Exception ex) { + LeavesLogger.LOGGER.severe("Failure to create leaves config", ex); + } + } else { + try { + config.load(file); + } catch (final Exception ex) { + LeavesLogger.LOGGER.severe("Failure to load leaves config", ex); + SneakyThrow.sneaky(ex); + throw new RuntimeException(ex); + } + } + + LeavesConfig.config.set("config-version", CURRENT_CONFIG_VERSION); + + GlobalConfigManager.init(); + + registerCommand("leaves", new LeavesCommand("leaves")); + } + + public static void save() { + try { + config.save(LeavesConfig.configFile); + } catch (final Exception ex) { + LeavesLogger.LOGGER.severe("Unable to save leaves config", ex); + } + } + + public static void registerCommand(String name, Command command) { + MinecraftServer.getServer().server.getCommandMap().register(name, "leaves", command); + MinecraftServer.getServer().server.syncCommands(); + } + + public static void unregisterCommand(String name) { + name = name.toLowerCase(Locale.ENGLISH).trim(); + MinecraftServer.getServer().server.getCommandMap().getKnownCommands().remove(name); + MinecraftServer.getServer().server.getCommandMap().getKnownCommands().remove("leaves:" + name); + MinecraftServer.getServer().server.syncCommands(); + } + + public static ModifyConfig modify = new ModifyConfig(); + + @GlobalConfigCategory("modify") + public static class ModifyConfig { + + public FakeplayerConfig fakeplayer = new FakeplayerConfig(); + + @GlobalConfigCategory("fakeplayer") + public static class FakeplayerConfig { + + @RemovedConfig(name = "enable", category = "fakeplayer", transform = true) + @GlobalConfig(value = "enable", validator = FakeplayerValidator.class) + public boolean enable = true; + + private static class FakeplayerValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (value) { + registerCommand("bot", new org.leavesmc.leaves.bot.BotCommand("bot")); + org.leavesmc.leaves.bot.agent.Actions.registerAll(); + } else { + unregisterCommand("bot"); + } + } + } + + @RemovedConfig(name = "unable-fakeplayer-names", category = "fakeplayer", convert = ListConfigValidator.STRING.class, transform = true) + @GlobalConfig(value = "unable-fakeplayer-names", validator = ListConfigValidator.STRING.class) + public List unableNames = List.of("player-name"); + + @GlobalConfig(value = "limit", validator = IntConfigValidator.class) + public int limit = 10; + + @GlobalConfig(value = "prefix", validator = StringConfigValidator.class) + public String prefix = ""; + + @GlobalConfig(value = "suffix", validator = StringConfigValidator.class) + public String suffix = ""; + + @GlobalConfig(value = "regen-amount", validator = RegenAmountValidator.class) + public double regenAmount = 0.0; + + private static class RegenAmountValidator extends DoubleConfigValidator { + @Override + public void verify(Double old, Double value) throws IllegalArgumentException { + if (value < 0.0) { + throw new IllegalArgumentException("regen-amount need >= 0.0"); + } + } + } + + @GlobalConfig("always-send-data") + public boolean canSendDataAlways = true; + + @GlobalConfig("resident-fakeplayer") + public boolean canResident = false; + + @GlobalConfig("open-fakeplayer-inventory") + public boolean canOpenInventory = false; + + @GlobalConfig("skip-sleep-check") + public boolean canSkipSleep = false; + + @GlobalConfig("spawn-phantom") + public boolean canSpawnPhantom = false; + + @GlobalConfig("use-action") + public boolean canUseAction = true; + + @GlobalConfig("modify-config") + public boolean canModifyConfig = false; + + @GlobalConfig("manual-save-and-load") + public boolean canManualSaveAndLoad = false; + + @GlobalConfig(value = "cache-skin", lock = true) + public boolean useSkinCache = false; + } + + public MinecraftOLDConfig oldMC = new MinecraftOLDConfig(); + + @GlobalConfigCategory("minecraft-old") + public static class MinecraftOLDConfig { + + public BlockUpdaterConfig updater = new BlockUpdaterConfig(); + + @GlobalConfigCategory("block-updater") + public static class BlockUpdaterConfig { + @RemovedConfig(name = "instant-block-updater-reintroduced", category = "modify", transform = true) + @RemovedConfig(name = "instant-block-updater-reintroduced", category = {"modify", "minecraft-old"}, transform = true) + @GlobalConfig(value = "instant-block-updater-reintroduced", lock = true) + public boolean instantBlockUpdaterReintroduced = false; + + @RemovedConfig(name = "cce-update-suppression", category = {"modify", "minecraft-old"}, transform = true) + @GlobalConfig("cce-update-suppression") + public boolean cceUpdateSuppression = false; + + @RemovedConfig(name = "redstone-wire-dont-connect-if-on-trapdoor", category = "modify", transform = true) + @RemovedConfig(name = "redstone-wire-dont-connect-if-on-trapdoor", category = {"modify", "minecraft-old"}, transform = true) + @GlobalConfig("redstone-wire-dont-connect-if-on-trapdoor") + public boolean redstoneDontCantOnTrapDoor = false; + } + + @RemovedConfig(name = "shears-in-dispenser-can-zero-amount", category = {}, transform = true) + @RemovedConfig(name = "shears-in-dispenser-can-zero-amount", category = "modify", transform = true) + @GlobalConfig("shears-in-dispenser-can-zero-amount") + public boolean shearsInDispenserCanZeroAmount = false; + + @GlobalConfig("armor-stand-cant-kill-by-mob-projectile") + public boolean armorStandCantKillByMobProjectile = false; + + @GlobalConfig(value = "villager-infinite-discounts", validator = VillagerInfiniteDiscountsValidator.class) + public boolean villagerInfiniteDiscounts = false; + + private static class VillagerInfiniteDiscountsValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + org.leavesmc.leaves.util.VillagerInfiniteDiscountHelper.doVillagerInfiniteDiscount(value); + } + } + + @GlobalConfig("copper-bulb-1gt-delay") + public boolean copperBulb1gt = false; + + @GlobalConfig("crafter-1gt-delay") + public boolean crafter1gt = false; + + @RemovedConfig(name = "zero-tick-plants", category = "modify", transform = true) + @GlobalConfig("zero-tick-plants") + public boolean zeroTickPlants = false; + + @RemovedConfig(name = "loot-world-random", category = {"modify", "minecraft-old"}, transform = true) + @GlobalConfig(value = "rng-fishing", lock = true, validator = RNGFishingValidator.class) + public boolean rngFishing = false; + + private static class RNGFishingValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + LeavesFeatureSet.register(LeavesFeature.of("rng_fishing", value)); + } + } + + @GlobalConfig("allow-grindstone-overstacking") + public boolean allowGrindstoneOverstacking = false; + + @GlobalConfig("allow-entity-portal-with-passenger") + public boolean allowEntityPortalWithPassenger = true; + + @GlobalConfig("disable-gateway-portal-entity-ticking") + public boolean disableGatewayPortalEntityTicking = false; + + @GlobalConfig("disable-LivingEntity-ai-step-alive-check") + public boolean disableLivingEntityAiStepAliveCheck = false; + + @GlobalConfig("fix-fortress-mob-spawn") + public boolean fixFortressMobSpawn = false; + + @GlobalConfig("old-block-entity-behaviour") + public boolean oldBlockEntityBehaviour = false; + + @GlobalConfig("old-hopper-suck-in-behavior") + public boolean oldHopperSuckInBehavior = false; + + public RaidConfig raid = new RaidConfig(); + + @GlobalConfigCategory("revert-raid-changes") + public static class RaidConfig { + @GlobalConfig("allow-bad-omen-trigger-raid") + public boolean allowBadOmenTriggerRaid = false; + + @GlobalConfig("give-bad-omen-when-kill-patrol-leader") + public boolean giveBadOmenWhenKillPatrolLeader = false; + } + + @GlobalConfig("allow-anvil-destroy-item-entities") + public boolean allowAnvilDestroyItemEntities = false; + + @GlobalConfig("string-tripwire-hook-duplicate") + public boolean stringTripwireHookDuplicate = false; + } + + public ElytraAeronauticsConfig elytraAeronautics = new ElytraAeronauticsConfig(); + + @GlobalConfigCategory("elytra-aeronautics") + public static class ElytraAeronauticsConfig { + @GlobalConfig("no-chunk-load") + public boolean noChunk = false; + + @GlobalConfig(value = "no-chunk-height", validator = DoubleConfigValidator.class) + public double noChunkHeight = 500.0D; + + @GlobalConfig(value = "no-chunk-speed", validator = DoubleConfigValidator.class) + public double noChunkSpeed = -1.0D; + + @GlobalConfig("message") + public boolean noChunkMes = true; + + @GlobalConfig(value = "message-start", validator = StringConfigValidator.class) + public String noChunkStartMes = "Flight enter cruise mode"; + + @GlobalConfig(value = "message-end", validator = StringConfigValidator.class) + public String noChunkEndMes = "Flight exit cruise mode"; + } + + @RemovedConfig(name = "redstone-shears-wrench", category = {}, transform = true) + @GlobalConfig("redstone-shears-wrench") + public boolean redstoneShearsWrench = true; + + @RemovedConfig(name = "budding-amethyst-can-push-by-piston", category = {}, transform = true) + @GlobalConfig("budding-amethyst-can-push-by-piston") + public boolean buddingAmethystCanPushByPiston = false; + + @RemovedConfig(name = "spectator-dont-get-advancement", category = {}, transform = true) + @GlobalConfig("spectator-dont-get-advancement") + public boolean spectatorDontGetAdvancement = false; + + @RemovedConfig(name = "stick-change-armorstand-arm-status", category = {}, transform = true) + @GlobalConfig("stick-change-armorstand-arm-status") + public boolean stickChangeArmorStandArmStatus = true; + + @RemovedConfig(name = "snowball-and-egg-can-knockback-player", category = {}, transform = true) + @GlobalConfig("snowball-and-egg-can-knockback-player") + public boolean snowballAndEggCanKnockback = true; + + @GlobalConfig("flatten-triangular-distribution") + public boolean flattenTriangularDistribution = false; + + @GlobalConfig("player-operation-limiter") + public boolean playerOperationLimiter = false; + + @GlobalConfig(value = "renewable-elytra", validator = RenewableElytraValidator.class) + public double renewableElytra = -1.0F; + + private static class RenewableElytraValidator extends DoubleConfigValidator { + @Override + public void verify(Double old, Double value) throws IllegalArgumentException { + if (value > 1.0) { + throw new IllegalArgumentException("renewable-elytra need <= 1.0f"); + } + } + } + + public int shulkerBoxStackSize = 1; + @GlobalConfig(value = "stackable-shulker-boxes", validator = StackableShulkerValidator.class) + private String stackableShulkerBoxes = "false"; + + private static class StackableShulkerValidator extends StringConfigValidator { + @Override + public void verify(String old, String value) throws IllegalArgumentException { + String realValue = MathUtils.isNumeric(value) ? value : value.equals("true") ? "2" : "1"; + LeavesConfig.modify.shulkerBoxStackSize = Integer.parseInt(realValue); + } + } + + @GlobalConfig("force-void-trade") + public boolean forceVoidTrade = false; + + @GlobalConfig(value = "mc-technical-survival-mode", validator = McTechnicalModeValidator.class, lock = true) + public boolean mcTechnicalMode = true; + + private static class McTechnicalModeValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (value) { + org.leavesmc.leaves.util.McTechnicalModeHelper.doMcTechnicalMode(); + } + } + } + + @GlobalConfig("return-nether-portal-fix") + public boolean netherPortalFix = false; + + @GlobalConfig(value = "use-vanilla-random", lock = true, validator = UseVanillaRandomValidator.class) + public boolean useVanillaRandom = false; + + private static class UseVanillaRandomValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + LeavesFeatureSet.register(LeavesFeature.of("use_vanilla_random", value)); + } + } + + @GlobalConfig("fix-update-suppression-crash") + public boolean updateSuppressionCrashFix = true; + + @GlobalConfig(value = "bedrock-break-list", lock = true) + public boolean bedrockBreakList = false; + + @GlobalConfig(value = "disable-distance-check-for-use-item", validator = DisableDistanceCheckForUseItemValidator.class) + public boolean disableDistanceCheckForUseItem = false; + + private static class DisableDistanceCheckForUseItemValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (!value && old != null && LeavesConfig.protocol.alternativeBlockPlacement != ProtocolConfig.AlternativePlaceType.NONE) { + throw new IllegalArgumentException("alternative-block-placement is enable, disable-distance-check-for-use-item always need true"); + } + } + } + + @GlobalConfig("no-feather-falling-trample") + public boolean noFeatherFallingTrample = false; + + @GlobalConfig("shared-villager-discounts") + public boolean sharedVillagerDiscounts = false; + + @GlobalConfig("disable-check-out-of-order-command") + public boolean disableCheckOutOfOrderCommand = false; + + @GlobalConfig("despawn-enderman-with-block") + public boolean despawnEndermanWithBlock = false; + + @GlobalConfig(value = "creative-no-clip", validator = CreativeNoClipValidator.class) + public boolean creativeNoClip = false; + + private static class CreativeNoClipValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + CarpetRules.register(CarpetRule.of("carpet", "creativeNoClip", value)); + } + } + + @GlobalConfig("shave-snow-layers") + public boolean shaveSnowLayers = true; + + @GlobalConfig("ignore-lc") + public boolean ignoreLC = false; + + @GlobalConfig("disable-packet-limit") + public boolean disablePacketLimit = false; + + @GlobalConfig(value = "lava-riptide", validator = LavaRiptideValidator.class) + public boolean lavaRiptide = false; + + private static class LavaRiptideValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + LeavesFeatureSet.register(LeavesFeature.of("lava_riptide", value)); + } + } + + @GlobalConfig(value = "no-block-update-command", validator = NoBlockUpdateValidator.class) + public boolean noBlockUpdateCommand = false; + + private static class NoBlockUpdateValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (value) { + registerCommand("blockupdate", new org.leavesmc.leaves.command.NoBlockUpdateCommand("blockupdate")); + } else { + unregisterCommand("blockupdate"); + } + } + } + + @GlobalConfig("no-tnt-place-update") + public boolean noTNTPlaceUpdate = false; + + @GlobalConfig("raider-die-skip-self-raid-check") + public boolean skipSelfRaidCheck = false; + + @GlobalConfig("container-passthrough") + public boolean containerPassthrough = false; + + @GlobalConfig(value = "avoid-anvil-too-expensive", validator = AnvilNotExpensiveValidator.class) + public boolean avoidAnvilTooExpensive = false; + + private static class AnvilNotExpensiveValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + CarpetRules.register(CarpetRule.of("pca", "avoidAnvilTooExpensive", value)); + } + } + + @GlobalConfig("bow-infinity-fix") + public boolean bowInfinityFix = false; + + @GlobalConfig("hopper-counter") + public boolean hopperCounter = false; + + @GlobalConfig(value = "spider-jockeys-drop-gapples", validator = JockeysDropGAppleValidator.class) + public double spiderJockeysDropGapples = -1.0; + + private static class JockeysDropGAppleValidator extends DoubleConfigValidator { + @Override + public void verify(Double old, Double value) throws IllegalArgumentException { + if (value > 1.0) { + throw new IllegalArgumentException("spider-jockeys-drop-gapples need <= 1.0f"); + } + } + } + + @GlobalConfig("renewable-deepslate") + public boolean renewableDeepslate = false; + + @GlobalConfig("renewable-sponges") + public boolean renewableSponges = false; + + @GlobalConfig(value = "renewable-coral", validator = RenewableCoralValidator.class) + public RenewableCoralType renewableCoral = RenewableCoralType.FALSE; + + public enum RenewableCoralType { + FALSE, TRUE, EXPANDED + } + + private static class RenewableCoralValidator extends EnumConfigValidator { + @Override + public void verify(RenewableCoralType old, RenewableCoralType value) throws IllegalArgumentException { + CarpetRules.register(CarpetRule.of("carpet", "renewableCoral", value)); + } + } + + @GlobalConfig("fast-resume") + public boolean fastResume = false; + + @GlobalConfig(value = "force-peaceful-mode", validator = ForcePeacefulModeValidator.class) + public int forcePeacefulMode = -1; + + private static class ForcePeacefulModeValidator extends IntConfigValidator { + @Override + public void verify(Integer old, Integer value) throws IllegalArgumentException { + for (ServerLevel level : MinecraftServer.getServer().getAllLevels()) { + level.chunkSource.peacefulModeSwitchTick = value; + } + } + } + + @GlobalConfig("disable-vault-blacklist") + public boolean disableVaultBlacklist = false; + + @RemovedConfig(name = "tick-command", category = "modify") + @RemovedConfig(name = "player-can-edit-sign", category = "modify") + @RemovedConfig(name = "mending-compatibility-infinity", category = {"modify", "minecraft-old"}) + @RemovedConfig(name = "protection-stacking", category = {"modify", "minecraft-old"}) + @RemovedConfig(name = "disable-moved-wrongly-threshold", category = {"modify"}) + private final boolean removed = false; + } + + public static PerformanceConfig performance = new PerformanceConfig(); + + @GlobalConfigCategory("performance") + public static class PerformanceConfig { + + public PerformanceRemoveConfig remove = new PerformanceRemoveConfig(); + + @GlobalConfigCategory("remove") + public static class PerformanceRemoveConfig { + @GlobalConfig("tick-guard-lambda") + public boolean tickGuardLambda = true; + + @GlobalConfig("damage-lambda") + public boolean damageLambda = true; + } + + @GlobalConfig("optimized-dragon-respawn") + public boolean optimizedDragonRespawn = false; + + @GlobalConfig("dont-send-useless-entity-packets") + public boolean dontSendUselessEntityPackets = true; + + @GlobalConfig("enable-suffocation-optimization") + public boolean enableSuffocationOptimization = true; + + @GlobalConfig("check-spooky-season-once-an-hour") + public boolean checkSpookySeasonOnceAnHour = true; + + @GlobalConfig("inactive-goal-selector-disable") + public boolean throttleInactiveGoalSelectorTick = false; + + @GlobalConfig("reduce-entity-allocations") + public boolean reduceEntityAllocations = true; + + @GlobalConfig("cache-climb-check") + public boolean cacheClimbCheck = true; + + @GlobalConfig(value = "biome-temperatures-use-aging-cache", lock = true) + public boolean biomeTemperaturesUseAgingCache = true; + + @GlobalConfig("reduce-chuck-load-and-lookup") + public boolean reduceChuckLoadAndLookup = true; + + @GlobalConfig("cache-ignite-odds") + public boolean cacheIgniteOdds = true; + + @GlobalConfig("faster-chunk-serialization") + public boolean fasterChunkSerialization = true; + + @GlobalConfig("skip-secondary-POI-sensor-if-absent") + public boolean skipSecondaryPOISensorIfAbsent = true; + + @GlobalConfig("store-mob-counts-in-array") + public boolean storeMobCountsInArray = true; + + @GlobalConfig("optimize-noise-generation") + public boolean optimizeNoiseGeneration = false; + + @GlobalConfig("optimize-sun-burn-tick") + public boolean optimizeSunBurnTick = true; + + @GlobalConfig("optimized-CubePointRange") + public boolean optimizedCubePointRange = true; + + @GlobalConfig("check-frozen-ticks-before-landing-block") + public boolean checkFrozenTicksBeforeLandingBlock = true; + + @GlobalConfig("skip-entity-move-if-movement-is-zero") + public boolean skipEntityMoveIfMovementIsZero = true; + + @GlobalConfig("skip-cloning-advancement-criteria") + public boolean skipCloningAdvancementCriteria = false; + + @GlobalConfig("skip-negligible-planar-movement-multiplication") + public boolean skipNegligiblePlanarMovementMultiplication = true; + + @GlobalConfig("fix-villagers-dont-release-memory") + public boolean villagersDontReleaseMemoryFix = false; + + @RemovedConfig(name = "cache-world-generator-sea-level", category = "performance") + @RemovedConfig(name = "cache-ominous-banner-item", category = "performance") + @RemovedConfig(name = "use-optimized-collection", category = "performance") + @RemovedConfig(name = "async-pathfinding", category = "performance") + @RemovedConfig(name = "async-mob-spawning", category = "performance") + @RemovedConfig(name = "async-entity-tracker", category = "performance") + @RemovedConfig(name = "fix-paper-6045", category = {"performance", "fix"}) + @RemovedConfig(name = "fix-paper-9372", category = {"performance", "fix"}) + @RemovedConfig(name = "skip-clone-loot-parameters", category = "performance") + @RemovedConfig(name = "skip-poi-find-in-vehicle", category = "performance") + @RemovedConfig(name = "strip-raytracing-for-entity", category = "performance") + @RemovedConfig(name = "get-nearby-players-streams", category = {"performance", "remove"}) + @RemovedConfig(name = "optimize-world-generation-and-block-access", category = "performance") + @RemovedConfig(name = "cache-CubeVoxelShape-shape-array", category = "performance") + @RemovedConfig(name = "reduce-entity-fluid-lookup", category = "performance") + @RemovedConfig(name = "optimize-entity-coordinate-key", category = "performance") + @RemovedConfig(name = "entity-target-find-optimization", category = "performance") + @RemovedConfig(name = "use-more-thread-unsafe-random", category = "performance") + @RemovedConfig(name = "range-check-streams-and-iterators", category = {"performance", "remove"}) + @RemovedConfig(name = "improve-fluid-direction-caching", category = "performance") + @RemovedConfig(name = "cache-BlockStatePairKey-hash", category = "performance") + @RemovedConfig(name = "optimize-chunk-ticking", category = "performance") + @RemovedConfig(name = "inventory-contains-iterators", category = {"performance", "remove"}) + private final boolean removedPerformance = true; + } + + public static ProtocolConfig protocol = new ProtocolConfig(); + + @GlobalConfigCategory("protocol") + public static class ProtocolConfig { + + public BladerenConfig bladeren = new BladerenConfig(); + + @GlobalConfigCategory("bladeren") + public static class BladerenConfig { + @GlobalConfig("protocol") + public boolean enable = true; + + @GlobalConfig(value = "mspt-sync-protocol", validator = MSPTSyncValidator.class) + public boolean msptSyncProtocol = false; + + private static class MSPTSyncValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + LeavesFeatureSet.register(LeavesFeature.of("mspt_sync", value)); + } + } + + @GlobalConfig(value = "mspt-sync-tick-interval", validator = MSPTSyncIntervalValidator.class) + public int msptSyncTickInterval = 20; + + private static class MSPTSyncIntervalValidator extends IntConfigValidator { + @Override + public void verify(Integer old, Integer value) throws IllegalArgumentException { + if (value <= 0) { + throw new IllegalArgumentException("mspt-sync-tick-interval need > 0"); + } + } + } + } + + public SyncmaticaConfig syncmatica = new SyncmaticaConfig(); + + @GlobalConfigCategory("syncmatica") + public static class SyncmaticaConfig { + @GlobalConfig(value = "enable", validator = SyncmaticaValidator.class) + public boolean enable = false; + + public static class SyncmaticaValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (value) { + org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol.init(); + } + } + } + + @GlobalConfig("quota") + public boolean useQuota = false; + + @GlobalConfig(value = "quota-limit", validator = IntConfigValidator.class) + public int quotaLimit = 40000000; + } + + public PCAConfig pca = new PCAConfig(); + + @GlobalConfigCategory("pca") + public static class PCAConfig { + @RemovedConfig(name = "pca-sync-protocol", category = "protocol", transform = true) + @GlobalConfig(value = "pca-sync-protocol", validator = PcaValidator.class) + public boolean enable = false; + + public static class PcaValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (old != null && old != value) { + org.leavesmc.leaves.protocol.PcaSyncProtocol.onConfigModify(value); + } + } + } + + @RemovedConfig(name = "pca-sync-player-entity", category = "protocol", convert = PcaPlayerEntityValidator.class, transform = true) + @GlobalConfig(value = "pca-sync-player-entity", validator = PcaPlayerEntityValidator.class) + public PcaPlayerEntityType syncPlayerEntity = PcaPlayerEntityType.OPS; + + public enum PcaPlayerEntityType { + NOBODY, BOT, OPS, OPS_AND_SELF, EVERYONE + } + + private static class PcaPlayerEntityValidator extends EnumConfigValidator { + } + } + + public AppleSkinConfig appleskin = new AppleSkinConfig(); + + @GlobalConfigCategory("appleskin") + public static class AppleSkinConfig { + @RemovedConfig(name = "appleskin-protocol", category = "protocol") + @GlobalConfig("protocol") + public boolean enable = false; + + @GlobalConfig("sync-tick-interval") + public int syncTickInterval = 20; + } + + public ServuxConfig servux = new ServuxConfig(); + + @GlobalConfigCategory("servux") + public static class ServuxConfig { + @RemovedConfig(name = "servux-protocol", category = "protocol", transform = true) + @GlobalConfig("structure-protocol") + public boolean structureProtocol = false; + + @GlobalConfig("entity-protocol") + public boolean entityProtocol = false; + } + + @GlobalConfig("bbor-protocol") + public boolean bborProtocol = false; + + @GlobalConfig("jade-protocol") + public boolean jadeProtocol = false; + + @GlobalConfig(value = "alternative-block-placement", validator = AlternativePlaceValidator.class) + public AlternativePlaceType alternativeBlockPlacement = AlternativePlaceType.NONE; + + public enum AlternativePlaceType { + NONE, CARPET, CARPET_FIX, LITEMATICA + } + + private static class AlternativePlaceValidator extends EnumConfigValidator { + @Override + public void runAfterLoader(AlternativePlaceType value, boolean firstLoad) { + if (value != AlternativePlaceType.NONE) { + LeavesConfig.modify.disableDistanceCheckForUseItem = true; + } + } + } + + @GlobalConfig("xaero-map-protocol") + public boolean xaeroMapProtocol = false; + + @GlobalConfig(value = "xaero-map-server-id", validator = IntConfigValidator.class) + public int xaeroMapServerID = new Random().nextInt(); + + @GlobalConfig("leaves-carpet-support") + public boolean leavesCarpetSupport = false; + + @GlobalConfig("lms-paster-protocol") + public boolean lmsPasterProtocol = false; + + @GlobalConfig("rei-server-protocol") + public boolean reiServerProtocol = false; + } + + public static MiscConfig mics = new MiscConfig(); + + @GlobalConfigCategory("misc") + public static class MiscConfig { + + public AutoUpdateConfig autoUpdate = new AutoUpdateConfig(); + + @GlobalConfigCategory("auto-update") + public static class AutoUpdateConfig { + @GlobalConfig(value = "enable", lock = true, validator = AutoUpdateValidator.class) + public boolean enable = false; + + private static class AutoUpdateValidator extends BooleanConfigValidator { + @Override + public void runAfterLoader(Boolean value, boolean firstLoad) { + if (firstLoad) { + org.leavesmc.leaves.util.LeavesUpdateHelper.init(); + if (value) { + LeavesLogger.LOGGER.warning("Auto-Update is not completely safe. Enabling it may cause data security problems!"); + } + } + } + } + + @GlobalConfig(value = "download-source", lock = true, validator = DownloadSourceValidator.class) + public String source = "application"; + + public static class DownloadSourceValidator extends StringConfigValidator { + private static final List suggestSourceList = List.of("application", "ghproxy", "cloud"); + + @Override + public List valueSuggest() { + return suggestSourceList; + } + } + + @GlobalConfig("allow-experimental") + public Boolean allowExperimental = false; + + @GlobalConfig(value = "time", lock = true, validator = ListConfigValidator.STRING.class) + public List updateTime = List.of("14:00", "2:00"); + } + + public ExtraYggdrasilConfig yggdrasil = new ExtraYggdrasilConfig(); + + @GlobalConfigCategory("extra-yggdrasil-service") + public static class ExtraYggdrasilConfig { + @GlobalConfig(value = "enable", validator = ExtraYggdrasilValidator.class) + public boolean enable = false; + + public static class ExtraYggdrasilValidator extends BooleanConfigValidator { + @Override + public void verify(Boolean old, Boolean value) throws IllegalArgumentException { + if (value) { + LeavesLogger.LOGGER.warning("extra-yggdrasil-service is an unofficial support. Enabling it may cause data security problems!"); + GlobalConfiguration.get().unsupportedSettings.performUsernameValidation = true; // always check username + } + } + } + + @GlobalConfig("login-protect") + public boolean loginProtect = false; + + @GlobalConfig(value = "urls", lock = true, validator = ExtraYggdrasilUrlsValidator.class) + public List serviceList = List.of("https://url.with.authlib-injector-yggdrasil"); + + public static class ExtraYggdrasilUrlsValidator extends ListConfigValidator.STRING { + @Override + public void verify(List old, List value) throws IllegalArgumentException { + org.leavesmc.leaves.profile.LeavesMinecraftSessionService.initExtraYggdrasilList(value); + } + } + } + + @GlobalConfig("disable-method-profiler") + public boolean disableMethodProfiler = true; + + @RemovedConfig(name = "no-chat-sign", category = {}, transform = true) + @GlobalConfig("no-chat-sign") + public boolean noChatSign = true; + + @GlobalConfig("dont-respond-ping-before-start-fully") + public boolean dontRespondPingBeforeStart = true; + + @GlobalConfig(value = "server-lang", lock = true, validator = ServerLangValidator.class) + public String serverLang = "en_us"; + + private static class ServerLangValidator extends StringConfigValidator { + private static final List supportLang = List.of("en_us", "zh_cn"); + + @Override + public void verify(String old, String value) throws IllegalArgumentException { + if (!supportLang.contains(value)) { + throw new IllegalArgumentException("lang " + value + " not supported"); + } + } + + @Override + public List valueSuggest() { + return supportLang; + } + } + + @GlobalConfig(value = "server-mod-name", validator = StringConfigValidator.class) + public String serverModName = "Leaves"; + + @GlobalConfig("bstats-privacy-mode") + public boolean bstatsPrivacyMode = false; + + @GlobalConfig("force-minecraft-command") + public boolean forceMinecraftCommand = false; + + @GlobalConfig("leaves-packet-event") + public boolean leavesPacketEvent = true; + } + + public static RegionConfig region = new RegionConfig(); + + @GlobalConfigCategory("region") + public static class RegionConfig { + + @GlobalConfig(value = "format", lock = true, validator = RegionFormatValidator.class) + public org.leavesmc.leaves.region.RegionFileFormat format = org.leavesmc.leaves.region.RegionFileFormat.ANVIL; + + private static class RegionFormatValidator extends EnumConfigValidator { + @Override + public void verify(RegionFileFormat old, RegionFileFormat value) throws IllegalArgumentException { + org.leavesmc.leaves.region.IRegionFileFactory.initFirstRegion(value); + } + } + + public LinearConfig linear = new LinearConfig(); + + @GlobalConfigCategory("linear") + public static class LinearConfig { + + @GlobalConfig(value = "version", lock = true, validator = LinearVersionValidator.class) + public org.leavesmc.leaves.region.linear.LinearVersion version = org.leavesmc.leaves.region.linear.LinearVersion.V2; + + private static class LinearVersionValidator extends EnumConfigValidator { + } + + @GlobalConfig(value = "auto-convert-anvil-to-linear", lock = true) + public boolean autoConvertAnvilToLinear = false; + + @GlobalConfig(value = "flush-max-threads", lock = true, validator = IntConfigValidator.class) + public int flushThreads = 6; + + public int getLinearFlushThreads() { + if (flushThreads <= 0) { + return Math.max(Runtime.getRuntime().availableProcessors() + flushThreads, 1); + } else { + return flushThreads; + } + } + + @GlobalConfig(value = "flush-delay-ms", lock = true, validator = IntConfigValidator.class) + public int flushDelayMs = 100; + + @GlobalConfig(value = "use-virtual-thread", lock = true) + public boolean useVirtualThread = true; + + @GlobalConfig(value = "compression-level", lock = true, validator = LinearCompressValidator.class) + public int compressionLevel = 1; + + private static class LinearCompressValidator extends IntConfigValidator { + @Override + public void verify(Integer old, Integer value) throws IllegalArgumentException { + if (value < 1 || value > 22) { + throw new IllegalArgumentException("linear.compression-level need between 1 and 22"); + } + } + } + + @RemovedConfig(name = "flush-frequency", category = {"region", "linear"}) + @RemovedConfig(name = "crash-on-broken-symlink", category = {"region", "linear"}) + private final boolean linearCrashOnBrokenSymlink = true; + } + } + + public static FixConfig fix = new FixConfig(); + + @GlobalConfigCategory("fix") + public static class FixConfig { + @GlobalConfig("vanilla-hopper") + public boolean vanillaHopper = false; + + @RemovedConfig(name = "spigot-EndPlatform-destroy", category = "fix") + private final boolean spigotEndPlatformDestroy = false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesLogger.java b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesLogger.java new file mode 100644 index 00000000..bc45935c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesLogger.java @@ -0,0 +1,24 @@ +package org.leavesmc.leaves; + +import org.bukkit.Bukkit; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LeavesLogger extends Logger { + public static final LeavesLogger LOGGER = new LeavesLogger(); + + private LeavesLogger() { + super("Leaves", null); + setParent(Bukkit.getLogger()); + setLevel(Level.ALL); + } + + public void severe(String msg, Exception exception) { + this.log(Level.SEVERE, msg, exception); + } + + public void warning(String msg, Exception exception) { + this.log(Level.WARNING, msg, exception); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotCommand.java new file mode 100644 index 00000000..4bf25cb2 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotCommand.java @@ -0,0 +1,544 @@ +package org.leavesmc.leaves.bot; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.World; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.command.ConsoleCommandSender; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.bukkit.generator.WorldInfo; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.bot.agent.Actions; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.bot.agent.BotConfig; +import org.leavesmc.leaves.bot.agent.Configs; +import org.leavesmc.leaves.bot.agent.actions.CraftCustomBotAction; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.command.LeavesCommandUtil; +import org.leavesmc.leaves.entity.Bot; +import org.leavesmc.leaves.event.bot.BotActionStopEvent; +import org.leavesmc.leaves.event.bot.BotConfigModifyEvent; +import org.leavesmc.leaves.event.bot.BotCreateEvent; +import org.leavesmc.leaves.event.bot.BotRemoveEvent; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; + +import java.util.*; + +import static net.kyori.adventure.text.Component.text; + +public class BotCommand extends Command { + + private final Component unknownMessage; + + public BotCommand(String name) { + super(name); + this.description = "FakePlayer Command"; + this.usageMessage = "/bot [create | remove | action | list | config]"; + this.unknownMessage = text("Usage: " + usageMessage, NamedTextColor.RED); + this.setPermission("bukkit.command.bot"); + final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); + if (pluginManager.getPermission("bukkit.command.bot") == null) { + pluginManager.addPermission(new Permission("bukkit.command.bot", PermissionDefault.OP)); + } + } + + @Override + public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args, Location location) throws IllegalArgumentException { + List list = new ArrayList<>(); + BotList botList = BotList.INSTANCE; + + if (args.length <= 1) { + list.add("create"); + list.add("remove"); + if (LeavesConfig.modify.fakeplayer.canUseAction) { + list.add("action"); + } + if (LeavesConfig.modify.fakeplayer.canModifyConfig) { + list.add("config"); + } + if (LeavesConfig.modify.fakeplayer.canManualSaveAndLoad) { + list.add("save"); + list.add("load"); + } + list.add("list"); + } + + if (args.length == 2) { + switch (args[0]) { + case "create" -> list.add(""); + case "remove", "action", "config", "save" -> list.addAll(botList.bots.stream().map(e -> e.getName().getString()).toList()); + case "list" -> list.addAll(Bukkit.getWorlds().stream().map(WorldInfo::getName).toList()); + case "load" -> list.addAll(botList.getSavedBotList().getAllKeys()); + } + } + + if (args.length == 3) { + switch (args[0]) { + case "action" -> { + list.add("list"); + list.add("stop"); + list.addAll(Actions.getNames()); + } + case "create" -> list.add(""); + case "config" -> list.addAll(acceptConfig); + case "remove" -> list.addAll(List.of("cancel", "[hour]")); + } + } + + if (args[0].equals("remove") && args.length >= 3) { + if (!Objects.equals(args[3], "cancel")) { + switch (args.length) { + case 4 -> list.add("[minute]"); + case 5 -> list.add("[second]"); + } + } + } + + if (args.length >= 4 && args[0].equals("action")) { + ServerBot bot = botList.getBotByName(args[1]); + + if (bot == null) { + return Collections.singletonList("<" + args[1] + " not found>"); + } + + if (args[2].equals("stop")) { + list.add("all"); + for (int i = 0; i < bot.getBotActions().size(); i++) { + list.add(String.valueOf(i)); + } + } else { + BotAction action = Actions.getForName(args[2]); + if (action != null) { + list.addAll(action.getArgument().tabComplete(args.length - 4)); + } + } + } + + if (args.length >= 4 && args[0].equals("config")) { + Configs config = Configs.getConfig(args[2]); + if (config != null) { + list.addAll(config.config.getArgument().tabComplete(args.length - 4)); + } + } + + return LeavesCommandUtil.getListMatchingLast(sender, args, list, "bukkit.command.bot.", "bukkit.command.bot"); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, String[] args) { + if (!testPermission(sender) || !LeavesConfig.modify.fakeplayer.enable) return true; + + if (args.length == 0) { + sender.sendMessage(unknownMessage); + return false; + } + + switch (args[0]) { + case "create" -> this.onCreate(sender, args); + case "remove" -> this.onRemove(sender, args); + case "action" -> this.onAction(sender, args); + case "config" -> this.onConfig(sender, args); + case "list" -> this.onList(sender, args); + case "save" -> this.onSave(sender, args); + case "load" -> this.onLoad(sender, args); + default -> { + sender.sendMessage(unknownMessage); + return false; + } + } + + return true; + } + + private void onCreate(CommandSender sender, String @NotNull [] args) { + if (args.length < 2) { + sender.sendMessage(text("Use /bot create [skin_name] to create a fakeplayer", NamedTextColor.RED)); + return; + } + + String botName = args[1]; + if (this.canCreate(sender, botName)) { + BotCreateState.Builder builder = BotCreateState.builder(botName, Bukkit.getWorlds().getFirst().getSpawnLocation()).createReason(BotCreateEvent.CreateReason.COMMAND).creator(sender); + + if (args.length >= 3) { + builder.skinName(args[2]); + } + + if (sender instanceof Player player) { + builder.location(player.getLocation()); + } else if (sender instanceof ConsoleCommandSender) { + if (args.length >= 7) { + try { + World world = Bukkit.getWorld(args[3]); + double x = Double.parseDouble(args[4]); + double y = Double.parseDouble(args[5]); + double z = Double.parseDouble(args[6]); + if (world != null) { + builder.location(new Location(world, x, y, z)); + } + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Can't build location", e); + } + } + } + + builder.spawnWithSkin(null); + } + } + + private boolean canCreate(CommandSender sender, @NotNull String name) { + BotList botList = BotList.INSTANCE; + if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { + sender.sendMessage(text("This name is illegal", NamedTextColor.RED)); + return false; + } + + if (Bukkit.getPlayerExact(name) != null || botList.getBotByName(name) != null) { + sender.sendMessage(text("This player is in server", NamedTextColor.RED)); + return false; + } + + if (LeavesConfig.modify.fakeplayer.unableNames.contains(name)) { + sender.sendMessage(text("This name is not allowed", NamedTextColor.RED)); + return false; + } + + if (botList.bots.size() >= LeavesConfig.modify.fakeplayer.limit) { + sender.sendMessage(text("Fakeplayer limit is full", NamedTextColor.RED)); + return false; + } + + return true; + } + + private void onRemove(CommandSender sender, String @NotNull [] args) { + if (args.length < 2 || args.length > 5) { + sender.sendMessage(text("Use /bot remove [hour] [minute] [second] to remove a fakeplayer", NamedTextColor.RED)); + return; + } + + BotList botList = BotList.INSTANCE; + ServerBot bot = botList.getBotByName(args[1]); + + if (bot == null) { + sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED)); + return; + } + + if (args.length > 2) { + if (args[2].equals("cancel")) { + if (bot.removeTaskId == -1) { + sender.sendMessage(text("This fakeplayer is not scheduled to be removed", NamedTextColor.RED)); + return; + } + Bukkit.getScheduler().cancelTask(bot.removeTaskId); + bot.removeTaskId = -1; + sender.sendMessage(text("Remove cancel")); + return; + } + + long time = 0; + int h; // Preventing out-of-range + long s = 0; + long m = 0; + + try { + h = Integer.parseInt(args[2]); + if (h < 0) { + throw new NumberFormatException(); + } + time += ((long) h) * 3600 * 20; + if (args.length > 3) { + m = Long.parseLong(args[3]); + if (m > 59 || m < 0) { + throw new NumberFormatException(); + } + time += m * 60 * 20; + } + if (args.length > 4) { + s = Long.parseLong(args[4]); + if (s > 59 || s < 0) { + throw new NumberFormatException(); + } + time += s * 20; + } + } catch (NumberFormatException e) { + sender.sendMessage(text("This fakeplayer is not scheduled to be removed", NamedTextColor.RED)); + return; + } + + boolean isReschedule = bot.removeTaskId != -1; + + if (isReschedule) { + Bukkit.getScheduler().cancelTask(bot.removeTaskId); + } + bot.removeTaskId = Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> { + bot.removeTaskId = -1; + botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, false); + }, time).getTaskId(); + + sender.sendMessage("This fakeplayer will be removed in " + h + "h " + m + "m " + s + "s" + (isReschedule ? " (rescheduled)" : "")); + + return; + } + + botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, false); + } + + private void onAction(CommandSender sender, String @NotNull [] args) { + if (!LeavesConfig.modify.fakeplayer.canUseAction) { + return; + } + + if (args.length < 3) { + sender.sendMessage(text("Use /bot action to make fakeplayer do action", NamedTextColor.RED)); + return; + } + + ServerBot bot = BotList.INSTANCE.getBotByName(args[1]); + if (bot == null) { + sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED)); + return; + } + + if (args[2].equals("list")) { + sender.sendMessage(bot.getScoreboardName() + "'s action list:"); + for (int i = 0; i < bot.getBotActions().size(); i++) { + sender.sendMessage(i + " " + bot.getBotActions().get(i).getName()); + } + return; + } + + if (args[2].equals("stop")) { + if (args.length < 4) { + sender.sendMessage(text("Invalid index", NamedTextColor.RED)); + return; + } + + String index = args[3]; + if (index.equals("all")) { + Set> forRemoval = new HashSet<>(); + for (int i = 0; i < bot.getBotActions().size(); i++) { + BotAction action = bot.getBotActions().get(i); + BotActionStopEvent event = new BotActionStopEvent( + bot.getBukkitEntity(), action.getName(), action.getUUID(), BotActionStopEvent.Reason.COMMAND, sender + ); + event.callEvent(); + if (!event.isCancelled()) { + forRemoval.add(action); + } + } + bot.getBotActions().removeAll(forRemoval); + sender.sendMessage(bot.getScoreboardName() + "'s action list cleared."); + } else { + try { + int i = Integer.parseInt(index); + if (i < 0 || i >= bot.getBotActions().size()) { + sender.sendMessage(text("Invalid index", NamedTextColor.RED)); + return; + } + + BotAction action = bot.getBotActions().get(i); + BotActionStopEvent event = new BotActionStopEvent( + bot.getBukkitEntity(), action.getName(), action.getUUID(), BotActionStopEvent.Reason.COMMAND, sender + ); + event.callEvent(); + if (!event.isCancelled()) { + bot.getBotActions().remove(i); + sender.sendMessage(bot.getScoreboardName() + "'s " + action.getName() + " stopped."); + + } + } catch (NumberFormatException e) { + sender.sendMessage(text("Invalid index", NamedTextColor.RED)); + } + } + return; + } + + BotAction action = Actions.getForName(args[2]); + if (action == null) { + sender.sendMessage(text("Invalid action", NamedTextColor.RED)); + return; + } + + CraftPlayer player; + if (sender instanceof CraftPlayer) { + player = (CraftPlayer) sender; + } else { + player = bot.getBukkitEntity(); + } + + String[] realArgs = new String[args.length - 3]; + if (realArgs.length != 0) { + System.arraycopy(args, 3, realArgs, 0, realArgs.length); + } + + BotAction newAction; + try { + if (action instanceof CraftCustomBotAction customBotAction) { + newAction = customBotAction.createCraft(player, realArgs); + } else { + newAction = action.create(); + newAction.loadCommand(player.getHandle(), action.getArgument().parse(0, realArgs)); + } + } catch (IllegalArgumentException e) { + sender.sendMessage(text("Action create error, please check your arguments, " + e.getMessage(), NamedTextColor.RED)); + return; + } + + if (newAction == null) { + return; + } + + if (bot.addBotAction(newAction, sender)) { + sender.sendMessage("Action " + action.getName() + " has been issued to " + bot.getName().getString()); + } + } + + private static final List acceptConfig = Configs.getConfigs().stream().map(config -> config.config.getName()).toList(); + + private void onConfig(CommandSender sender, String @NotNull [] args) { + if (!LeavesConfig.modify.fakeplayer.canModifyConfig) { + return; + } + + if (args.length < 3) { + sender.sendMessage(text("Use /bot config to modify fakeplayer's config", NamedTextColor.RED)); + return; + } + + ServerBot bot = BotList.INSTANCE.getBotByName(args[1]); + if (bot == null) { + sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED)); + return; + } + + if (!acceptConfig.contains(args[2])) { + sender.sendMessage(text("This config is not accept", NamedTextColor.RED)); + return; + } + + BotConfig config = Objects.requireNonNull(Configs.getConfig(args[2])).config; + if (args.length < 4) { + config.getMessage().forEach(sender::sendMessage); + } else { + String[] realArgs = new String[args.length - 3]; + System.arraycopy(args, 3, realArgs, 0, realArgs.length); + + BotConfigModifyEvent event = new BotConfigModifyEvent(bot.getBukkitEntity(), config.getName(), realArgs, sender); + Bukkit.getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return; + } + CommandArgumentResult result = config.getArgument().parse(0, realArgs); + + try { + config.setValue(result); + config.getChangeMessage().forEach(sender::sendMessage); + } catch (IllegalArgumentException e) { + sender.sendMessage(text(e.getMessage(), NamedTextColor.RED)); + } + } + } + + private void onSave(CommandSender sender, String @NotNull [] args) { + if (!LeavesConfig.modify.fakeplayer.canManualSaveAndLoad) { + return; + } + + if (args.length < 2) { + sender.sendMessage(text("Use /bot save to save a fakeplayer", NamedTextColor.RED)); + return; + } + + BotList botList = BotList.INSTANCE; + ServerBot bot = botList.getBotByName(args[1]); + + if (bot == null) { + sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED)); + return; + } + + if (botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, true)) { + sender.sendMessage(bot.getScoreboardName() + " saved to " + bot.createState.realName()); + } + } + + private void onLoad(CommandSender sender, String @NotNull [] args) { + if (!LeavesConfig.modify.fakeplayer.canManualSaveAndLoad) { + return; + } + + if (args.length < 2) { + sender.sendMessage(text("Use /bot save to save a fakeplayer", NamedTextColor.RED)); + return; + } + + String realName = args[1]; + BotList botList = BotList.INSTANCE; + if (!botList.getSavedBotList().contains(realName)) { + sender.sendMessage(text("This fakeplayer is not saved", NamedTextColor.RED)); + return; + } + + if (botList.loadNewBot(realName) == null) { + sender.sendMessage(text("Can't load bot, please check", NamedTextColor.RED)); + } + } + + private void onList(CommandSender sender, String @NotNull [] args) { + BotList botList = BotList.INSTANCE; + if (args.length < 2) { + Map> botMap = new HashMap<>(); + for (World world : Bukkit.getWorlds()) { + botMap.put(world, new ArrayList<>()); + } + + for (ServerBot bot : botList.bots) { + Bot bukkitBot = bot.getBukkitEntity(); + botMap.get(bukkitBot.getWorld()).add(bukkitBot.getName()); + } + + sender.sendMessage("Total number: (" + botList.bots.size() + "/" + LeavesConfig.modify.fakeplayer.limit + ")"); + for (World world : botMap.keySet()) { + sender.sendMessage(world.getName() + "(" + botMap.get(world).size() + "): " + formatPlayerNameList(botMap.get(world))); + } + } else { + World world = Bukkit.getWorld(args[1]); + + if (world == null) { + sender.sendMessage(text("Unknown world", NamedTextColor.RED)); + return; + } + + List snowBotList = new ArrayList<>(); + for (ServerBot bot : botList.bots) { + Bot bukkitBot = bot.getBukkitEntity(); + if (bukkitBot.getWorld() == world) { + snowBotList.add(bukkitBot.getName()); + } + } + + sender.sendMessage(world.getName() + "(" + botList.bots.size() + "): " + formatPlayerNameList(snowBotList)); + } + } + + @NotNull + private static String formatPlayerNameList(@NotNull List list) { + if (list.isEmpty()) { + return ""; + } + String string = list.toString(); + return string.substring(1, string.length() - 1); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java new file mode 100644 index 00000000..bc09bb05 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java @@ -0,0 +1,120 @@ +package org.leavesmc.leaves.bot; + +import net.minecraft.server.MinecraftServer; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.entity.Bot; +import org.leavesmc.leaves.entity.BotCreator; +import org.leavesmc.leaves.entity.CraftBot; +import org.leavesmc.leaves.event.bot.BotCreateEvent; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; + +import java.util.Objects; +import java.util.function.Consumer; + +public record BotCreateState(String realName, String name, String skinName, String[] skin, Location location, BotCreateEvent.CreateReason createReason, CommandSender creator) { + + private static final MinecraftServer server = MinecraftServer.getServer(); + + public ServerBot createNow() { + return server.getBotList().createNewBot(this); + } + + @NotNull + public static Builder builder(@NotNull String realName, @Nullable Location location) { + return new Builder(realName, location); + } + + public static class Builder implements BotCreator { + + private final String realName; + + private String name; + private Location location; + + private String skinName; + private String[] skin; + + private BotCreateEvent.CreateReason createReason; + private CommandSender creator; + + private Builder(@NotNull String realName, @Nullable Location location) { + Objects.requireNonNull(realName); + + this.realName = realName; + this.location = location; + + this.name = LeavesConfig.modify.fakeplayer.prefix + realName + LeavesConfig.modify.fakeplayer.suffix; + this.skinName = this.realName; + this.skin = null; + this.createReason = BotCreateEvent.CreateReason.UNKNOWN; + this.creator = null; + } + + public Builder name(@NotNull String name) { + Objects.requireNonNull(name); + this.name = name; + return this; + } + + public Builder skinName(@Nullable String skinName) { + this.skinName = skinName; + return this; + } + + public Builder skin(@Nullable String[] skin) { + this.skin = skin; + return this; + } + + public Builder mojangAPISkin() { + if (this.skinName != null) { + this.skin = MojangAPI.getSkin(this.skinName); + } + return this; + } + + public Builder location(@NotNull Location location) { + this.location = location; + return this; + } + + public Builder createReason(@NotNull BotCreateEvent.CreateReason createReason) { + Objects.requireNonNull(createReason); + this.createReason = createReason; + return this; + } + + public Builder creator(CommandSender creator) { + this.creator = creator; + return this; + } + + public BotCreateState build() { + return new BotCreateState(realName, name, skinName, skin, location, createReason, creator); + } + + public void spawnWithSkin(Consumer consumer) { + Bukkit.getScheduler().runTaskAsynchronously(MinecraftInternalPlugin.INSTANCE, () -> { + this.mojangAPISkin(); + Bukkit.getScheduler().runTask(MinecraftInternalPlugin.INSTANCE, () -> { + CraftBot bot = this.spawn(); + if (bot != null && consumer != null) { + consumer.accept(bot); + } + }); + }); + } + + @Nullable + public CraftBot spawn() { + Objects.requireNonNull(this.location); + ServerBot bot = this.build().createNow(); + return bot != null ? bot.getBukkitEntity() : null; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java new file mode 100644 index 00000000..53bece66 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java @@ -0,0 +1,121 @@ +package org.leavesmc.leaves.bot; + +import com.mojang.logging.LogUtils; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.NbtIo; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.storage.LevelResource; +import net.minecraft.world.level.storage.LevelStorageSource; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; + +public class BotDataStorage implements IPlayerDataStorage { + + private static final LevelResource BOT_DATA_DIR = new LevelResource("fakeplayerdata"); + private static final LevelResource BOT_LIST_FILE = new LevelResource("fakeplayer.dat"); + + private static final Logger LOGGER = LogUtils.getLogger(); + private final File botDir; + private final File botListFile; + + private CompoundTag savedBotList; + + public BotDataStorage(LevelStorageSource.@NotNull LevelStorageAccess session) { + this.botDir = session.getLevelPath(BOT_DATA_DIR).toFile(); + this.botListFile = session.getLevelPath(BOT_LIST_FILE).toFile(); + this.botDir.mkdirs(); + + this.savedBotList = new CompoundTag(); + if (this.botListFile.exists() && this.botListFile.isFile()) { + try { + Optional.of(NbtIo.readCompressed(this.botListFile.toPath(), NbtAccounter.unlimitedHeap())).ifPresent(tag -> this.savedBotList = tag); + } catch (Exception exception) { + BotDataStorage.LOGGER.warn("Failed to load player data list"); + } + } + } + + @Override + public void save(Player player) { + boolean flag = true; + try { + CompoundTag nbt = player.saveWithoutId(new CompoundTag()); + File file = new File(this.botDir, player.getStringUUID() + ".dat"); + + if (file.exists() && file.isFile()) { + if (!file.delete()) { + throw new IOException("Failed to delete file: " + file); + } + } + if (!file.createNewFile()) { + throw new IOException("Failed to create nbt file: " + file); + } + NbtIo.writeCompressed(nbt, file.toPath()); + } catch (Exception exception) { + BotDataStorage.LOGGER.warn("Failed to save fakeplayer data for {}", player.getScoreboardName(), exception); + flag = false; + } + + if (flag && player instanceof ServerBot bot) { + CompoundTag nbt = new CompoundTag(); + nbt.putString("name", bot.createState.name()); + nbt.putUUID("uuid", bot.getUUID()); + nbt.putBoolean("resume", bot.resume); + this.savedBotList.put(bot.createState.realName(), nbt); + this.saveBotList(); + } + } + + @Override + public Optional load(Player player) { + return this.load(player.getScoreboardName(), player.getStringUUID()).map((nbt) -> { + player.load(nbt); + return nbt; + }); + } + + private Optional load(String name, String uuid) { + File file = new File(this.botDir, uuid + ".dat"); + + if (file.exists() && file.isFile()) { + try { + Optional optional = Optional.of(NbtIo.readCompressed(file.toPath(), NbtAccounter.unlimitedHeap())); + if (!file.delete()) { + throw new IOException("Failed to delete fakeplayer data"); + } + this.savedBotList.remove(name); + this.saveBotList(); + return optional; + } catch (Exception exception) { + BotDataStorage.LOGGER.warn("Failed to load fakeplayer data for {}", name); + } + } + + return Optional.empty(); + } + + private void saveBotList() { + try { + if (this.botListFile.exists() && this.botListFile.isFile()) { + if (!this.botListFile.delete()) { + throw new IOException("Failed to delete file: " + this.botListFile); + } + } + if (!this.botListFile.createNewFile()) { + throw new IOException("Failed to create nbt file: " + this.botListFile); + } + NbtIo.writeCompressed(this.savedBotList, this.botListFile.toPath()); + } catch (Exception exception) { + BotDataStorage.LOGGER.warn("Failed to save player data list"); + } + } + + public CompoundTag getSavedBotList() { + return savedBotList; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java new file mode 100644 index 00000000..4f5e6e5c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java @@ -0,0 +1,191 @@ +package org.leavesmc.leaves.bot; + +import com.google.common.collect.ImmutableList; +import com.mojang.datafixers.util.Pair; +import net.minecraft.core.NonNullList; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.world.ContainerHelper; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.CustomData; + +import javax.annotation.Nonnull; +import java.util.List; + +// Power by gugle-carpet-addition(https://github.com/Gu-ZT/gugle-carpet-addition) +public class BotInventoryContainer extends SimpleContainer { + + public final NonNullList items; + public final NonNullList armor; + public final NonNullList offhand; + private final List> compartments; + private final NonNullList buttons = NonNullList.withSize(13, ItemStack.EMPTY); + private final ServerBot player; + + public BotInventoryContainer(ServerBot player) { + this.player = player; + this.items = this.player.getInventory().items; + this.armor = this.player.getInventory().armor; + this.offhand = this.player.getInventory().offhand; + this.compartments = ImmutableList.of(this.items, this.armor, this.offhand, this.buttons); + createButton(); + } + + @Override + public int getContainerSize() { + return this.items.size() + this.armor.size() + this.offhand.size() + this.buttons.size(); + } + + @Override + public boolean isEmpty() { + for (ItemStack itemStack : this.items) { + if (itemStack.isEmpty()) { + continue; + } + return false; + } + for (ItemStack itemStack : this.armor) { + if (itemStack.isEmpty()) { + continue; + } + return false; + } + for (ItemStack itemStack : this.offhand) { + if (itemStack.isEmpty()) { + continue; + } + return false; + } + return true; + } + + @Override + @Nonnull + public ItemStack getItem(int slot) { + Pair, Integer> pair = getItemSlot(slot); + if (pair != null) { + return pair.getFirst().get(pair.getSecond()); + } else { + return ItemStack.EMPTY; + } + } + + public Pair, Integer> getItemSlot(int slot) { + switch (slot) { + case 0 -> { + return new Pair<>(buttons, 0); + } + case 1, 2, 3, 4 -> { + return new Pair<>(armor, 4 - slot); + } + case 5, 6 -> { + return new Pair<>(buttons, slot - 4); + } + case 7 -> { + return new Pair<>(offhand, 0); + } + case 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 -> { + return new Pair<>(buttons, slot - 5); + } + case 18, 19, 20, 21, 22, 23, 24, 25, 26, + 27, 28, 29, 30, 31, 32, 33, 34, 35, + 36, 37, 38, 39, 40, 41, 42, 43, 44 -> { + return new Pair<>(items, slot - 9); + } + case 45, 46, 47, 48, 49, 50, 51, 52, 53 -> { + return new Pair<>(items, slot - 45); + } + default -> { + return null; + } + } + } + + @Override + @Nonnull + public ItemStack removeItem(int slot, int amount) { + Pair, Integer> pair = getItemSlot(slot); + NonNullList list = null; + ItemStack itemStack = ItemStack.EMPTY; + if (pair != null) { + list = pair.getFirst(); + slot = pair.getSecond(); + } + if (list != null && !list.get(slot).isEmpty()) { + itemStack = ContainerHelper.removeItem(list, slot, amount); + player.detectEquipmentUpdatesPublic(); + } + return itemStack; + } + + @Override + @Nonnull + public ItemStack removeItemNoUpdate(int slot) { + Pair, Integer> pair = getItemSlot(slot); + NonNullList list = null; + if (pair != null) { + list = pair.getFirst(); + slot = pair.getSecond(); + } + if (list != null && !list.get(slot).isEmpty()) { + ItemStack itemStack = list.get(slot); + list.set(slot, ItemStack.EMPTY); + return itemStack; + } + return ItemStack.EMPTY; + } + + @Override + public void setItem(int slot, @Nonnull ItemStack stack) { + Pair, Integer> pair = getItemSlot(slot); + NonNullList list = null; + if (pair != null) { + list = pair.getFirst(); + slot = pair.getSecond(); + } + if (list != null) { + list.set(slot, stack); + player.detectEquipmentUpdatesPublic(); + } + } + + @Override + public void setChanged() { + } + + @Override + public boolean stillValid(@Nonnull Player player) { + if (this.player.isRemoved()) { + return false; + } + return !(player.distanceToSqr(this.player) > 64.0); + } + + @Override + public void clearContent() { + for (List list : this.compartments) { + list.clear(); + } + } + + private void createButton() { + CompoundTag customData = new CompoundTag(); + customData.putBoolean("Leaves.Gui.Placeholder", true); + + DataComponentPatch patch = DataComponentPatch.builder() + .set(DataComponents.CUSTOM_NAME, Component.empty()) + .set(DataComponents.CUSTOM_DATA, CustomData.of(customData)) + .build(); + + for (int i = 0; i < 13; i++) { + ItemStack button = new ItemStack(Items.STRUCTURE_VOID); + button.applyComponents(patch); + buttons.set(i, button); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotList.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotList.java new file mode 100644 index 00000000..3881aa5c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotList.java @@ -0,0 +1,297 @@ +package org.leavesmc.leaves.bot; + +import com.google.common.collect.Maps; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.properties.Property; +import com.mojang.logging.LogUtils; +import io.papermc.paper.adventure.PaperAdventure; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; +import net.minecraft.resources.ResourceKey; +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.Level; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.CraftWorld; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.event.bot.BotCreateEvent; +import org.leavesmc.leaves.event.bot.BotJoinEvent; +import org.leavesmc.leaves.event.bot.BotLoadEvent; +import org.leavesmc.leaves.event.bot.BotRemoveEvent; +import org.leavesmc.leaves.event.bot.BotSpawnLocationEvent; +import org.slf4j.Logger; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; +import java.util.concurrent.CopyOnWriteArrayList; + +public class BotList { + + public static BotList INSTANCE; + + private static final Logger LOGGER = LogUtils.getLogger(); + + private final MinecraftServer server; + + public final List bots = new CopyOnWriteArrayList<>(); + private final BotDataStorage dataStorage; + + private final Map botsByUUID = Maps.newHashMap(); + private final Map botsByName = Maps.newHashMap(); + + public BotList(MinecraftServer server) { + this.server = server; + this.dataStorage = new BotDataStorage(server.storageSource); + INSTANCE = this; + } + + public ServerBot createNewBot(BotCreateState state) { + BotCreateEvent event = new BotCreateEvent(state.name(), state.skinName(), state.location(), state.createReason(), state.creator()); + event.setCancelled(!isCreateLegal(state.name())); + this.server.server.getPluginManager().callEvent(event); + + if (event.isCancelled()) { + return null; + } + + Location location = event.getCreateLocation(); + ServerLevel world = ((CraftWorld) location.getWorld()).getHandle(); + + CustomGameProfile profile = new CustomGameProfile(BotUtil.getBotUUID(state), state.name(), state.skin()); + ServerBot bot = new ServerBot(this.server, world, profile); + bot.createState = state; + if (event.getCreator() instanceof org.bukkit.entity.Player player) { + bot.createPlayer = player.getUniqueId(); + } + + return this.placeNewBot(bot, world, location, null); + } + + public ServerBot loadNewBot(String realName) { + return this.loadNewBot(realName, this.dataStorage); + } + + public ServerBot loadNewBot(String realName, IPlayerDataStorage playerIO) { + UUID uuid = BotUtil.getBotUUID(realName); + + BotLoadEvent event = new BotLoadEvent(realName, uuid); + this.server.server.getPluginManager().callEvent(event); + if (event.isCancelled()) { + return null; + } + + ServerBot bot = new ServerBot(this.server, this.server.getLevel(Level.OVERWORLD), new GameProfile(uuid, realName)); + bot.connection = new ServerBotPacketListenerImpl(this.server, bot); + Optional optional = playerIO.load(bot); + + if (optional.isEmpty()) { + return null; + } + + ResourceKey resourcekey = null; + if (optional.get().contains("WorldUUIDMost") && optional.get().contains("WorldUUIDLeast")) { + org.bukkit.World bWorld = Bukkit.getServer().getWorld(new UUID(optional.get().getLong("WorldUUIDMost"), optional.get().getLong("WorldUUIDLeast"))); + if (bWorld != null) { + resourcekey = ((CraftWorld) bWorld).getHandle().dimension(); + } + } + if (resourcekey == null) { + return null; + } + + ServerLevel world = this.server.getLevel(resourcekey); + return this.placeNewBot(bot, world, bot.getLocation(), optional.get()); + } + + public ServerBot placeNewBot(ServerBot bot, ServerLevel world, Location location, @Nullable CompoundTag nbt) { + Optional optional = Optional.ofNullable(nbt); + + bot.isRealPlayer = true; + bot.loginTime = System.currentTimeMillis(); + bot.connection = new ServerBotPacketListenerImpl(this.server, bot); + bot.setServerLevel(world); + + BotSpawnLocationEvent event = new BotSpawnLocationEvent(bot.getBukkitEntity(), location); + this.server.server.getPluginManager().callEvent(event); + location = event.getSpawnLocation(); + + bot.spawnIn(world); + bot.gameMode.setLevel(bot.serverLevel()); + + bot.setPosRaw(location.getX(), location.getY(), location.getZ()); + bot.setRot(location.getYaw(), location.getPitch()); + + bot.connection.teleport(bot.getX(), bot.getY(), bot.getZ(), bot.getYRot(), bot.getXRot()); + + this.bots.add(bot); + this.botsByName.put(bot.getScoreboardName().toLowerCase(Locale.ROOT), bot); + this.botsByUUID.put(bot.getUUID(), bot); + + bot.supressTrackerForLogin = true; + world.addNewPlayer(bot); + bot.loadAndSpawnEnderpearls(optional); + bot.loadAndSpawnParentVehicle(optional); + + BotJoinEvent event1 = new BotJoinEvent(bot.getBukkitEntity(), PaperAdventure.asAdventure(Component.translatable("multiplayer.player.joined", bot.getDisplayName())).style(Style.style(NamedTextColor.YELLOW))); + this.server.server.getPluginManager().callEvent(event1); + + net.kyori.adventure.text.Component joinMessage = event1.joinMessage(); + if (joinMessage != null && !joinMessage.equals(net.kyori.adventure.text.Component.empty())) { + this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(joinMessage), false); + } + + bot.renderAll(); + bot.supressTrackerForLogin = false; + + bot.serverLevel().getChunkSource().chunkMap.addEntity(bot); + BotList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", bot.getName().getString(), "Local", bot.getId(), bot.serverLevel().serverLevelData.getLevelName(), bot.getX(), bot.getY(), bot.getZ()); + return bot; + } + + public boolean removeBot(@NotNull ServerBot bot, @NotNull BotRemoveEvent.RemoveReason reason, @Nullable CommandSender remover, boolean saved) { + return this.removeBot(bot, reason, remover, saved, this.dataStorage); + } + + public boolean removeBot(@NotNull ServerBot bot, @NotNull BotRemoveEvent.RemoveReason reason, @Nullable CommandSender remover, boolean saved, IPlayerDataStorage playerIO) { + BotRemoveEvent event = new BotRemoveEvent(bot.getBukkitEntity(), reason, remover, PaperAdventure.asAdventure(Component.translatable("multiplayer.player.left", bot.getDisplayName())).style(Style.style(NamedTextColor.YELLOW)), saved); + this.server.server.getPluginManager().callEvent(event); + + if (event.isCancelled() && event.getReason() != BotRemoveEvent.RemoveReason.INTERNAL) { + return event.isCancelled(); + } + + if (bot.removeTaskId != -1) { + Bukkit.getScheduler().cancelTask(bot.removeTaskId); + bot.removeTaskId = -1; + } + + if (this.server.isSameThread()) { + bot.doTick(); + } + + if (event.shouldSave()) { + playerIO.save(bot); + } else { + bot.dropAll(); + } + + if (bot.isPassenger()) { + Entity entity = bot.getRootVehicle(); + if (entity.hasExactlyOnePlayerPassenger()) { + bot.stopRiding(); + entity.getPassengersAndSelf().forEach((entity1) -> { + if (entity1 instanceof net.minecraft.world.entity.npc.AbstractVillager villager) { + final net.minecraft.world.entity.player.Player human = villager.getTradingPlayer(); + if (human != null) { + villager.setTradingPlayer(null); + } + } + entity1.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER); + }); + } + } + + bot.unRide(); + bot.serverLevel().removePlayerImmediately(bot, Entity.RemovalReason.UNLOADED_WITH_PLAYER); + this.bots.remove(bot); + this.botsByName.remove(bot.getScoreboardName().toLowerCase(Locale.ROOT)); + + UUID uuid = bot.getUUID(); + ServerBot bot1 = this.botsByUUID.get(uuid); + if (bot1 == bot) { + this.botsByUUID.remove(uuid); + } + + bot.removeTab(); + for (ServerPlayer player : bot.serverLevel().players()) { + if (!(player instanceof ServerBot) && !bot.needSendFakeData(player)) { + player.connection.send(new ClientboundRemoveEntitiesPacket(bot.getId())); + } + } + + net.kyori.adventure.text.Component removeMessage = event.removeMessage(); + if (removeMessage != null && !removeMessage.equals(net.kyori.adventure.text.Component.empty())) { + this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(removeMessage), false); + } + return true; + } + + public void removeAll() { + for (ServerBot bot : this.bots) { + bot.resume = LeavesConfig.modify.fakeplayer.canResident; + this.removeBot(bot, BotRemoveEvent.RemoveReason.INTERNAL, null, LeavesConfig.modify.fakeplayer.canResident); + } + } + + public void loadResume() { + if (LeavesConfig.modify.fakeplayer.enable && LeavesConfig.modify.fakeplayer.canResident) { + CompoundTag savedBotList = this.getSavedBotList().copy(); + for (String realName : savedBotList.getAllKeys()) { + CompoundTag nbt = savedBotList.getCompound(realName); + if (nbt.getBoolean("resume")) { + this.loadNewBot(realName); + } + } + } + } + + public void networkTick() { + this.bots.forEach(ServerBot::doTick); + } + + @Nullable + public ServerBot getBot(@NotNull UUID uuid) { + return this.botsByUUID.get(uuid); + } + + @Nullable + public ServerBot getBotByName(@NotNull String name) { + return this.botsByName.get(name.toLowerCase(Locale.ROOT)); + } + + public CompoundTag getSavedBotList() { + return this.dataStorage.getSavedBotList(); + } + + public boolean isCreateLegal(@NotNull String name) { + if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { + return false; + } + + if (Bukkit.getPlayerExact(name) != null || this.getBotByName(name) != null) { + return false; + } + + if (LeavesConfig.modify.fakeplayer.unableNames.contains(name)) { + return false; + } + + return this.bots.size() < LeavesConfig.modify.fakeplayer.limit; + } + + public static class CustomGameProfile extends GameProfile { + + public CustomGameProfile(UUID uuid, String name, String[] skin) { + super(uuid, name); + this.setSkin(skin); + } + + public void setSkin(String[] skin) { + if (skin != null) { + this.getProperties().put("textures", new Property("textures", skin[0], skin[1])); + } + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java new file mode 100644 index 00000000..10494446 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java @@ -0,0 +1,36 @@ +package org.leavesmc.leaves.bot; + +import com.mojang.datafixers.DataFixer; +import net.minecraft.server.MinecraftServer; +import net.minecraft.stats.ServerStatsCounter; +import net.minecraft.stats.Stat; +import net.minecraft.world.entity.player.Player; +import org.jetbrains.annotations.NotNull; + +import java.io.File; + +public class BotStatsCounter extends ServerStatsCounter { + + private static final File UNKOWN_FILE = new File("BOT_STATS_REMOVE_THIS"); + + public BotStatsCounter(MinecraftServer server) { + super(server, UNKOWN_FILE); + } + + @Override + public void save() { + } + + @Override + public void setValue(@NotNull Player player, @NotNull Stat stat, int value) { + } + + @Override + public void parseLocal(@NotNull DataFixer dataFixer, @NotNull String json) { + } + + @Override + public int getValue(@NotNull Stat stat) { + return 0; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotUtil.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotUtil.java new file mode 100644 index 00000000..78414d1f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/BotUtil.java @@ -0,0 +1,73 @@ +package org.leavesmc.leaves.bot; + +import com.google.common.base.Charsets; +import net.minecraft.core.NonNullList; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class BotUtil { + + public static void replenishment(@NotNull ItemStack itemStack, NonNullList itemStackList) { + int count = itemStack.getMaxStackSize() / 2; + if (itemStack.getCount() <= 8 && count > 8) { + for (ItemStack itemStack1 : itemStackList) { + if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) { + continue; + } + + if (ItemStack.isSameItemSameComponents(itemStack1, itemStack)) { + if (itemStack1.getCount() > count) { + itemStack.setCount(itemStack.getCount() + count); + itemStack1.setCount(itemStack1.getCount() - count); + } else { + itemStack.setCount(itemStack.getCount() + itemStack1.getCount()); + itemStack1.setCount(0); + } + break; + } + } + } + } + + public static void replaceTool(@NotNull EquipmentSlot slot, @NotNull ServerBot bot) { + ItemStack itemStack = bot.getItemBySlot(slot); + for (int i = 0; i < 36; i++) { + ItemStack itemStack1 = bot.getInventory().getItem(i); + if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) { + continue; + } + + if (itemStack1.getItem().getClass() == itemStack.getItem().getClass() && !isDamage(itemStack1, 10)) { + ItemStack itemStack2 = itemStack1.copy(); + bot.getInventory().setItem(i, itemStack); + bot.setItemSlot(slot, itemStack2); + return; + } + } + + for (int i = 0; i < 36; i++) { + ItemStack itemStack1 = bot.getInventory().getItem(i); + if (itemStack1 == ItemStack.EMPTY && itemStack1 != itemStack) { + bot.getInventory().setItem(i, itemStack); + bot.setItemSlot(slot, ItemStack.EMPTY); + return; + } + } + } + + public static boolean isDamage(@NotNull ItemStack item, int minDamage) { + return item.isDamageableItem() && (item.getMaxDamage() - item.getDamageValue()) <= minDamage; + } + + @NotNull + public static UUID getBotUUID(@NotNull BotCreateState state) { + return getBotUUID(state.realName()); + } + + public static UUID getBotUUID(@NotNull String realName) { + return UUID.nameUUIDFromBytes(("Fakeplayer:" + realName).getBytes(Charsets.UTF_8)); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java new file mode 100644 index 00000000..7ebe4d6c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java @@ -0,0 +1,13 @@ +package org.leavesmc.leaves.bot; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.player.Player; + +import java.util.Optional; + +public interface IPlayerDataStorage { + + void save(Player player); + + Optional load(Player player); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java new file mode 100644 index 00000000..4162df88 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java @@ -0,0 +1,39 @@ +package org.leavesmc.leaves.bot; + +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.leavesmc.leaves.LeavesConfig; + +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +public class MojangAPI { + + private static final Map CACHE = new HashMap<>(); + + public static String[] getSkin(String name) { + if (LeavesConfig.modify.fakeplayer.useSkinCache && CACHE.containsKey(name)) { + return CACHE.get(name); + } + + String[] values = pullFromAPI(name); + CACHE.put(name, values); + return values; + } + + // Laggggggggggggggggggggggggggggggggggggggggg + public static String[] pullFromAPI(String name) { + try { + String uuid = JsonParser.parseReader(new InputStreamReader(URI.create("https://api.mojang.com/users/profiles/minecraft/" + name).toURL().openStream())) + .getAsJsonObject().get("id").getAsString(); + JsonObject property = JsonParser.parseReader(new InputStreamReader(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false").toURL().openStream())) + .getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject(); + return new String[]{property.get("value").getAsString(), property.get("signature").getAsString()}; + } catch (IOException | IllegalStateException | IllegalArgumentException e) { + return null; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java new file mode 100644 index 00000000..022812a2 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBot.java @@ -0,0 +1,553 @@ +package org.leavesmc.leaves.bot; + +import com.google.common.collect.ImmutableMap; +import com.mojang.authlib.GameProfile; +import io.papermc.paper.adventure.PaperAdventure; +import io.papermc.paper.event.entity.EntityKnockbackEvent; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.BlockParticleOption; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.StringTag; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; +import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; +import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ChunkMap; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerPlayerConnection; +import net.minecraft.stats.ServerStatsCounter; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.SimpleMenuProvider; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.PositionMoveRotation; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.item.ItemEntity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.ChestMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.gameevent.GameEvent; +import net.minecraft.world.level.portal.TeleportTransition; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.Vec3; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.CommandSender; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.bot.agent.Actions; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.bot.agent.BotConfig; +import org.leavesmc.leaves.bot.agent.Configs; +import org.leavesmc.leaves.entity.CraftBot; +import org.leavesmc.leaves.event.bot.BotActionScheduleEvent; +import org.leavesmc.leaves.event.bot.BotCreateEvent; +import org.leavesmc.leaves.event.bot.BotDeathEvent; +import org.leavesmc.leaves.event.bot.BotInventoryOpenEvent; +import org.leavesmc.leaves.event.bot.BotRemoveEvent; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; +import org.leavesmc.leaves.util.MathUtils; + +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.function.Predicate; + +// TODO test +public class ServerBot extends ServerPlayer { + + private final Map, BotConfig> configs; + private final List> actions; + + public boolean resume = false; + public BotCreateState createState; + public UUID createPlayer; + + private final int tracingRange; + private final ServerStatsCounter stats; + private final BotInventoryContainer container; + + public int notSleepTicks; + + public int removeTaskId = -1; + + private Vec3 knockback = Vec3.ZERO; + + public ServerBot(MinecraftServer server, ServerLevel world, GameProfile profile) { + super(server, world, profile, ClientInformation.createDefault()); + this.entityData.set(Player.DATA_PLAYER_MODE_CUSTOMISATION, (byte) -2); + + this.gameMode = new ServerBotGameMode(this); + this.actions = new ArrayList<>(); + + ImmutableMap.Builder, BotConfig> configBuilder = ImmutableMap.builder(); + for (Configs config : Configs.getConfigs()) { + configBuilder.put(config, config.config.create(this)); + } + this.configs = configBuilder.build(); + + this.stats = new BotStatsCounter(server); + this.container = new BotInventoryContainer(this); + this.tracingRange = world.spigotConfig.playerTrackingRange * world.spigotConfig.playerTrackingRange; + + this.notSleepTicks = 0; + this.fauxSleeping = LeavesConfig.modify.fakeplayer.canSkipSleep; + } + + public void sendPlayerInfo(ServerPlayer player) { + player.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), List.of(this))); + } + + public boolean needSendFakeData(ServerPlayer player) { + return this.getConfigValue(Configs.ALWAYS_SEND_DATA) && (player.level() == this.level() && player.position().distanceToSqr(this.position()) > this.tracingRange); + } + + public void sendFakeDataIfNeed(ServerPlayer player, boolean login) { + if (needSendFakeData(player)) { + this.sendFakeData(player.connection, login); + } + } + + public void sendFakeData(ServerPlayerConnection playerConnection, boolean login) { + ChunkMap.TrackedEntity entityTracker = ((ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId()); + + if (entityTracker == null) { + LeavesLogger.LOGGER.warning("Fakeplayer cant get entity tracker for " + this.getId()); + return; + } + + playerConnection.send(this.getAddEntityPacket(entityTracker.serverEntity)); + if (login) { + Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> playerConnection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f))), 10); + } else { + playerConnection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f))); + } + } + + public void renderAll() { + this.server.getPlayerList().getPlayers().forEach( + player -> { + this.sendPlayerInfo(player); + this.sendFakeDataIfNeed(player, false); + } + ); + } + + private void sendPacket(Packet packet) { + this.server.getPlayerList().getPlayers().forEach(player -> player.connection.send(packet)); + } + + @Override + public void die(@NotNull DamageSource damageSource) { + boolean flag = this.serverLevel().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES); + Component defaultMessage = this.getCombatTracker().getDeathMessage(); + + BotDeathEvent event = new BotDeathEvent(this.getBukkitEntity(), PaperAdventure.asAdventure(defaultMessage), flag); + this.server.server.getPluginManager().callEvent(event); + + if (event.isCancelled()) { + if (this.getHealth() <= 0) { + this.setHealth(0.1f); + } + return; + } + + this.gameEvent(GameEvent.ENTITY_DIE); + + net.kyori.adventure.text.Component deathMessage = event.deathMessage(); + if (event.isSendDeathMessage() && deathMessage != null && !deathMessage.equals(net.kyori.adventure.text.Component.empty())) { + this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(deathMessage), false); + } + + this.server.getBotList().removeBot(this, BotRemoveEvent.RemoveReason.DEATH, null, false); + } + + public void removeTab() { + this.sendPacket(new ClientboundPlayerInfoRemovePacket(List.of(this.getUUID()))); + } + + @Override + public @Nullable ServerBot teleport(@NotNull TeleportTransition teleportTarget) { + if (this.isSleeping() || this.isRemoved()) { + return null; + } + if (teleportTarget.newLevel().dimension() != this.serverLevel().dimension()) { + return null; + } else { + if (!teleportTarget.asPassenger()) { + this.stopRiding(); + } + + this.connection.internalTeleport(PositionMoveRotation.of(teleportTarget), teleportTarget.relatives()); + this.connection.resetPosition(); + teleportTarget.postTeleportTransition().onTransition(this); + return this; + } + } + + @Override + public void handlePortal() { + } + + @Override + public void tick() { + if (!this.isAlive()) { + return; + } + super.tick(); + + if (this.getConfigValue(Configs.SPAWN_PHANTOM)) { + notSleepTicks++; + } + + if (LeavesConfig.modify.fakeplayer.regenAmount > 0.0 && server.getTickCount() % 20 == 0) { + float health = getHealth(); + float maxHealth = getMaxHealth(); + float regenAmount = (float) (LeavesConfig.modify.fakeplayer.regenAmount * 20); + float amount; + + if (health < maxHealth - regenAmount) { + amount = health + regenAmount; + } else { + amount = maxHealth; + } + + this.setHealth(amount); + } + } + + @Override + public void onItemPickup(@NotNull ItemEntity item) { + super.onItemPickup(item); + this.updateItemInHand(InteractionHand.MAIN_HAND); + } + + public void updateItemInHand(InteractionHand hand) { + ItemStack item = this.getItemInHand(hand); + + if (!item.isEmpty()) { + BotUtil.replenishment(item, getInventory().items); + if (BotUtil.isDamage(item, 10)) { + BotUtil.replaceTool(hand == InteractionHand.MAIN_HAND ? EquipmentSlot.MAINHAND : EquipmentSlot.OFFHAND, this); + } + } + this.detectEquipmentUpdatesPublic(); + } + + @Override + public @NotNull InteractionResult interact(@NotNull Player player, @NotNull InteractionHand hand) { + if (LeavesConfig.modify.fakeplayer.canOpenInventory) { + if (player instanceof ServerPlayer player1 && player.getMainHandItem().isEmpty()) { + BotInventoryOpenEvent event = new BotInventoryOpenEvent(this.getBukkitEntity(), player1.getBukkitEntity()); + this.server.server.getPluginManager().callEvent(event); + if (!event.isCancelled()) { + player.openMenu(new SimpleMenuProvider((i, inventory, p) -> ChestMenu.sixRows(i, inventory, this.container), this.getDisplayName())); + return InteractionResult.SUCCESS; + } + } + } + return super.interact(player, hand); + } + + @Override + public void checkFallDamage(double y, boolean onGround, @NotNull BlockState state, @NotNull BlockPos pos) { + ServerLevel serverLevel = this.serverLevel(); + if (onGround && this.fallDistance > 0.0F) { + this.onChangedBlock(serverLevel, pos); + double attributeValue = this.getAttributeValue(Attributes.SAFE_FALL_DISTANCE); + if (this.fallDistance > attributeValue && !state.isAir()) { + double x = this.getX(); + double y1 = this.getY(); + double z = this.getZ(); + BlockPos blockPos = this.blockPosition(); + if (pos.getX() != blockPos.getX() || pos.getZ() != blockPos.getZ()) { + double d = x - pos.getX() - 0.5; + double d1 = z - pos.getZ() - 0.5; + double max = Math.max(Math.abs(d), Math.abs(d1)); + x = pos.getX() + 0.5 + d / max * 0.5; + z = pos.getZ() + 0.5 + d1 / max * 0.5; + } + + float f = Mth.ceil(this.fallDistance - attributeValue); + double min = Math.min(0.2F + f / 15.0F, 2.5); + int i = (int) (150.0 * min); + serverLevel.sendParticlesSource(this, new BlockParticleOption(ParticleTypes.BLOCK, state), false, false, x, y1, z, i, 0.0, 0.0, 0.0, 0.15F); + } + } + + if (onGround) { + if (this.fallDistance > 0.0F) { + state.getBlock().fallOn(serverLevel, state, pos, this, this.fallDistance); + serverLevel.gameEvent(GameEvent.HIT_GROUND, this.position(), + GameEvent.Context.of(this, this.mainSupportingBlockPos.map(supportingPos -> this.level().getBlockState(supportingPos)).orElse(state)) + ); + } + + this.resetFallDistance(); + } else if (y < 0.0D) { + this.fallDistance -= (float) y; + } + } + + @Override + public void doTick() { + this.absMoveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); + + if (this.takeXpDelay > 0) { + --this.takeXpDelay; + } + + if (this.isSleeping()) { + ++this.sleepCounter; + if (this.sleepCounter > 100) { + this.sleepCounter = 100; + this.notSleepTicks = 0; + } + + if (!this.level().isClientSide && this.level().isDay()) { + this.stopSleepInBed(false, true); + } + } else if (this.sleepCounter > 0) { + ++this.sleepCounter; + if (this.sleepCounter >= 110) { + this.sleepCounter = 0; + } + } + + this.updateIsUnderwater(); + + this.addDeltaMovement(knockback); + this.knockback = Vec3.ZERO; + + this.server.scheduleOnMain(this::runAction); + + this.livingEntityTick(); + + this.foodData.tick(this); + + ++this.attackStrengthTicker; + ItemStack itemstack = this.getMainHandItem(); + if (!ItemStack.matches(this.lastItemInMainHand, itemstack)) { + if (!ItemStack.isSameItem(this.lastItemInMainHand, itemstack)) { + this.resetAttackStrengthTicker(); + } + + this.lastItemInMainHand = itemstack.copy(); + } + + this.getCooldowns().tick(); + this.updatePlayerPose(); + + if (this.hurtTime > 0) { + this.hurtTime -= 1; + } + } + + @Override + public void knockback(double strength, double x, double z, @Nullable Entity attacker, @NotNull EntityKnockbackEvent.Cause cause) { + strength *= 1.0D - this.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE); + if (strength > 0.0D) { + Vec3 vec3d = this.getDeltaMovement(); + Vec3 vec3d1 = (new Vec3(x, 0.0D, z)).normalize().scale(strength); + this.hasImpulse = true; + this.knockback = new Vec3(vec3d.x / 2.0D - vec3d1.x, this.onGround() ? Math.min(0.4D, vec3d.y / 2.0D + strength) : vec3d.y, vec3d.z / 2.0D - vec3d1.z).subtract(vec3d); + } + } + + @Override + public void attack(@NotNull Entity target) { + super.attack(target); + this.swing(InteractionHand.MAIN_HAND); + } + + @Override + public void addAdditionalSaveData(@NotNull CompoundTag nbt) { + super.addAdditionalSaveData(nbt); + nbt.putBoolean("isShiftKeyDown", this.isShiftKeyDown()); + + CompoundTag createNbt = new CompoundTag(); + createNbt.putString("realName", this.createState.realName()); + createNbt.putString("name", this.createState.name()); + + createNbt.putString("skinName", this.createState.skinName()); + if (this.createState.skin() != null) { + ListTag skin = new ListTag(); + for (String s : this.createState.skin()) { + skin.add(StringTag.valueOf(s)); + } + createNbt.put("skin", skin); + } + + nbt.put("createStatus", createNbt); + + if (!this.actions.isEmpty()) { + ListTag actionNbt = new ListTag(); + for (BotAction action : this.actions) { + actionNbt.add(action.save(new CompoundTag())); + } + nbt.put("actions", actionNbt); + } + + if (!this.configs.isEmpty()) { + ListTag configNbt = new ListTag(); + for (BotConfig config : this.configs.values()) { + configNbt.add(config.save(new CompoundTag())); + } + nbt.put("configs", configNbt); + } + } + + @Override + public void readAdditionalSaveData(@NotNull CompoundTag nbt) { + super.readAdditionalSaveData(nbt); + this.setShiftKeyDown(nbt.getBoolean("isShiftKeyDown")); + + CompoundTag createNbt = nbt.getCompound("createStatus"); + BotCreateState.Builder createBuilder = BotCreateState.builder(createNbt.getString("realName"), null).name(createNbt.getString("name")); + + String[] skin = null; + if (createNbt.contains("skin")) { + ListTag skinTag = createNbt.getList("skin", 8); + skin = new String[skinTag.size()]; + for (int i = 0; i < skinTag.size(); i++) { + skin[i] = skinTag.getString(i); + } + } + + createBuilder.skinName(createNbt.getString("skinName")).skin(skin); + createBuilder.createReason(BotCreateEvent.CreateReason.INTERNAL).creator(null); + + this.createState = createBuilder.build(); + this.gameProfile = new BotList.CustomGameProfile(this.getUUID(), this.createState.name(), this.createState.skin()); + + + if (nbt.contains("actions")) { + ListTag actionNbt = nbt.getList("actions", 10); + for (int i = 0; i < actionNbt.size(); i++) { + CompoundTag actionTag = actionNbt.getCompound(i); + BotAction action = Actions.getForName(actionTag.getString("actionName")); + if (action != null) { + BotAction newAction = action.create(); + newAction.load(actionTag); + this.actions.add(newAction); + } + } + } + + if (nbt.contains("configs")) { + ListTag configNbt = nbt.getList("configs", 10); + for (int i = 0; i < configNbt.size(); i++) { + CompoundTag configTag = configNbt.getCompound(i); + Configs configKey = Configs.getConfig(configTag.getString("configName")); + if (configKey != null) { + this.configs.get(configKey).load(configTag); + } + } + } + } + + public void faceLocation(@NotNull Location loc) { + this.look(loc.toVector().subtract(getLocation().toVector()), false); + } + + public void look(Vector dir, boolean keepYaw) { + float yaw, pitch; + + if (keepYaw) { + yaw = this.getYHeadRot(); + pitch = MathUtils.fetchPitch(dir); + } else { + float[] vals = MathUtils.fetchYawPitch(dir); + yaw = vals[0]; + pitch = vals[1]; + + this.sendPacket(new ClientboundRotateHeadPacket(this, (byte) (yaw * 256 / 360f))); + } + + this.setRot(yaw, pitch); + } + + public Location getLocation() { + return this.getBukkitEntity().getLocation(); + } + + public Entity getTargetEntity(int maxDistance, Predicate predicate) { + List entities = this.level().getEntities((Entity) null, this.getBoundingBox(), (e -> e != this && (predicate == null || predicate.test(e)))); + if (!entities.isEmpty()) { + return entities.getFirst(); + } else { + EntityHitResult result = this.getBukkitEntity().rayTraceEntity(maxDistance, false); + if (result != null && (predicate == null || predicate.test(result.getEntity()))) { + return result.getEntity(); + } + } + return null; + } + + public void dropAll() { + this.getInventory().dropAll(); + this.detectEquipmentUpdatesPublic(); + } + + private void runAction() { + if (LeavesConfig.modify.fakeplayer.canUseAction) { + this.actions.forEach(action -> action.tryTick(this)); + this.actions.removeIf(BotAction::isCancelled); + } + } + + public boolean addBotAction(BotAction action, CommandSender sender) { + if (!LeavesConfig.modify.fakeplayer.canUseAction) { + return false; + } + + if (!new BotActionScheduleEvent(this.getBukkitEntity(), action.getName(), action.getUUID(), sender).callEvent()) { + return false; + } + + action.init(); + this.actions.add(action); + return true; + } + + public List> getBotActions() { + return actions; + } + + @Override + public @NotNull ServerStatsCounter getStats() { + return stats; + } + + @SuppressWarnings("unchecked") + public BotConfig getConfig(Configs config) { + return (BotConfig) Objects.requireNonNull(this.configs.get(config)); + } + + public E getConfigValue(Configs config) { + return this.getConfig(config).getValue(); + } + + @Override + @NotNull + public CraftBot getBukkitEntity() { + return (CraftBot) super.getBukkitEntity(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java new file mode 100644 index 00000000..fc9e957e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java @@ -0,0 +1,127 @@ +package org.leavesmc.leaves.bot; + +import net.kyori.adventure.text.Component; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.context.UseOnContext; +import net.minecraft.world.level.GameType; +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.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.bukkit.event.player.PlayerGameModeChangeEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ServerBotGameMode extends ServerPlayerGameMode { + + public ServerBotGameMode(ServerBot bot) { + super(bot); + super.setGameModeForPlayer(GameType.SURVIVAL, null); + } + + @Override + public boolean changeGameModeForPlayer(@NotNull GameType gameMode) { + return false; + } + + @Nullable + @Override + public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) { + return null; + } + + @Override + protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) { + } + + @Override + public void tick() { + } + + @Override + public void destroyAndAck(@NotNull BlockPos pos, int sequence, @NotNull String reason) { + this.destroyBlock(pos); + } + + @Override + public boolean destroyBlock(@NotNull BlockPos pos) { + BlockState iblockdata = this.level.getBlockState(pos); + BlockEntity tileentity = this.level.getBlockEntity(pos); + Block block = iblockdata.getBlock(); + + if (this.player.blockActionRestricted(this.level, pos, this.getGameModeForPlayer())) { + return false; + } else { + this.level.captureDrops = null; + BlockState iblockdata1 = block.playerWillDestroy(this.level, pos, iblockdata, this.player); + boolean flag = this.level.removeBlock(pos, false); + + if (flag) { + block.destroy(this.level, pos, iblockdata1); + } + + ItemStack itemstack = this.player.getMainHandItem(); + ItemStack itemstack1 = itemstack.copy(); + + boolean flag1 = this.player.hasCorrectToolForDrops(iblockdata1); + + itemstack.mineBlock(this.level, iblockdata1, pos, this.player); + if (flag && flag1) { + Block.dropResources(iblockdata1, this.level, pos, tileentity, this.player, itemstack1, true); + } + + if (flag) { + iblockdata.getBlock().popExperience(this.level, pos, block.getExpDrop(iblockdata, this.level, pos, itemstack, true), this.player); + } + + return true; + } + } + + @NotNull + @Override + public InteractionResult useItemOn(@NotNull ServerPlayer player, Level level, @NotNull ItemStack stack, @NotNull InteractionHand hand, BlockHitResult hitResult) { + BlockPos blockPos = hitResult.getBlockPos(); + BlockState blockState = level.getBlockState(blockPos); + + if (!blockState.getBlock().isEnabled(level.enabledFeatures())) { + return InteractionResult.FAIL; + } + + boolean flag = !player.getMainHandItem().isEmpty() || !player.getOffhandItem().isEmpty(); + boolean flag1 = player.isSecondaryUseActive() && flag; + + if (!flag1) { + InteractionResult iteminteractionresult = blockState.useItemOn(player.getItemInHand(hand), level, player, hand, hitResult); + + if (iteminteractionresult.consumesAction()) { + return iteminteractionresult; + } + + if (iteminteractionresult instanceof InteractionResult.TryEmptyHandInteraction && hand == InteractionHand.MAIN_HAND) { + InteractionResult interactionResult = blockState.useWithoutItem(level, player, hitResult); + if (interactionResult.consumesAction()) { + return interactionResult; + } + } + } + + if (!stack.isEmpty() && !player.getCooldowns().isOnCooldown(stack)) { + UseOnContext itemactioncontext = new UseOnContext(player, hand, hitResult); + return stack.useOn(itemactioncontext); + } else { + return InteractionResult.PASS; + } + } + + @Override + public void setLevel(@NotNull ServerLevel world) { + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java new file mode 100644 index 00000000..c62f9258 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java @@ -0,0 +1,85 @@ +package org.leavesmc.leaves.bot; + +import net.minecraft.network.Connection; +import net.minecraft.network.DisconnectionDetails; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.CommonListenerCookie; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.bukkit.event.player.PlayerKickEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ServerBotPacketListenerImpl extends ServerGamePacketListenerImpl { + + public ServerBotPacketListenerImpl(MinecraftServer server, ServerBot bot) { + super(server, BotConnection.INSTANCE, bot, CommonListenerCookie.createInitial(bot.gameProfile, false)); + } + + @Override + public void sendPacket(@NotNull Packet packet) { + } + + @Override + public void send(@NotNull Packet packet) { + } + + @Override + public void send(@NotNull Packet packet, @Nullable PacketSendListener callbacks) { + } + + @Override + public void disconnect(@NotNull DisconnectionDetails disconnectionInfo, PlayerKickEvent.@NotNull Cause cause) { + } + + @Override + public boolean isAcceptingMessages() { + return true; + } + + @Override + public void tick() { + } + + public static class BotConnection extends Connection { + + private static final BotConnection INSTANCE = new BotConnection(); + + public BotConnection() { + super(PacketFlow.SERVERBOUND); + } + + @Override + public void tick() { + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public boolean isConnecting() { + return false; + } + + @Override + public boolean isMemoryConnection() { + return false; + } + + @Override + public void send(@NotNull Packet packet) { + } + + @Override + public void send(@NotNull Packet packet, @Nullable PacketSendListener packetsendlistener) { + } + + @Override + public void send(@NotNull Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java new file mode 100644 index 00000000..a37513e1 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java @@ -0,0 +1,67 @@ +package org.leavesmc.leaves.bot.agent; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.agent.actions.*; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class Actions { + + private static final Map> actions = new HashMap<>(); + + public static void registerAll() { + register(new AttackAction()); + register(new BreakBlockAction()); + register(new DropAction()); + register(new JumpAction()); + register(new RotateAction()); + register(new SneakAction()); + register(new UseItemAction()); + register(new UseItemOnAction()); + register(new UseItemToAction()); + register(new LookAction()); + register(new FishAction()); + register(new SwimAction()); + register(new UseItemOffHandAction()); + register(new UseItemOnOffhandAction()); + register(new UseItemToOffhandAction()); + register(new RotationAction()); + } + + public static boolean register(@NotNull BotAction action) { + if (!actions.containsKey(action.getName())) { + actions.put(action.getName(), action); + return true; + } + return false; + } + + public static boolean unregister(@NotNull String name) { + if (actions.containsKey(name)) { + actions.remove(name); + return true; + } + return false; + } + + @NotNull + @Contract(pure = true) + public static Collection> getAll() { + return actions.values(); + } + + @NotNull + public static Set getNames() { + return actions.keySet(); + } + + @Nullable + public static BotAction getForName(String name) { + return actions.get(name); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java new file mode 100644 index 00000000..3bd512b4 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java @@ -0,0 +1,163 @@ +package org.leavesmc.leaves.bot.agent; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.event.bot.BotActionExecuteEvent; +import org.leavesmc.leaves.event.bot.BotActionStopEvent; + +import java.util.List; +import java.util.UUID; +import java.util.function.Supplier; + +public abstract class BotAction> { + + private final String name; + private final CommandArgument argument; + private final Supplier creator; + + private boolean cancel; + private int tickDelay; + private int number; + private UUID uuid; + + private int needWaitTick; + private int canDoNumber; + + public BotAction(String name, CommandArgument argument, Supplier creator) { + this.name = name; + this.argument = argument; + this.uuid = UUID.randomUUID(); + this.creator = creator; + + this.cancel = false; + this.tickDelay = 20; + this.number = -1; + } + + public void init() { + this.needWaitTick = 0; + this.canDoNumber = this.getNumber(); + this.setCancelled(false); + } + + public String getName() { + return this.name; + } + + public UUID getUUID() { + return uuid; + } + + public int getTickDelay() { + return this.tickDelay; + } + + @SuppressWarnings("unchecked") + public E setTickDelay(int tickDelay) { + this.tickDelay = Math.max(0, tickDelay); + return (E) this; + } + + public int getNumber() { + return this.number; + } + + @SuppressWarnings("unchecked") + public E setNumber(int number) { + this.number = Math.max(-1, number); + return (E) this; + } + + public int getCanDoNumber() { + return this.canDoNumber; + } + + public boolean isCancelled() { + return cancel; + } + + public void setCancelled(boolean cancel) { + this.cancel = cancel; + } + + public void stop(@NotNull ServerBot bot, BotActionStopEvent.Reason reason) { + new BotActionStopEvent(bot.getBukkitEntity(), this.name, this.uuid, reason, null).callEvent(); + this.setCancelled(true); + } + + public CommandArgument getArgument() { + return this.argument; + } + + @SuppressWarnings("unchecked") + public E setTabComplete(int index, List list) { + this.argument.setTabComplete(index, list); + return (E) this; + } + + public void tryTick(ServerBot bot) { + if (this.canDoNumber == 0) { + this.stop(bot, BotActionStopEvent.Reason.DONE); + return; + } + + if (this.needWaitTick <= 0) { + BotActionExecuteEvent event = new BotActionExecuteEvent(bot.getBukkitEntity(), name, uuid); + + event.callEvent(); + if (event.getResult() == BotActionExecuteEvent.Result.SOFT_CANCEL) { + this.needWaitTick = this.getTickDelay(); + return; + } else if (event.getResult() == BotActionExecuteEvent.Result.HARD_CANCEL) { + if (this.canDoNumber > 0) { + this.canDoNumber--; + } + this.needWaitTick = this.getTickDelay(); + return; + } + + if (this.doTick(bot)) { + if (this.canDoNumber > 0) { + this.canDoNumber--; + } + this.needWaitTick = this.getTickDelay(); + } + } else { + this.needWaitTick--; + } + } + + @NotNull + public E create() { + return this.creator.get(); + } + + @NotNull + public CompoundTag save(@NotNull CompoundTag nbt) { + if (!this.cancel) { + nbt.putString("actionName", this.name); + nbt.putUUID("actionUUID", this.uuid); + + nbt.putInt("canDoNumber", this.canDoNumber); + nbt.putInt("needWaitTick", this.needWaitTick); + nbt.putInt("tickDelay", this.tickDelay); + } + return nbt; + } + + public void load(@NotNull CompoundTag nbt) { + this.tickDelay = nbt.getInt("tickDelay"); + this.needWaitTick = nbt.getInt("needWaitTick"); + this.canDoNumber = nbt.getInt("canDoNumber"); + this.uuid = nbt.getUUID("actionUUID"); + } + + public abstract void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result); + + public abstract boolean doTick(@NotNull ServerBot bot); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java new file mode 100644 index 00000000..c889a240 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java @@ -0,0 +1,62 @@ +package org.leavesmc.leaves.bot.agent; + +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; + +import java.util.List; +import java.util.function.Supplier; + +public abstract class BotConfig { + + private final String name; + private final CommandArgument argument; + private final Supplier> creator; + protected ServerBot bot; + + public BotConfig(String name, CommandArgument argument, Supplier> creator) { + this.name = name; + this.argument = argument; + this.creator = creator; + } + + public BotConfig setBot(ServerBot bot) { + this.bot = bot; + return this; + } + + public abstract E getValue(); + + public abstract void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException; + + public List getMessage() { + return List.of(this.bot.getScoreboardName() + "'s " + this.getName() + ": " + this.getValue()); + } + + public List getChangeMessage() { + return List.of(this.bot.getScoreboardName() + "'s " + this.getName() + " changed: " + this.getValue()); + } + + public String getName() { + return name; + } + + public CommandArgument getArgument() { + return argument; + } + + @NotNull + public BotConfig create(ServerBot bot) { + return this.creator.get().setBot(bot); + } + + @NotNull + public CompoundTag save(@NotNull CompoundTag nbt) { + nbt.putString("configName", this.name); + return nbt; + } + + public abstract void load(@NotNull CompoundTag nbt); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java new file mode 100644 index 00000000..d99f459b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java @@ -0,0 +1,44 @@ +package org.leavesmc.leaves.bot.agent; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.agent.configs.*; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +public class Configs { + + private static final Map> configs = new HashMap<>(); + + public static final Configs SKIP_SLEEP = register(new SkipSleepConfig()); + public static final Configs ALWAYS_SEND_DATA = register(new AlwaysSendDataConfig()); + public static final Configs SPAWN_PHANTOM = register(new SpawnPhantomConfig()); + public static final Configs SIMULATION_DISTANCE = register(new SimulationDistanceConfig()); + + public final BotConfig config; + + private Configs(BotConfig config) { + this.config = config; + } + + @NotNull + @Contract(pure = true) + public static Collection> getConfigs() { + return configs.values(); + } + + @Nullable + public static Configs getConfig(String name) { + return configs.get(name); + } + + @NotNull + private static Configs register(BotConfig botConfig) { + Configs config = new Configs<>(botConfig); + configs.put(botConfig.getName(), config); + return config; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java new file mode 100644 index 00000000..be55a308 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java @@ -0,0 +1,25 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.command.CommandArgumentType; + +import java.util.List; +import java.util.function.Supplier; + +public abstract class AbstractTimerAction> extends BotAction { + + public AbstractTimerAction(String name, Supplier creator) { + super(name, CommandArgument.of(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER), creator); + this.setTabComplete(0, List.of("[TickDelay]")).setTabComplete(1, List.of("[DoNumber]")); + } + + @Override + public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { + this.setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java new file mode 100644 index 00000000..6c66ef29 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java @@ -0,0 +1,22 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; + +public class AttackAction extends AbstractTimerAction { + + public AttackAction() { + super("attack", AttackAction::new); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + Entity entity = bot.getTargetEntity(3, target -> target.isAttackable() && !target.skipAttackInteraction(bot)); + if (entity != null) { + bot.attack(entity); + return true; + } + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java new file mode 100644 index 00000000..bf7d2037 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java @@ -0,0 +1,75 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.block.Block; +import org.bukkit.craftbukkit.block.CraftBlock; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; + +public class BreakBlockAction extends AbstractTimerAction { + + public BreakBlockAction() { + super("break", BreakBlockAction::new); + } + + private BlockPos lastPos = null; + private int destroyProgressTime = 0; + private int lastSentState = -1; + + @Override + public boolean doTick(@NotNull ServerBot bot) { + Block block = bot.getBukkitEntity().getTargetBlockExact(5); + if (block != null) { + BlockPos pos = ((CraftBlock) block).getPosition(); + + if (lastPos == null || !lastPos.equals(pos)) { + lastPos = pos; + destroyProgressTime = 0; + lastSentState = -1; + } + + BlockState iblockdata = bot.level().getBlockState(pos); + if (!iblockdata.isAir()) { + bot.swing(InteractionHand.MAIN_HAND); + + if (iblockdata.getDestroyProgress(bot, bot.level(), pos) >= 1.0F) { + bot.gameMode.destroyAndAck(pos, 0, "insta mine"); + bot.level().destroyBlockProgress(bot.getId(), pos, -1); + bot.updateItemInHand(InteractionHand.MAIN_HAND); + finalBreak(); + return true; + } + + float damage = this.incrementDestroyProgress(bot, iblockdata, pos); + if (damage >= 1.0F) { + bot.gameMode.destroyAndAck(pos, 0, "destroyed"); + bot.level().destroyBlockProgress(bot.getId(), pos, -1); + bot.updateItemInHand(InteractionHand.MAIN_HAND); + finalBreak(); + return true; + } + } + } + return false; + } + + private void finalBreak() { + lastPos = null; + destroyProgressTime = 0; + lastSentState = -1; + } + + private float incrementDestroyProgress(ServerBot bot, @NotNull BlockState state, BlockPos pos) { + float f = state.getDestroyProgress(bot, bot.level(), pos) * (float) (++destroyProgressTime); + int k = (int) (f * 10.0F); + + if (k != lastSentState) { + bot.level().destroyBlockProgress(bot.getId(), pos, k); + lastSentState = k; + } + + return f; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java new file mode 100644 index 00000000..d96fc7b9 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java @@ -0,0 +1,54 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.agent.Actions; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.entity.botaction.BotActionType; +import org.leavesmc.leaves.entity.botaction.LeavesBotAction; + +public class CraftBotAction extends LeavesBotAction { + + private final BotAction handle; + + public CraftBotAction(@NotNull BotAction action) { + super(BotActionType.valueOf(action.getName()), action.getTickDelay(), action.getCanDoNumber()); + this.handle = action; + } + + @Contract("_ -> new") + @NotNull + public static LeavesBotAction asAPICopy(BotAction action) { + return new CraftBotAction(action); + } + + @NotNull + public static BotAction asInternalCopy(@NotNull LeavesBotAction action) { + BotAction act = Actions.getForName(action.getActionName()); + if (act == null) { + throw new IllegalArgumentException("Invalid action name!"); + } + + BotAction newAction = null; + String[] args = new String[]{String.valueOf(action.getExecuteInterval()), String.valueOf(action.getRemainingExecuteTime())}; + try { + if (act instanceof CraftCustomBotAction customBotAction) { + newAction = customBotAction.createCraft(action.getActionPlayer(), args); + } else { + newAction = act.create(); + newAction.loadCommand(action.getActionPlayer() == null ? null : ((CraftPlayer) action.getActionPlayer()).getHandle(), act.getArgument().parse(0, args)); + } + } catch (IllegalArgumentException ignore) { + } + + if (newAction == null) { + throw new IllegalArgumentException("Invalid action!"); // TODO look action + } + return newAction; + } + + public BotAction getHandle() { + return handle; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java new file mode 100644 index 00000000..7b149243 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java @@ -0,0 +1,49 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.entity.botaction.CustomBotAction; + +public class CraftCustomBotAction extends BotAction { + + private final CustomBotAction realAction; + + public CraftCustomBotAction(String name, @NotNull CustomBotAction realAction) { + super(name, CommandArgument.of().setAllTabComplete(realAction.getTabComplete()), null); + this.realAction = realAction; + } + + @Override + public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { + throw new UnsupportedOperationException("Not supported."); + } + + public CraftCustomBotAction createCraft(@Nullable Player player, String[] args) { + CustomBotAction newRealAction = realAction.getNew(player, args); + if (newRealAction != null) { + return new CraftCustomBotAction(this.getName(), newRealAction); + } + return null; + } + + @Override + public int getNumber() { + return realAction.getNumber(); + } + + @Override + public int getTickDelay() { + return realAction.getTickDelay(); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + return realAction.doTick(bot.getBukkitEntity()); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java new file mode 100644 index 00000000..c71e483e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java @@ -0,0 +1,25 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.command.CommandArgumentResult; + +public class DropAction extends AbstractTimerAction { + + public DropAction() { + super("drop", DropAction::new); + } + + @Override + public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { + this.setTickDelay(result.readInt(100)).setNumber(result.readInt(1)); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + bot.dropAll(); + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java new file mode 100644 index 00000000..3a13f8ac --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java @@ -0,0 +1,73 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.projectile.FishingHook; +import net.minecraft.world.item.FishingRodItem; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; + +public class FishAction extends AbstractTimerAction { + + public FishAction() { + super("fish", FishAction::new); + } + + private int delay = 0; + private int nowDelay = 0; + + @Override + public FishAction setTickDelay(int tickDelay) { + super.setTickDelay(0); + this.delay = tickDelay; + return this; + } + + @Override + @NotNull + public CompoundTag save(@NotNull CompoundTag nbt) { + super.save(nbt); + nbt.putInt("fishDelay", this.delay); + nbt.putInt("fishNowDelay", this.nowDelay); + return nbt; + } + + @Override + public void load(@NotNull CompoundTag nbt) { + super.load(nbt); + this.delay = nbt.getInt("fishDelay"); + this.nowDelay = nbt.getInt("fishNowDelay"); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + if (this.nowDelay > 0) { + this.nowDelay--; + return false; + } + + ItemStack mainHand = bot.getMainHandItem(); + if (mainHand == ItemStack.EMPTY || mainHand.getItem().getClass() != FishingRodItem.class) { + return false; + } + + FishingHook fishingHook = bot.fishing; + if (fishingHook != null) { + if (fishingHook.currentState == FishingHook.FishHookState.HOOKED_IN_ENTITY) { + mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); + this.nowDelay = 20; + return false; + } + if (fishingHook.nibble > 0) { + mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); + this.nowDelay = this.delay; + return true; + } + } else { + mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); + } + + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java new file mode 100644 index 00000000..6fc9ba9b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java @@ -0,0 +1,21 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; + +public class JumpAction extends AbstractTimerAction { + + public JumpAction() { + super("jump", JumpAction::new); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + if (bot.onGround()) { + bot.jumpFromGround(); + return true; + } else { + return false; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java new file mode 100644 index 00000000..8be962cf --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java @@ -0,0 +1,63 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.command.CommandArgumentType; + +import java.util.List; + +public class LookAction extends BotAction { + + public LookAction() { + super("look", CommandArgument.of(CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE), LookAction::new); + this.setTabComplete(0, List.of("")); + this.setTabComplete(1, List.of("")); + this.setTabComplete(2, List.of("")); + } + + private Vector pos; + + @Override + public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) throws IllegalArgumentException { + Vector pos = result.readVector(); + if (pos != null) { + this.setPos(pos).setTickDelay(0).setNumber(1); + } else { + throw new IllegalArgumentException("pos?"); + } + } + + @Override + @NotNull + public CompoundTag save(@NotNull CompoundTag nbt) { + super.save(nbt); + nbt.putDouble("x", this.pos.getX()); + nbt.putDouble("y", this.pos.getY()); + nbt.putDouble("z", this.pos.getZ()); + return nbt; + } + + @Override + public void load(@NotNull CompoundTag nbt) { + super.load(nbt); + this.setPos(new Vector(nbt.getDouble("x"), nbt.getDouble("y"), nbt.getDouble("z"))); + } + + public LookAction setPos(Vector pos) { + this.pos = pos; + return this; + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + bot.look(pos.subtract(bot.getLocation().toVector()), false); + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java new file mode 100644 index 00000000..84eb7bd7 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java @@ -0,0 +1,51 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; + +public class RotateAction extends BotAction { + + public RotateAction() { + super("rotate", CommandArgument.EMPTY, RotateAction::new); + } + + private ServerPlayer player; + + @Override + public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { + this.setPlayer(player).setTickDelay(0).setNumber(1); + } + + public RotateAction setPlayer(ServerPlayer player) { + this.player = player; + return this; + } + + @Override + @NotNull + public CompoundTag save(@NotNull CompoundTag nbt) { + super.save(nbt); + nbt.putString("actionName", "look"); // to player loc + nbt.putDouble("x", player.getX()); + nbt.putDouble("y", player.getY()); + nbt.putDouble("z", player.getZ()); + return nbt; + } + + @Override + public void load(@NotNull CompoundTag nbt) { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + bot.faceLocation(player.getBukkitEntity().getLocation()); + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java new file mode 100644 index 00000000..6f6ea32f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java @@ -0,0 +1,65 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.command.CommandArgumentType; + +import java.util.List; + +public class RotationAction extends BotAction { + + public RotationAction() { + super("rotation", CommandArgument.of(CommandArgumentType.FLOAT, CommandArgumentType.FLOAT), RotationAction::new); + this.setTabComplete(0, List.of("")); + this.setTabComplete(1, List.of("")); + } + + private float yaw; + private float pitch; + + @Override + public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { + if (player == null) { + return; + } + + this.setYaw(result.readFloat(player.getYRot())).setPitch(result.readFloat(player.getXRot())).setTickDelay(0).setNumber(1); + } + + public RotationAction setYaw(float yaw) { + this.yaw = yaw; + return this; + } + + public RotationAction setPitch(float pitch) { + this.pitch = pitch; + return this; + } + + @Override + @NotNull + public CompoundTag save(@NotNull CompoundTag nbt) { + super.save(nbt); + nbt.putFloat("yaw", this.yaw); + nbt.putFloat("pitch", this.pitch); + return nbt; + } + + @Override + public void load(@NotNull CompoundTag nbt) { + super.load(nbt); + this.setYaw(nbt.getFloat("yaw")).setPitch(nbt.getFloat("pitch")); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + bot.setRot(yaw, pitch); + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java new file mode 100644 index 00000000..923cf55d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java @@ -0,0 +1,27 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; + +public class SneakAction extends BotAction { + + public SneakAction() { + super("sneak", CommandArgument.EMPTY, SneakAction::new); + } + + @Override + public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { + this.setTickDelay(0).setNumber(1); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + bot.setShiftKeyDown(!bot.isShiftKeyDown()); + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java new file mode 100644 index 00000000..b5ccedee --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java @@ -0,0 +1,30 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; + +public class SwimAction extends BotAction { + + public SwimAction() { + super("swim", CommandArgument.EMPTY, SwimAction::new); + } + + @Override + public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { + this.setTickDelay(0).setNumber(-1); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + if (bot.isInWater()) { + bot.addDeltaMovement(new Vec3(0, 0.03, 0)); + } + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java new file mode 100644 index 00000000..c511ed17 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java @@ -0,0 +1,26 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.world.InteractionHand; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; + +public class UseItemAction extends AbstractTimerAction { + + public UseItemAction() { + super("use", UseItemAction::new); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + if (bot.isUsingItem()) { + return false; + } + + boolean flag = bot.gameMode.useItem(bot, bot.level(), bot.getItemInHand(InteractionHand.MAIN_HAND), InteractionHand.MAIN_HAND).consumesAction(); + if (flag) { + bot.swing(InteractionHand.MAIN_HAND); + bot.updateItemInHand(InteractionHand.MAIN_HAND); + } + return flag; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java new file mode 100644 index 00000000..26d7286f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java @@ -0,0 +1,26 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.world.InteractionHand; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; + +public class UseItemOffHandAction extends AbstractTimerAction { + + public UseItemOffHandAction() { + super("use_offhand", UseItemOffHandAction::new); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + if (bot.isUsingItem()) { + return false; + } + + boolean flag = bot.gameMode.useItem(bot, bot.level(), bot.getItemInHand(InteractionHand.OFF_HAND), InteractionHand.OFF_HAND).consumesAction(); + if (flag) { + bot.swing(InteractionHand.OFF_HAND); + bot.updateItemInHand(InteractionHand.OFF_HAND); + } + return flag; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java new file mode 100644 index 00000000..232d0abe --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java @@ -0,0 +1,45 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.world.InteractionHand; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TrappedChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; + +public class UseItemOnAction extends AbstractTimerAction { + + public UseItemOnAction() { + super("use_on", UseItemOnAction::new); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + HitResult result = bot.getRayTrace(5, ClipContext.Fluid.NONE); + if (result instanceof BlockHitResult blockHitResult) { + BlockState state = bot.serverLevel().getBlockState(blockHitResult.getBlockPos()); + if (state.isAir()) { + return false; + } + bot.swing(InteractionHand.MAIN_HAND); + if (state.getBlock() == Blocks.TRAPPED_CHEST) { + BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos()); + if (entity instanceof TrappedChestBlockEntity chestBlockEntity) { + chestBlockEntity.startOpen(bot); + Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> chestBlockEntity.stopOpen(bot), 1); + return true; + } + } else { + bot.updateItemInHand(InteractionHand.MAIN_HAND); + return bot.gameMode.useItemOn(bot, bot.level(), bot.getItemInHand(InteractionHand.MAIN_HAND), InteractionHand.MAIN_HAND, (BlockHitResult) result).consumesAction(); + } + } + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java new file mode 100644 index 00000000..3616802c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java @@ -0,0 +1,45 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.world.InteractionHand; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TrappedChestBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.HitResult; +import org.bukkit.Bukkit; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; + +public class UseItemOnOffhandAction extends AbstractTimerAction { + + public UseItemOnOffhandAction() { + super("use_on_offhand", UseItemOnOffhandAction::new); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + HitResult result = bot.getRayTrace(5, ClipContext.Fluid.NONE); + if (result instanceof BlockHitResult blockHitResult) { + BlockState state = bot.serverLevel().getBlockState(blockHitResult.getBlockPos()); + if (state.isAir()) { + return false; + } + bot.swing(InteractionHand.OFF_HAND); + if (state.getBlock() == Blocks.TRAPPED_CHEST) { + BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos()); + if (entity instanceof TrappedChestBlockEntity chestBlockEntity) { + chestBlockEntity.startOpen(bot); + Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> chestBlockEntity.stopOpen(bot), 1); + return true; + } + } else { + bot.updateItemInHand(InteractionHand.OFF_HAND); + return bot.gameMode.useItemOn(bot, bot.level(), bot.getItemInHand(InteractionHand.OFF_HAND), InteractionHand.OFF_HAND, (BlockHitResult) result).consumesAction(); + } + } + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java new file mode 100644 index 00000000..05be3dd5 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java @@ -0,0 +1,27 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; + +public class UseItemToAction extends AbstractTimerAction { + + public UseItemToAction() { + super("use_to", UseItemToAction::new); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + Entity entity = bot.getTargetEntity(3, null); + if (entity != null) { + boolean flag = bot.interactOn(entity, InteractionHand.MAIN_HAND).consumesAction(); + if (flag) { + bot.swing(InteractionHand.MAIN_HAND); + bot.updateItemInHand(InteractionHand.MAIN_HAND); + } + return flag; + } + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java new file mode 100644 index 00000000..f8334858 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java @@ -0,0 +1,27 @@ +package org.leavesmc.leaves.bot.agent.actions; + +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.Entity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.ServerBot; + +public class UseItemToOffhandAction extends AbstractTimerAction { + + public UseItemToOffhandAction() { + super("use_to_offhand", UseItemToOffhandAction::new); + } + + @Override + public boolean doTick(@NotNull ServerBot bot) { + Entity entity = bot.getTargetEntity(3, null); + if (entity != null) { + boolean flag = bot.interactOn(entity, InteractionHand.OFF_HAND).consumesAction(); + if (flag) { + bot.swing(InteractionHand.OFF_HAND); + bot.updateItemInHand(InteractionHand.OFF_HAND); + } + return flag; + } + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java new file mode 100644 index 00000000..1ee9a806 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java @@ -0,0 +1,45 @@ +package org.leavesmc.leaves.bot.agent.configs; + +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; + +import org.leavesmc.leaves.bot.agent.BotConfig; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.command.CommandArgumentType; + +import java.util.List; + +public class AlwaysSendDataConfig extends BotConfig { + + private boolean value; + + public AlwaysSendDataConfig() { + super("always_send_data", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("true", "false")), AlwaysSendDataConfig::new); + this.value = LeavesConfig.modify.fakeplayer.canSendDataAlways; + } + + @Override + public Boolean getValue() { + return value; + } + + @Override + public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException { + this.value = result.readBoolean(this.value); + } + + @Override + @NotNull + public CompoundTag save(@NotNull CompoundTag nbt) { + super.save(nbt); + nbt.putBoolean("always_send_data", this.getValue()); + return nbt; + } + + @Override + public void load(@NotNull CompoundTag nbt) { + this.value = nbt.getBoolean("always_send_data"); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java new file mode 100644 index 00000000..c8a22433 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java @@ -0,0 +1,47 @@ +package org.leavesmc.leaves.bot.agent.configs; + +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.agent.BotConfig; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.command.CommandArgumentType; + +import java.util.ArrayList; +import java.util.List; + +public class SimulationDistanceConfig extends BotConfig { + + public SimulationDistanceConfig() { + super("simulation_distance", CommandArgument.of(CommandArgumentType.INTEGER).setTabComplete(0, List.of("2", "10", "")), SimulationDistanceConfig::new); + } + + @Override + public Integer getValue() { + return this.bot.getBukkitEntity().getSimulationDistance(); + } + + @Override + public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException { + int realValue = result.readInt(this.bot.getBukkitEntity().getSimulationDistance()); + if (realValue < 2 || realValue > 32) { + throw new IllegalArgumentException("simulation_distance must be a number between 2 and 32, got: " + result); + } + this.bot.getBukkitEntity().setSimulationDistance(realValue); + } + + @Override + @NotNull + public CompoundTag save(@NotNull CompoundTag nbt) { + super.save(nbt); + nbt.putInt("simulation_distance", this.getValue()); + return nbt; + } + + @Override + public void load(@NotNull CompoundTag nbt) { + this.setValue(new CommandArgumentResult(new ArrayList<>(){{ + add(nbt.getInt("simulation_distance")); + }})); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java new file mode 100644 index 00000000..b2b1be24 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java @@ -0,0 +1,42 @@ +package org.leavesmc.leaves.bot.agent.configs; + +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.bot.agent.BotConfig; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.command.CommandArgumentType; + +import java.util.ArrayList; +import java.util.List; + +public class SkipSleepConfig extends BotConfig { + + public SkipSleepConfig() { + super("skip_sleep", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("true", "false")), SkipSleepConfig::new); + } + + @Override + public Boolean getValue() { + return bot.fauxSleeping; + } + + @Override + public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException { + bot.fauxSleeping = result.readBoolean(bot.fauxSleeping); + } + + @Override + public @NotNull CompoundTag save(@NotNull CompoundTag nbt) { + super.save(nbt); + nbt.putBoolean("skip_sleep", this.getValue()); + return nbt; + } + + @Override + public void load(@NotNull CompoundTag nbt) { + this.setValue(new CommandArgumentResult(new ArrayList<>() {{ + add(nbt.getBoolean("skip_sleep")); + }})); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java new file mode 100644 index 00000000..24e3c3cf --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java @@ -0,0 +1,51 @@ +package org.leavesmc.leaves.bot.agent.configs; + +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.bot.agent.BotConfig; +import org.leavesmc.leaves.command.CommandArgument; +import org.leavesmc.leaves.command.CommandArgumentResult; +import org.leavesmc.leaves.command.CommandArgumentType; + +import java.util.List; + +public class SpawnPhantomConfig extends BotConfig { + + private boolean value; + + public SpawnPhantomConfig() { + super("spawn_phantom", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("true", "false")), SpawnPhantomConfig::new); + this.value = LeavesConfig.modify.fakeplayer.canSpawnPhantom; + } + + @Override + public Boolean getValue() { + return value; + } + + @Override + public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException { + this.value = result.readBoolean(this.value); + } + + @Override + public List getMessage() { + return List.of( + bot.getScoreboardName() + "'s spawn_phantom: " + this.getValue(), + bot.getScoreboardName() + "'s not_sleeping_ticks: " + bot.notSleepTicks + ); + } + + @Override + public @NotNull CompoundTag save(@NotNull CompoundTag nbt) { + super.save(nbt); + nbt.putBoolean("spawn_phantom", this.getValue()); + return nbt; + } + + @Override + public void load(@NotNull CompoundTag nbt) { + this.value = nbt.getBoolean("spawn_phantom"); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/SimpleBytebufManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/SimpleBytebufManager.java new file mode 100644 index 00000000..443f7f6e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/SimpleBytebufManager.java @@ -0,0 +1,35 @@ +package org.leavesmc.leaves.bytebuf; + +import io.netty.buffer.Unpooled; +import org.bukkit.plugin.Plugin; +import org.leavesmc.leaves.bytebuf.internal.InternalBytebufHandler; +import org.leavesmc.leaves.bytebuf.packet.PacketListener; + +public class SimpleBytebufManager implements BytebufManager { + + private final InternalBytebufHandler internal; + + public SimpleBytebufManager(InternalBytebufHandler internal) { + this.internal = internal; + } + + @Override + public void registerListener(Plugin plugin, PacketListener listener) { + internal.listenerMap.put(listener, plugin); + } + + @Override + public void unregisterListener(Plugin plugin, PacketListener listener) { + internal.listenerMap.remove(listener); + } + + @Override + public Bytebuf newBytebuf(int size) { + return new WrappedBytebuf(Unpooled.buffer(size)); + } + + @Override + public Bytebuf toBytebuf(byte[] bytes) { + return new WrappedBytebuf(Unpooled.wrappedBuffer(bytes)); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/WrappedBytebuf.java b/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/WrappedBytebuf.java new file mode 100644 index 00000000..9601eb5a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/WrappedBytebuf.java @@ -0,0 +1,277 @@ +package org.leavesmc.leaves.bytebuf; + +import com.google.gson.JsonElement; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.RegistryAccess; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.server.MinecraftServer; +import org.bukkit.inventory.ItemStack; +import org.bukkit.craftbukkit.inventory.CraftItemStack; + +import java.util.List; +import java.util.UUID; + +public class WrappedBytebuf implements Bytebuf { + + private final FriendlyByteBuf buf; + private final RegistryFriendlyByteBuf registryBuf; + + public WrappedBytebuf(ByteBuf buf) { + if (buf instanceof RegistryFriendlyByteBuf) { + this.buf = (FriendlyByteBuf) buf; + this.registryBuf = (RegistryFriendlyByteBuf) buf; + } else { + this.buf = new FriendlyByteBuf(buf); + this.registryBuf = new RegistryFriendlyByteBuf(this.buf, MinecraftServer.getServer().registryAccess()); + } + } + + public RegistryFriendlyByteBuf getRegistryBuf() { + return registryBuf; + } + + @Override + public byte[] toArray() { + int length = buf.readableBytes(); + byte[] data = new byte[length]; + buf.getBytes(buf.readerIndex(), data); + return data; + } + + @Override + public Bytebuf skipBytes(int i) { + buf.skipBytes(i); + return this; + } + + @Override + public int readerIndex() { + return buf.readerIndex(); + } + + @Override + public Bytebuf readerIndex(int i) { + buf.readerIndex(i); + return this; + } + + @Override + public int writerIndex() { + return buf.writerIndex(); + } + + @Override + public Bytebuf writerIndex(int i) { + buf.writerIndex(i); + return this; + } + + @Override + public Bytebuf resetReaderIndex() { + buf.resetReaderIndex(); + return this; + } + + @Override + public Bytebuf resetWriterIndex() { + buf.resetWriterIndex(); + return this; + } + + @Override + public Bytebuf writeByte(int i) { + buf.writeByte(i); + return this; + } + + @Override + public byte readByte() { + return buf.readByte(); + } + + @Override + public Bytebuf writeBoolean(boolean b) { + buf.writeBoolean(b); + return this; + } + + @Override + public boolean readBoolean() { + return buf.readBoolean(); + } + + @Override + public Bytebuf writeFloat(float f) { + buf.writeFloat(f); + return this; + } + + @Override + public float readFloat() { + return buf.readFloat(); + } + + @Override + public Bytebuf writeDouble(double d) { + buf.writeDouble(d); + return this; + } + + @Override + public double readDouble() { + return buf.readDouble(); + } + + @Override + public Bytebuf writeShort(int i) { + buf.writeShort(i); + return this; + } + + @Override + public short readShort() { + return buf.readShort(); + } + + @Override + public Bytebuf writeInt(int i) { + buf.writeShort(i); + return this; + } + + @Override + public int readInt() { + return buf.readInt(); + } + + @Override + public Bytebuf writeLong(long i) { + buf.writeLong(i); + return this; + } + + @Override + public long readLong() { + return buf.readLong(); + } + + @Override + public Bytebuf writeVarInt(int i) { + this.buf.writeVarInt(i); + return this; + } + + @Override + public int readVarInt() { + return this.buf.readVarInt(); + } + + @Override + public Bytebuf writeVarLong(long i) { + this.buf.writeVarLong(i); + return this; + } + + @Override + public long readVarLong() { + return this.buf.readVarLong(); + } + + @Override + public Bytebuf writeUUID(UUID uuid) { + this.buf.writeUUID(uuid); + return this; + } + + @Override + public UUID readUUID() { + return this.buf.readUUID(); + } + + @Override + public Bytebuf writeEnum(Enum instance) { + this.buf.writeEnum(instance); + return this; + } + + @Override + public > T readEnum(Class enumClass) { + return this.buf.readEnum(enumClass); + } + + @Override + public Bytebuf writeUTFString(String utf) { + buf.writeUtf(utf); + return this; + } + + @Override + public String readUTFString() { + return buf.readUtf(); + } + + @Override + public Bytebuf writeComponentPlain(String str) { + ComponentSerialization.STREAM_CODEC.encode(new RegistryFriendlyByteBuf(this.buf, RegistryAccess.EMPTY), Component.literal(str)); + return this; + } + + @Override + public String readComponentPlain() { + return ComponentSerialization.STREAM_CODEC.decode(new RegistryFriendlyByteBuf(buf, RegistryAccess.EMPTY)).getString(); + } + + @Override + public Bytebuf writeComponentJson(JsonElement json) { + Component component = Component.Serializer.fromJson(json, RegistryAccess.EMPTY); + if (component == null) { + throw new IllegalArgumentException("Null can not be serialize to Minecraft chat component"); + } + ComponentSerialization.STREAM_CODEC.encode(new RegistryFriendlyByteBuf(buf, RegistryAccess.EMPTY), component); + return this; + } + + @Override + public JsonElement readComponentJson() { + return Component.Serializer.serialize(ComponentSerialization.STREAM_CODEC.decode(new RegistryFriendlyByteBuf(buf, RegistryAccess.EMPTY)), RegistryAccess.EMPTY); + } + + @Override + public Bytebuf writeItemStack(ItemStack itemStack) { + net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.unwrap(itemStack); + net.minecraft.world.item.ItemStack.OPTIONAL_STREAM_CODEC.encode(this.registryBuf, nmsItem); + return this; + } + + @Override + public ItemStack readItemStack() { + net.minecraft.world.item.ItemStack nmsItem = net.minecraft.world.item.ItemStack.OPTIONAL_STREAM_CODEC.decode(this.registryBuf); + return nmsItem.asBukkitMirror(); + } + + @Override + public Bytebuf writeItemStackList(List itemStacks) { + List nmsItemList = itemStacks.stream().map(CraftItemStack::unwrap).toList(); + net.minecraft.world.item.ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(this.registryBuf, nmsItemList); + return this; + } + + @Override + public List readItemStackList() { + List nmsItemList = net.minecraft.world.item.ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(this.registryBuf); + return nmsItemList.stream().map(net.minecraft.world.item.ItemStack::asBukkitMirror).toList(); + } + + @Override + public Bytebuf copy() { + return new WrappedBytebuf(this.buf.copy()); + } + + @Override + public boolean release() { + return this.buf.release(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/internal/InternalBytebufHandler.java b/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/internal/InternalBytebufHandler.java new file mode 100644 index 00000000..04dbb4cb --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/bytebuf/internal/InternalBytebufHandler.java @@ -0,0 +1,234 @@ +package org.leavesmc.leaves.bytebuf.internal; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableMap; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelDuplexHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import net.minecraft.network.Connection; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.BundleDelimiterPacket; +import net.minecraft.network.protocol.BundlePacket; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.game.*; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.entity.Player; +import org.bukkit.plugin.Plugin; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.bytebuf.BytebufManager; +import org.leavesmc.leaves.bytebuf.SimpleBytebufManager; +import org.leavesmc.leaves.bytebuf.WrappedBytebuf; +import org.leavesmc.leaves.bytebuf.packet.Packet; +import org.leavesmc.leaves.bytebuf.packet.PacketListener; +import org.leavesmc.leaves.bytebuf.packet.PacketType; + +import java.lang.reflect.Field; +import java.util.HashMap; +import java.util.Map; + +import static org.leavesmc.leaves.bytebuf.packet.PacketType.*; + +public class InternalBytebufHandler { + + private class PacketHandler extends ChannelDuplexHandler { + + private final static String handlerName = "leaves-bytebuf-handler"; + private final Player player; + + public PacketHandler(Player player) { + this.player = player; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof BundlePacket || msg instanceof BundleDelimiterPacket) { + super.channelRead(ctx, msg); + return; + } + + if (msg instanceof net.minecraft.network.protocol.Packet nmsPacket) { + try { + msg = callPacketInEvent(player, nmsPacket); + } catch (Exception e) { + MinecraftServer.LOGGER.error("Error on PacketInEvent.", e); + } + } + + if (msg != null) { + super.channelRead(ctx, msg); + } + } + + @Override + public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { + if (msg instanceof BundlePacket || msg instanceof BundleDelimiterPacket) { + super.write(ctx, msg, promise); + return; + } + + if (msg instanceof net.minecraft.network.protocol.Packet nmsPacket) { + try { + msg = callPacketOutEvent(player, nmsPacket); + } catch (Exception e) { + MinecraftServer.LOGGER.error("Error on PacketInEvent.", e); + } + } + + if (msg != null) { + super.write(ctx, msg, promise); + } + } + } + + public final Map listenerMap = new HashMap<>(); + private final BytebufManager manager = new SimpleBytebufManager(this); + private final ImmutableMap type2CodecMap; + private final Cache, PacketType> resultCache = CacheBuilder.newBuilder().build(); + + public InternalBytebufHandler() { + ImmutableMap.Builder builder = ImmutableMap.builder(); + for (PacketType packet : PacketType.values()) { + Class packetClass; + try { + packetClass = Class.forName("net.minecraft.network.protocol.game." + packet.name() + "Packet"); + } catch (ClassNotFoundException e) { + try { + packetClass = Class.forName("net.minecraft.network.protocol.common." + packet.name() + "Packet"); + } catch (ClassNotFoundException e2) { + try { + packetClass = Class.forName("net.minecraft.network.protocol.ping." + packet.name() + "Packet"); + } catch (ClassNotFoundException ignored) { + continue; + } + } + } + try { + Field field = packetClass.getDeclaredField("STREAM_CODEC"); + builder.put(packet, (StreamCodec>) field.get(null)); + } catch (Exception ignored) { + } + } + + builder.put(ClientboundMoveEntityPos, ClientboundMoveEntityPacket.Pos.STREAM_CODEC); + builder.put(ClientboundMoveEntityPosRot, ClientboundMoveEntityPacket.PosRot.STREAM_CODEC); + builder.put(ClientboundMoveEntityRot, ClientboundMoveEntityPacket.Rot.STREAM_CODEC); + builder.put(ServerboundMovePlayerPos, ServerboundMovePlayerPacket.Pos.STREAM_CODEC); + builder.put(ServerboundMovePlayerPosRot, ServerboundMovePlayerPacket.PosRot.STREAM_CODEC); + builder.put(ServerboundMovePlayerRot, ServerboundMovePlayerPacket.Rot.STREAM_CODEC); + builder.put(ServerboundMovePlayerStatusOnly, ServerboundMovePlayerPacket.StatusOnly.STREAM_CODEC); + builder.put(ClientboundCustomPayload, ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC); + + type2CodecMap = builder.build(); + } + + public void injectPlayer(ServerPlayer player) { + if (LeavesConfig.mics.leavesPacketEvent) { + player.connection.connection.channel.pipeline().addBefore("packet_handler", PacketHandler.handlerName, new PacketHandler(player.getBukkitEntity())); + } + } + + public BytebufManager getManager() { + return manager; + } + + public net.minecraft.network.protocol.Packet callPacketInEvent(Player player, net.minecraft.network.protocol.Packet nmsPacket) { + if (listenerMap.isEmpty()) { + return nmsPacket; + } + PacketType type = toEnumType(nmsPacket.type()); + if (type == null) { + return nmsPacket; + } + Packet packet = createBytebufPacket(type, nmsPacket); + for (PacketListener listener : listenerMap.keySet()) { + if (listenerMap.get(listener).isEnabled()) { + packet = listener.onPacketIn(player, packet); + packet.bytebuf().resetReaderIndex(); + } else { + listenerMap.remove(listener); + } + } + return createNMSPacket(packet); + } + + public net.minecraft.network.protocol.Packet callPacketOutEvent(Player player, net.minecraft.network.protocol.Packet nmsPacket) { + if (listenerMap.isEmpty()) { + return nmsPacket; + } + PacketType type = toEnumType(nmsPacket.type()); + if (type == null) { + return nmsPacket; + } + Packet packet = createBytebufPacket(type, nmsPacket); + for (PacketListener listener : listenerMap.keySet()) { + if (listenerMap.get(listener).isEnabled()) { + packet = listener.onPacketOut(player, packet); + packet.bytebuf().resetReaderIndex(); + } else { + listenerMap.remove(listener); + } + } + return createNMSPacket(packet); + } + + public void applyPacketToPlayer(ServerPlayer player, Packet packet) { + Connection sp = player.connection.connection; + sp.send(createNMSPacket(packet)); + } + + public net.minecraft.network.protocol.Packet createNMSPacket(Packet packet) { + StreamCodec> codec = type2CodecMap.get(packet.type()); + if (codec == null) { + throw new UnsupportedOperationException("This feature is not completely finished yet, packet type " + packet.type() + " is not supported temporary."); + } + return codec.decode(((WrappedBytebuf) packet.bytebuf()).getRegistryBuf()); + } + + @Nullable + private PacketType toEnumType(net.minecraft.network.protocol.PacketType type) { + try { + return this.resultCache.get(type, () -> { + StringBuilder builder = new StringBuilder(); + String bound = type.toString().split("/")[0]; + String name = type.toString().split(":")[1]; + builder.append(bound.substring(0, 1).toUpperCase()).append(bound.substring(1)); + boolean flag = true; + for (int i = 0; i < name.length(); i++) { + if (flag) { + builder.append(name.substring(i, i + 1).toUpperCase()); + flag = false; + continue; + } + if (name.charAt(i) == '_') { + flag = true; + } else { + builder.append(name.charAt(i)); + } + } + try { + return PacketType.valueOf(builder.toString()); + } catch (IllegalArgumentException e) { + throw new RuntimeException(e); + } + }); + } catch (Exception ignore) { + return null; + } + } + + public Packet createBytebufPacket(PacketType type, net.minecraft.network.protocol.Packet nmsPacket) { + RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), MinecraftServer.getServer().registryAccess()); + StreamCodec> codec = type2CodecMap.get(type); + if (codec == null) { + throw new UnsupportedOperationException("This feature is not completely finished yet, packet type " + type + " is not supported temporary."); + } + codec.encode(buf, nmsPacket); + return new Packet(type, new WrappedBytebuf(buf)); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgument.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgument.java new file mode 100644 index 00000000..0bccbf78 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgument.java @@ -0,0 +1,55 @@ +package org.leavesmc.leaves.command; + +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class CommandArgument { + + public static final CommandArgument EMPTY = new CommandArgument(); + + private final List> argumentTypes; + private final List> tabComplete; + + private CommandArgument(CommandArgumentType... argumentTypes) { + this.argumentTypes = List.of(argumentTypes); + this.tabComplete = new ArrayList<>(); + for (int i = 0; i < argumentTypes.length; i++) { + tabComplete.add(new ArrayList<>()); + } + } + + public static CommandArgument of(CommandArgumentType... argumentTypes) { + return new CommandArgument(argumentTypes); + } + + public List tabComplete(int n) { + if (tabComplete.size() > n) { + return tabComplete.get(n); + } else { + return List.of(); + } + } + + public CommandArgument setTabComplete(int index, List list) { + tabComplete.set(index, list); + return this; + } + + public CommandArgument setAllTabComplete(List> tabComplete) { + this.tabComplete.clear(); + this.tabComplete.addAll(tabComplete); + return this; + } + + public CommandArgumentResult parse(int index, String @NotNull [] args) { + Object[] result = new Object[argumentTypes.size()]; + Arrays.fill(result, null); + for (int i = index, j = 0; i < args.length && j < result.length; i++, j++) { + result[j] = argumentTypes.get(j).pasre(args[i]); + } + return new CommandArgumentResult(new ArrayList<>(Arrays.asList(result))); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java new file mode 100644 index 00000000..46aa6eaa --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java @@ -0,0 +1,69 @@ +package org.leavesmc.leaves.command; + +import net.minecraft.core.BlockPos; +import org.bukkit.util.Vector; + +import java.util.List; +import java.util.Objects; + +public class CommandArgumentResult { + + private final List result; + + public CommandArgumentResult(List result) { + this.result = result; + } + + public int readInt(int def) { + return Objects.requireNonNullElse(read(Integer.class), def); + } + + public double readDouble(double def) { + return Objects.requireNonNullElse(read(Double.class), def); + } + + public float readFloat(float def) { + return Objects.requireNonNullElse(read(Float.class), def); + } + + public String readString(String def) { + return Objects.requireNonNullElse(read(String.class), def); + } + + public boolean readBoolean(boolean def) { + return Objects.requireNonNullElse(read(Boolean.class), def); + } + + public BlockPos readPos() { + Integer[] pos = {read(Integer.class), read(Integer.class), read(Integer.class)}; + for (Integer po : pos) { + if (po == null) { + return null; + } + } + return new BlockPos(pos[0], pos[1], pos[2]); + } + + public Vector readVector() { + Double[] pos = {read(Double.class), read(Double.class), read(Double.class)}; + for (Double po : pos) { + if (po == null) { + return null; + } + } + return new Vector(pos[0], pos[1], pos[2]); + } + + public T read(Class tClass) { + if (result.isEmpty()) { + return null; + } + + Object obj = result.remove(0); + if (tClass.isInstance(obj)) { + return tClass.cast(obj); + } else { + return null; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java new file mode 100644 index 00000000..4ca35084 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java @@ -0,0 +1,55 @@ +package org.leavesmc.leaves.command; + +import org.jetbrains.annotations.NotNull; + +public abstract class CommandArgumentType { + + public static final CommandArgumentType INTEGER = new CommandArgumentType<>() { + @Override + public Integer pasre(@NotNull String arg) { + try { + return Integer.parseInt(arg); + } catch (NumberFormatException e) { + return null; + } + } + }; + + public static final CommandArgumentType DOUBLE = new CommandArgumentType<>() { + @Override + public Double pasre(@NotNull String arg) { + try { + return Double.parseDouble(arg); + } catch (NumberFormatException e) { + return null; + } + } + }; + + public static final CommandArgumentType FLOAT = new CommandArgumentType<>() { + @Override + public Float pasre(@NotNull String arg) { + try { + return Float.parseFloat(arg); + } catch (NumberFormatException e) { + return null; + } + } + }; + + public static final CommandArgumentType STRING = new CommandArgumentType<>() { + @Override + public String pasre(@NotNull String arg) { + return arg; + } + }; + + public static final CommandArgumentType BOOLEAN = new CommandArgumentType<>() { + @Override + public Boolean pasre(@NotNull String arg) { + return Boolean.parseBoolean(arg); + } + }; + + public abstract E pasre(@NotNull String arg); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java new file mode 100644 index 00000000..bc8720e3 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java @@ -0,0 +1,121 @@ +package org.leavesmc.leaves.command; + +import it.unimi.dsi.fastutil.Pair; +import net.minecraft.Util; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.command.subcommands.*; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.RED; + +public final class LeavesCommand extends Command { + static final String BASE_PERM = "bukkit.command.leaves."; + // subcommand label -> subcommand + private static final Map SUBCOMMANDS = Util.make(() -> { + final Map, LeavesSubcommand> commands = new HashMap<>(); + commands.put(Set.of("config"), new ConfigCommand()); + commands.put(Set.of("update"), new UpdateCommand()); + commands.put(Set.of("peaceful"), new PeacefulModeSwitchCommand()); + commands.put(Set.of("counter"), new CounterCommand()); + commands.put(Set.of("reload"), new ReloadCommand()); + + return commands.entrySet().stream() + .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + }); + private static final Set COMPLETABLE_SUBCOMMANDS = SUBCOMMANDS.entrySet().stream().filter(entry -> entry.getValue().tabCompletes()).map(Map.Entry::getKey).collect(Collectors.toSet()); + + public LeavesCommand(final String name) { + super(name); + this.description = "Leaves related commands"; + this.usageMessage = "/leaves [" + String.join(" | ", SUBCOMMANDS.keySet()) + "]"; + final List permissions = new ArrayList<>(); + permissions.add("bukkit.command.leaves"); + permissions.addAll(SUBCOMMANDS.keySet().stream().map(s -> BASE_PERM + s).toList()); + this.setPermission(String.join(";", permissions)); + final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); + for (final String perm : permissions) { + if (pluginManager.getPermission(perm) == null) { + pluginManager.addPermission(new Permission(perm, PermissionDefault.OP)); + } + } + } + + private static boolean testPermission(final CommandSender sender, final String permission) { + if (sender.hasPermission(BASE_PERM + permission) || sender.hasPermission("bukkit.command.leaves")) { + return true; + } + sender.sendMessage(Bukkit.permissionMessage()); + return false; + } + + @NotNull + + @Override + public List tabComplete(final @NotNull CommandSender sender, final @NotNull String alias, final String[] args, final @Nullable Location location) throws IllegalArgumentException { + if (args.length <= 1) { + return LeavesCommandUtil.getListMatchingLast(sender, args, COMPLETABLE_SUBCOMMANDS); + } + + final @Nullable Pair subCommand = resolveCommand(args[0]); + if (subCommand != null) { + return subCommand.second().tabComplete(sender, subCommand.first(), Arrays.copyOfRange(args, 1, args.length)); + } + + return Collections.emptyList(); + } + + @Override + public boolean execute(final @NotNull CommandSender sender, final @NotNull String commandLabel, final String[] args) { + if (!testPermission(sender)) { + return true; + } + + if (args.length == 0) { + sender.sendMessage(text("Usage: " + this.usageMessage, RED)); + return false; + } + final Pair subCommand = resolveCommand(args[0]); + + if (subCommand == null) { + sender.sendMessage(text("Usage: " + this.usageMessage, RED)); + return false; + } + + if (!testPermission(sender, subCommand.first())) { + return true; + } + final String[] choppedArgs = Arrays.copyOfRange(args, 1, args.length); + return subCommand.second().execute(sender, subCommand.first(), choppedArgs); + } + + @Nullable + private static Pair resolveCommand(String label) { + label = label.toLowerCase(Locale.ENGLISH); + LeavesSubcommand subCommand = SUBCOMMANDS.get(label); + + if (subCommand != null) { + return Pair.of(label, subCommand); + } + + return null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java new file mode 100644 index 00000000..d110bf39 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java @@ -0,0 +1,81 @@ +package org.leavesmc.leaves.command; + +import com.google.common.base.Functions; +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import net.minecraft.resources.ResourceLocation; +import org.bukkit.command.CommandSender; +import org.checkerframework.checker.nullness.qual.NonNull; +import org.checkerframework.framework.qual.DefaultQualifier; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +@DefaultQualifier(NonNull.class) +public class LeavesCommandUtil { + + private LeavesCommandUtil() { + } + + // Code from Mojang - copyright them + public static List getListMatchingLast( + final CommandSender sender, + final String[] args, + final String... matches + ) { + return getListMatchingLast(sender, args, Arrays.asList(matches)); + } + + public static boolean matches(final String s, final String s1) { + return s1.regionMatches(true, 0, s, 0, s.length()); + } + + public static List getListMatchingLast( + final CommandSender sender, + final String[] strings, + final Collection collection + ) { + return getListMatchingLast(sender, strings, collection, LeavesCommand.BASE_PERM, "bukkit.command.leaves"); + } + + public static List getListMatchingLast( + final CommandSender sender, + final String[] strings, + final Collection collection, + final String basePermission, + final String overridePermission + ) { + String last = strings[strings.length - 1]; + ArrayList results = Lists.newArrayList(); + + if (!collection.isEmpty()) { + Iterator iterator = Iterables.transform(collection, Functions.toStringFunction()).iterator(); + + while (iterator.hasNext()) { + String s1 = (String) iterator.next(); + + if (matches(last, s1) && (sender.hasPermission(basePermission + s1) || sender.hasPermission(overridePermission))) { + results.add(s1); + } + } + + if (results.isEmpty()) { + iterator = collection.iterator(); + + while (iterator.hasNext()) { + Object object = iterator.next(); + + if (object instanceof ResourceLocation && matches(last, ((ResourceLocation) object).getPath())) { + results.add(String.valueOf(object)); + } + } + } + } + + return results; + } + // end copy stuff +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java new file mode 100644 index 00000000..4b61fccc --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.command; + +import org.bukkit.command.CommandSender; + +import java.util.Collections; +import java.util.List; + +public interface LeavesSubcommand { + boolean execute(CommandSender sender, String subCommand, String[] args); + + default List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { + return Collections.emptyList(); + } + + default boolean tabCompletes() { + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/NoBlockUpdateCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/NoBlockUpdateCommand.java new file mode 100644 index 00000000..fffcac8c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/NoBlockUpdateCommand.java @@ -0,0 +1,52 @@ +package org.leavesmc.leaves.command; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.Bukkit; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; + +import java.util.List; + +public class NoBlockUpdateCommand extends Command { + + private static boolean noBlockUpdate = false; + + public NoBlockUpdateCommand(@NotNull String name) { + super(name); + this.description = "No Block Update Command"; + this.usageMessage = "/blockupdate"; + this.setPermission("bukkit.command.blockupdate"); + final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); + if (pluginManager.getPermission("bukkit.command.blockupdate") == null) { + pluginManager.addPermission(new Permission("bukkit.command.blockupdate", PermissionDefault.OP)); + } + } + + @Override + public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args) throws IllegalArgumentException { + return List.of(); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, String @NotNull [] args) { + if (!testPermission(sender)) return true; + noBlockUpdate = !noBlockUpdate; + Bukkit.broadcast(Component.join(JoinConfiguration.noSeparators(), + Component.text("Block update status: ", NamedTextColor.GRAY), + Component.text(!noBlockUpdate, noBlockUpdate ? NamedTextColor.AQUA : NamedTextColor.GRAY) + ), "bukkit.command.blockupdate"); + + return true; + } + + public static boolean isNoBlockUpdate() { + return LeavesConfig.modify.noBlockUpdateCommand && noBlockUpdate; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java new file mode 100644 index 00000000..56ad63ae --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java @@ -0,0 +1,83 @@ +package org.leavesmc.leaves.command.subcommands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import org.bukkit.command.CommandSender; +import org.leavesmc.leaves.command.LeavesCommandUtil; +import org.leavesmc.leaves.command.LeavesSubcommand; +import org.leavesmc.leaves.config.GlobalConfigManager; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class ConfigCommand implements LeavesSubcommand { + + @Override + public boolean execute(CommandSender sender, String subCommand, String[] args) { + if (args.length < 1) { + sender.sendMessage(Component.text("Leaves Config", NamedTextColor.GRAY)); + return true; + } + + GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(args[0]); + if (verifiedConfig == null) { + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Config ", NamedTextColor.GRAY), + Component.text(args[0], NamedTextColor.RED), + Component.text(" is Not Found.", NamedTextColor.GRAY) + )); + return true; + } + + if (args.length > 1) { + try { + verifiedConfig.set(args[1]); + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Config ", NamedTextColor.GRAY), + Component.text(args[0], NamedTextColor.AQUA), + Component.text(" changed to ", NamedTextColor.GRAY), + Component.text(verifiedConfig.getString(), NamedTextColor.AQUA) + )); + } catch (IllegalArgumentException exception) { + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Config ", NamedTextColor.GRAY), + Component.text(args[0], NamedTextColor.RED), + Component.text(" modify error by ", NamedTextColor.GRAY), + Component.text(exception.getMessage(), NamedTextColor.RED) + )); + } + } else { + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Config ", NamedTextColor.GRAY), + Component.text(args[0], NamedTextColor.AQUA), + Component.text(" value is ", NamedTextColor.GRAY), + Component.text(verifiedConfig.getString(), NamedTextColor.AQUA) + )); + } + + return true; + } + + @Override + public List tabComplete(CommandSender sender, String subCommand, String[] args) { + switch (args.length) { + case 1 -> { + List list = new ArrayList<>(GlobalConfigManager.getVerifiedConfigPaths()); + return LeavesCommandUtil.getListMatchingLast(sender, args, list); + } + + case 2 -> { + GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(args[0]); + if (verifiedConfig != null) { + return LeavesCommandUtil.getListMatchingLast(sender, args, verifiedConfig.validator().valueSuggest()); + } else { + return Collections.singletonList(""); + } + } + } + + return Collections.emptyList(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/CounterCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/CounterCommand.java new file mode 100644 index 00000000..6d0e3c9a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/CounterCommand.java @@ -0,0 +1,121 @@ +package org.leavesmc.leaves.command.subcommands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.item.DyeColor; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.command.LeavesCommandUtil; +import org.leavesmc.leaves.command.LeavesSubcommand; +import org.leavesmc.leaves.util.HopperCounter; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class CounterCommand implements LeavesSubcommand { + + @Override + public boolean execute(CommandSender sender, String subCommand, String[] args) { + if (!LeavesConfig.modify.hopperCounter) { + return false; + } + + if (args.length < 1) { + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Hopper Counter: ", NamedTextColor.GRAY), + Component.text(HopperCounter.isEnabled(), HopperCounter.isEnabled() ? NamedTextColor.AQUA : NamedTextColor.GRAY) + )); + return true; + } + + if (!HopperCounter.isEnabled()) { + if (args[0].equals("enable")) { + HopperCounter.setEnabled(true); + sender.sendMessage(Component.text("Hopper Counter now is enabled", NamedTextColor.AQUA)); + } else { + sender.sendMessage(Component.text("Hopper Counter is not enabled", NamedTextColor.RED)); + } + return true; + } + + DyeColor color = DyeColor.byName(args[0], null); + if (color != null) { + HopperCounter counter = HopperCounter.getCounter(color); + if (args.length < 2) { + displayCounter(sender, counter, false); + } else { + switch (args[1]) { + case "reset" -> { + counter.reset(MinecraftServer.getServer()); + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Restarted "), + Component.text(color.getName(), TextColor.color(color.getTextColor())), + Component.text(" counter") + )); + } + case "realtime" -> displayCounter(sender, counter, true); + } + } + return true; + } + + switch (args[0]) { + case "reset" -> { + HopperCounter.resetAll(MinecraftServer.getServer(), false); + sender.sendMessage(Component.text("Restarted all counters")); + } + case "disable" -> { + HopperCounter.setEnabled(false); + HopperCounter.resetAll(MinecraftServer.getServer(), true); + sender.sendMessage(Component.text("Hopper Counter now is disabled", NamedTextColor.GRAY)); + } + } + + return true; + } + + private void displayCounter(CommandSender sender, @NotNull HopperCounter counter, boolean realTime) { + for (Component component : counter.format(MinecraftServer.getServer(), realTime)) { + sender.sendMessage(component); + } + } + + @Override + public List tabComplete(CommandSender sender, String subCommand, String[] args) { + if (!LeavesConfig.modify.hopperCounter) { + return Collections.emptyList(); + } + + switch (args.length) { + case 1 -> { + if (!HopperCounter.isEnabled()) { + return Collections.singletonList("enable"); + } + + List list = new ArrayList<>(Arrays.stream(DyeColor.values()).map(DyeColor::getName).toList()); + list.add("reset"); + list.add("disable"); + return LeavesCommandUtil.getListMatchingLast(sender, args, list); + } + + case 2 -> { + if (DyeColor.byName(args[0], null) != null) { + return LeavesCommandUtil.getListMatchingLast(sender, args, "reset", "realtime"); + } + } + } + + return Collections.emptyList(); + } + + @Override + public boolean tabCompletes() { + return LeavesConfig.modify.hopperCounter; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/PeacefulModeSwitchCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/PeacefulModeSwitchCommand.java new file mode 100644 index 00000000..1fe1d08c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/PeacefulModeSwitchCommand.java @@ -0,0 +1,87 @@ +package org.leavesmc.leaves.command.subcommands; + +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.JoinConfiguration; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.NaturalSpawner; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.command.CommandSender; +import org.bukkit.craftbukkit.CraftWorld; +import org.bukkit.entity.Player; +import org.leavesmc.leaves.command.LeavesCommandUtil; +import org.leavesmc.leaves.command.LeavesSubcommand; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PeacefulModeSwitchCommand implements LeavesSubcommand { + + @Override + public boolean execute(CommandSender sender, String subCommand, String[] args) { + World world; + if (args.length == 0) { + if (sender instanceof Player player) { + world = player.getWorld(); + } else { + sender.sendMessage(Component.text("Must specify a world! ex: '/leaves peaceful world'", NamedTextColor.RED)); + return true; + } + } else { + final String input = args[0]; + final World inputWorld = Bukkit.getWorld(input); + if (inputWorld == null) { + sender.sendMessage(Component.text("'" + input + "' is not a valid world!", NamedTextColor.RED)); + return true; + } else { + world = inputWorld; + } + } + + final ServerLevel level = ((CraftWorld) world).getHandle(); + int chunks = 0; + if (level.chunkSource.getLastSpawnState() != null) { + chunks = level.chunkSource.getLastSpawnState().getSpawnableChunkCount(); + } + + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Peaceful Mode Switch for world: "), + Component.text(world.getName(), NamedTextColor.AQUA) + )); + + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Refuses per "), + Component.text(level.chunkSource.peacefulModeSwitchTick, level.chunkSource.peacefulModeSwitchTick > 0 ? NamedTextColor.AQUA : NamedTextColor.GRAY), + Component.text(" tick") + )); + + int count = level.chunkSource.peacefulModeSwitchCount; + int limit = NaturalSpawner.globalLimitForCategory(level, MobCategory.MONSTER, chunks); + NamedTextColor color = count >= limit ? NamedTextColor.AQUA : NamedTextColor.GRAY; + + sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), + Component.text("Now count "), + Component.text(count, color), + Component.text("/", color), + Component.text(limit, color) + )); + + return true; + } + + @Override + public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { + return LeavesCommandUtil.getListMatchingLast(sender, args, this.suggestPeacefulModeSwitch(args)); + } + + private List suggestPeacefulModeSwitch(final String[] args) { + if (args.length == 1) { + return new ArrayList<>(Bukkit.getWorlds().stream().map(World::getName).toList()); + } + + return Collections.emptyList(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java new file mode 100644 index 00000000..74971828 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java @@ -0,0 +1,22 @@ +package org.leavesmc.leaves.command.subcommands; + +import net.minecraft.server.MinecraftServer; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.command.LeavesSubcommand; + +import java.io.File; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.NamedTextColor.GREEN; + +public class ReloadCommand implements LeavesSubcommand { + @Override + public boolean execute(CommandSender sender, String subCommand, String[] args) { + MinecraftServer server = MinecraftServer.getServer(); + LeavesConfig.init((File) server.options.valueOf("leaves-settings")); + Command.broadcastCommandMessage(sender, text("Leaves config reload complete.", GREEN)); + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java new file mode 100644 index 00000000..7f94df60 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java @@ -0,0 +1,21 @@ +package org.leavesmc.leaves.command.subcommands; + +import org.bukkit.ChatColor; +import org.bukkit.command.CommandSender; +import org.leavesmc.leaves.command.LeavesSubcommand; +import org.leavesmc.leaves.util.LeavesUpdateHelper; + +public class UpdateCommand implements LeavesSubcommand { + + @Override + public boolean execute(CommandSender sender, String subCommand, String[] args) { + sender.sendMessage(ChatColor.GRAY + "Trying to update Leaves, see the console for more info."); + LeavesUpdateHelper.tryUpdateLeaves(); + return true; + } + + @Override + public boolean tabCompletes() { + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigConverter.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigConverter.java new file mode 100644 index 00000000..2f8192d4 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigConverter.java @@ -0,0 +1,20 @@ +package org.leavesmc.leaves.config; + +public interface ConfigConverter { + E convert(String value) throws IllegalArgumentException; + + @SuppressWarnings("unchecked") + default E loadConvert(Object value) throws IllegalArgumentException { + try { + return (E) value; + } catch (ClassCastException e) { + throw new IllegalArgumentException(e); + } + } + + default Object saveConvert(E value) { + return value; + } + + Class getFieldClass(); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java new file mode 100644 index 00000000..fe892adb --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java @@ -0,0 +1,15 @@ +package org.leavesmc.leaves.config; + +import java.util.List; + +public interface ConfigValidator extends ConfigConverter { + default void verify(E old, E value) throws IllegalArgumentException { + } + + default List valueSuggest() { + return List.of(""); + } + + default void runAfterLoader(E value, boolean firstLoad) { + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidatorImpl.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidatorImpl.java new file mode 100644 index 00000000..0e9cdc0f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/ConfigValidatorImpl.java @@ -0,0 +1,108 @@ +package org.leavesmc.leaves.config; + +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public abstract class ConfigValidatorImpl implements ConfigValidator { + + private Class fieldClass; + + @SuppressWarnings("unchecked") + public ConfigValidatorImpl() { + Type superClass = getClass().getGenericSuperclass(); + if (superClass instanceof ParameterizedType) { + Type[] actualTypeArguments = ((ParameterizedType) superClass).getActualTypeArguments(); + if (actualTypeArguments[0] instanceof Class) { + this.fieldClass = (Class) actualTypeArguments[0]; + } + } + } + + @Override + public Class getFieldClass() { + return fieldClass; + } + + public static class BooleanConfigValidator extends ConfigValidatorImpl { + + @Override + public Boolean convert(String value) throws IllegalArgumentException { + return Boolean.parseBoolean(value); + } + + @Override + public List valueSuggest() { + return List.of("false", "true"); + } + } + + public static class IntConfigValidator extends ConfigValidatorImpl { + @Override + public Integer convert(String value) throws IllegalArgumentException { + return Integer.parseInt(value); + } + } + + public static class StringConfigValidator extends ConfigValidatorImpl { + @Override + public String convert(String value) throws IllegalArgumentException { + return value; + } + } + + public static class DoubleConfigValidator extends ConfigValidatorImpl { + @Override + public Double convert(String value) throws IllegalArgumentException { + return Double.parseDouble(value); + } + } + + public abstract static class ListConfigValidator extends ConfigValidatorImpl> { + public static class STRING extends ListConfigValidator { + } + + @Override + public List convert(String value) throws IllegalArgumentException { + throw new IllegalArgumentException("not support"); // TODO + } + } + + public abstract static class EnumConfigValidator> extends ConfigValidatorImpl { + + private final List enumValues; + + public EnumConfigValidator() { + super(); + this.enumValues = new ArrayList<>() {{ + for (E e : getFieldClass().getEnumConstants()) { + add(e.name().toLowerCase(Locale.ROOT)); + } + }}; + } + + @Override + public E convert(@NotNull String value) throws IllegalArgumentException { + return Enum.valueOf(getFieldClass(), value.toUpperCase(Locale.ROOT)); + } + + @Override + public E loadConvert(@NotNull Object value) throws IllegalArgumentException { + return this.convert(value.toString()); + } + + @Override + public Object saveConvert(@NotNull E value) { + return value.toString().toUpperCase(Locale.ROOT); + } + + @Override + public List valueSuggest() { + return enumValues; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java new file mode 100644 index 00000000..7323d446 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java @@ -0,0 +1,16 @@ +package org.leavesmc.leaves.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface GlobalConfig { + String value(); + + boolean lock() default false; + + Class> validator() default ConfigValidatorImpl.BooleanConfigValidator.class; +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java new file mode 100644 index 00000000..1e109a8b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java @@ -0,0 +1,12 @@ +package org.leavesmc.leaves.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface GlobalConfigCategory { + String value(); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java new file mode 100644 index 00000000..dfc38bd1 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java @@ -0,0 +1,84 @@ +package org.leavesmc.leaves.config; + +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Collections; + +import static org.leavesmc.leaves.config.GlobalConfigManager.CONFIG_START; + +@SuppressWarnings("CallToPrintStackTrace") +public class GlobalConfigCreator { + + private static YamlConfiguration config; + + @SuppressWarnings("ResultOfMethodCallIgnored") + public static void main(String[] args) { + config = new YamlConfiguration(); + config.options().setHeader(Collections.singletonList(LeavesConfig.CONFIG_HEADER)); + + config.set("config-version", LeavesConfig.CURRENT_CONFIG_VERSION); + + for (Field field : LeavesConfig.class.getDeclaredFields()) { + initField(field, null, CONFIG_START); + } + + config.set("settings.protocol.xaero-map-server-id", 0); + + try { + File file = new File("leaves.yml"); + if (file.exists()) { + file.delete(); + } + file.createNewFile(); + config.save(file); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void initCategory(@NotNull Field categoryField, @NotNull GlobalConfigCategory globalCategory, @Nullable Object upstreamField, @NotNull String upstreamPath) { + try { + Object category = categoryField.get(upstreamField); + String categoryPath = upstreamPath + globalCategory.value() + "."; + for (Field field : categoryField.getType().getDeclaredFields()) { + initField(field, category, categoryPath); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + private static void initField(@NotNull Field field, @Nullable Object upstreamField, @NotNull String upstreamPath) { + if (upstreamField != null || Modifier.isStatic(field.getModifiers())) { + field.setAccessible(true); + + GlobalConfig globalConfig = field.getAnnotation(GlobalConfig.class); + if (globalConfig != null) { + initConfig(field, globalConfig, upstreamField, upstreamPath); + return; + } + + GlobalConfigCategory globalCategory = field.getType().getAnnotation(GlobalConfigCategory.class); + if (globalCategory != null) { + initCategory(field, globalCategory, upstreamField, upstreamPath); + } + } + } + + + private static void initConfig(@NotNull Field field, GlobalConfig globalConfig, @Nullable Object upstreamField, @NotNull String upstreamPath) { + try { + GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath); + config.set(verifiedConfig.path(), verifiedConfig.validator().saveConvert(field.get(upstreamField))); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java new file mode 100644 index 00000000..adc023d5 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java @@ -0,0 +1,232 @@ +package org.leavesmc.leaves.config; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +public class GlobalConfigManager { + + public final static String CONFIG_START = "settings."; + + private static boolean firstLoad = true; + private static final Map verifiedConfigs = new HashMap<>(); + + public static void init() { + verifiedConfigs.clear(); + + for (Field field : LeavesConfig.class.getDeclaredFields()) { + initField(field, null, CONFIG_START); + } + + verifiedConfigs.forEach((path, config) -> config.validator.runAfterLoader(config.get(), firstLoad)); + + firstLoad = false; + LeavesConfig.save(); + } + + private static void initCategory(@NotNull Field categoryField, @NotNull GlobalConfigCategory globalCategory, @Nullable Object upstreamField, @NotNull String upstreamPath) { + try { + Object category = categoryField.get(upstreamField); + String categoryPath = upstreamPath + globalCategory.value() + "."; + for (Field field : categoryField.getType().getDeclaredFields()) { + initField(field, category, categoryPath); + } + } catch (Exception e) { + LeavesLogger.LOGGER.severe("Failure to load leaves config" + upstreamPath, e); + } + } + + private static void initField(@NotNull Field field, @Nullable Object upstreamField, @NotNull String upstreamPath) { + if (upstreamField != null || Modifier.isStatic(field.getModifiers())) { + field.setAccessible(true); + + for (RemovedConfig config : field.getAnnotationsByType(RemovedConfig.class)) { + RemovedVerifiedConfig.build(config, field, upstreamField).run(); + } + + GlobalConfig globalConfig = field.getAnnotation(GlobalConfig.class); + if (globalConfig != null) { + initConfig(field, globalConfig, upstreamField, upstreamPath); + return; + } + + GlobalConfigCategory globalCategory = field.getType().getAnnotation(GlobalConfigCategory.class); + if (globalCategory != null) { + initCategory(field, globalCategory, upstreamField, upstreamPath); + } + } + } + + private static void initConfig(@NotNull Field field, GlobalConfig globalConfig, @Nullable Object upstreamField, @NotNull String upstreamPath) { + try { + VerifiedConfig verifiedConfig = VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath); + + if (globalConfig.lock() && !firstLoad) { + verifiedConfigs.put(verifiedConfig.path.substring(CONFIG_START.length()), verifiedConfig); + } + + ConfigValidator validator = verifiedConfig.validator; + + Object defValue = validator.saveConvert(field.get(upstreamField)); + LeavesConfig.config.addDefault(verifiedConfig.path, defValue); + + try { + Object savedValue = LeavesConfig.config.get(verifiedConfig.path); + if (savedValue == null) { + throw new IllegalArgumentException("?"); + } + + if (savedValue.getClass() != validator.getFieldClass()) { + savedValue = validator.loadConvert(savedValue); + } + + validator.verify(null, savedValue); + + field.set(upstreamField, savedValue); + } catch (IllegalArgumentException | ClassCastException e) { + LeavesConfig.config.set(verifiedConfig.path, defValue); + LeavesLogger.LOGGER.warning(e.getMessage() + ", reset to " + defValue); + } + + verifiedConfigs.put(verifiedConfig.path.substring(CONFIG_START.length()), verifiedConfig); + } catch (Exception e) { + LeavesLogger.LOGGER.severe("Failure to load leaves config", e); + throw new RuntimeException(); + } + } + + public static VerifiedConfig getVerifiedConfig(String path) { + return verifiedConfigs.get(path); + } + + @Contract(pure = true) + public static @NotNull Set getVerifiedConfigPaths() { + return verifiedConfigs.keySet(); + } + + public record RemovedVerifiedConfig(ConfigConverter convert, boolean transform, Field field, Object upstreamField, String path) { + + public void run() { + if (transform) { + if (LeavesConfig.config.contains(path)) { + Object savedValue = LeavesConfig.config.get(path); + if (savedValue != null) { + try { + if (savedValue.getClass() != convert.getFieldClass()) { + savedValue = convert.loadConvert(savedValue); + } + field.set(upstreamField, savedValue); + } catch (IllegalAccessException | IllegalArgumentException e) { + LeavesLogger.LOGGER.warning("Failure to load leaves config" + path, e); + } + } else { + LeavesLogger.LOGGER.warning("Failed to convert saved value for " + path + ", reset to default"); + } + } + } + LeavesConfig.config.set(path, null); + } + + @Contract("_, _, _ -> new") + public static @NotNull RemovedVerifiedConfig build(@NotNull RemovedConfig config, @NotNull Field field, @Nullable Object upstreamField) { + StringBuilder path = new StringBuilder("settings."); + for (String category : config.category()) { + path.append(category).append("."); + } + path.append(config.name()); + + ConfigConverter converter = null; + try { + Constructor> constructor = config.convert().getDeclaredConstructor(); + constructor.setAccessible(true); + converter = constructor.newInstance(); + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Failure to load leaves config" + path, e); + } + + return new RemovedVerifiedConfig(converter, config.transform(), field, upstreamField, path.toString()); + } + } + + public record VerifiedConfig(ConfigValidator validator, boolean lock, Field field, Object upstreamField, String path) { + + public void set(String stringValue) throws IllegalArgumentException { + Object value; + try { + value = validator.convert(stringValue); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("value parse error: " + e.getMessage()); + } + + validator.verify(this.get(), value); + + try { + LeavesConfig.config.set(path, validator.saveConvert(value)); + LeavesConfig.save(); + if (lock) { + throw new IllegalArgumentException("locked, will load after restart"); + } + field.set(upstreamField, value); + } catch (IllegalAccessException e) { + throw new IllegalArgumentException(e.getMessage()); + } + } + + public Object get() { + try { + return field.get(upstreamField); + } catch (IllegalAccessException e) { + LeavesLogger.LOGGER.severe("Failure to get " + path + " value", e); + return ""; + } + } + + public String getString() { + Object value = this.get(); + + Object savedValue = LeavesConfig.config.get(path); + try { + if (savedValue != null) { + if (validator.getFieldClass() != savedValue.getClass()) { + savedValue = validator.loadConvert(savedValue); + } + + if (!savedValue.equals(value)) { + return value.toString() + "(" + savedValue + " after restart)"; + } + } + } catch (IllegalArgumentException e) { + LeavesLogger.LOGGER.severe("Failure to get " + path + " value", e); + } + return value.toString(); + } + + @SuppressWarnings("unchecked") + @NotNull + @Contract("_, _, _, _ -> new") + public static VerifiedConfig build(@NotNull GlobalConfig config, @NotNull Field field, @Nullable Object upstreamField, @NotNull String upstreamPath) { + String path = upstreamPath + config.value(); + + ConfigValidator validator; + try { + Constructor> constructor = config.validator().getDeclaredConstructor(); + constructor.setAccessible(true); + validator = (ConfigValidator) constructor.newInstance(); + } catch (Exception e) { + LeavesLogger.LOGGER.severe("Failure to load leaves config" + path, e); + throw new RuntimeException(); + } + + return new VerifiedConfig(validator, config.lock(), field, upstreamField, path); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java new file mode 100644 index 00000000..3b3c02fc --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java @@ -0,0 +1,26 @@ +package org.leavesmc.leaves.config; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(RemovedConfig.Array.class) +public @interface RemovedConfig { + String name(); + + String[] category(); + + boolean transform() default false; + + Class> convert() default ConfigValidatorImpl.BooleanConfigValidator.class; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface Array { + RemovedConfig[] value(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftBot.java b/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftBot.java new file mode 100644 index 00000000..aaa5f8a3 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftBot.java @@ -0,0 +1,94 @@ +package org.leavesmc.leaves.entity; + +import com.google.common.base.Preconditions; +import org.bukkit.Location; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.event.player.PlayerTeleportEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.BotList; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.bot.agent.actions.CraftBotAction; +import org.leavesmc.leaves.entity.botaction.LeavesBotAction; +import org.leavesmc.leaves.event.bot.BotActionStopEvent; +import org.leavesmc.leaves.event.bot.BotRemoveEvent; + +import java.util.UUID; + +public class CraftBot extends CraftPlayer implements Bot { + + public CraftBot(CraftServer server, ServerBot entity) { + super(server, entity); + } + + @Override + public String getSkinName() { + return this.getHandle().createState.skinName(); + } + + @Override + public @NotNull String getRealName() { + return this.getHandle().createState.realName(); + } + + @Override + public @Nullable UUID getCreatePlayerUUID() { + return this.getHandle().createPlayer; + } + + @Override + public void addAction(@NotNull LeavesBotAction action) { + this.getHandle().addBotAction(CraftBotAction.asInternalCopy(action), null); + } + + @Override + public LeavesBotAction getAction(int index) { + return CraftBotAction.asAPICopy(this.getHandle().getBotActions().get(index)); + } + + @Override + public int getActionSize() { + return this.getHandle().getBotActions().size(); + } + + @Override + public void stopAction(int index) { + this.getHandle().getBotActions().get(index).stop(this.getHandle(), BotActionStopEvent.Reason.PLUGIN); + } + + @Override + public void stopAllActions() { + for (BotAction action : this.getHandle().getBotActions()) { + action.stop(this.getHandle(), BotActionStopEvent.Reason.PLUGIN); + } + } + + @Override + public boolean remove(boolean save) { + BotList.INSTANCE.removeBot(this.getHandle(), BotRemoveEvent.RemoveReason.PLUGIN, null, save); + return true; + } + + @Override + public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) { + Preconditions.checkArgument(location != null, "location cannot be null"); + Preconditions.checkState(location.getWorld().equals(this.getWorld()), "[Leaves] Fakeplayers do not support changing world, Please use leaves fakeplayer-api instead!"); + return super.teleport(location, cause, flags); + } + + @Override + public ServerBot getHandle() { + return (ServerBot) entity; + } + + public void setHandle(final ServerBot entity) { + super.setHandle(entity); + } + + @Override + public String toString() { + return "CraftBot{" + "name=" + getName() + '}'; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java new file mode 100644 index 00000000..422640df --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java @@ -0,0 +1,80 @@ +package org.leavesmc.leaves.entity; + +import com.google.common.base.Function; +import com.google.common.collect.Lists; +import net.minecraft.server.MinecraftServer; +import org.bukkit.Location; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.BotCreateState; +import org.leavesmc.leaves.bot.BotList; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.bot.agent.Actions; +import org.leavesmc.leaves.bot.agent.BotAction; +import org.leavesmc.leaves.bot.agent.actions.CraftCustomBotAction; +import org.leavesmc.leaves.entity.botaction.CustomBotAction; +import org.leavesmc.leaves.event.bot.BotCreateEvent; + +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +public class CraftBotManager implements BotManager { + + private final BotList botList; + private final Collection botViews; + + public CraftBotManager() { + this.botList = MinecraftServer.getServer().getBotList(); + this.botViews = Collections.unmodifiableList(Lists.transform(botList.bots, new Function() { + @Override + public CraftBot apply(ServerBot bot) { + return bot.getBukkitEntity(); + } + })); + } + + @Override + public @Nullable Bot getBot(@NotNull UUID uuid) { + ServerBot bot = botList.getBot(uuid); + if (bot != null) { + return bot.getBukkitEntity(); + } else { + return null; + } + } + + @Override + public @Nullable Bot getBot(@NotNull String name) { + ServerBot bot = botList.getBotByName(name); + if (bot != null) { + return bot.getBukkitEntity(); + } else { + return null; + } + } + + @Override + public Collection getBots() { + return botViews; + } + + @Override + public boolean registerCustomBotAction(String name, CustomBotAction action) { + return Actions.register(new CraftCustomBotAction(name, action)); + } + + @Override + public boolean unregisterCustomBotAction(String name) { + BotAction action = Actions.getForName(name); + if (action instanceof CraftCustomBotAction) { + return Actions.unregister(name); + } + return false; + } + + @Override + public BotCreator botCreator(@NotNull String realName, @NotNull Location location) { + return BotCreateState.builder(realName, location).createReason(BotCreateEvent.CreateReason.PLUGIN); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftPhotographer.java b/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftPhotographer.java new file mode 100644 index 00000000..fed2005c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftPhotographer.java @@ -0,0 +1,73 @@ +package org.leavesmc.leaves.entity; + +import net.minecraft.server.level.ServerPlayer; +import org.bukkit.craftbukkit.CraftServer; +import org.bukkit.craftbukkit.entity.CraftPlayer; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.replay.ServerPhotographer; + +import java.io.File; + +public class CraftPhotographer extends CraftPlayer implements Photographer { + + public CraftPhotographer(CraftServer server, ServerPhotographer entity) { + super(server, entity); + } + + @Override + public void stopRecording() { + this.stopRecording(true); + } + + @Override + public void stopRecording(boolean async) { + this.stopRecording(async, true); + } + + @Override + public void stopRecording(boolean async, boolean save) { + this.getHandle().remove(async, save); + } + + @Override + public void pauseRecording() { + this.getHandle().pauseRecording(); + } + + @Override + public void resumeRecording() { + this.getHandle().resumeRecording(); + } + + @Override + public void setRecordFile(@NotNull File file) { + this.getHandle().setSaveFile(file); + } + + @Override + public void setFollowPlayer(@Nullable Player player) { + ServerPlayer serverPlayer = player != null ? ((CraftPlayer) player).getHandle() : null; + this.getHandle().setFollowPlayer(serverPlayer); + } + + @Override + public @NotNull String getId() { + return this.getHandle().createState.id; + } + + @Override + public ServerPhotographer getHandle() { + return (ServerPhotographer) entity; + } + + public void setHandle(final ServerPhotographer entity) { + super.setHandle(entity); + } + + @Override + public String toString() { + return "CraftPhotographer{" + "name=" + getName() + '}'; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftPhotographerManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftPhotographerManager.java new file mode 100644 index 00000000..202e1694 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/entity/CraftPhotographerManager.java @@ -0,0 +1,84 @@ +package org.leavesmc.leaves.entity; + +import com.google.common.collect.Lists; +import org.bukkit.Location; +import org.bukkit.util.Consumer; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.replay.BukkitRecorderOption; +import org.leavesmc.leaves.replay.RecorderOption; +import org.leavesmc.leaves.replay.ServerPhotographer; + +import java.util.Collection; +import java.util.Collections; +import java.util.UUID; + +public class CraftPhotographerManager implements PhotographerManager { + + private final Collection photographerViews = Collections.unmodifiableList(Lists.transform(ServerPhotographer.getPhotographers(), ServerPhotographer::getBukkitPlayer)); + + @Override + public @Nullable Photographer getPhotographer(@NotNull UUID uuid) { + ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid); + if (photographer != null) { + return photographer.getBukkitPlayer(); + } + return null; + } + + @Override + public @Nullable Photographer getPhotographer(@NotNull String id) { + ServerPhotographer photographer = ServerPhotographer.getPhotographer(id); + if (photographer != null) { + return photographer.getBukkitPlayer(); + } + return null; + } + + @Override + public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location) { + ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createDefaultOption()).createSync(); + if (photographer != null) { + return photographer.getBukkitPlayer(); + } + return null; + } + + @Override + public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption) { + ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createFromBukkit(recorderOption)).createSync(); + if (photographer != null) { + return photographer.getBukkitPlayer(); + } + return null; + } + + @Override + public void removePhotographer(@NotNull String id) { + ServerPhotographer photographer = ServerPhotographer.getPhotographer(id); + if (photographer != null) { + photographer.remove(true); + } + } + + @Override + public void removePhotographer(@NotNull UUID uuid) { + ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid); + if (photographer != null) { + photographer.remove(true); + } + } + + @Override + public void removeAllPhotographers() { + for (ServerPhotographer photographer : ServerPhotographer.getPhotographers()) { + photographer.remove(true); + } + } + + @Override + public Collection getPhotographers() { + return photographerViews; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/world/chunk/LithiumHashPalette.java b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/world/chunk/LithiumHashPalette.java new file mode 100644 index 00000000..da32fdd8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/lithium/common/world/chunk/LithiumHashPalette.java @@ -0,0 +1,197 @@ +package org.leavesmc.leaves.lithium.common.world.chunk; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.HashCommon; +import it.unimi.dsi.fastutil.objects.Reference2IntMap; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import net.minecraft.core.IdMap; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.VarInt; +import net.minecraft.world.level.chunk.Palette; +import net.minecraft.world.level.chunk.PaletteResize; +import org.jetbrains.annotations.NotNull; + +import static it.unimi.dsi.fastutil.Hash.FAST_LOAD_FACTOR; + +// Powered by Gale(https://github.com/GaleMC/Gale) + +/** + * Generally provides better performance over the vanilla {@link net.minecraft.world.level.chunk.HashMapPalette} when calling + * {@link LithiumHashPalette#idFor(Object)} through using a faster backing map and reducing pointer chasing. + */ +public class LithiumHashPalette implements Palette { + private static final int ABSENT_VALUE = -1; + + private final IdMap idList; + private final PaletteResize resizeHandler; + private final int indexBits; + + private final Reference2IntMap table; + private T[] entries; + private int size = 0; + + public LithiumHashPalette(IdMap idList, PaletteResize resizeHandler, int indexBits, T[] entries, Reference2IntMap table, int size) { + this.idList = idList; + this.resizeHandler = resizeHandler; + this.indexBits = indexBits; + this.entries = entries; + this.table = table; + this.size = size; + } + + public LithiumHashPalette(IdMap idList, int bits, PaletteResize resizeHandler, @NotNull List list) { + this(idList, bits, resizeHandler); + + for (T t : list) { + this.addEntry(t); + } + } + + @SuppressWarnings("unchecked") + public LithiumHashPalette(IdMap idList, int bits, PaletteResize resizeHandler) { + this.idList = idList; + this.indexBits = bits; + this.resizeHandler = resizeHandler; + + int capacity = 1 << bits; + + this.entries = (T[]) new Object[capacity]; + this.table = new Reference2IntOpenHashMap<>(capacity, FAST_LOAD_FACTOR); + this.table.defaultReturnValue(ABSENT_VALUE); + } + + @Override + public int idFor(@NotNull T obj) { + int id = this.table.getInt(obj); + + if (id == ABSENT_VALUE) { + id = this.computeEntry(obj); + } + + return id; + } + + @Override + public boolean maybeHas(@NotNull Predicate predicate) { + for (int i = 0; i < this.size; ++i) { + if (predicate.test(this.entries[i])) { + return true; + } + } + + return false; + } + + private int computeEntry(T obj) { + int id = this.addEntry(obj); + + if (id >= 1 << this.indexBits) { + if (this.resizeHandler == null) { + throw new IllegalStateException("Cannot grow"); + } else { + id = this.resizeHandler.onResize(this.indexBits + 1, obj); + } + } + + return id; + } + + private int addEntry(T obj) { + int nextId = this.size; + + if (nextId >= this.entries.length) { + this.resize(this.size); + } + + this.table.put(obj, nextId); + this.entries[nextId] = obj; + + this.size++; + + return nextId; + } + + private void resize(int neededCapacity) { + this.entries = Arrays.copyOf(this.entries, HashCommon.nextPowerOfTwo(neededCapacity + 1)); + } + + @Override + public T valueFor(int id) { + T[] entries = this.entries; + + if (id >= 0 && id < entries.length) { + return entries[id]; + } + + return null; + } + + @Override + public void read(FriendlyByteBuf buf) { + this.clear(); + + int entryCount = buf.readVarInt(); + + for (int i = 0; i < entryCount; ++i) { + this.addEntry(this.idList.byId(buf.readVarInt())); + } + } + + @Override + public void write(FriendlyByteBuf buf) { + int size = this.size; + buf.writeVarInt(size); + + for (int i = 0; i < size; ++i) { + buf.writeVarInt(this.idList.getId(this.valueFor(i))); + } + } + + @Override + public int getSerializedSize() { + int size = VarInt.getByteSize(this.size); + + for (int i = 0; i < this.size; ++i) { + size += VarInt.getByteSize(this.idList.getId(this.valueFor(i))); + } + + return size; + } + + @Override + public int getSize() { + return this.size; + } + + @NotNull + @Override + public Palette copy(@NotNull PaletteResize resizeListener) { + return new LithiumHashPalette<>(this.idList, resizeHandler, this.indexBits, this.entries.clone(), new Reference2IntOpenHashMap<>(this.table), this.size); + } + + private void clear() { + Arrays.fill(this.entries, null); + this.table.clear(); + this.size = 0; + } + + public List getElements() { + ImmutableList.Builder builder = new ImmutableList.Builder<>(); + for (T entry : this.entries) { + if (entry != null) { + builder.add(entry); + } + } + return builder.build(); + } + + public static Palette create(int bits, IdMap idList, PaletteResize listener, List list) { + return new LithiumHashPalette<>(idList, bits, listener, list); + } +} + diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java b/leaves-server/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java new file mode 100644 index 00000000..de06c854 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java @@ -0,0 +1,151 @@ +package org.leavesmc.leaves.plugin; + +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 MinecraftInternalPlugin extends PluginBase { + + public static final MinecraftInternalPlugin INSTANCE = new MinecraftInternalPlugin(); + + private boolean enabled = true; + + private final PluginDescriptionFile pdf; + + public MinecraftInternalPlugin() { + String pluginName = "Minecraft"; + pdf = new PluginDescriptionFile(pluginName, "1.0", "nms"); + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public File getDataFolder() { + throw new UnsupportedOperationException("Not supported."); + } + + @Override + public PluginDescriptionFile getDescription() { + return pdf; + } + + @Override + public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() { + return pdf; + } + + @Override + public FileConfiguration getConfig() { + throw new UnsupportedOperationException("Not supported."); + } + + @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; + } + + @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."); + } + + @Override + public @NotNull io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager getLifecycleManager() { + throw new UnsupportedOperationException("Not supported."); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java b/leaves-server/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java new file mode 100644 index 00000000..35fe2f6a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java @@ -0,0 +1,57 @@ +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 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) { + 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/leaves-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java b/leaves-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java new file mode 100644 index 00000000..4eb3396f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java @@ -0,0 +1,90 @@ +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.spongepowered.configurate.CommentedConfigurationNode; +import org.spongepowered.configurate.ConfigurateException; +import org.spongepowered.configurate.hocon.HoconConfigurationLoader; +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.List; +import java.util.function.Predicate; + +@ConfigSerializable +public class LeavesPluginMeta extends PaperPluginMeta { + private List mixins; + static final ApiVersion MINIMUM = ApiVersion.getOrCreateVersion("1.21"); + + public static LeavesPluginMeta create(BufferedReader reader) throws ConfigurateException { + HoconConfigurationLoader loader = HoconConfigurationLoader.builder() + .prettyPrinting(true) + .emitComments(true) + .emitJsonCompatible(true) + .source(() -> reader) + .defaultOptions((options) -> + options.serializers((serializers) -> + serializers.register(new ScalarSerializer<>(ApiVersion.class) { + @Override + public ApiVersion deserialize(final Type type, final 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 Object serialize(final ApiVersion item, final 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(); + CommentedConfigurationNode node = loader.load(); + LegacyPaperMeta.migrate(node); + LeavesPluginMeta pluginConfiguration = node.require(LeavesPluginMeta.class); + + if (!node.node("author").virtual()) { + pluginConfiguration.authors = ImmutableList.builder() + .addAll(pluginConfiguration.authors) + .add(node.node("author").getString()) + .build(); + } + + return pluginConfiguration; + } + + public List getMixins() { + return mixins; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/profile/LeavesAuthenticationService.java b/leaves-server/src/main/java/org/leavesmc/leaves/profile/LeavesAuthenticationService.java new file mode 100644 index 00000000..6f14a66d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/profile/LeavesAuthenticationService.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.profile; + +import com.destroystokyo.paper.profile.PaperAuthenticationService; +import com.mojang.authlib.minecraft.MinecraftSessionService; + +import java.net.Proxy; + +public class LeavesAuthenticationService extends PaperAuthenticationService { + + public LeavesAuthenticationService(Proxy proxy) { + super(proxy); + } + + @Override + public MinecraftSessionService createMinecraftSessionService() { + return new LeavesMinecraftSessionService(this.getServicesKeySet(), this.getProxy(), this.environment); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/profile/LeavesMinecraftSessionService.java b/leaves-server/src/main/java/org/leavesmc/leaves/profile/LeavesMinecraftSessionService.java new file mode 100644 index 00000000..133c7619 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/profile/LeavesMinecraftSessionService.java @@ -0,0 +1,102 @@ +package org.leavesmc.leaves.profile; + +import com.destroystokyo.paper.profile.PaperMinecraftSessionService; +import com.mojang.authlib.Environment; +import com.mojang.authlib.GameProfile; +import com.mojang.authlib.HttpAuthenticationService; +import com.mojang.authlib.exceptions.AuthenticationUnavailableException; +import com.mojang.authlib.exceptions.MinecraftClientException; +import com.mojang.authlib.yggdrasil.ProfileActionType; +import com.mojang.authlib.yggdrasil.ProfileResult; +import com.mojang.authlib.yggdrasil.ServicesKeySet; +import com.mojang.authlib.yggdrasil.response.HasJoinedMinecraftServerResponse; +import com.mojang.authlib.yggdrasil.response.ProfileAction; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.bot.ServerBot; + +import java.net.InetAddress; +import java.net.Proxy; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class LeavesMinecraftSessionService extends PaperMinecraftSessionService { + + protected LeavesMinecraftSessionService(ServicesKeySet keySet, Proxy authenticationService, Environment environment) { + super(keySet, authenticationService, environment); + } + + private static List extraYggdrasilList = List.of(); + + public static void initExtraYggdrasilList(List extraYggdrasilServiceList) { + List list = new ArrayList<>(); + for (String str : extraYggdrasilServiceList) { + list.add(HttpAuthenticationService.constantURL(str + "/sessionserver/session/minecraft/hasJoined")); + } + extraYggdrasilList = Collections.unmodifiableList(list); + } + + @Nullable + @Override + public ProfileResult hasJoinedServer(String profileName, String serverId, @Nullable InetAddress address) throws AuthenticationUnavailableException { + ProfileResult result = super.hasJoinedServer(profileName, serverId, address); // mojang + + ServerPlayer player = MinecraftServer.getServer().getPlayerList().getPlayerByName(profileName); + if (player != null && !(player instanceof ServerBot)) { + return null; // if it has same name, return null + } + + if (LeavesConfig.mics.yggdrasil.enable && result == null) { + final Map arguments = new HashMap<>(); + arguments.put("username", profileName); + arguments.put("serverId", serverId); + + if (address != null) { + arguments.put("ip", address.getHostAddress()); + } + + GameProfile cache = null; + if (LeavesConfig.mics.yggdrasil.loginProtect) { + cache = MinecraftServer.getServer().services.profileCache().getProfileIfCached(profileName); + } + + for (URL checkUrl : extraYggdrasilList) { + URL url = HttpAuthenticationService.concatenateURL(checkUrl, HttpAuthenticationService.buildQuery(arguments)); + try { + final HasJoinedMinecraftServerResponse response = client.get(url, HasJoinedMinecraftServerResponse.class); + if (response != null && response.id() != null) { + if (LeavesConfig.mics.yggdrasil.loginProtect && cache != null) { + if (response.id() != cache.getId()) { + continue; + } + } + + final GameProfile result1 = new GameProfile(response.id(), profileName); + + if (response.properties() != null) { + result1.getProperties().putAll(response.properties()); + } + + final Set profileActions = response.profileActions().stream() + .map(ProfileAction::type) + .collect(Collectors.toSet()); + return new ProfileResult(result1, profileActions); + } + } catch (final MinecraftClientException e) { + if (e.toAuthenticationException() instanceof final AuthenticationUnavailableException unavailable) { + throw unavailable; + } + } + } + } + return result; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java new file mode 100644 index 00000000..2368f0ca --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java @@ -0,0 +1,127 @@ +package org.leavesmc.leaves.protocol; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.food.FoodData; +import net.minecraft.world.level.GameRules; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@LeavesProtocol(namespace = "appleskin") +public class AppleSkinProtocol { + + public static final String PROTOCOL_ID = "appleskin"; + + private static final ResourceLocation SATURATION_KEY = id("saturation"); + private static final ResourceLocation EXHAUSTION_KEY = id("exhaustion"); + private static final ResourceLocation NATURAL_REGENERATION_KEY = id("natural_regeneration"); + + private static final float MINIMUM_EXHAUSTION_CHANGE_THRESHOLD = 0.01F; + + private static final Map previousSaturationLevels = new HashMap<>(); + private static final Map previousExhaustionLevels = new HashMap<>(); + private static final Map previousNaturalRegeneration = new HashMap<>(); + + private static final Map> subscribedChannels = new HashMap<>(); + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerLoggedIn(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.appleskin.enable) { + resetPlayerData(player); + } + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.appleskin.enable) { + subscribedChannels.remove(player); + resetPlayerData(player); + } + } + + @ProtocolHandler.MinecraftRegister(ignoreId = true) + public static void onPlayerSubscribed(@NotNull ServerPlayer player, String channel) { + if (LeavesConfig.protocol.appleskin.enable) { + subscribedChannels.computeIfAbsent(player, k -> new HashSet<>()).add(channel); + } + } + + @ProtocolHandler.Ticker + public static void tick() { + if (LeavesConfig.protocol.appleskin.enable) { + if (MinecraftServer.getServer().getTickCount() % LeavesConfig.protocol.appleskin.syncTickInterval != 0) { + return; + } + + for (Map.Entry> entry : subscribedChannels.entrySet()) { + ServerPlayer player = entry.getKey(); + FoodData data = player.getFoodData(); + + for (String channel : entry.getValue()) { + switch (channel) { + case "saturation" -> { + float saturation = data.getSaturationLevel(); + Float previousSaturation = previousSaturationLevels.get(player); + if (previousSaturation == null || saturation != previousSaturation) { + ProtocolUtils.sendPayloadPacket(player, SATURATION_KEY, buf -> buf.writeFloat(saturation)); + previousSaturationLevels.put(player, saturation); + } + } + + case "exhaustion" -> { + float exhaustion = data.exhaustionLevel; + Float previousExhaustion = previousExhaustionLevels.get(player); + if (previousExhaustion == null || Math.abs(exhaustion - previousExhaustion) >= MINIMUM_EXHAUSTION_CHANGE_THRESHOLD) { + ProtocolUtils.sendPayloadPacket(player, EXHAUSTION_KEY, buf -> buf.writeFloat(exhaustion)); + previousExhaustionLevels.put(player, exhaustion); + } + } + + case "natural_regeneration" -> { + boolean regeneration = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_NATURAL_REGENERATION); + Boolean previousRegeneration = previousNaturalRegeneration.get(player); + if (previousRegeneration == null || regeneration != previousRegeneration) { + ProtocolUtils.sendPayloadPacket(player, NATURAL_REGENERATION_KEY, buf -> buf.writeBoolean(regeneration)); + previousNaturalRegeneration.put(player, regeneration); + } + } + } + } + } + } + } + + @ProtocolHandler.ReloadServer + public static void onServerReload() { + if (!LeavesConfig.protocol.appleskin.enable) { + disableAllPlayer(); + } + } + + public static void disableAllPlayer() { + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + onPlayerLoggedOut(player); + } + } + + private static void resetPlayerData(@NotNull ServerPlayer player) { + previousExhaustionLevels.remove(player); + previousSaturationLevels.remove(player); + previousNaturalRegeneration.remove(player); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java new file mode 100644 index 00000000..6da4b4bf --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java @@ -0,0 +1,227 @@ +package org.leavesmc.leaves.protocol; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +import static org.leavesmc.leaves.protocol.core.LeavesProtocolManager.EmptyPayload; + +@LeavesProtocol(namespace = "bbor") +public class BBORProtocol { + + public static final String PROTOCOL_ID = "bbor"; + + // send + private static final ResourceLocation INITIALIZE_CLIENT = id("initialize"); + private static final ResourceLocation ADD_BOUNDING_BOX = id("add_bounding_box_v2"); + private static final ResourceLocation STRUCTURE_LIST_SYNC = id("structure_list_sync_v1"); + // call + private static final Map players = new ConcurrentHashMap<>(); + private static final Map> playerBoundingBoxesCache = new HashMap<>(); + private static final Map>> dimensionCache = new ConcurrentHashMap<>(); + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.Ticker + public static void tick() { + if (LeavesConfig.protocol.bborProtocol) { + for (var playerEntry : players.entrySet()) { + sendBoundingToPlayer(playerEntry.getKey(), playerEntry.getValue()); + } + } + } + + @ProtocolHandler.ReloadServer + public static void onServerReload() { + if (LeavesConfig.protocol.bborProtocol) { + initAllPlayer(); + } else { + loggedOutAllPlayer(); + } + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerLoggedIn(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.bborProtocol) { + ServerLevel overworld = MinecraftServer.getServer().overworld(); + ProtocolUtils.sendPayloadPacket(player, INITIALIZE_CLIENT, buf -> { + buf.writeLong(overworld.getSeed()); + buf.writeInt(overworld.levelData.getSpawnPos().getX()); + buf.writeInt(overworld.levelData.getSpawnPos().getZ()); + }); + sendStructureList(player); + } + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.bborProtocol) { + players.remove(player.getId()); + playerBoundingBoxesCache.remove(player.getId()); + } + } + + @ProtocolHandler.PayloadReceiver(payload = EmptyPayload.class, payloadId = "subscribe") + public static void onPlayerSubscribed(@NotNull ServerPlayer player, EmptyPayload payload) { + if (LeavesConfig.protocol.bborProtocol) { + players.put(player.getId(), player); + sendBoundingToPlayer(player.getId(), player); + } + } + + public static void onDataPackReload() { + if (LeavesConfig.protocol.bborProtocol) { + players.values().forEach(BBORProtocol::sendStructureList); + } + } + + public static void onChunkLoaded(@NotNull LevelChunk chunk) { + if (LeavesConfig.protocol.bborProtocol) { + Map structures = new HashMap<>(); + final Registry structureFeatureRegistry = chunk.getLevel().registryAccess().lookupOrThrow(Registries.STRUCTURE); + for (var es : chunk.getAllStarts().entrySet()) { + final var optional = structureFeatureRegistry.getResourceKey(es.getKey()); + optional.ifPresent(key -> structures.put(key.location().toString(), es.getValue())); + } + if (!structures.isEmpty()) { + onStructuresLoaded(chunk.getLevel().dimension().location(), structures); + } + } + } + + public static void onStructuresLoaded(@NotNull ResourceLocation dimensionID, @NotNull Map structures) { + Map> cache = getOrCreateCache(dimensionID); + for (var entry : structures.entrySet()) { + StructureStart structureStart = entry.getValue(); + if (structureStart == null) { + return; + } + + String type = "structure:" + entry.getKey(); + BoundingBox bb = structureStart.getBoundingBox(); + BBoundingBox boundingBox = buildStructure(bb, type); + if (cache.containsKey(boundingBox)) { + return; + } + + Set structureBoundingBoxes = new HashSet<>(); + for (StructurePiece structureComponent : structureStart.getPieces()) { + structureBoundingBoxes.add(buildStructure(structureComponent.getBoundingBox(), type)); + } + cache.put(boundingBox, structureBoundingBoxes); + } + } + + private static @NotNull BBoundingBox buildStructure(@NotNull BoundingBox bb, String type) { + BlockPos min = new BlockPos(bb.minX(), bb.minY(), bb.minZ()); + BlockPos max = new BlockPos(bb.maxX(), bb.maxY(), bb.maxZ()); + return new BBoundingBox(type, min, max); + } + + private static void sendStructureList(@NotNull ServerPlayer player) { + final Registry structureRegistry = player.server.registryAccess().lookupOrThrow(Registries.STRUCTURE); + final Set structureIds = structureRegistry.entrySet().stream() + .map(e -> e.getKey().location().toString()).collect(Collectors.toSet()); + ProtocolUtils.sendPayloadPacket(player, STRUCTURE_LIST_SYNC, buf -> { + buf.writeVarInt(structureIds.size()); + structureIds.forEach(buf::writeUtf); + }); + } + + private static void sendBoundingToPlayer(int id, ServerPlayer player) { + for (var entry : dimensionCache.entrySet()) { + if (entry.getValue() == null) { + return; + } + + Set playerBoundingBoxes = playerBoundingBoxesCache.computeIfAbsent(id, k -> new HashSet<>()); + Map> boundingBoxMap = entry.getValue(); + for (BBoundingBox key : boundingBoxMap.keySet()) { + if (playerBoundingBoxes.contains(key)) { + continue; + } + + Set boundingBoxes = boundingBoxMap.get(key); + ProtocolUtils.sendPayloadPacket(player, ADD_BOUNDING_BOX, buf -> { + buf.writeResourceLocation(entry.getKey()); + key.serialize(buf); + if (boundingBoxes != null && boundingBoxes.size() > 1) { + for (BBoundingBox box : boundingBoxes) { + box.serialize(buf); + } + } + }); + playerBoundingBoxes.add(key); + } + } + } + + public static void initAllPlayer() { + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + onPlayerLoggedIn(player); + } + } + + public static void loggedOutAllPlayer() { + players.clear(); + playerBoundingBoxesCache.clear(); + for (var cache : dimensionCache.values()) { + cache.clear(); + } + dimensionCache.clear(); + } + + private static Map> getOrCreateCache(ResourceLocation dimensionId) { + return dimensionCache.computeIfAbsent(dimensionId, dt -> new ConcurrentHashMap<>()); + } + + private record BBoundingBox(String type, BlockPos min, BlockPos max) { + + public void serialize(@NotNull FriendlyByteBuf buf) { + buf.writeChar('S'); + buf.writeInt(type.hashCode()); + buf.writeVarInt(min.getX()).writeVarInt(min.getY()).writeVarInt(min.getZ()); + buf.writeVarInt(max.getX()).writeVarInt(max.getY()).writeVarInt(max.getZ()); + } + + @Override + public int hashCode() { + return combineHashCodes(min.hashCode(), max.hashCode()); + } + + private static int combineHashCodes(int @NotNull ... hashCodes) { + final int prime = 31; + int result = 0; + for (int hashCode : hashCodes) { + result = prime * result + hashCode; + } + return result; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/CarpetAlternativeBlockPlacement.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/CarpetAlternativeBlockPlacement.java new file mode 100644 index 00000000..e5c5bee2 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/CarpetAlternativeBlockPlacement.java @@ -0,0 +1,167 @@ +package org.leavesmc.leaves.protocol; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BedBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.ComparatorBlock; +import net.minecraft.world.level.block.DirectionalBlock; +import net.minecraft.world.level.block.DispenserBlock; +import net.minecraft.world.level.block.GlazedTerracottaBlock; +import net.minecraft.world.level.block.ObserverBlock; +import net.minecraft.world.level.block.RepeaterBlock; +import net.minecraft.world.level.block.StairBlock; +import net.minecraft.world.level.block.TrapDoorBlock; +import net.minecraft.world.level.block.piston.PistonBaseBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.ComparatorMode; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.Half; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.block.state.properties.SlabType; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import java.util.Objects; + +public class CarpetAlternativeBlockPlacement { + + @Nullable + public static BlockState alternativeBlockPlacement(@NotNull Block block, @NotNull BlockPlaceContext context) { + Vec3 hitPos = context.getClickLocation(); + BlockPos blockPos = context.getClickedPos(); + double relativeHitX = hitPos.x - blockPos.getX(); + BlockState state = block.getStateForPlacement(context); + Player player = context.getPlayer(); + + if (relativeHitX < 2 || state == null || player == null) { + return null; + } + + EnumProperty directionProp = getFirstDirectionProperty(state); + int protocolValue = ((int) relativeHitX - 2) / 2; + + if (directionProp != null) { + Direction origFacing = state.getValue(directionProp); + Direction facing = origFacing; + int facingIndex = protocolValue & 0xF; + + if (facingIndex == 6) { + facing = facing.getOpposite(); + } else if (facingIndex <= 5) { + facing = Direction.from3DDataValue(facingIndex); + } + + if (!directionProp.getPossibleValues().contains(facing)) { + facing = player.getDirection().getOpposite(); + } + + if (facing != origFacing && directionProp.getPossibleValues().contains(facing)) { + if (state.getBlock() instanceof BedBlock) { + BlockPos headPos = blockPos.relative(facing); + + if (!context.getLevel().getBlockState(headPos).canBeReplaced(context)) { + return null; + } + } + + state = state.setValue(directionProp, facing); + } + } else if (state.hasProperty(BlockStateProperties.AXIS)) { + Direction.Axis axis = Direction.Axis.VALUES[protocolValue % 3]; + state = state.setValue(BlockStateProperties.AXIS, axis); + } + + protocolValue &= 0xFFFFFFF0; + + if (protocolValue >= 16) { + if (block instanceof RepeaterBlock) { + Integer delay = (protocolValue / 16); + + if (RepeaterBlock.DELAY.getPossibleValues().contains(delay)) { + state = state.setValue(RepeaterBlock.DELAY, delay); + } + } else if (protocolValue == 16) { + if (block instanceof ComparatorBlock) { + state = state.setValue(ComparatorBlock.MODE, ComparatorMode.SUBTRACT); + } else if (state.hasProperty(BlockStateProperties.HALF) && state.getValue(BlockStateProperties.HALF) == Half.BOTTOM) { + state = state.setValue(BlockStateProperties.HALF, Half.TOP); + } else if (state.hasProperty(BlockStateProperties.SLAB_TYPE) && state.getValue(BlockStateProperties.SLAB_TYPE) == SlabType.BOTTOM) { + state = state.setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP); + } + } + } + + return state; + } + + @Nullable + public static BlockState alternativeBlockPlacementFix(Block block, @NotNull BlockPlaceContext context) { + Direction facing; + Vec3 vec3d = context.getClickLocation(); + BlockPos pos = context.getClickedPos(); + double hitX = vec3d.x - pos.getX(); + if (hitX < 2) { + return null; + } + int code = (int) (hitX - 2) / 2; + Player placer = Objects.requireNonNull(context.getPlayer()); + Level world = context.getLevel(); + + if (block instanceof GlazedTerracottaBlock) { + facing = Direction.from3DDataValue(code); + if (facing == Direction.UP || facing == Direction.DOWN) { + facing = placer.getDirection().getOpposite(); + } + return block.defaultBlockState().setValue(GlazedTerracottaBlock.FACING, facing); + } else if (block instanceof ObserverBlock) { + return block.defaultBlockState().setValue(ObserverBlock.FACING, Direction.from3DDataValue(code)).setValue(ObserverBlock.POWERED, true); + } else if (block instanceof RepeaterBlock) { + facing = Direction.from3DDataValue(code % 16); + if (facing == Direction.UP || facing == Direction.DOWN) { + facing = placer.getDirection().getOpposite(); + } + return block.defaultBlockState().setValue(RepeaterBlock.FACING, facing).setValue(RepeaterBlock.DELAY, Mth.clamp(code / 16, 1, 4)).setValue(RepeaterBlock.LOCKED, Boolean.FALSE); + } else if (block instanceof TrapDoorBlock) { + facing = Direction.from3DDataValue(code % 16); + if (facing == Direction.UP || facing == Direction.DOWN) { + facing = placer.getDirection().getOpposite(); + } + return block.defaultBlockState().setValue(TrapDoorBlock.FACING, facing).setValue(TrapDoorBlock.OPEN, Boolean.FALSE).setValue(TrapDoorBlock.HALF, (code >= 16) ? Half.TOP : Half.BOTTOM).setValue(TrapDoorBlock.OPEN, world.hasNeighborSignal(pos)); + } else if (block instanceof ComparatorBlock) { + facing = Direction.from3DDataValue(code % 16); + if ((facing == Direction.UP) || (facing == Direction.DOWN)) { + facing = placer.getDirection().getOpposite(); + } + ComparatorMode m = (hitX >= 16) ? ComparatorMode.SUBTRACT : ComparatorMode.COMPARE; + return block.defaultBlockState().setValue(ComparatorBlock.FACING, facing).setValue(ComparatorBlock.POWERED, Boolean.FALSE).setValue(ComparatorBlock.MODE, m); + } else if (block instanceof DispenserBlock) { + return block.defaultBlockState().setValue(DispenserBlock.FACING, Direction.from3DDataValue(code)).setValue(DispenserBlock.TRIGGERED, Boolean.FALSE); + } else if (block instanceof PistonBaseBlock) { + return block.defaultBlockState().setValue(DirectionalBlock.FACING, Direction.from3DDataValue(code)).setValue(PistonBaseBlock.EXTENDED, Boolean.FALSE); + } else if (block instanceof StairBlock) { + return Objects.requireNonNull(block.getStateForPlacement(context)).setValue(StairBlock.FACING, Direction.from3DDataValue(code % 16)).setValue(StairBlock.HALF, (hitX >= 16) ? Half.TOP : Half.BOTTOM); + } + return null; + } + + @SuppressWarnings("unchecked") + @Nullable + public static EnumProperty getFirstDirectionProperty(@NotNull BlockState state) { + for (Property prop : state.getProperties()) { + if (prop instanceof EnumProperty enumProperty) { + if (enumProperty.getValueClass().equals(Direction.class)) { + return (EnumProperty) enumProperty; + } + } + } + + return null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java new file mode 100644 index 00000000..5186d381 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java @@ -0,0 +1,120 @@ +package org.leavesmc.leaves.protocol; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +@LeavesProtocol(namespace = "carpet") +public class CarpetServerProtocol { + + public static final String PROTOCOL_ID = "carpet"; + public static final String VERSION = ProtocolUtils.buildProtocolVersion(PROTOCOL_ID); + + private static final ResourceLocation HELLO_ID = CarpetServerProtocol.id("hello"); + + private static final String HI = "69"; + private static final String HELLO = "420"; + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerJoin(ServerPlayer player) { + if (LeavesConfig.protocol.leavesCarpetSupport) { + CompoundTag data = new CompoundTag(); + data.putString(HI, VERSION); + ProtocolUtils.sendPayloadPacket(player, new CarpetPayload(data)); + } + } + + @ProtocolHandler.PayloadReceiver(payload = CarpetPayload.class, payloadId = "hello") + private static void handleHello(@NotNull ServerPlayer player, @NotNull CarpetServerProtocol.CarpetPayload payload) { + if (LeavesConfig.protocol.leavesCarpetSupport) { + if (payload.nbt.contains(HELLO)) { + LeavesLogger.LOGGER.info("Player " + player.getScoreboardName() + " joined with carpet " + payload.nbt.getString(HELLO)); + CompoundTag data = new CompoundTag(); + CarpetRules.write(data); + ProtocolUtils.sendPayloadPacket(player, new CarpetPayload(data)); + } + } + } + + public static class CarpetRules { + + private static final Map rules = new HashMap<>(); + + public static void write(@NotNull CompoundTag tag) { + CompoundTag rulesNbt = new CompoundTag(); + rules.values().forEach(rule -> rule.writeNBT(rulesNbt)); + + tag.put("Rules", rulesNbt); + } + + public static void register(CarpetRule rule) { + rules.put(rule.name, rule); + } + } + + public record CarpetRule(String identifier, String name, String value) { + + @NotNull + @Contract("_, _, _ -> new") + public static CarpetRule of(String identifier, String name, Enum value) { + return new CarpetRule(identifier, name, value.name().toLowerCase(Locale.ROOT)); + } + + @NotNull + @Contract("_, _, _ -> new") + public static CarpetRule of(String identifier, String name, boolean value) { + return new CarpetRule(identifier, name, Boolean.toString(value)); + } + + public void writeNBT(@NotNull CompoundTag rules) { + CompoundTag rule = new CompoundTag(); + String key = name; + + while (rules.contains(key)) { + key = key + "2"; + } + + rule.putString("Value", value); + rule.putString("Manager", identifier); + rule.putString("Rule", name); + rules.put(key, rule); + } + } + + public record CarpetPayload(CompoundTag nbt) implements LeavesCustomPayload { + + @New + public CarpetPayload(ResourceLocation location, FriendlyByteBuf buf) { + this(buf.readNbt()); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeNbt(nbt); + } + + @Override + @NotNull + public ResourceLocation id() { + return HELLO_ID; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LMSPasterProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LMSPasterProtocol.java new file mode 100644 index 00000000..03c45db2 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LMSPasterProtocol.java @@ -0,0 +1,150 @@ +package org.leavesmc.leaves.protocol; + +import com.google.common.collect.Sets; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.function.Consumer; + +@LeavesProtocol(namespace = "litematica-server-paster") +public class LMSPasterProtocol { + + public static final String MOD_ID = "litematica-server-paster"; + public static final String MOD_VERSION = "1.3.5"; + + private static final ResourceLocation PACKET_ID = ResourceLocation.fromNamespaceAndPath(MOD_ID, "network_v2"); + + private static final Map VERY_LONG_CHATS = new WeakHashMap<>(); + + @ProtocolHandler.PayloadReceiver(payload = LmsPasterPayload.class, payloadId = "network_v2") + public static void handlePackets(ServerPlayer player, LmsPasterPayload payload) { + if (!LeavesConfig.protocol.lmsPasterProtocol) { + return; + } + + String playerName = player.getName().getString(); + int id = payload.getPacketId(); + CompoundTag nbt = payload.getNbt(); + switch (id) { + case LMSPasterProtocol.C2S.HI -> { + String clientModVersion = nbt.getString("mod_version"); + LeavesLogger.LOGGER.info(String.format("Player %s connected with %s @ %s", playerName, LMSPasterProtocol.MOD_ID, clientModVersion)); + ProtocolUtils.sendPayloadPacket(player, LMSPasterProtocol.S2C.build(LMSPasterProtocol.S2C.HI, nbt2 -> nbt2.putString("mod_version", LMSPasterProtocol.MOD_VERSION))); + ProtocolUtils.sendPayloadPacket(player, LMSPasterProtocol.S2C.build(LMSPasterProtocol.S2C.ACCEPT_PACKETS, nbt2 -> nbt2.putIntArray("ids", C2S.ALL_PACKET_IDS))); + } + case LMSPasterProtocol.C2S.CHAT -> { + String message = nbt.getString("chat"); + triggerCommand(player, playerName, message); + } + case LMSPasterProtocol.C2S.VERY_LONG_CHAT_START -> VERY_LONG_CHATS.put(player.connection, new StringBuilder()); + case LMSPasterProtocol.C2S.VERY_LONG_CHAT_CONTENT -> { + String segment = nbt.getString("segment"); + getVeryLongChatBuilder(player).ifPresent(builder -> builder.append(segment)); + } + case LMSPasterProtocol.C2S.VERY_LONG_CHAT_END -> { + getVeryLongChatBuilder(player).ifPresent(builder -> triggerCommand(player, playerName, builder.toString())); + VERY_LONG_CHATS.remove(player.connection); + } + } + } + + private static Optional getVeryLongChatBuilder(ServerPlayer player) { + return Optional.ofNullable(VERY_LONG_CHATS.get(player.connection)); + } + + private static void triggerCommand(ServerPlayer player, String playerName, String command) { + if (command.isEmpty()) { + LeavesLogger.LOGGER.warning(String.format("Player %s sent an empty command", playerName)); + } else { + player.getBukkitEntity().performCommand(command); + } + } + + private static class C2S { + public static final int HI = 0; + public static final int CHAT = 1; + public static final int VERY_LONG_CHAT_START = 2; + public static final int VERY_LONG_CHAT_CONTENT = 3; + public static final int VERY_LONG_CHAT_END = 4; + + public static final int[] ALL_PACKET_IDS; + + static { + Set allPacketIds = Sets.newLinkedHashSet(); + for (Field field : C2S.class.getFields()) { + if (field.getType() == int.class && Modifier.isStatic(field.getModifiers()) && Modifier.isFinal(field.getModifiers())) { + try { + allPacketIds.add((int) field.get(null)); + } catch (Exception e) { + LeavesLogger.LOGGER.severe("Failed to initialized Lms Paster: ", e); + } + } + } + ALL_PACKET_IDS = new int[allPacketIds.size()]; + int i = 0; + for (Integer id : allPacketIds) { + ALL_PACKET_IDS[i++] = id; + } + } + } + + private static class S2C { + public static final int HI = 0; + public static final int ACCEPT_PACKETS = 1; + + public static CustomPacketPayload build(int packetId, Consumer payloadBuilder) { + CompoundTag nbt = new CompoundTag(); + payloadBuilder.accept(nbt); + return new LmsPasterPayload(packetId, nbt); + } + } + + public static class LmsPasterPayload implements LeavesCustomPayload { + private final int id; + private final CompoundTag nbt; + + public LmsPasterPayload(int id, CompoundTag nbt) { + this.id = id; + this.nbt = nbt; + } + + @New + public LmsPasterPayload(ResourceLocation location, FriendlyByteBuf buf) { + this(buf.readVarInt(), buf.readNbt()); + } + + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.id); + buf.writeNbt(this.nbt); + } + + @Override + public ResourceLocation id() { + return PACKET_ID; + } + + public int getPacketId() { + return this.id; + } + + public CompoundTag getNbt() { + return this.nbt; + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java new file mode 100644 index 00000000..eceb1b83 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java @@ -0,0 +1,214 @@ +package org.leavesmc.leaves.protocol; + +import com.google.common.collect.ImmutableSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.util.Mth; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BedBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.level.block.state.properties.EnumProperty; +import net.minecraft.world.level.block.state.properties.Property; +import net.minecraft.world.level.block.state.properties.SlabType; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesLogger; + +import javax.annotation.Nullable; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +public class LitematicaEasyPlaceProtocol { + + public static final ImmutableSet> WHITELISTED_PROPERTIES = ImmutableSet.of( + BlockStateProperties.INVERTED, + BlockStateProperties.OPEN, + BlockStateProperties.BELL_ATTACHMENT, + BlockStateProperties.AXIS, + BlockStateProperties.BED_PART, + BlockStateProperties.HALF, + BlockStateProperties.ATTACH_FACE, + BlockStateProperties.CHEST_TYPE, + BlockStateProperties.MODE_COMPARATOR, + BlockStateProperties.DOOR_HINGE, + BlockStateProperties.FACING_HOPPER, + BlockStateProperties.HORIZONTAL_FACING, + BlockStateProperties.ORIENTATION, + BlockStateProperties.RAIL_SHAPE, + BlockStateProperties.RAIL_SHAPE_STRAIGHT, + BlockStateProperties.SLAB_TYPE, + BlockStateProperties.STAIRS_SHAPE, + BlockStateProperties.BITES, + BlockStateProperties.DELAY, + BlockStateProperties.NOTE, + BlockStateProperties.ROTATION_16 + ); + + public static BlockState applyPlacementProtocol(BlockState state, BlockPlaceContext context) { + return applyPlacementProtocolV3(state, UseContext.from(context, context.getHand())); + } + + @Nullable + private static > BlockState applyPlacementProtocolV3(BlockState state, @NotNull UseContext context) { + int protocolValue = (int) (context.getHitVec().x - (double) context.getPos().getX()) - 2; + BlockState oldState = state; + if (protocolValue < 0) { + return oldState; + } + + EnumProperty property = CarpetAlternativeBlockPlacement.getFirstDirectionProperty(state); + + if (property != null && property != BlockStateProperties.VERTICAL_DIRECTION) { + state = applyDirectionProperty(state, context, property, protocolValue); + + if (state == null) { + return null; + } + + if (state.canSurvive(context.getWorld(), context.getPos())) { + oldState = state; + } else { + state = oldState; + } + + protocolValue >>>= 3; + } + + protocolValue >>>= 1; + + List> propList = new ArrayList<>(state.getBlock().getStateDefinition().getProperties()); + propList.sort(Comparator.comparing(Property::getName)); + + try { + for (Property p : propList) { + if (((p instanceof EnumProperty ep) && !ep.getValueClass().equals(Direction.class)) && WHITELISTED_PROPERTIES.contains(p)) { + @SuppressWarnings("unchecked") + Property prop = (Property) p; + List list = new ArrayList<>(prop.getPossibleValues()); + list.sort(Comparable::compareTo); + + int requiredBits = Mth.log2(Mth.smallestEncompassingPowerOfTwo(list.size())); + int bitMask = ~(0xFFFFFFFF << requiredBits); + int valueIndex = protocolValue & bitMask; + + if (valueIndex < list.size()) { + T value = list.get(valueIndex); + + if (!state.getValue(prop).equals(value) && value != SlabType.DOUBLE) { + state = state.setValue(prop, value); + + if (state.canSurvive(context.getWorld(), context.getPos())) { + oldState = state; + } else { + state = oldState; + } + } + + protocolValue >>>= requiredBits; + } + } + } + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Exception trying to apply placement protocol value", e); + } + + if (state.canSurvive(context.getWorld(), context.getPos())) { + return state; + } else { + return null; + } + } + + private static BlockState applyDirectionProperty(BlockState state, UseContext context, EnumProperty property, int protocolValue) { + Direction facingOrig = state.getValue(property); + Direction facing = facingOrig; + int decodedFacingIndex = (protocolValue & 0xF) >> 1; + + if (decodedFacingIndex == 6) { + facing = facing.getOpposite(); + } else if (decodedFacingIndex <= 5) { + facing = Direction.from3DDataValue(decodedFacingIndex); + + if (!property.getPossibleValues().contains(facing)) { + facing = context.getEntity().getDirection().getOpposite(); + } + } + + if (facing != facingOrig && property.getPossibleValues().contains(facing)) { + if (state.getBlock() instanceof BedBlock) { + BlockPos headPos = context.pos.relative(facing); + BlockPlaceContext ctx = context.getItemPlacementContext(); + + if (ctx == null || !context.getWorld().getBlockState(headPos).canBeReplaced(ctx)) { + return null; + } + } + + state = state.setValue(property, facing); + } + + return state; + } + + public static class UseContext { + + private final Level world; + private final BlockPos pos; + private final Direction side; + private final Vec3 hitVec; + private final LivingEntity entity; + private final InteractionHand hand; + @Nullable + private final BlockPlaceContext itemPlacementContext; + + private UseContext(Level world, BlockPos pos, Direction side, Vec3 hitVec, LivingEntity entity, InteractionHand hand, @Nullable BlockPlaceContext itemPlacementContext) { + this.world = world; + this.pos = pos; + this.side = side; + this.hitVec = hitVec; + this.entity = entity; + this.hand = hand; + this.itemPlacementContext = itemPlacementContext; + } + + @NotNull + public static UseContext from(@NotNull BlockPlaceContext ctx, InteractionHand hand) { + Vec3 pos = ctx.getClickLocation(); + return new UseContext(ctx.getLevel(), ctx.getClickedPos(), ctx.getClickedFace(), new Vec3(pos.x, pos.y, pos.z), ctx.getPlayer(), hand, ctx); + } + + public Level getWorld() { + return this.world; + } + + public BlockPos getPos() { + return this.pos; + } + + public Direction getSide() { + return this.side; + } + + public Vec3 getHitVec() { + return this.hitVec; + } + + public LivingEntity getEntity() { + return this.entity; + } + + public InteractionHand getHand() { + return this.hand; + } + + @Nullable + public BlockPlaceContext getItemPlacementContext() { + return this.itemPlacementContext; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java new file mode 100644 index 00000000..b7cd2c86 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java @@ -0,0 +1,432 @@ +package org.leavesmc.leaves.protocol; + +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.ChestType; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.MutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.bot.ServerBot; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +import static org.leavesmc.leaves.protocol.core.LeavesProtocolManager.EmptyPayload; + +@LeavesProtocol(namespace = "pca") +public class PcaSyncProtocol { + + public static final String PROTOCOL_ID = "pca"; + + public static final ReentrantLock lock = new ReentrantLock(true); + public static final ReentrantLock pairLock = new ReentrantLock(true); + + // send + private static final ResourceLocation ENABLE_PCA_SYNC_PROTOCOL = id("enable_pca_sync_protocol"); + private static final ResourceLocation DISABLE_PCA_SYNC_PROTOCOL = id("disable_pca_sync_protocol"); + + private static final Map> playerWatchBlockPos = new HashMap<>(); + private static final Map> playerWatchEntity = new HashMap<>(); + private static final Map, Set> blockPosWatchPlayerSet = new HashMap<>(); + private static final Map, Set> entityWatchPlayerSet = new HashMap<>(); + private static final MutablePair ResourceLocationEntityPair = new MutablePair<>(); + private static final MutablePair ResourceLocationBlockPosPair = new MutablePair<>(); + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.PlayerJoin + private static void onJoin(ServerPlayer player) { + if (LeavesConfig.protocol.pca.enable) { + enablePcaSyncProtocol(player); + } + } + + @ProtocolHandler.PayloadReceiver(payload = EmptyPayload.class, payloadId = "cancel_sync_block_entity") + private static void cancelSyncBlockEntityHandler(ServerPlayer player, EmptyPayload payload) { + if (!LeavesConfig.protocol.pca.enable) { + return; + } + PcaSyncProtocol.clearPlayerWatchBlock(player); + } + + @ProtocolHandler.PayloadReceiver(payload = EmptyPayload.class, payloadId = "cancel_sync_entity") + private static void cancelSyncEntityHandler(ServerPlayer player, EmptyPayload payload) { + if (!LeavesConfig.protocol.pca.enable) { + return; + } + PcaSyncProtocol.clearPlayerWatchEntity(player); + } + + @ProtocolHandler.PayloadReceiver(payload = SyncBlockEntityPayload.class, payloadId = "sync_block_entity") + private static void syncBlockEntityHandler(ServerPlayer player, SyncBlockEntityPayload payload) { + if (!LeavesConfig.protocol.pca.enable) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + BlockPos pos = payload.pos; + ServerLevel world = player.serverLevel(); + + server.execute(() -> { + BlockState blockState = world.getBlockState(pos); + clearPlayerWatchData(player); + + BlockEntity blockEntityAdj = null; + if (blockState.getBlock() instanceof ChestBlock) { + if (blockState.getValue(ChestBlock.TYPE) != ChestType.SINGLE) { + BlockPos posAdj = pos.relative(ChestBlock.getConnectedDirection(blockState)); + // The method in World now checks that the caller is from the same thread... + blockEntityAdj = world.getChunk(posAdj).getBlockEntity(posAdj); + } + } + + if (blockEntityAdj != null) { + updateBlockEntity(player, blockEntityAdj); + } + + // The method in World now checks that the caller is from the same thread... + BlockEntity blockEntity = world.getChunk(pos).getBlockEntity(pos); + if (blockEntity != null) { + updateBlockEntity(player, blockEntity); + } + + Pair pair = new ImmutablePair<>(player.level().dimension().location(), pos); + lock.lock(); + playerWatchBlockPos.put(player, pair); + if (!blockPosWatchPlayerSet.containsKey(pair)) { + blockPosWatchPlayerSet.put(pair, new HashSet<>()); + } + blockPosWatchPlayerSet.get(pair).add(player); + lock.unlock(); + }); + } + + @ProtocolHandler.PayloadReceiver(payload = SyncEntityPayload.class, payloadId = "sync_entity") + private static void syncEntityHandler(ServerPlayer player, SyncEntityPayload payload) { + if (!LeavesConfig.protocol.pca.enable) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + int entityId = payload.entityId; + ServerLevel world = player.serverLevel(); + + server.execute(() -> { + Entity entity = world.getEntity(entityId); + + if (entity != null) { + clearPlayerWatchData(player); + + if (entity instanceof Player) { + switch (LeavesConfig.protocol.pca.syncPlayerEntity) { + case NOBODY -> { + return; + } + case BOT -> { + if (!(entity instanceof ServerBot)) { + return; + } + } + case OPS -> { + if (!(entity instanceof ServerBot) && server.getPlayerList().isOp(player.gameProfile)) { + return; + } + } + case OPS_AND_SELF -> { + if (!(entity instanceof ServerBot) && server.getPlayerList().isOp(player.gameProfile) && entity != player) { + return; + } + } + case EVERYONE -> {} + case null -> LeavesLogger.LOGGER.warning("pcaSyncPlayerEntity wtf???"); + } + } + updateEntity(player, entity); + + Pair pair = new ImmutablePair<>(entity.level().dimension().location(), entity); + lock.lock(); + playerWatchEntity.put(player, pair); + if (!entityWatchPlayerSet.containsKey(pair)) { + entityWatchPlayerSet.put(pair, new HashSet<>()); + } + entityWatchPlayerSet.get(pair).add(player); + lock.unlock(); + } + }); + } + + public static void onConfigModify(boolean enable) { + if (enable) { + enablePcaSyncProtocolGlobal(); + } else { + disablePcaSyncProtocolGlobal(); + } + } + + public static void enablePcaSyncProtocol(@NotNull ServerPlayer player) { + ProtocolUtils.sendEmptyPayloadPacket(player, ENABLE_PCA_SYNC_PROTOCOL); + lock.lock(); + lock.unlock(); + } + + public static void disablePcaSyncProtocol(@NotNull ServerPlayer player) { + ProtocolUtils.sendEmptyPayloadPacket(player, DISABLE_PCA_SYNC_PROTOCOL); + } + + public static void updateEntity(@NotNull ServerPlayer player, @NotNull Entity entity) { + CompoundTag nbt = entity.saveWithoutId(new CompoundTag()); + ProtocolUtils.sendPayloadPacket(player, new UpdateEntityPayload(entity.level().dimension().location(), entity.getId(), nbt)); + } + + public static void updateBlockEntity(@NotNull ServerPlayer player, @NotNull BlockEntity blockEntity) { + Level world = blockEntity.getLevel(); + + if (world == null) { + return; + } + + ProtocolUtils.sendPayloadPacket(player, new UpdateBlockEntityPayload(world.dimension().location(), blockEntity.getBlockPos(), blockEntity.saveWithoutMetadata(world.registryAccess()))); + } + + private static MutablePair getResourceLocationEntityPair(ResourceLocation ResourceLocation, Entity entity) { + pairLock.lock(); + ResourceLocationEntityPair.setLeft(ResourceLocation); + ResourceLocationEntityPair.setRight(entity); + pairLock.unlock(); + return ResourceLocationEntityPair; + } + + private static MutablePair getResourceLocationBlockPosPair(ResourceLocation ResourceLocation, BlockPos pos) { + pairLock.lock(); + ResourceLocationBlockPosPair.setLeft(ResourceLocation); + ResourceLocationBlockPosPair.setRight(pos); + pairLock.unlock(); + return ResourceLocationBlockPosPair; + } + + private static @Nullable Set getWatchPlayerList(@NotNull Entity entity) { + return entityWatchPlayerSet.get(getResourceLocationEntityPair(entity.level().dimension().location(), entity)); + } + + private static @Nullable Set getWatchPlayerList(@NotNull Level world, @NotNull BlockPos blockPos) { + return blockPosWatchPlayerSet.get(getResourceLocationBlockPosPair(world.dimension().location(), blockPos)); + } + + public static boolean syncEntityToClient(@NotNull Entity entity) { + if (entity.level().isClientSide()) { + return false; + } + lock.lock(); + Set playerList = getWatchPlayerList(entity); + boolean ret = false; + if (playerList != null) { + for (ServerPlayer player : playerList) { + updateEntity(player, entity); + ret = true; + } + } + lock.unlock(); + return ret; + } + + public static boolean syncBlockEntityToClient(@NotNull BlockEntity blockEntity) { + boolean ret = false; + Level world = blockEntity.getLevel(); + BlockPos pos = blockEntity.getBlockPos(); + if (world != null) { + if (world.isClientSide()) { + return false; + } + BlockState blockState = world.getBlockState(pos); + lock.lock(); + Set playerList = getWatchPlayerList(world, blockEntity.getBlockPos()); + + Set playerListAdj = null; + + if (blockState.getBlock() instanceof ChestBlock) { + if (blockState.getValue(ChestBlock.TYPE) != ChestType.SINGLE) { + BlockPos posAdj = pos.relative(ChestBlock.getConnectedDirection(blockState)); + playerListAdj = getWatchPlayerList(world, posAdj); + } + } + if (playerListAdj != null) { + if (playerList == null) { + playerList = playerListAdj; + } else { + playerList.addAll(playerListAdj); + } + } + + if (playerList != null) { + for (ServerPlayer player : playerList) { + updateBlockEntity(player, blockEntity); + ret = true; + } + } + lock.unlock(); + } + return ret; + } + + private static void clearPlayerWatchEntity(ServerPlayer player) { + lock.lock(); + Pair pair = playerWatchEntity.get(player); + if (pair != null) { + Set playerSet = entityWatchPlayerSet.get(pair); + playerSet.remove(player); + if (playerSet.isEmpty()) { + entityWatchPlayerSet.remove(pair); + } + playerWatchEntity.remove(player); + } + lock.unlock(); + } + + private static void clearPlayerWatchBlock(ServerPlayer player) { + lock.lock(); + Pair pair = playerWatchBlockPos.get(player); + if (pair != null) { + Set playerSet = blockPosWatchPlayerSet.get(pair); + playerSet.remove(player); + if (playerSet.isEmpty()) { + blockPosWatchPlayerSet.remove(pair); + } + playerWatchBlockPos.remove(player); + } + lock.unlock(); + } + + public static void disablePcaSyncProtocolGlobal() { + lock.lock(); + playerWatchBlockPos.clear(); + playerWatchEntity.clear(); + blockPosWatchPlayerSet.clear(); + entityWatchPlayerSet.clear(); + lock.unlock(); + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + disablePcaSyncProtocol(player); + } + } + + public static void enablePcaSyncProtocolGlobal() { + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + enablePcaSyncProtocol(player); + } + } + + + public static void clearPlayerWatchData(ServerPlayer player) { + PcaSyncProtocol.clearPlayerWatchBlock(player); + PcaSyncProtocol.clearPlayerWatchEntity(player); + } + + public record UpdateEntityPayload(ResourceLocation dimension, int entityId, CompoundTag tag) implements LeavesCustomPayload { + + public static final ResourceLocation UPDATE_ENTITY = PcaSyncProtocol.id("update_entity"); + + @New + public UpdateEntityPayload(ResourceLocation location, FriendlyByteBuf byteBuf) { + this(byteBuf.readResourceLocation(), byteBuf.readInt(), byteBuf.readNbt()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeResourceLocation(this.dimension); + buf.writeInt(this.entityId); + buf.writeNbt(this.tag); + } + + @Override + public ResourceLocation id() { + return UPDATE_ENTITY; + } + } + + public record UpdateBlockEntityPayload(ResourceLocation dimension, BlockPos blockPos, CompoundTag tag) implements LeavesCustomPayload { + + private static final ResourceLocation UPDATE_BLOCK_ENTITY = PcaSyncProtocol.id("update_block_entity"); + + @New + public UpdateBlockEntityPayload(ResourceLocation location, @NotNull FriendlyByteBuf byteBuf) { + this(byteBuf.readResourceLocation(), byteBuf.readBlockPos(), byteBuf.readNbt()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeResourceLocation(this.dimension); + buf.writeBlockPos(this.blockPos); + buf.writeNbt(this.tag); + } + + @Override + public ResourceLocation id() { + return UPDATE_BLOCK_ENTITY; + } + } + + public record SyncBlockEntityPayload(BlockPos pos) implements LeavesCustomPayload { + + public static final ResourceLocation SYNC_BLOCK_ENTITY = PcaSyncProtocol.id("sync_block_entity"); + + @New + public SyncBlockEntityPayload(ResourceLocation id, @NotNull FriendlyByteBuf buf) { + this(buf.readBlockPos()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeBlockPos(pos); + } + + @Override + public @NotNull ResourceLocation id() { + return SYNC_BLOCK_ENTITY; + } + } + + public record SyncEntityPayload(int entityId) implements LeavesCustomPayload { + + public static final ResourceLocation SYNC_ENTITY = PcaSyncProtocol.id("sync_entity"); + + @New + public SyncEntityPayload(ResourceLocation id, @NotNull FriendlyByteBuf buf) { + this(buf.readInt()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeInt(entityId); + } + + @Override + public @NotNull ResourceLocation id() { + return SYNC_ENTITY; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java new file mode 100644 index 00000000..444c17df --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java @@ -0,0 +1,42 @@ +package org.leavesmc.leaves.protocol; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +@LeavesProtocol(namespace = {"xaerominimap", "xaeroworldmap"}) +public class XaeroMapProtocol { + + public static final String PROTOCOL_ID_MINI = "xaerominimap"; + public static final String PROTOCOL_ID_WORLD = "xaeroworldmap"; + + private static final ResourceLocation MINIMAP_KEY = idMini("main"); + private static final ResourceLocation WORLDMAP_KEY = idWorld("main"); + + @Contract("_ -> new") + public static ResourceLocation idMini(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID_MINI, path); + } + + @Contract("_ -> new") + public static ResourceLocation idWorld(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID_WORLD, path); + } + + public static void onSendWorldInfo(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.xaeroMapProtocol) { + ProtocolUtils.sendPayloadPacket(player, MINIMAP_KEY, buf -> { + buf.writeByte(0); + buf.writeInt(LeavesConfig.protocol.xaeroMapServerID); + }); + ProtocolUtils.sendPayloadPacket(player, WORLDMAP_KEY, buf -> { + buf.writeByte(0); + buf.writeInt(LeavesConfig.protocol.xaeroMapServerID); + }); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/bladeren/BladerenProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/bladeren/BladerenProtocol.java new file mode 100644 index 00000000..84b65fe5 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/bladeren/BladerenProtocol.java @@ -0,0 +1,150 @@ +package org.leavesmc.leaves.protocol.bladeren; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; + +@LeavesProtocol(namespace = "bladeren") +public class BladerenProtocol { + + public static final String PROTOCOL_ID = "bladeren"; + public static final String PROTOCOL_VERSION = ProtocolUtils.buildProtocolVersion(PROTOCOL_ID); + + private static final ResourceLocation HELLO_ID = id("hello"); + private static final ResourceLocation FEATURE_MODIFY_ID = id("feature_modify"); + + private static final Map> registeredFeatures = new HashMap<>(); + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.PayloadReceiver(payload = BladerenHelloPayload.class, payloadId = "hello") + private static void handleHello(@NotNull ServerPlayer player, @NotNull BladerenHelloPayload payload) { + if (LeavesConfig.protocol.bladeren.enable) { + String clientVersion = payload.version; + CompoundTag tag = payload.nbt; + + LeavesLogger.LOGGER.info("Player " + player.getScoreboardName() + " joined with bladeren " + clientVersion); + + if (tag != null) { + CompoundTag featureNbt = tag.getCompound("Features"); + for (String name : featureNbt.getAllKeys()) { + if (registeredFeatures.containsKey(name)) { + registeredFeatures.get(name).accept(player, featureNbt.getCompound(name)); + } + } + } + } + } + + @ProtocolHandler.PayloadReceiver(payload = BladerenFeatureModifyPayload.class, payloadId = "feature_modify") + private static void handleModify(@NotNull ServerPlayer player, @NotNull BladerenFeatureModifyPayload payload) { + if (LeavesConfig.protocol.bladeren.enable) { + String name = payload.name; + CompoundTag tag = payload.nbt; + + if (registeredFeatures.containsKey(name)) { + registeredFeatures.get(name).accept(player, tag); + } + } + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerJoin(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.bladeren.enable) { + CompoundTag tag = new CompoundTag(); + LeavesFeatureSet.writeNBT(tag); + ProtocolUtils.sendPayloadPacket(player, new BladerenHelloPayload(PROTOCOL_VERSION, tag)); + } + } + + public static void registerFeature(String name, BiConsumer consumer) { + registeredFeatures.put(name, consumer); + } + + public static class LeavesFeatureSet { + + private static final Map features = new HashMap<>(); + + public static void writeNBT(@NotNull CompoundTag tag) { + CompoundTag featureNbt = new CompoundTag(); + features.values().forEach(feature -> feature.writeNBT(featureNbt)); + tag.put("Features", featureNbt); + } + + public static void register(LeavesFeature feature) { + features.put(feature.name, feature); + } + } + + public record LeavesFeature(String name, String value) { + + @NotNull + @Contract("_, _ -> new") + public static LeavesFeature of(String name, boolean value) { + return new LeavesFeature(name, Boolean.toString(value)); + } + + public void writeNBT(@NotNull CompoundTag rules) { + CompoundTag rule = new CompoundTag(); + rule.putString("Feature", name); + rule.putString("Value", value); + rules.put(name, rule); + } + } + + public record BladerenFeatureModifyPayload(String name, CompoundTag nbt) implements LeavesCustomPayload { + + @New + public BladerenFeatureModifyPayload(ResourceLocation location, FriendlyByteBuf buf) { + this(buf.readUtf(), buf.readNbt()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeUtf(name); + buf.writeNbt(nbt); + } + + @Override + @NotNull + public ResourceLocation id() { + return FEATURE_MODIFY_ID; + } + } + + public record BladerenHelloPayload(String version, CompoundTag nbt) implements LeavesCustomPayload { + + @New + public BladerenHelloPayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { + this(buf.readUtf(64), buf.readNbt()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeUtf(version); + buf.writeNbt(nbt); + } + + @Override + @NotNull + public ResourceLocation id() { + return HELLO_ID; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/bladeren/MsptSyncProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/bladeren/MsptSyncProtocol.java new file mode 100644 index 00000000..347b7d9c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/bladeren/MsptSyncProtocol.java @@ -0,0 +1,77 @@ +package org.leavesmc.leaves.protocol.bladeren; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.OptionalDouble; + +@LeavesProtocol(namespace = "bladeren") +public class MsptSyncProtocol { + + public static final String PROTOCOL_ID = "bladeren"; + + private static final ResourceLocation MSPT_SYNC = id("mspt_sync"); + + private static final List players = new ArrayList<>(); + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.Init + public static void init() { + BladerenProtocol.registerFeature("mspt_sync", (player, compoundTag) -> { + if (compoundTag.getString("Value").equals("true")) { + onPlayerSubmit(player); + } else { + onPlayerLoggedOut(player); + } + }); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.bladeren.msptSyncProtocol) { + players.remove(player); + } + } + + @ProtocolHandler.Ticker + public static void tick() { + if (LeavesConfig.protocol.bladeren.msptSyncProtocol) { + if (players.isEmpty()) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + if (server.getTickCount() % LeavesConfig.protocol.bladeren.msptSyncTickInterval == 0) { + OptionalDouble msptArr = Arrays.stream(server.getTickTimesNanos()).average(); + if (msptArr.isPresent()) { + double mspt = msptArr.getAsDouble() * 1.0E-6D; + double tps = 1000.0D / Math.max(mspt, 50); + players.forEach(player -> ProtocolUtils.sendPayloadPacket(player, MSPT_SYNC, buf -> { + buf.writeDouble(mspt); + buf.writeDouble(tps); + })); + } + } + } + } + + public static void onPlayerSubmit(@NotNull ServerPlayer player) { + if (LeavesConfig.protocol.bladeren.msptSyncProtocol) { + players.add(player); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java new file mode 100644 index 00000000..b09a7bfe --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java @@ -0,0 +1,29 @@ +package org.leavesmc.leaves.protocol.core; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public interface LeavesCustomPayload> extends CustomPacketPayload { + + @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) + @Retention(RetentionPolicy.RUNTIME) + @interface New { + } + + void write(FriendlyByteBuf buf); + + ResourceLocation id(); + + @Override + @NotNull + default Type type() { + return new CustomPacketPayload.Type<>(id()); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java new file mode 100644 index 00000000..986d2a66 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java @@ -0,0 +1,12 @@ +package org.leavesmc.leaves.protocol.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface LeavesProtocol { + String[] namespace(); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java new file mode 100644 index 00000000..4a3f9d3e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java @@ -0,0 +1,432 @@ +package org.leavesmc.leaves.protocol.core; + +import com.google.common.collect.ImmutableSet; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.apache.commons.lang.ArrayUtils; +import org.bukkit.event.player.PlayerKickEvent; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesLogger; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class LeavesProtocolManager { + + private static final Class[] PAYLOAD_PARAMETER_TYPES = {ResourceLocation.class, FriendlyByteBuf.class}; + + private static final LeavesLogger LOGGER = LeavesLogger.LOGGER; + + private static final Map> KNOWN_TYPES = new HashMap<>(); + private static final Map> KNOW_RECEIVERS = new HashMap<>(); + private static Set ALL_KNOWN_ID = new HashSet<>(); + + private static final List TICKERS = new ArrayList<>(); + private static final List PLAYER_JOIN = new ArrayList<>(); + private static final List PLAYER_LEAVE = new ArrayList<>(); + private static final List RELOAD_SERVER = new ArrayList<>(); + private static final Map> MINECRAFT_REGISTER = new HashMap<>(); + + public static void init() { + for (Class clazz : getClasses("org.leavesmc.leaves.protocol")) { + final LeavesProtocol protocol = clazz.getAnnotation(LeavesProtocol.class); + if (protocol != null) { + Set methods; + try { + Method[] publicMethods = clazz.getMethods(); + Method[] privateMethods = clazz.getDeclaredMethods(); + methods = new HashSet<>(publicMethods.length + privateMethods.length, 1.0f); + Collections.addAll(methods, publicMethods); + Collections.addAll(methods, privateMethods); + } catch (NoClassDefFoundError error) { + LOGGER.severe("Failed to load class " + clazz.getName() + " due to missing dependencies, " + error.getCause() + ": " + error.getMessage()); + return; + } + + Map map = KNOWN_TYPES.getOrDefault(protocol, new HashMap<>()); + for (final Method method : methods) { + if (method.isBridge() || method.isSynthetic() || !Modifier.isStatic(method.getModifiers())) { + continue; + } + + method.setAccessible(true); + + final ProtocolHandler.Init init = method.getAnnotation(ProtocolHandler.Init.class); + if (init != null) { + try { + method.invoke(null); + } catch (InvocationTargetException | IllegalAccessException exception) { + LOGGER.severe("Failed to invoke init method " + method.getName() + " in " + clazz.getName() + ", " + exception.getCause() + ": " + exception.getMessage()); + } + continue; + } + + final ProtocolHandler.PayloadReceiver receiver = method.getAnnotation(ProtocolHandler.PayloadReceiver.class); + if (receiver != null) { + try { + boolean found = false; + for (Method payloadMethod : receiver.payload().getDeclaredMethods()) { + if (payloadMethod.isAnnotationPresent(LeavesCustomPayload.New.class)) { + if (Arrays.equals(payloadMethod.getParameterTypes(), PAYLOAD_PARAMETER_TYPES) && payloadMethod.getReturnType() == receiver.payload() && Modifier.isStatic(payloadMethod.getModifiers())) { + payloadMethod.setAccessible(true); + map.put(receiver, payloadMethod); + found = true; + break; + } + } + } + + if (!found) { + Constructor> constructor = receiver.payload().getConstructor(PAYLOAD_PARAMETER_TYPES); + if (constructor.isAnnotationPresent(LeavesCustomPayload.New.class)) { + constructor.setAccessible(true); + map.put(receiver, constructor); + } else { + throw new NoSuchMethodException(); + } + } + } catch (NoSuchMethodException exception) { + LOGGER.severe("Failed to find constructor for " + receiver.payload().getName() + ", " + exception.getCause() + ": " + exception.getMessage()); + continue; + } + + if (!KNOW_RECEIVERS.containsKey(protocol)) { + KNOW_RECEIVERS.put(protocol, new HashMap<>()); + } + + KNOW_RECEIVERS.get(protocol).put(receiver, method); + continue; + } + + final ProtocolHandler.Ticker ticker = method.getAnnotation(ProtocolHandler.Ticker.class); + if (ticker != null) { + TICKERS.add(method); + continue; + } + + final ProtocolHandler.PlayerJoin playerJoin = method.getAnnotation(ProtocolHandler.PlayerJoin.class); + if (playerJoin != null) { + PLAYER_JOIN.add(method); + continue; + } + + final ProtocolHandler.PlayerLeave playerLeave = method.getAnnotation(ProtocolHandler.PlayerLeave.class); + if (playerLeave != null) { + PLAYER_LEAVE.add(method); + continue; + } + + final ProtocolHandler.ReloadServer reloadServer = method.getAnnotation(ProtocolHandler.ReloadServer.class); + if (reloadServer != null) { + RELOAD_SERVER.add(method); + continue; + } + + final ProtocolHandler.MinecraftRegister minecraftRegister = method.getAnnotation(ProtocolHandler.MinecraftRegister.class); + if (minecraftRegister != null) { + if (!MINECRAFT_REGISTER.containsKey(protocol)) { + MINECRAFT_REGISTER.put(protocol, new HashMap<>()); + } + + MINECRAFT_REGISTER.get(protocol).put(minecraftRegister, method); + } + } + KNOWN_TYPES.put(protocol, map); + } + } + + for (LeavesProtocol protocol : KNOWN_TYPES.keySet()) { + Map map = KNOWN_TYPES.get(protocol); + for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) { + if (receiver.sendFabricRegister() && !receiver.ignoreId()) { + for (String payloadId : receiver.payloadId()) { + for (String namespace : protocol.namespace()) { + ALL_KNOWN_ID.add(ResourceLocation.tryBuild(namespace, payloadId)); + } + } + } + } + } + ALL_KNOWN_ID = ImmutableSet.copyOf(ALL_KNOWN_ID); + } + + public static LeavesCustomPayload decode(ResourceLocation id, FriendlyByteBuf buf) { + for (LeavesProtocol protocol : KNOWN_TYPES.keySet()) { + if (!ArrayUtils.contains(protocol.namespace(), id.getNamespace())) { + continue; + } + + Map map = KNOWN_TYPES.get(protocol); + for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) { + if (receiver.ignoreId() || ArrayUtils.contains(receiver.payloadId(), id.getPath())) { + try { + if (map.get(receiver) instanceof Constructor constructor) { + return (LeavesCustomPayload) constructor.newInstance(id, buf); + } else if (map.get(receiver) instanceof Method method) { + return (LeavesCustomPayload) method.invoke(null, id, buf); + } + } catch (InvocationTargetException | InstantiationException | IllegalAccessException exception) { + LOGGER.warning("Failed to create payload for " + id + " in " + ArrayUtils.toString(protocol.namespace()) + ", " + exception.getCause() + ": " + exception.getMessage()); + buf.readBytes(buf.readableBytes()); + return new ErrorPayload(id, protocol.namespace(), receiver.payloadId()); + } + } + } + } + return null; + } + + public static void handlePayload(ServerPlayer player, LeavesCustomPayload payload) { + if (payload instanceof ErrorPayload errorPayload) { + player.connection.disconnect(Component.literal("Payload " + Arrays.toString(errorPayload.packetID) + " from " + Arrays.toString(errorPayload.protocolID) + " error"), PlayerKickEvent.Cause.INVALID_PAYLOAD); + return; + } + + for (LeavesProtocol protocol : KNOW_RECEIVERS.keySet()) { + if (!ArrayUtils.contains(protocol.namespace(), payload.type().id().getNamespace())) { + continue; + } + + Map map = KNOW_RECEIVERS.get(protocol); + for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) { + if (payload.getClass() == receiver.payload()) { + if (receiver.ignoreId() || ArrayUtils.contains(receiver.payloadId(), payload.type().id().getPath())) { + try { + map.get(receiver).invoke(null, player, payload); + } catch (InvocationTargetException | IllegalAccessException exception) { + LOGGER.warning("Failed to handle payload " + payload.type().id() + " in " + ArrayUtils.toString(protocol.namespace()) + ", " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + } + } + } + + public static void handleTick() { + if (!TICKERS.isEmpty()) { + try { + for (Method method : TICKERS) { + method.invoke(null); + } + } catch (InvocationTargetException | IllegalAccessException exception) { + LOGGER.warning("Failed to tick, " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + + public static void handlePlayerJoin(ServerPlayer player) { + if (!PLAYER_JOIN.isEmpty()) { + try { + for (Method method : PLAYER_JOIN) { + method.invoke(null, player); + } + } catch (InvocationTargetException | IllegalAccessException exception) { + LOGGER.warning("Failed to handle player join, " + exception.getCause() + ": " + exception.getMessage()); + } + } + + ProtocolUtils.sendPayloadPacket(player, new FabricRegisterPayload(ALL_KNOWN_ID)); + } + + public static void handlePlayerLeave(ServerPlayer player) { + if (!PLAYER_LEAVE.isEmpty()) { + try { + for (Method method : PLAYER_LEAVE) { + method.invoke(null, player); + } + } catch (InvocationTargetException | IllegalAccessException exception) { + LOGGER.warning("Failed to handle player leave, " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + + public static void handleServerReload() { + if (!RELOAD_SERVER.isEmpty()) { + try { + for (Method method : RELOAD_SERVER) { + method.invoke(null); + } + } catch (InvocationTargetException | IllegalAccessException exception) { + LOGGER.warning("Failed to handle server reload, " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + + public static void handleMinecraftRegister(String channelId, ServerPlayer player) { + for (LeavesProtocol protocol : MINECRAFT_REGISTER.keySet()) { + String[] channel = channelId.split(":"); + if (!ArrayUtils.contains(protocol.namespace(), channel[0])) { + continue; + } + + Map map = MINECRAFT_REGISTER.get(protocol); + for (ProtocolHandler.MinecraftRegister register : map.keySet()) { + if (register.ignoreId() || ArrayUtils.contains(register.channelId(), channel[1])) { + try { + map.get(register).invoke(null, player, channel[1]); + } catch (InvocationTargetException | IllegalAccessException exception) { + LOGGER.warning("Failed to handle minecraft register, " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + } + } + + public static Set> getClasses(String pack) { + Set> classes = new LinkedHashSet<>(); + String packageDirName = pack.replace('.', '/'); + Enumeration dirs; + try { + dirs = Thread.currentThread().getContextClassLoader().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 exception) { + LOGGER.warning("Failed to load jar file, " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + } catch (IOException exception) { + LOGGER.warning("Failed to load classes, " + exception.getCause() + ": " + exception.getMessage()); + } + 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 exception) { + LOGGER.warning("Failed to load class " + className + ", " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + } + } + + 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 exception) { + LOGGER.warning("Failed to load class " + className + ", " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + } + } + + public record ErrorPayload(ResourceLocation id, String[] protocolID, String[] packetID) implements LeavesCustomPayload { + @Override + public void write(@NotNull FriendlyByteBuf buf) { + } + } + + public record EmptyPayload(ResourceLocation id) implements LeavesCustomPayload { + @New + public EmptyPayload(ResourceLocation location, FriendlyByteBuf buf) { + this(location); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + } + } + + public record LeavesPayload(FriendlyByteBuf data, ResourceLocation id) implements LeavesCustomPayload { + @New + public LeavesPayload(ResourceLocation location, FriendlyByteBuf buf) { + this(new FriendlyByteBuf(buf.readBytes(buf.readableBytes())), location); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeBytes(data); + } + } + + public record FabricRegisterPayload(Set channels) implements LeavesCustomPayload { + + public static final ResourceLocation CHANNEL = ResourceLocation.withDefaultNamespace("register"); + + @New + public FabricRegisterPayload(ResourceLocation location, FriendlyByteBuf buf) { + this(buf.readCollection(HashSet::new, FriendlyByteBuf::readResourceLocation)); + } + + @Override + public void write(FriendlyByteBuf buf) { + boolean first = true; + + ResourceLocation channel; + for (Iterator var3 = this.channels.iterator(); var3.hasNext(); buf.writeBytes(channel.toString().getBytes(StandardCharsets.US_ASCII))) { + channel = var3.next(); + if (first) { + first = false; + } else { + buf.writeByte(0); + } + } + } + + @Override + public ResourceLocation id() { + return CHANNEL; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java new file mode 100644 index 00000000..202fb071 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java @@ -0,0 +1,55 @@ +package org.leavesmc.leaves.protocol.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public class ProtocolHandler { + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface Init { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface PayloadReceiver { + Class> payload(); + + String[] payloadId() default ""; + + boolean ignoreId() default false; + + boolean sendFabricRegister() default true; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface Ticker { + int delay() default 0; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface PlayerJoin { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface PlayerLeave { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ReloadServer { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface MinecraftRegister { + String[] channelId() default ""; + + boolean ignoreId() default false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java new file mode 100644 index 00000000..72fb1e65 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java @@ -0,0 +1,52 @@ +package org.leavesmc.leaves.protocol.core; + +import io.netty.buffer.ByteBuf; +import io.papermc.paper.ServerBuildInfo; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class ProtocolUtils { + + private static final Function bufDecorator = RegistryFriendlyByteBuf.decorator(MinecraftServer.getServer().registryAccess()); + + public static String buildProtocolVersion(String protocol) { + return protocol + "-leaves-" + ServerBuildInfo.buildInfo().asString(ServerBuildInfo.StringRepresentation.VERSION_SIMPLE); + } + + public static void sendEmptyPayloadPacket(ServerPlayer player, ResourceLocation id) { + player.connection.send(new ClientboundCustomPayloadPacket(new LeavesProtocolManager.EmptyPayload(id))); + } + + @SuppressWarnings("all") + public static void sendPayloadPacket(@NotNull ServerPlayer player, ResourceLocation id, Consumer consumer) { + player.connection.send(new ClientboundCustomPayloadPacket(new LeavesCustomPayload() { + @Override + public void write(@NotNull FriendlyByteBuf buf) { + consumer.accept(buf); + } + + @Override + @NotNull + public ResourceLocation id() { + return id; + } + })); + } + + public static void sendPayloadPacket(ServerPlayer player, CustomPacketPayload payload) { + player.connection.send(new ClientboundCustomPayloadPacket(payload)); + } + + public static RegistryFriendlyByteBuf decorate(ByteBuf buf) { + return bufDecorator.apply(buf); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java new file mode 100644 index 00000000..e27205f6 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java @@ -0,0 +1,296 @@ +package org.leavesmc.leaves.protocol.jade; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.Chicken; +import net.minecraft.world.entity.animal.allay.Allay; +import net.minecraft.world.entity.animal.armadillo.Armadillo; +import net.minecraft.world.entity.animal.frog.Tadpole; +import net.minecraft.world.entity.monster.ZombieVillager; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.CampfireBlock; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.world.level.block.entity.BeehiveBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; +import net.minecraft.world.level.block.entity.CalibratedSculkSensorBlockEntity; +import net.minecraft.world.level.block.entity.ChiseledBookShelfBlockEntity; +import net.minecraft.world.level.block.entity.CommandBlockEntity; +import net.minecraft.world.level.block.entity.ComparatorBlockEntity; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import net.minecraft.world.level.block.entity.JukeboxBlockEntity; +import net.minecraft.world.level.block.entity.LecternBlockEntity; +import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.payload.ClientHandshakePayload; +import org.leavesmc.leaves.protocol.jade.payload.ReceiveDataPayload; +import org.leavesmc.leaves.protocol.jade.payload.RequestBlockPayload; +import org.leavesmc.leaves.protocol.jade.payload.RequestEntityPayload; +import org.leavesmc.leaves.protocol.jade.payload.ServerHandshakePayload; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; +import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; +import org.leavesmc.leaves.protocol.jade.provider.ItemStorageExtensionProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.BeehiveProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.BrewingStandProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.CampfireProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.ChiseledBookshelfProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.CommandBlockProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.FurnaceProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.HopperLockProvider; +import org.leavesmc.leaves.protocol.jade.provider.ItemStorageProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.JukeboxProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.LecternProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.MobSpawnerCooldownProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.ObjectNameProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.RedstoneProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.AnimalOwnerProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.MobBreedingProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.MobGrowthProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.NextEntityDropProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.PetArmorProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.StatusEffectsProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.ZombieVillagerProvider; +import org.leavesmc.leaves.protocol.jade.util.HierarchyLookup; +import org.leavesmc.leaves.protocol.jade.util.LootTableMineableCollector; +import org.leavesmc.leaves.protocol.jade.util.PairHierarchyLookup; +import org.leavesmc.leaves.protocol.jade.util.PriorityStore; +import org.leavesmc.leaves.protocol.jade.util.WrappedHierarchyLookup; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@LeavesProtocol(namespace = "jade") +public class JadeProtocol { + + public static PriorityStore priorities; + private static List shearableBlocks = null; + + public static final String PROTOCOL_ID = "jade"; + public static final String PROTOCOL_VERSION = "7"; + + public static final HierarchyLookup> entityDataProviders = new HierarchyLookup<>(Entity.class); + public static final PairHierarchyLookup> blockDataProviders = new PairHierarchyLookup<>(new HierarchyLookup<>(Block.class), new HierarchyLookup<>(BlockEntity.class)); + public static final WrappedHierarchyLookup> itemStorageProviders = WrappedHierarchyLookup.forAccessor(); + private static final Set enabledPlayers = new HashSet<>(); + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @Contract("_ -> new") + public static @NotNull ResourceLocation mc_id(String path) { + return ResourceLocation.withDefaultNamespace(path); + } + + @ProtocolHandler.Init + public static void init() { + priorities = new PriorityStore<>(IJadeProvider::getDefaultPriority, IJadeProvider::getUid); + + // core plugin + blockDataProviders.register(BlockEntity.class, ObjectNameProvider.ForBlock.INSTANCE); + + // universal plugin + entityDataProviders.register(Entity.class, ItemStorageProvider.getEntity()); + blockDataProviders.register(Block.class, ItemStorageProvider.getBlock()); + + itemStorageProviders.register(Object.class, ItemStorageExtensionProvider.INSTANCE); + itemStorageProviders.register(Block.class, ItemStorageExtensionProvider.INSTANCE); + + // vanilla plugin + entityDataProviders.register(Entity.class, AnimalOwnerProvider.INSTANCE); + entityDataProviders.register(LivingEntity.class, StatusEffectsProvider.INSTANCE); + entityDataProviders.register(AgeableMob.class, MobGrowthProvider.INSTANCE); + entityDataProviders.register(Tadpole.class, MobGrowthProvider.INSTANCE); + entityDataProviders.register(Animal.class, MobBreedingProvider.INSTANCE); + entityDataProviders.register(Allay.class, MobBreedingProvider.INSTANCE); + entityDataProviders.register(Mob.class, PetArmorProvider.INSTANCE); + + entityDataProviders.register(Chicken.class, NextEntityDropProvider.INSTANCE); + entityDataProviders.register(Armadillo.class, NextEntityDropProvider.INSTANCE); + + entityDataProviders.register(ZombieVillager.class, ZombieVillagerProvider.INSTANCE); + + blockDataProviders.register(BrewingStandBlockEntity.class, BrewingStandProvider.INSTANCE); + blockDataProviders.register(BeehiveBlockEntity.class, BeehiveProvider.INSTANCE); + blockDataProviders.register(CommandBlockEntity.class, CommandBlockProvider.INSTANCE); + blockDataProviders.register(JukeboxBlockEntity.class, JukeboxProvider.INSTANCE); + blockDataProviders.register(LecternBlockEntity.class, LecternProvider.INSTANCE); + + blockDataProviders.register(ComparatorBlockEntity.class, RedstoneProvider.INSTANCE); + blockDataProviders.register(HopperBlockEntity.class, HopperLockProvider.INSTANCE); + blockDataProviders.register(CalibratedSculkSensorBlockEntity.class, RedstoneProvider.INSTANCE); + + blockDataProviders.register(AbstractFurnaceBlockEntity.class, FurnaceProvider.INSTANCE); + blockDataProviders.register(ChiseledBookShelfBlockEntity.class, ChiseledBookshelfProvider.INSTANCE); + blockDataProviders.register(TrialSpawnerBlockEntity.class, MobSpawnerCooldownProvider.INSTANCE); + + blockDataProviders.idMapped(); + entityDataProviders.idMapped(); + itemStorageProviders.register(CampfireBlock.class, CampfireProvider.INSTANCE); + + blockDataProviders.loadComplete(priorities); + entityDataProviders.loadComplete(priorities); + itemStorageProviders.loadComplete(priorities); + + rebuildShearableBlocks(); + } + + @ProtocolHandler.PayloadReceiver(payload = ClientHandshakePayload.class, payloadId = "client_handshake") + public static void clientHandshake(ServerPlayer player, ClientHandshakePayload payload) { + if (!LeavesConfig.protocol.jadeProtocol) { + return; + } + + if (!payload.protocolVersion().equals(PROTOCOL_VERSION)) { + player.sendSystemMessage(Component.literal("You are using a different version of Jade than the server. Please update Jade or report to the server operator").withColor(0xff0000)); + return; + } + ProtocolUtils.sendPayloadPacket(player, new ServerHandshakePayload(Collections.emptyMap(), shearableBlocks, blockDataProviders.mappedIds(), entityDataProviders.mappedIds())); + enabledPlayers.add(player); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLeave(ServerPlayer player) { + enabledPlayers.remove(player); + } + + @ProtocolHandler.PayloadReceiver(payload = RequestEntityPayload.class, payloadId = "request_entity") + public static void requestEntityData(ServerPlayer player, RequestEntityPayload payload) { + if (!LeavesConfig.protocol.jadeProtocol) { + return; + } + + MinecraftServer.getServer().execute(() -> { + EntityAccessor accessor = payload.data().unpack(player); + if (accessor == null) { + return; + } + + Entity entity = accessor.getEntity(); + double maxDistance = Mth.square(player.entityInteractionRange() + 21); + if (entity == null || player.distanceToSqr(entity) > maxDistance) { + return; + } + + List> providers = entityDataProviders.get(entity); + if (providers.isEmpty()) { + return; + } + + CompoundTag tag = new CompoundTag(); + for (IServerDataProvider provider : providers) { + if (!payload.dataProviders().contains(provider)) { + continue; + } + try { + provider.appendServerData(tag, accessor); + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Error while saving data for entity " + entity); + } + } + tag.putInt("EntityId", entity.getId()); + + ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag)); + }); + } + + @ProtocolHandler.PayloadReceiver(payload = RequestBlockPayload.class, payloadId = "request_block") + public static void requestBlockData(ServerPlayer player, RequestBlockPayload payload) { + if (!LeavesConfig.protocol.jadeProtocol) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + server.execute(() -> { + BlockAccessor accessor = payload.data().unpack(player); + if (accessor == null) { + return; + } + + BlockPos pos = accessor.getPosition(); + Block block = accessor.getBlock(); + BlockEntity blockEntity = accessor.getBlockEntity(); + double maxDistance = Mth.square(player.blockInteractionRange() + 21); + if (pos.distSqr(player.blockPosition()) > maxDistance || !accessor.getLevel().isLoaded(pos)) { + return; + } + + List> providers; + if (blockEntity != null) { + providers = blockDataProviders.getMerged(block, blockEntity); + } else { + providers = blockDataProviders.first.get(block); + } + + if (providers.isEmpty()) { + return; + } + + CompoundTag tag = new CompoundTag(); + for (IServerDataProvider provider : providers) { + if (!payload.dataProviders().contains(provider)) { + continue; + } + try { + provider.appendServerData(tag, accessor); + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Error while saving data for block " + accessor.getBlockState()); + } + } + tag.putInt("x", pos.getX()); + tag.putInt("y", pos.getY()); + tag.putInt("z", pos.getZ()); + tag.putString("BlockId", BuiltInRegistries.BLOCK.getKey(block).toString()); + + ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag)); + }); + } + + @ProtocolHandler.ReloadServer + public static void onServerReload() { + if (LeavesConfig.protocol.jadeProtocol) { + rebuildShearableBlocks(); + for (ServerPlayer player : enabledPlayers) { + ProtocolUtils.sendPayloadPacket(player, new ServerHandshakePayload(Collections.emptyMap(), shearableBlocks, blockDataProviders.mappedIds(), entityDataProviders.mappedIds())); + } + } + } + + private static void rebuildShearableBlocks() { + try { + shearableBlocks = Collections.unmodifiableList(LootTableMineableCollector.execute( + MinecraftServer.getServer().reloadableRegistries().lookup().lookupOrThrow(Registries.LOOT_TABLE), + Items.SHEARS.getDefaultInstance() + )); + } catch (Throwable ignore) { + shearableBlocks = List.of(); + LeavesLogger.LOGGER.severe("Failed to collect shearable blocks"); + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java new file mode 100644 index 00000000..1a637045 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java @@ -0,0 +1,32 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamEncoder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.HitResult; +import org.jetbrains.annotations.Nullable; + +public interface Accessor { + + Level getLevel(); + + Player getPlayer(); + + Tag encodeAsNbt(StreamEncoder codec, D value); + + T getHitResult(); + + /** + * @return {@code true} if the dedicated server has Jade installed. + */ + boolean isServerConnected(); + + boolean showDetails(); + + @Nullable + Object getTarget(); + + float tickRate(); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java new file mode 100644 index 00000000..7e57bd40 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java @@ -0,0 +1,82 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamEncoder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.HitResult; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.function.Supplier; + +public abstract class AccessorImpl implements Accessor { + + private final Level level; + private final Player player; + private final Supplier hit; + private final boolean serverConnected; + private final boolean showDetails; + protected boolean verify; + private RegistryFriendlyByteBuf buffer; + + public AccessorImpl(Level level, Player player, Supplier hit, boolean serverConnected, boolean showDetails) { + this.level = level; + this.player = player; + this.hit = hit; + this.serverConnected = serverConnected; + this.showDetails = showDetails; + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public Player getPlayer() { + return player; + } + + private RegistryFriendlyByteBuf buffer() { + if (buffer == null) { + buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), level.registryAccess()); + } + buffer.clear(); + return buffer; + } + + @Override + public Tag encodeAsNbt(StreamEncoder streamCodec, D value) { + RegistryFriendlyByteBuf buffer = buffer(); + streamCodec.encode(buffer, value); + ByteArrayTag tag = new ByteArrayTag(ArrayUtils.subarray(buffer.array(), 0, buffer.readableBytes())); + buffer.clear(); + return tag; + } + + @Override + public T getHitResult() { + return hit.get(); + } + + /** + * Returns true if dedicated server has Jade installed. + */ + @Override + public boolean isServerConnected() { + return serverConnected; + } + + @Override + public boolean showDetails() { + return showDetails; + } + + @Override + public float tickRate() { + return getLevel().tickRateManager().tickrate(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java new file mode 100644 index 00000000..12d689ca --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java @@ -0,0 +1,50 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Player; +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.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Supplier; + +public interface BlockAccessor extends Accessor { + + Block getBlock(); + + BlockState getBlockState(); + + BlockEntity getBlockEntity(); + + BlockPos getPosition(); + + Direction getSide(); + + @ApiStatus.NonExtendable + interface Builder { + Builder level(Level level); + + Builder player(Player player); + + Builder showDetails(boolean showDetails); + + Builder hit(BlockHitResult hit); + + Builder blockState(BlockState state); + + default Builder blockEntity(BlockEntity blockEntity) { + return blockEntity(() -> blockEntity); + } + + Builder blockEntity(Supplier blockEntity); + + Builder from(BlockAccessor accessor); + + BlockAccessor build(); + } + +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java new file mode 100644 index 00000000..9e4a321b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java @@ -0,0 +1,163 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import java.util.function.Supplier; + +import org.jetbrains.annotations.Nullable; + +import com.google.common.base.Suppliers; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +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.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; + +/** + * Class to get information of block target and context. + */ +public class BlockAccessorImpl extends AccessorImpl implements BlockAccessor { + + private final BlockState blockState; + @Nullable + private final Supplier blockEntity; + + private BlockAccessorImpl(Builder builder) { + super(builder.level, builder.player, Suppliers.ofInstance(builder.hit), builder.connected, builder.showDetails); + blockState = builder.blockState; + blockEntity = builder.blockEntity; + } + + @Override + public Block getBlock() { + return getBlockState().getBlock(); + } + + @Override + public BlockState getBlockState() { + return blockState; + } + + @Override + public BlockEntity getBlockEntity() { + return blockEntity == null ? null : blockEntity.get(); + } + + @Override + public BlockPos getPosition() { + return getHitResult().getBlockPos(); + } + + @Override + public Direction getSide() { + return getHitResult().getDirection(); + } + + @Nullable + @Override + public Object getTarget() { + return getBlockEntity(); + } + + public static class Builder implements BlockAccessor.Builder { + + private Level level; + private Player player; + private boolean connected; + private boolean showDetails; + private BlockHitResult hit; + private BlockState blockState = Blocks.AIR.defaultBlockState(); + private Supplier blockEntity; + + @Override + public Builder level(Level level) { + this.level = level; + return this; + } + + @Override + public Builder player(Player player) { + this.player = player; + return this; + } + + @Override + public Builder showDetails(boolean showDetails) { + this.showDetails = showDetails; + return this; + } + + @Override + public Builder hit(BlockHitResult hit) { + this.hit = hit; + return this; + } + + @Override + public Builder blockState(BlockState blockState) { + this.blockState = blockState; + return this; + } + + @Override + public Builder blockEntity(Supplier blockEntity) { + this.blockEntity = blockEntity; + return this; + } + + @Override + public Builder from(BlockAccessor accessor) { + level = accessor.getLevel(); + player = accessor.getPlayer(); + connected = accessor.isServerConnected(); + showDetails = accessor.showDetails(); + hit = accessor.getHitResult(); + blockEntity = accessor::getBlockEntity; + blockState = accessor.getBlockState(); + return this; + } + + @Override + public BlockAccessor build() { + return new BlockAccessorImpl(this); + } + } + + public record SyncData(boolean showDetails, BlockHitResult hit, BlockState blockState, ItemStack fakeBlock) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, + SyncData::showDetails, + StreamCodec.of(FriendlyByteBuf::writeBlockHitResult, FriendlyByteBuf::readBlockHitResult), + SyncData::hit, + ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY), + SyncData::blockState, + ItemStack.OPTIONAL_STREAM_CODEC, + SyncData::fakeBlock, + SyncData::new + ); + + public BlockAccessor unpack(ServerPlayer player) { + Supplier blockEntity = null; + if (blockState.hasBlockEntity()) { + blockEntity = Suppliers.memoize(() -> player.level().getBlockEntity(hit.getBlockPos())); + } + return new Builder() + .level(player.level()) + .player(player) + .showDetails(showDetails) + .hit(hit) + .blockState(blockState) + .blockEntity(blockEntity) + .build(); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java new file mode 100644 index 00000000..454360d5 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java @@ -0,0 +1,44 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.EntityHitResult; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Supplier; + +public interface EntityAccessor extends Accessor { + + Entity getEntity(); + + /** + * For part entity like ender dragon's, getEntity() will return the parent entity. + */ + Entity getRawEntity(); + + @ApiStatus.NonExtendable + interface Builder { + Builder level(Level level); + + Builder player(Player player); + + Builder showDetails(boolean showDetails); + + default Builder hit(EntityHitResult hit) { + return hit(() -> hit); + } + + Builder hit(Supplier hit); + + default Builder entity(Entity entity) { + return entity(() -> entity); + } + + Builder entity(Supplier entity); + + Builder from(EntityAccessor accessor); + + EntityAccessor build(); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java new file mode 100644 index 00000000..65d16c00 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java @@ -0,0 +1,123 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import com.google.common.base.Suppliers; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.util.CommonUtil; + +import java.util.function.Supplier; + +public class EntityAccessorImpl extends AccessorImpl implements EntityAccessor { + + private final Supplier entity; + + public EntityAccessorImpl(Builder builder) { + super(builder.level, builder.player, builder.hit, builder.connected, builder.showDetails); + entity = builder.entity; + } + + @Override + public Entity getEntity() { + return CommonUtil.wrapPartEntityParent(getRawEntity()); + } + + @Override + public Entity getRawEntity() { + return entity.get(); + } + + @NotNull + @Override + public Object getTarget() { + return getEntity(); + } + + public static class Builder implements EntityAccessor.Builder { + + public boolean showDetails; + private Level level; + private Player player; + private boolean connected; + private Supplier hit; + private Supplier entity; + + @Override + public Builder level(Level level) { + this.level = level; + return this; + } + + @Override + public Builder player(Player player) { + this.player = player; + return this; + } + + @Override + public Builder showDetails(boolean showDetails) { + this.showDetails = showDetails; + return this; + } + + @Override + public Builder hit(Supplier hit) { + this.hit = hit; + return this; + } + + @Override + public Builder entity(Supplier entity) { + this.entity = entity; + return this; + } + + @Override + public Builder from(EntityAccessor accessor) { + level = accessor.getLevel(); + player = accessor.getPlayer(); + connected = accessor.isServerConnected(); + showDetails = accessor.showDetails(); + hit = accessor::getHitResult; + entity = accessor::getEntity; + return this; + } + + @Override + public EntityAccessor build() { + return new EntityAccessorImpl(this); + } + } + + public record SyncData(boolean showDetails, int id, int partIndex, Vec3 hitVec) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, + SyncData::showDetails, + ByteBufCodecs.VAR_INT, + SyncData::id, + ByteBufCodecs.VAR_INT, + SyncData::partIndex, + ByteBufCodecs.VECTOR3F.map(Vec3::new, Vec3::toVector3f), + SyncData::hitVec, + SyncData::new + ); + + public EntityAccessor unpack(ServerPlayer player) { + Supplier entity = Suppliers.memoize(() -> CommonUtil.getPartEntity(player.level().getEntity(id), partIndex)); + return new EntityAccessorImpl.Builder() + .level(player.level()) + .player(player) + .showDetails(showDetails) + .entity(entity) + .hit(Suppliers.memoize(() -> new EntityHitResult(entity.get(), hitVec))) + .build(); + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java new file mode 100644 index 00000000..1be4efcc --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java @@ -0,0 +1,35 @@ +package org.leavesmc.leaves.protocol.jade.payload; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; + +public record ClientHandshakePayload(String protocolVersion) implements LeavesCustomPayload { + + private static final ResourceLocation PACKET_CLIENT_HANDSHAKE = JadeProtocol.id("client_handshake"); + + private static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, + ClientHandshakePayload::protocolVersion, + ClientHandshakePayload::new); + + @Override + public void write(FriendlyByteBuf buf) { + CODEC.encode(ProtocolUtils.decorate(buf), this); + } + + @Override + public ResourceLocation id() { + return PACKET_CLIENT_HANDSHAKE; + } + + @New + public static ClientHandshakePayload create(ResourceLocation location, FriendlyByteBuf buf) { + return CODEC.decode(ProtocolUtils.decorate(buf)); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java new file mode 100644 index 00000000..1b474ea8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java @@ -0,0 +1,28 @@ +package org.leavesmc.leaves.protocol.jade.payload; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; + +public record ReceiveDataPayload(CompoundTag tag) implements LeavesCustomPayload { + + private static final ResourceLocation PACKET_RECEIVE_DATA = JadeProtocol.id("receive_data"); + + @New + public ReceiveDataPayload(ResourceLocation id, FriendlyByteBuf buf) { + this(buf.readNbt()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeNbt(tag); + } + + @Override + public ResourceLocation id() { + return PACKET_RECEIVE_DATA; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java new file mode 100644 index 00000000..5e5b4a0c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java @@ -0,0 +1,51 @@ +package org.leavesmc.leaves.protocol.jade.payload; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessorImpl; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +import java.util.List; +import java.util.Objects; + +import static org.leavesmc.leaves.protocol.jade.JadeProtocol.blockDataProviders; + +public record RequestBlockPayload(BlockAccessorImpl.SyncData data, List<@Nullable IServerDataProvider> dataProviders) implements LeavesCustomPayload { + + private static final ResourceLocation PACKET_REQUEST_BLOCK = JadeProtocol.id("request_block"); + private static final StreamCodec CODEC = StreamCodec.composite( + BlockAccessorImpl.SyncData.STREAM_CODEC, + RequestBlockPayload::data, + ByteBufCodecs.>list() + .apply(ByteBufCodecs.idMapper( + $ -> Objects.requireNonNull(blockDataProviders.idMapper()).byId($), + $ -> Objects.requireNonNull(blockDataProviders.idMapper()).getIdOrThrow($))), + RequestBlockPayload::dataProviders, + RequestBlockPayload::new); + + @Override + public void write(FriendlyByteBuf buf) { + CODEC.encode(ProtocolUtils.decorate(buf), this); + } + + @Override + @NotNull + public ResourceLocation id() { + return PACKET_REQUEST_BLOCK; + } + + @New + public static RequestBlockPayload create(ResourceLocation location, FriendlyByteBuf buf) { + return CODEC.decode(ProtocolUtils.decorate(buf)); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java new file mode 100644 index 00000000..515dc5d1 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java @@ -0,0 +1,53 @@ +package org.leavesmc.leaves.protocol.jade.payload; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessorImpl; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +import java.util.List; +import java.util.Objects; + +import static org.leavesmc.leaves.protocol.jade.JadeProtocol.entityDataProviders; + +public record RequestEntityPayload(EntityAccessorImpl.SyncData data, List<@Nullable IServerDataProvider> dataProviders) implements LeavesCustomPayload { + + private static final ResourceLocation PACKET_REQUEST_ENTITY = JadeProtocol.id("request_entity"); + private static final StreamCodec CODEC = StreamCodec.composite( + EntityAccessorImpl.SyncData.STREAM_CODEC, + RequestEntityPayload::data, + ByteBufCodecs.>list() + .apply(ByteBufCodecs.idMapper( + $ -> Objects.requireNonNull(entityDataProviders.idMapper()).byId($), + $ -> Objects.requireNonNull(entityDataProviders.idMapper()).getIdOrThrow($) + )), + RequestEntityPayload::dataProviders, + RequestEntityPayload::new); + + + @Override + public void write(FriendlyByteBuf buf) { + CODEC.encode(ProtocolUtils.decorate(buf), this); + } + + @Override + @NotNull + public ResourceLocation id() { + return PACKET_REQUEST_ENTITY; + } + + @New + public static RequestEntityPayload create(ResourceLocation location, FriendlyByteBuf buf) { + return CODEC.decode(ProtocolUtils.decorate(buf)); + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java new file mode 100644 index 00000000..f6a2ed7c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java @@ -0,0 +1,52 @@ +package org.leavesmc.leaves.protocol.jade.payload; + + +import com.google.common.collect.Maps; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; + +import java.util.List; +import java.util.Map; + +import static org.leavesmc.leaves.protocol.jade.util.JadeCodec.PRIMITIVE_STREAM_CODEC; + +public record ServerHandshakePayload(Map serverConfig, List shearableBlocks, List blockProviderIds, List entityProviderIds) implements LeavesCustomPayload { + + private static final ResourceLocation PACKET_SERVER_HANDSHAKE = JadeProtocol.id("server_handshake"); + private static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.map(Maps::newHashMapWithExpectedSize, ResourceLocation.STREAM_CODEC, PRIMITIVE_STREAM_CODEC), + ServerHandshakePayload::serverConfig, + ByteBufCodecs.registry(Registries.BLOCK).apply(ByteBufCodecs.list()), + ServerHandshakePayload::shearableBlocks, + ByteBufCodecs.list().apply(ResourceLocation.STREAM_CODEC), + ServerHandshakePayload::blockProviderIds, + ByteBufCodecs.list().apply(ResourceLocation.STREAM_CODEC), + ServerHandshakePayload::entityProviderIds, + ServerHandshakePayload::new); + + @Override + public void write(FriendlyByteBuf buf) { + CODEC.encode(ProtocolUtils.decorate(buf), this); + } + + @Override + public ResourceLocation id() { + return PACKET_SERVER_HANDSHAKE; + } + + @New + public static ServerHandshakePayload create(ResourceLocation location, FriendlyByteBuf buf) { + return CODEC.decode(ProtocolUtils.decorate(buf)); + } +} + diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java new file mode 100644 index 00000000..d62fc8f9 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java @@ -0,0 +1,12 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import net.minecraft.resources.ResourceLocation; + +public interface IJadeProvider { + + ResourceLocation getUid(); + + default int getDefaultPriority() { + return 0; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java new file mode 100644 index 00000000..7d839f17 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java @@ -0,0 +1,8 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import net.minecraft.nbt.CompoundTag; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; + +public interface IServerDataProvider> extends IJadeProvider { + void appendServerData(CompoundTag data, T accessor); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java new file mode 100644 index 00000000..6e32eed1 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java @@ -0,0 +1,10 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; + +public interface IServerExtensionProvider extends IJadeProvider { + List> getGroups(Accessor request); +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java new file mode 100644 index 00000000..3efa3ceb --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java @@ -0,0 +1,138 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Container; +import net.minecraft.world.LockCode; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.WorldlyContainerHolder; +import net.minecraft.world.entity.animal.horse.AbstractHorse; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.vehicle.ContainerEntity; +import net.minecraft.world.inventory.PlayerEnderChestContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.entity.EnderChestBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.util.ItemCollector; +import org.leavesmc.leaves.protocol.jade.util.ItemIterator; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public enum ItemStorageExtensionProvider implements IServerExtensionProvider { + INSTANCE; + + public static final Cache> targetCache = CacheBuilder.newBuilder().weakKeys().expireAfterAccess(60, TimeUnit.SECONDS).build(); + + private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage.default"); + + @Override + public List> getGroups(Accessor request) { + Object target = request.getTarget(); + + switch (target) { + case null -> { + return createItemCollector(request).update(request); + } + case RandomizableContainer te when te.getLootTable() != null -> { + return List.of(); + } + case ContainerEntity containerEntity when containerEntity.getContainerLootTable() != null -> { + return List.of(); + } + default -> { + } + } + + Player player = request.getPlayer(); + if (!player.isCreative() && !player.isSpectator() && target instanceof BaseContainerBlockEntity te) { + if (te.lockKey != LockCode.NO_LOCK) { + return List.of(); + } + } + + if (target instanceof EnderChestBlockEntity) { + PlayerEnderChestContainer inventory = player.getEnderChestInventory(); + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(x -> inventory, 0)).update(request); + } + + ItemCollector itemCollector; + try { + itemCollector = targetCache.get(target, () -> createItemCollector(request)); + } catch (ExecutionException e) { + LeavesLogger.LOGGER.severe("Failed to get item collector for " + target); + return null; + } + + return itemCollector.update(request); + } + + @Override + public ResourceLocation getUid() { + return UNIVERSAL_ITEM_STORAGE; + } + + public static ItemCollector createItemCollector(Accessor request) { + if (request.getTarget() instanceof AbstractHorse) { + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(o -> { + if (o instanceof AbstractHorse horse) { + return horse.inventory; + } + return null; + }, 2)); + } + + // TODO BlockEntity like fabric's ItemStorage + + final Container container = findContainer(request); + if (container != null) { + if (container instanceof ChestBlockEntity) { + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(o -> { + if (o instanceof ChestBlockEntity blockEntity) { + if (blockEntity.getBlockState().getBlock() instanceof ChestBlock chestBlock) { + Container compound = null; + if (blockEntity.getLevel() != null) { + compound = ChestBlock.getContainer(chestBlock, blockEntity.getBlockState(), blockEntity.getLevel(), blockEntity.getBlockPos(), false); + } + if (compound != null) { + return compound; + } + } + return blockEntity; + } + return null; + }, 0)); + } + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(0)); + } + + return ItemCollector.EMPTY; + } + + public static @Nullable Container findContainer(@NotNull Accessor accessor) { + Object target = accessor.getTarget(); + if (target == null && accessor instanceof BlockAccessor blockAccessor && + blockAccessor.getBlock() instanceof WorldlyContainerHolder holder) { + return holder.getContainer(blockAccessor.getBlockState(), accessor.getLevel(), blockAccessor.getPosition()); + } else if (target instanceof Container container) { + return container; + } + return null; + } + + @Override + public int getDefaultPriority() { + return IServerExtensionProvider.super.getDefaultPriority() + 1000; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java new file mode 100644 index 00000000..8289b5c4 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java @@ -0,0 +1,88 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.LockCode; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.util.CommonUtil; +import org.leavesmc.leaves.protocol.jade.util.ItemCollector; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; +import java.util.Map; + +public abstract class ItemStorageProvider> implements IServerDataProvider { + + private static final StreamCodec>>> STREAM_CODEC = ViewGroup.listCodec( + ItemStack.OPTIONAL_STREAM_CODEC); + + private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage"); + + public static ForBlock getBlock() { + return ForBlock.INSTANCE; + } + + public static ForEntity getEntity() { + return ForEntity.INSTANCE; + } + + public static class ForBlock extends ItemStorageProvider { + private static final ForBlock INSTANCE = new ForBlock(); + } + + public static class ForEntity extends ItemStorageProvider { + private static final ForEntity INSTANCE = new ForEntity(); + } + + public static void putData(CompoundTag tag, @NotNull Accessor accessor) { + Object target = accessor.getTarget(); + Player player = accessor.getPlayer(); + Map.Entry>> entry = CommonUtil.getServerExtensionData(accessor, JadeProtocol.itemStorageProviders); + if (entry != null) { + List> groups = entry.getValue(); + for (ViewGroup group : groups) { + if (group.views.size() > ItemCollector.MAX_SIZE) { + group.views = group.views.subList(0, ItemCollector.MAX_SIZE); + } + } + tag.put(UNIVERSAL_ITEM_STORAGE.toString(), accessor.encodeAsNbt(STREAM_CODEC, entry)); + return; + } + if (target instanceof RandomizableContainer containerEntity && containerEntity.getLootTable() != null) { + tag.putBoolean("Loot", true); + } else if (!player.isCreative() && !player.isSpectator() && target instanceof BaseContainerBlockEntity te) { + if (te.lockKey != LockCode.NO_LOCK) { + tag.putBoolean("Locked", true); + } + } + } + + @Override + public ResourceLocation getUid() { + return UNIVERSAL_ITEM_STORAGE; + } + + @Override + public void appendServerData(CompoundTag tag, @NotNull T accessor) { + if (accessor.getTarget() instanceof AbstractFurnaceBlockEntity) { + return; + } + putData(tag, accessor); + } + + @Override + public int getDefaultPriority() { + return 9999; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java new file mode 100644 index 00000000..52887edb --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java @@ -0,0 +1,26 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; + +import java.util.Optional; + +public interface StreamServerDataProvider, D> extends IServerDataProvider { + + @Override + default void appendServerData(CompoundTag data, T accessor) { + D value = streamData(accessor); + if (value != null) { + data.put(getUid().toString(), accessor.encodeAsNbt(streamCodec(), value)); + } + } + + @Nullable + D streamData(T accessor); + + StreamCodec streamCodec(); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java new file mode 100644 index 00000000..ee92d79b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java @@ -0,0 +1,34 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BeehiveBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum BeehiveProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_BEEHIVE = JadeProtocol.mc_id("beehive"); + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.BYTE.cast(); + } + + @Override + public Byte streamData(@NotNull BlockAccessor accessor) { + BeehiveBlockEntity beehive = (BeehiveBlockEntity) accessor.getBlockEntity(); + int bees = beehive.getOccupantCount(); + return (byte) (beehive.isFull() ? bees : -bees); + } + + @Override + public ResourceLocation getUid() { + return MC_BEEHIVE; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java new file mode 100644 index 00000000..e6f15f87 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java @@ -0,0 +1,43 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum BrewingStandProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_BREWING_STAND = JadeProtocol.mc_id("brewing_stand"); + + @Override + public @NotNull Data streamData(@NotNull BlockAccessor accessor) { + BrewingStandBlockEntity brewingStand = (BrewingStandBlockEntity) accessor.getBlockEntity(); + return new Data(brewingStand.fuel, brewingStand.brewTime); + } + + @Override + public @NotNull StreamCodec streamCodec() { + return Data.STREAM_CODEC.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_BREWING_STAND; + } + + public record Data(int fuel, int time) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.VAR_INT, + Data::fuel, + ByteBufCodecs.VAR_INT, + Data::time, + Data::new); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java new file mode 100644 index 00000000..2deb3777 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java @@ -0,0 +1,55 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import com.google.common.collect.Lists; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.block.entity.CampfireBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; + +public enum CampfireProvider implements IServerExtensionProvider { + INSTANCE; + + private static final MapCodec COOKING_TIME_CODEC = Codec.INT.fieldOf("jade:cooking"); + private static final ResourceLocation MC_CAMPFIRE = JadeProtocol.mc_id("campfire"); + + @Override + public @Nullable @Unmodifiable List> getGroups(@NotNull Accessor request) { + if (request.getTarget() instanceof CampfireBlockEntity campfire) { + List list = Lists.newArrayList(); + for (int i = 0; i < campfire.cookingTime.length; i++) { + ItemStack stack = campfire.getItems().get(i); + if (stack.isEmpty()) { + continue; + } + stack = stack.copy(); + + CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .update(NbtOps.INSTANCE, COOKING_TIME_CODEC, campfire.cookingTime[i] - campfire.cookingProgress[i]) + .getOrThrow(); + stack.set(DataComponents.CUSTOM_DATA, customData); + + list.add(stack); + } + return List.of(new ViewGroup<>(list)); + } + return null; + } + + @Override + public ResourceLocation getUid() { + return MC_CAMPFIRE; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java new file mode 100644 index 00000000..bde872cc --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java @@ -0,0 +1,39 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.ChiseledBookShelfBlock; +import net.minecraft.world.level.block.entity.ChiseledBookShelfBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum ChiseledBookshelfProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_CHISELED_BOOKSHELF = JadeProtocol.mc_id("chiseled_bookshelf"); + + @Override + public @Nullable ItemStack streamData(@NotNull BlockAccessor accessor) { + int slot = ((ChiseledBookShelfBlock) accessor.getBlock()).getHitSlot(accessor.getHitResult(), accessor.getBlockState()).orElse(-1); + if (slot == -1) { + return null; + } + return ((ChiseledBookShelfBlockEntity) accessor.getBlockEntity()).getItem(slot); + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + + @Override + public ResourceLocation getUid() { + return MC_CHISELED_BOOKSHELF; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java new file mode 100644 index 00000000..5f71fada --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java @@ -0,0 +1,40 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.CommandBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum CommandBlockProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_COMMAND_BLOCK = JadeProtocol.mc_id("command_block"); + + @Nullable + public String streamData(@NotNull BlockAccessor accessor) { + if (!accessor.getPlayer().canUseGameMasterBlocks()) { + return null; + } + String command = ((CommandBlockEntity) accessor.getBlockEntity()).getCommandBlock().getCommand(); + if (command.length() > 40) { + command = command.substring(0, 37) + "..."; + } + return command; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.STRING_UTF8.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_COMMAND_BLOCK; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java new file mode 100644 index 00000000..090e6a35 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java @@ -0,0 +1,60 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +import java.util.List; + +public enum FurnaceProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_FURNACE = JadeProtocol.mc_id("furnace"); + + @Override + public @Nullable Data streamData(@NotNull BlockAccessor accessor) { + if (!(accessor.getTarget() instanceof AbstractFurnaceBlockEntity furnace)) { + return null; + } + + if (furnace.isEmpty()) { + return null; + } + + CompoundTag furnaceTag = furnace.saveWithoutMetadata(accessor.getLevel().registryAccess()); + return new Data( + furnaceTag.getInt("CookTime"), + furnaceTag.getInt("CookTimeTotal"), + List.of(furnace.getItem(0), furnace.getItem(1), furnace.getItem(2))); + } + + @Override + public StreamCodec streamCodec() { + return Data.STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_FURNACE; + } + + public record Data(int progress, int total, List inventory) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.VAR_INT, + Data::progress, + ByteBufCodecs.VAR_INT, + Data::total, + ItemStack.OPTIONAL_LIST_STREAM_CODEC, + Data::inventory, + Data::new); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java new file mode 100644 index 00000000..a3937081 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java @@ -0,0 +1,32 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum HopperLockProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_HOPPER_LOCK = JadeProtocol.mc_id("hopper_lock"); + + @Override + public Boolean streamData(@NotNull BlockAccessor accessor) { + return !accessor.getBlockState().getValue(BlockStateProperties.ENABLED); + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.BOOL.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_HOPPER_LOCK; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java new file mode 100644 index 00000000..0b6e224e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java @@ -0,0 +1,32 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.JukeboxBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum JukeboxProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_JUKEBOX = JadeProtocol.mc_id("jukebox"); + + @Override + public @NotNull ItemStack streamData(BlockAccessor accessor) { + return ((JukeboxBlockEntity) accessor.getBlockEntity()).getTheItem(); + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_JUKEBOX; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java new file mode 100644 index 00000000..c363bd61 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java @@ -0,0 +1,33 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.LecternBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum LecternProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_LECTERN = JadeProtocol.mc_id("lectern"); + + @Override + public @NotNull ItemStack streamData(@NotNull BlockAccessor accessor) { + return ((LecternBlockEntity) accessor.getBlockEntity()).getBook(); + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + + @Override + public ResourceLocation getUid() { + return MC_LECTERN; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java new file mode 100644 index 00000000..a70f4a81 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java @@ -0,0 +1,42 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity; +import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum MobSpawnerCooldownProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_MOB_SPAWNER_COOLDOWN = JadeProtocol.mc_id("mob_spawner.cooldown"); + + @Override + public @Nullable Integer streamData(@NotNull BlockAccessor accessor) { + TrialSpawnerBlockEntity spawner = (TrialSpawnerBlockEntity) accessor.getBlockEntity(); + TrialSpawnerData spawnerData = spawner.getTrialSpawner().getData(); + ServerLevel level = ((ServerLevel) accessor.getLevel()); + if (spawner.getTrialSpawner().canSpawnInLevel(level) && level.getGameTime() < spawnerData.cooldownEndsAt) { + return (int) (spawnerData.cooldownEndsAt - level.getGameTime()); + } + return null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + + @Override + public ResourceLocation getUid() { + return MC_MOB_SPAWNER_COOLDOWN; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java new file mode 100644 index 00000000..6a060c8a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java @@ -0,0 +1,63 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.Nameable; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.properties.ChestType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public abstract class ObjectNameProvider implements StreamServerDataProvider { + + private static final ResourceLocation CORE_OBJECT_NAME = JadeProtocol.id("object_name"); + + public static class ForBlock extends ObjectNameProvider implements StreamServerDataProvider { + public static final ForBlock INSTANCE = new ForBlock(); + + @Override + @Nullable + public Component streamData(@NotNull BlockAccessor accessor) { + if (!(accessor.getBlockEntity() instanceof Nameable nameable)) { + return null; + } + if (nameable instanceof ChestBlockEntity && accessor.getBlock() instanceof ChestBlock && accessor.getBlockState().getValue(ChestBlock.TYPE) != ChestType.SINGLE) { + MenuProvider menuProvider = accessor.getBlockState().getMenuProvider(accessor.getLevel(), accessor.getPosition()); + if (menuProvider != null) { + Component name = menuProvider.getDisplayName(); + if (!(name.getContents() instanceof TranslatableContents contents) || !"container.chestDouble".equals(contents.getKey())) { + return name; + } + } + } else if (nameable.hasCustomName()) { + return nameable.getDisplayName(); + } + return accessor.getBlockEntity().components().get(DataComponents.ITEM_NAME); + } + + @Override + public StreamCodec streamCodec() { + return ComponentSerialization.STREAM_CODEC; + } + } + + @Override + public ResourceLocation getUid() { + return CORE_OBJECT_NAME; + } + + @Override + public int getDefaultPriority() { + return -10100; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java new file mode 100644 index 00000000..1cdcf21e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java @@ -0,0 +1,36 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.CalibratedSculkSensorBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.CalibratedSculkSensorBlockEntity; +import net.minecraft.world.level.block.entity.ComparatorBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +public enum RedstoneProvider implements IServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_REDSTONE = JadeProtocol.mc_id("redstone"); + + @Override + public void appendServerData(CompoundTag data, @NotNull BlockAccessor accessor) { + BlockEntity blockEntity = accessor.getBlockEntity(); + if (blockEntity instanceof ComparatorBlockEntity comparator) { + data.putInt("Signal", comparator.getOutputSignal()); + } else if (blockEntity instanceof CalibratedSculkSensorBlockEntity) { + Direction direction = accessor.getBlockState().getValue(CalibratedSculkSensorBlock.FACING).getOpposite(); + int signal = accessor.getLevel().getSignal(accessor.getPosition().relative(direction), direction); + data.putInt("Signal", signal); + } + } + + @Override + public ResourceLocation getUid() { + return MC_REDSTONE; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java new file mode 100644 index 00000000..ff50ef60 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java @@ -0,0 +1,43 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.OwnableEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; +import org.leavesmc.leaves.protocol.jade.util.CommonUtil; + +import java.util.UUID; + +public enum AnimalOwnerProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_ANIMAL_OWNER = JadeProtocol.mc_id("animal_owner"); + + @Override + public String streamData(@NotNull EntityAccessor accessor) { + return CommonUtil.getLastKnownUsername(getOwnerUUID(accessor.getEntity())); + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.STRING_UTF8.cast(); + } + + public static UUID getOwnerUUID(Entity entity) { + if (entity instanceof OwnableEntity ownableEntity) { + return ownableEntity.getOwnerUUID(); + } + return null; + } + + @Override + public ResourceLocation getUid() { + return MC_ANIMAL_OWNER; + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java new file mode 100644 index 00000000..0acba2f9 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java @@ -0,0 +1,44 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.allay.Allay; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum MobBreedingProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_MOB_BREEDING = JadeProtocol.mc_id("mob_breeding"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = 0; + Entity entity = accessor.getEntity(); + if (entity instanceof Allay allay) { + if (allay.duplicationCooldown > 0 && allay.duplicationCooldown < Integer.MAX_VALUE) { + time = (int) allay.duplicationCooldown; + } + } else { + time = ((Animal) entity).getAge(); + } + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_MOB_BREEDING; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java new file mode 100644 index 00000000..44f5f4b8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java @@ -0,0 +1,43 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.frog.Tadpole; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum MobGrowthProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_MOB_GROWTH = JadeProtocol.mc_id("mob_growth"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = -1; + Entity entity = accessor.getEntity(); + if (entity instanceof AgeableMob ageable) { + time = -ageable.getAge(); + } else if (entity instanceof Tadpole tadpole) { + time = tadpole.getTicksLeftUntilAdult(); + } + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + + @Override + public ResourceLocation getUid() { + return MC_MOB_GROWTH; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java new file mode 100644 index 00000000..892911a3 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java @@ -0,0 +1,35 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.animal.Chicken; +import net.minecraft.world.entity.animal.armadillo.Armadillo; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +public enum NextEntityDropProvider implements IServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_NEXT_ENTITY_DROP = JadeProtocol.mc_id("next_entity_drop"); + + @Override + public void appendServerData(CompoundTag tag, @NotNull EntityAccessor accessor) { + int max = 24000 * 2; + if (accessor.getEntity() instanceof Chicken chicken) { + if (!chicken.isBaby() && chicken.eggTime < max) { + tag.putInt("NextEggIn", chicken.eggTime); + } + } else if (accessor.getEntity() instanceof Armadillo armadillo) { + if (!armadillo.isBaby() && armadillo.scuteTime < max) { + tag.putInt("NextScuteIn", armadillo.scuteTime); + } + } + } + + @Override + public ResourceLocation getUid() { + return MC_NEXT_ENTITY_DROP; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java new file mode 100644 index 00000000..cb8d1736 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java @@ -0,0 +1,35 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum PetArmorProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_PET_ARMOR = JadeProtocol.mc_id("pet_armor"); + + @Nullable + @Override + public ItemStack streamData(@NotNull EntityAccessor accessor) { + ItemStack armor = ((Mob) accessor.getEntity()).getBodyArmorItem(); + return armor.isEmpty() ? null : armor; + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_PET_ARMOR; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java new file mode 100644 index 00000000..ffc57187 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java @@ -0,0 +1,45 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.LivingEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +import java.util.List; + +public enum StatusEffectsProvider implements StreamServerDataProvider> { + INSTANCE; + + + private static final StreamCodec> STREAM_CODEC = ByteBufCodecs.list() + .apply(MobEffectInstance.STREAM_CODEC); + private static final ResourceLocation MC_POTION_EFFECTS = JadeProtocol.mc_id("potion_effects"); + + @Override + @Nullable + public List streamData(@NotNull EntityAccessor accessor) { + List effects = ((LivingEntity) accessor.getEntity()).getActiveEffects() + .stream() + .filter(MobEffectInstance::isVisible) + .toList(); + return effects.isEmpty() ? null : effects; + } + + @Override + public StreamCodec> streamCodec() { + return STREAM_CODEC; + } + + + @Override + public ResourceLocation getUid() { + return MC_POTION_EFFECTS; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java new file mode 100644 index 00000000..b7c9afd2 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java @@ -0,0 +1,34 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.monster.ZombieVillager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum ZombieVillagerProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_ZOMBIE_VILLAGER = JadeProtocol.mc_id("zombie_villager"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = ((ZombieVillager) accessor.getEntity()).villagerConversionTime; + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_ZOMBIE_VILLAGER; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java new file mode 100644 index 00000000..6a9cd9f0 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java @@ -0,0 +1,32 @@ +package org.leavesmc.leaves.protocol.jade.tool; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; + +import java.util.List; + +public class ShearsToolHandler extends SimpleToolHandler { + + private static final ShearsToolHandler INSTANCE = new ShearsToolHandler(); + + public static ShearsToolHandler getInstance() { + return INSTANCE; + } + + public ShearsToolHandler() { + super(JadeProtocol.id("shears"), List.of(Items.SHEARS.getDefaultInstance()), true); + } + + @Override + public ItemStack test(BlockState state, Level world, BlockPos pos) { + if (state.is(Blocks.TRIPWIRE)) { + return tools.getFirst(); + } + return super.test(state, world, pos); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java new file mode 100644 index 00000000..d45ecdb1 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java @@ -0,0 +1,71 @@ +package org.leavesmc.leaves.protocol.jade.tool; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Lists; +import net.minecraft.core.BlockPos; +import net.minecraft.core.component.DataComponents; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.Tool; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class SimpleToolHandler implements ToolHandler { + + protected final List tools = Lists.newArrayList(); + private final ResourceLocation uid; + private final boolean skipInstaBreakingBlock; + + protected SimpleToolHandler(ResourceLocation uid, @NotNull List tools, boolean skipInstaBreakingBlock) { + this.uid = uid; + Preconditions.checkArgument(!tools.isEmpty(), "tools cannot be empty"); + this.tools.addAll(tools); + this.skipInstaBreakingBlock = skipInstaBreakingBlock; + } + + @Contract("_, _ -> new") + public static @NotNull SimpleToolHandler create(ResourceLocation uid, List tools) { + return create(uid, tools, true); + } + + @Contract("_, _, _ -> new") + public static @NotNull SimpleToolHandler create(ResourceLocation uid, List tools, boolean skipInstaBreakingBlock) { + return new SimpleToolHandler(uid, Lists.transform(tools, Item::getDefaultInstance), skipInstaBreakingBlock); + } + + @Override + public ItemStack test(BlockState state, Level world, BlockPos pos) { + if (skipInstaBreakingBlock && !state.requiresCorrectToolForDrops() && state.getDestroySpeed(world, pos) == 0) { + return ItemStack.EMPTY; + } + return test(state); + } + + public ItemStack test(BlockState state) { + for (ItemStack toolItem : tools) { + if (toolItem.isCorrectToolForDrops(state)) { + return toolItem; + } + Tool tool = toolItem.get(DataComponents.TOOL); + if (tool != null && tool.getMiningSpeed(state) > tool.defaultMiningSpeed()) { + return toolItem; + } + } + return ItemStack.EMPTY; + } + + @Override + public List getTools() { + return tools; + } + + @Override + public ResourceLocation getUid() { + return uid; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java new file mode 100644 index 00000000..18f11e70 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java @@ -0,0 +1,17 @@ +package org.leavesmc.leaves.protocol.jade.tool; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +import java.util.List; + +public interface ToolHandler extends IJadeProvider { + + ItemStack test(BlockState state, Level world, BlockPos pos); + + List getTools(); + +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java new file mode 100644 index 00000000..a0a85361 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java @@ -0,0 +1,79 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.mojang.authlib.GameProfile; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.boss.EnderDragonPart; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.SkullBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class CommonUtil { + + public static @NotNull ResourceLocation getId(Block block) { + return BuiltInRegistries.BLOCK.getKey(block); + } + + public static Entity wrapPartEntityParent(Entity target) { + if (target instanceof EnderDragonPart part) { + return part.parentMob; + } + return target; + } + + public static Entity getPartEntity(Entity parent, int index) { + if (parent == null) { + return null; + } + if (index < 0) { + return parent; + } + if (parent instanceof EnderDragon dragon) { + EnderDragonPart[] parts = dragon.getSubEntities(); + if (index < parts.length) { + return parts[index]; + } + } + return parent; + } + + + @Nullable + public static String getLastKnownUsername(@Nullable UUID uuid) { + if (uuid == null) { + return null; + } + Optional optional = SkullBlockEntity.fetchGameProfile(String.valueOf(uuid)).getNow(Optional.empty()); + return optional.map(GameProfile::getName).orElse(null); + } + + + public static Map.Entry>> getServerExtensionData( + Accessor accessor, + WrappedHierarchyLookup> lookup) { + for (var provider : lookup.wrappedGet(accessor)) { + List> groups; + try { + groups = provider.getGroups(accessor); + } catch (Exception e) { + LeavesLogger.LOGGER.severe(e.toString()); + continue; + } + if (groups != null) { + return Map.entry(provider.getUid(), groups); + } + } + return null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java new file mode 100644 index 00000000..0070fd22 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java @@ -0,0 +1,139 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +public class HierarchyLookup implements IHierarchyLookup { + private final Class baseClass; + private final Cache, List> resultCache = CacheBuilder.newBuilder().build(); + private final boolean singleton; + protected boolean idMapped; + @Nullable + protected IdMapper idMapper; + private ListMultimap, T> objects = ArrayListMultimap.create(); + + public HierarchyLookup(Class baseClass) { + this(baseClass, false); + } + + public HierarchyLookup(Class baseClass, boolean singleton) { + this.baseClass = baseClass; + this.singleton = singleton; + } + + @Override + public void idMapped() { + this.idMapped = true; + } + + @Override + @Nullable + public IdMapper idMapper() { + return idMapper; + } + + @Override + public void register(Class clazz, T provider) { + Preconditions.checkArgument(isClassAcceptable(clazz), "Class %s is not acceptable", clazz); + Objects.requireNonNull(provider.getUid()); + JadeProtocol.priorities.put(provider); + objects.put(clazz, provider); + } + + @Override + public boolean isClassAcceptable(Class clazz) { + return baseClass.isAssignableFrom(clazz); + } + + @Override + public List get(Class clazz) { + try { + return resultCache.get(clazz, () -> { + List list = Lists.newArrayList(); + getInternal(clazz, list); + list = ImmutableList.sortedCopyOf(Comparator.comparingInt(JadeProtocol.priorities::byValue), list); + if (singleton && !list.isEmpty()) { + return ImmutableList.of(list.getFirst()); + } + return list; + }); + } catch (ExecutionException e) { + LeavesLogger.LOGGER.warning("HierarchyLookup error", e); + } + return List.of(); + } + + private void getInternal(Class clazz, List list) { + if (clazz != baseClass && clazz != Object.class) { + getInternal(clazz.getSuperclass(), list); + } + list.addAll(objects.get(clazz)); + } + + @Override + public boolean isEmpty() { + return objects.isEmpty(); + } + + @Override + public Stream, Collection>> entries() { + return objects.asMap().entrySet().stream(); + } + + @Override + public void invalidate() { + resultCache.invalidateAll(); + } + + @Override + public void loadComplete(PriorityStore priorityStore) { + objects.asMap().forEach((clazz, list) -> { + if (list.size() < 2) { + return; + } + Set set = Sets.newHashSetWithExpectedSize(list.size()); + for (T provider : list) { + if (set.contains(provider.getUid())) { + throw new IllegalStateException("Duplicate UID: %s for %s".formatted(provider.getUid(), list.stream() + .filter(p -> p.getUid().equals(provider.getUid())) + .map(p -> p.getClass().getName()) + .toList() + )); + } + set.add(provider.getUid()); + } + }); + + objects = ImmutableListMultimap., T>builder() + .orderValuesBy(Comparator.comparingInt(priorityStore::byValue)) + .putAll(objects) + .build(); + + if (idMapped) { + idMapper = createIdMapper(); + } + } + +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java new file mode 100644 index 00000000..0536309c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java @@ -0,0 +1,66 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.google.common.collect.Streams; +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Stream; + +public interface IHierarchyLookup { + default IHierarchyLookup cast() { + return this; + } + + void idMapped(); + + @Nullable + IdMapper idMapper(); + + default List mappedIds() { + return Streams.stream(Objects.requireNonNull(idMapper())) + .map(IJadeProvider::getUid) + .toList(); + } + + void register(Class clazz, T provider); + + boolean isClassAcceptable(Class clazz); + + default List get(Object obj) { + if (obj == null) { + return List.of(); + } + return get(obj.getClass()); + } + + List get(Class clazz); + + boolean isEmpty(); + + Stream, Collection>> entries(); + + void invalidate(); + + void loadComplete(PriorityStore priorityStore); + + default IdMapper createIdMapper() { + List list = entries().flatMap(entry -> entry.getValue().stream()).toList(); + IdMapper idMapper = idMapper(); + if (idMapper == null) { + idMapper = new IdMapper<>(list.size()); + } + for (T provider : list) { + if (idMapper.getId(provider) == IdMapper.DEFAULT) { + idMapper.add(provider); + } + } + return idMapper; + } +} + diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java new file mode 100644 index 00000000..408c81a0 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java @@ -0,0 +1,118 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; + +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +public class ItemCollector { + public static final int MAX_SIZE = 54; + public static final ItemCollector EMPTY = new ItemCollector<>(null); + private static final Predicate NON_EMPTY = stack -> { + if (stack.isEmpty()) { + return false; + } + CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY); + if (customData.contains("CustomModelData")) { + CompoundTag tag = customData.copyTag(); + for (String key : tag.getAllKeys()) { + if (key.toLowerCase(Locale.ENGLISH).endsWith("clear") && tag.getBoolean(key)) { + return false; + } + } + } + return true; + }; + private final Object2IntLinkedOpenHashMap items = new Object2IntLinkedOpenHashMap<>(); + private final ItemIterator iterator; + public long version; + public long lastTimeFinished; + public boolean lastTimeIsEmpty; + public List> mergedResult; + + public ItemCollector(ItemIterator iterator) { + this.iterator = iterator; + } + + public List> update(Accessor request) { + if (iterator == null) { + return null; + } + T container = iterator.find(request.getTarget()); + if (container == null) { + return null; + } + long currentVersion = iterator.getVersion(container); + long gameTime = request.getLevel().getGameTime(); + if (mergedResult != null && iterator.isFinished()) { + if (version == currentVersion) { + return mergedResult; // content not changed + } + if (lastTimeFinished + 5 > gameTime) { + return mergedResult; // avoid update too frequently + } + iterator.reset(); + } + AtomicInteger count = new AtomicInteger(); + iterator.populate(container).forEach(stack -> { + count.incrementAndGet(); + if (NON_EMPTY.test(stack)) { + ItemDefinition def = new ItemDefinition(stack); + items.addTo(def, stack.getCount()); + } + }); + iterator.afterPopulate(count.get()); + if (mergedResult != null && !iterator.isFinished()) { + updateCollectingProgress(mergedResult.getFirst()); + return mergedResult; + } + List partialResult = items.object2IntEntrySet().stream().limit(MAX_SIZE ).map(entry -> { + ItemDefinition def = entry.getKey(); + return def.toStack(entry.getIntValue()); + }).toList(); + List> groups = List.of(updateCollectingProgress(new ViewGroup<>(partialResult))); + if (iterator.isFinished()) { + mergedResult = groups; + lastTimeIsEmpty = mergedResult.getFirst().views.isEmpty(); + version = currentVersion; + lastTimeFinished = gameTime; + items.clear(); + } + return groups; + } + + protected ViewGroup updateCollectingProgress(ViewGroup group) { + if (lastTimeIsEmpty && group.views.isEmpty()) { + return group; + } + float progress = iterator.getCollectingProgress(); + CompoundTag data = group.getExtraData(); + if (Float.isNaN(progress) || progress >= 1) { + data.remove("Collecting"); + } else { + data.putFloat("Collecting", progress); + } + return group; + } + + public record ItemDefinition(Item item, DataComponentPatch components) { + ItemDefinition(ItemStack stack) { + this(stack.getItem(), stack.getComponentsPatch()); + } + + public ItemStack toStack(int count) { + ItemStack itemStack = new ItemStack(item, count); + itemStack.applyComponents(components); + return itemStack; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java new file mode 100644 index 00000000..4d65e9a8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java @@ -0,0 +1,102 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public abstract class ItemIterator { + public static final AtomicLong version = new AtomicLong(); + protected final Function containerFinder; + protected final int fromIndex; + protected boolean finished; + protected int currentIndex; + + protected ItemIterator(Function containerFinder, int fromIndex) { + this.containerFinder = containerFinder; + this.currentIndex = this.fromIndex = fromIndex; + } + + public @Nullable T find(Object target) { + return containerFinder.apply(target); + } + + public final boolean isFinished() { + return finished; + } + + public long getVersion(T container) { + return version.getAndIncrement(); + } + + public abstract Stream populate(T container); + + public void reset() { + currentIndex = fromIndex; + finished = false; + } + + public void afterPopulate(int count) { + currentIndex += count; + if (count == 0 || currentIndex >= 10000) { + finished = true; + } + } + + public float getCollectingProgress() { + return Float.NaN; + } + + public static abstract class SlottedItemIterator extends ItemIterator { + protected float progress; + + public SlottedItemIterator(Function containerFinder, int fromIndex) { + super(containerFinder, fromIndex); + } + + protected abstract int getSlotCount(T container); + + protected abstract ItemStack getItemInSlot(T container, int slot); + + @Override + public Stream populate(T container) { + int slotCount = getSlotCount(container); + int toIndex = currentIndex + ItemCollector.MAX_SIZE * 2; + if (toIndex >= slotCount) { + toIndex = slotCount; + finished = true; + } + progress = (float) (currentIndex - fromIndex) / (slotCount - fromIndex); + return IntStream.range(currentIndex, toIndex).mapToObj(slot -> getItemInSlot(container, slot)); + } + + @Override + public float getCollectingProgress() { + return progress; + } + } + + public static class ContainerItemIterator extends SlottedItemIterator { + public ContainerItemIterator(int fromIndex) { + this(Container.class::cast, fromIndex); + } + + public ContainerItemIterator(Function containerFinder, int fromIndex) { + super(containerFinder, fromIndex); + } + + @Override + protected int getSlotCount(Container container) { + return container.getContainerSize(); + } + + @Override + protected ItemStack getItemInSlot(Container container, int slot) { + return container.getItem(slot); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java new file mode 100644 index 00000000..a046ae4e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java @@ -0,0 +1,59 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.NotNull; + +public class JadeCodec { + public static final StreamCodec PRIMITIVE_STREAM_CODEC = new StreamCodec<>() { + @Override + public @NotNull Object decode(@NotNull ByteBuf buf) { + byte b = buf.readByte(); + if (b == 0) { + return false; + } else if (b == 1) { + return true; + } else if (b == 2) { + return ByteBufCodecs.VAR_INT.decode(buf); + } else if (b == 3) { + return ByteBufCodecs.FLOAT.decode(buf); + } else if (b == 4) { + return ByteBufCodecs.STRING_UTF8.decode(buf); + } else if (b > 20) { + return b - 20; + } + throw new IllegalArgumentException("Unknown primitive type: " + b); + } + + @Override + public void encode(@NotNull ByteBuf buf, @NotNull Object o) { + switch (o) { + case Boolean b -> buf.writeByte(b ? 1 : 0); + case Number n -> { + float f = n.floatValue(); + if (f != (int) f) { + buf.writeByte(3); + ByteBufCodecs.FLOAT.encode(buf, f); + } + int i = n.intValue(); + if (i <= Byte.MAX_VALUE - 20 && i >= 0) { + buf.writeByte(i + 20); + } else { + ByteBufCodecs.VAR_INT.encode(buf, i); + } + } + case String s -> { + buf.writeByte(4); + ByteBufCodecs.STRING_UTF8.encode(buf, s); + } + case Enum anEnum -> { + buf.writeByte(4); + ByteBufCodecs.STRING_UTF8.encode(buf, anEnum.name()); + } + case null -> throw new NullPointerException(); + default -> throw new IllegalArgumentException("Unknown primitive type: %s (%s)".formatted(o, o.getClass())); + } + } + }; +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java new file mode 100644 index 00000000..81575cfa --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java @@ -0,0 +1,109 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.google.common.collect.Lists; +import net.minecraft.advancements.critereon.ItemPredicate; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.storage.loot.LootPool; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraft.world.level.storage.loot.entries.AlternativesEntry; +import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; +import net.minecraft.world.level.storage.loot.entries.NestedLootTable; +import net.minecraft.world.level.storage.loot.predicates.AnyOfCondition; +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; +import net.minecraft.world.level.storage.loot.predicates.MatchTool; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.tool.ShearsToolHandler; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +public class LootTableMineableCollector { + + private final HolderGetter lootRegistry; + private final ItemStack toolItem; + + public LootTableMineableCollector(HolderGetter lootRegistry, ItemStack toolItem) { + this.lootRegistry = lootRegistry; + this.toolItem = toolItem; + } + + public static @NotNull List execute(HolderGetter lootRegistry, ItemStack toolItem) { + LootTableMineableCollector collector = new LootTableMineableCollector(lootRegistry, toolItem); + List list = Lists.newArrayList(); + for (Block block : BuiltInRegistries.BLOCK) { + if (!ShearsToolHandler.getInstance().test(block.defaultBlockState()).isEmpty()) { + continue; + } + + if (block.getLootTable().isPresent()) { + LootTable lootTable = lootRegistry.get(block.getLootTable().get()).map(Holder::value).orElse(null); + if (collector.doLootTable(lootTable)) { + list.add(block); + } + } + } + return list; + } + + private boolean doLootTable(LootTable lootTable) { + if (lootTable == null || lootTable == LootTable.EMPTY) { + return false; + } + + for (LootPool pool : lootTable.pools) { + if (doLootPool(pool)) { + return true; + } + } + return false; + } + + private boolean doLootPool(@NotNull LootPool lootPool) { + for (LootPoolEntryContainer entry : lootPool.entries) { + if (doLootPoolEntry(entry)) { + return true; + } + } + return false; + } + + private boolean doLootPoolEntry(LootPoolEntryContainer entry) { + if (entry instanceof AlternativesEntry alternativesEntry) { + for (LootPoolEntryContainer child : alternativesEntry.children) { + if (doLootPoolEntry(child)) { + return true; + } + } + } else if (entry instanceof NestedLootTable nestedLootTable) { + LootTable lootTable = nestedLootTable.contents.map($ -> lootRegistry.get($).map(Holder::value).orElse(null), Function.identity()); + return doLootTable(lootTable); + } else { + return isCorrectConditions(entry.conditions, toolItem); + } + return false; + } + + public static boolean isCorrectConditions(@NotNull List conditions, ItemStack toolItem) { + if (conditions.size() != 1) { + return false; + } + + LootItemCondition condition = conditions.getFirst(); + if (condition instanceof MatchTool(Optional predicate)) { + ItemPredicate itemPredicate = predicate.orElse(null); + return itemPredicate != null && itemPredicate.test(toolItem); + } else if (condition instanceof AnyOfCondition anyOfCondition) { + for (LootItemCondition child : anyOfCondition.terms) { + if (isCorrectConditions(List.of(child), toolItem)) { + return true; + } + } + } + return false; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java new file mode 100644 index 00000000..cb5c8201 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java @@ -0,0 +1,120 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +public class PairHierarchyLookup implements IHierarchyLookup { + public final IHierarchyLookup first; + public final IHierarchyLookup second; + private final Cache, Class>, List> mergedCache = CacheBuilder.newBuilder().build(); + protected boolean idMapped; + @Nullable + protected IdMapper idMapper; + + public PairHierarchyLookup(IHierarchyLookup first, IHierarchyLookup second) { + this.first = first; + this.second = second; + } + + @SuppressWarnings("unchecked") + public List getMerged(Object first, Object second) { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + try { + return (List) mergedCache.get(Pair.of(first.getClass(), second.getClass()), () -> { + List firstList = this.first.get(first); + List secondList = this.second.get(second); + if (firstList.isEmpty()) { + return secondList; + } else if (secondList.isEmpty()) { + return firstList; + } + return ImmutableList.sortedCopyOf( + Comparator.comparingInt(JadeProtocol.priorities::byValue), + Iterables.concat(firstList, secondList) + ); + }); + } catch (ExecutionException e) { + LeavesLogger.LOGGER.severe(e.toString()); + } + return List.of(); + } + + @Override + public void idMapped() { + idMapped = true; + } + + @Override + public @Nullable IdMapper idMapper() { + return idMapper; + } + + @Override + public void register(Class clazz, T provider) { + if (first.isClassAcceptable(clazz)) { + first.register(clazz, provider); + } else if (second.isClassAcceptable(clazz)) { + second.register(clazz, provider); + } else { + throw new IllegalArgumentException("Class " + clazz + " is not acceptable"); + } + } + + @Override + public boolean isClassAcceptable(Class clazz) { + return first.isClassAcceptable(clazz) || second.isClassAcceptable(clazz); + } + + @Override + public List get(Class clazz) { + List result = first.get(clazz); + if (result.isEmpty()) { + result = second.get(clazz); + } + return result; + } + + @Override + public boolean isEmpty() { + return first.isEmpty() && second.isEmpty(); + } + + @Override + public Stream, Collection>> entries() { + return Stream.concat(first.entries(), second.entries()); + } + + @Override + public void invalidate() { + first.invalidate(); + second.invalidate(); + mergedCache.invalidateAll(); + } + + @Override + public void loadComplete(PriorityStore priorityStore) { + first.loadComplete(priorityStore); + second.loadComplete(priorityStore); + if (idMapped) { + idMapper = createIdMapper(); + } + } +} \ No newline at end of file diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java new file mode 100644 index 00000000..da4d5a77 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java @@ -0,0 +1,40 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; + +import java.util.Objects; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +public class PriorityStore { + + private final Object2IntMap priorities = new Object2IntLinkedOpenHashMap<>(); + private final Function keyGetter; + private final ToIntFunction defaultPriorityGetter; + + public PriorityStore(ToIntFunction defaultPriorityGetter, Function keyGetter) { + this.defaultPriorityGetter = defaultPriorityGetter; + this.keyGetter = keyGetter; + } + + public void put(V provider) { + Objects.requireNonNull(provider); + put(provider, defaultPriorityGetter.applyAsInt(provider)); + } + + public void put(V provider, int priority) { + Objects.requireNonNull(provider); + K uid = keyGetter.apply(provider); + Objects.requireNonNull(uid); + priorities.put(uid, priority); + } + + public int byValue(V value) { + return byKey(keyGetter.apply(value)); + } + + public int byKey(K id) { + return priorities.getInt(id); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java new file mode 100644 index 00000000..520eadbf --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java @@ -0,0 +1,62 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import io.netty.buffer.ByteBuf; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class ViewGroup { + public static StreamCodec> codec(StreamCodec viewCodec) { + return StreamCodec.composite( + ByteBufCodecs.list().apply(viewCodec), + $ -> $.views, + ByteBufCodecs.optional(ByteBufCodecs.STRING_UTF8), + $ -> Optional.ofNullable($.id), + ByteBufCodecs.optional(ByteBufCodecs.COMPOUND_TAG), + $ -> Optional.ofNullable($.extraData), + ViewGroup::new); + } + + public static StreamCodec>>> listCodec(StreamCodec viewCodec) { + return StreamCodec.composite( + ResourceLocation.STREAM_CODEC, + Map.Entry::getKey, + ByteBufCodecs.>list().apply(codec(viewCodec)), + Map.Entry::getValue, + Map::entry); + } + + public List views; + @Nullable + public String id; + @Nullable + protected CompoundTag extraData; + + public ViewGroup(List views) { + this(views, Optional.empty(), Optional.empty()); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public ViewGroup(List views, Optional id, Optional extraData) { + this.views = views; + this.id = id.orElse(null); + this.extraData = extraData.orElse(null); + } + + public CompoundTag getExtraData() { + if (extraData == null) { + extraData = new CompoundTag(); + } + return extraData; + } + + public void setProgress(float progress) { + getExtraData().putFloat("Progress", progress); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java new file mode 100644 index 00000000..9b49efd3 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java @@ -0,0 +1,104 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.google.common.collect.Lists; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Stream; + +public class WrappedHierarchyLookup extends HierarchyLookup { + public final List, Function, @Nullable Object>>> overrides = Lists.newArrayList(); + private boolean empty = true; + + public WrappedHierarchyLookup() { + super(Object.class); + } + + @NotNull + public static WrappedHierarchyLookup forAccessor() { + WrappedHierarchyLookup lookup = new WrappedHierarchyLookup<>(); + lookup.overrides.add(Pair.of( + new HierarchyLookup<>(Block.class), accessor -> { + if (accessor instanceof BlockAccessor blockAccessor) { + return blockAccessor.getBlock(); + } + return null; + })); + return lookup; + } + + public List wrappedGet(Accessor accessor) { + List list = Lists.newArrayList(); + for (var override : overrides) { + Object o = override.getRight().apply(accessor); + if (o != null) { + list.addAll(override.getLeft().get(o)); + } + } + list.addAll(get(accessor.getTarget())); + return list; + } + + @Override + public void register(Class clazz, T provider) { + for (var override : overrides) { + if (override.getLeft().isClassAcceptable(clazz)) { + override.getLeft().register(clazz, provider); + empty = false; + return; + } + } + super.register(clazz, provider); + empty = false; + } + + @Override + public boolean isClassAcceptable(Class clazz) { + for (var override : overrides) { + if (override.getLeft().isClassAcceptable(clazz)) { + return true; + } + } + return super.isClassAcceptable(clazz); + } + + @Override + public void invalidate() { + for (var override : overrides) { + override.getLeft().invalidate(); + } + super.invalidate(); + } + + @Override + public void loadComplete(PriorityStore priorityStore) { + for (var override : overrides) { + override.getLeft().loadComplete(priorityStore); + } + super.loadComplete(priorityStore); + } + + @Override + public boolean isEmpty() { + return empty; + } + + @Override + public Stream, Collection>> entries() { + Stream, Collection>> stream = super.entries(); + for (var override : overrides) { + stream = Stream.concat(stream, override.getLeft().entries()); + } + return stream; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java new file mode 100644 index 00000000..75f384bf --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java @@ -0,0 +1,107 @@ +package org.leavesmc.leaves.protocol.rei; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Contract; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.LeavesProtocolManager.EmptyPayload; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.rei.payload.CreateItemGrabPayload; +import org.leavesmc.leaves.protocol.rei.payload.CreateItemHotbarPayload; +import org.leavesmc.leaves.protocol.rei.payload.CreateItemMessagePayload; +import org.leavesmc.leaves.protocol.rei.payload.CreateItemPayload; + +@LeavesProtocol(namespace = "roughlyenoughitems") +public class REIServerProtocol { + + public static final String PROTOCOL_ID = "roughlyenoughitems"; + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.PayloadReceiver(payload = EmptyPayload.class, payloadId = "delete_item") + public static void handleDeleteItem(ServerPlayer player, EmptyPayload payload) { + if (!check(player, true)) { + return; + } + + AbstractContainerMenu menu = player.containerMenu; + if (!menu.getCarried().isEmpty()) { + menu.setCarried(ItemStack.EMPTY); + menu.broadcastChanges(); + } + } + + @ProtocolHandler.PayloadReceiver(payload = CreateItemPayload.class, payloadId = "create_item") + public static void handleCreateItem(ServerPlayer player, CreateItemPayload payload) { + if (!check(player, true)) { + return; + } + + ItemStack stack = payload.item(); + if (player.getInventory().add(stack.copy())) { + ProtocolUtils.sendPayloadPacket(player, new CreateItemMessagePayload(stack.copy(), player.getScoreboardName())); + } else { + player.displayClientMessage(Component.translatable("text.rei.failed_cheat_items"), false); + } + } + + @ProtocolHandler.PayloadReceiver(payload = CreateItemGrabPayload.class, payloadId = "create_item_grab") + public static void handleCreateItemGrab(ServerPlayer player, CreateItemGrabPayload payload) { + if (!check(player, true)) { + return; + } + + AbstractContainerMenu menu = player.containerMenu; + ItemStack itemStack = payload.item(); + ItemStack stack = itemStack.copy(); + if (!menu.getCarried().isEmpty() && ItemStack.isSameItemSameComponents(menu.getCarried(), stack)) { + stack.setCount(Mth.clamp(stack.getCount() + menu.getCarried().getCount(), 1, stack.getMaxStackSize())); + } else if (!menu.getCarried().isEmpty()) { + return; + } + menu.setCarried(stack.copy()); + menu.broadcastChanges(); + ProtocolUtils.sendPayloadPacket(player, new CreateItemMessagePayload(stack, player.getScoreboardName())); + } + + @ProtocolHandler.PayloadReceiver(payload = CreateItemHotbarPayload.class, payloadId = "create_item_hotbar") + public static void handleCreateItemHotbar(ServerPlayer player, CreateItemHotbarPayload payload) { + if (!check(player, true)) { + return; + } + + ItemStack stack = payload.item(); + int hotbarSlotId = payload.hotbarSlot(); + if (hotbarSlotId >= 0 && hotbarSlotId < 9) { + AbstractContainerMenu menu = player.containerMenu; + player.getInventory().items.set(hotbarSlotId, stack.copy()); + menu.broadcastChanges(); + ProtocolUtils.sendPayloadPacket(player, new CreateItemMessagePayload(stack, player.getScoreboardName())); + } else { + player.displayClientMessage(Component.translatable("text.rei.failed_cheat_items"), false); + } + } + + private static boolean check(ServerPlayer player, boolean needOP) { + if (!LeavesConfig.protocol.reiServerProtocol) { + return false; + } + + if (needOP && MinecraftServer.getServer().getPlayerList().isOp(player.gameProfile)) { // TODO check permission node + player.displayClientMessage(Component.translatable("text.rei.no_permission_cheat").withStyle(ChatFormatting.RED), false); + return false; + } + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemGrabPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemGrabPayload.java new file mode 100644 index 00000000..8be991f8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemGrabPayload.java @@ -0,0 +1,28 @@ +package org.leavesmc.leaves.protocol.rei.payload; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.rei.REIServerProtocol; + +public record CreateItemGrabPayload(ItemStack item) implements LeavesCustomPayload { + + private static final ResourceLocation ID = REIServerProtocol.id("create_item_grab"); + + @New + public CreateItemGrabPayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { + this(buf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC)); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, item); + } + + @Override + public ResourceLocation id() { + return ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemHotbarPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemHotbarPayload.java new file mode 100644 index 00000000..8728f3e6 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemHotbarPayload.java @@ -0,0 +1,28 @@ +package org.leavesmc.leaves.protocol.rei.payload; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.rei.REIServerProtocol; + +public record CreateItemHotbarPayload(ItemStack item, int hotbarSlot) implements LeavesCustomPayload { + + private static final ResourceLocation ID = REIServerProtocol.id("create_item_hotbar"); + + @New + public CreateItemHotbarPayload(ResourceLocation location, FriendlyByteBuf buf) { + this(buf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC), buf.readVarInt()); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, item); + buf.writeVarInt(hotbarSlot); + } + + @Override + public ResourceLocation id() { + return ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemMessagePayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemMessagePayload.java new file mode 100644 index 00000000..ef6c70f8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemMessagePayload.java @@ -0,0 +1,29 @@ +package org.leavesmc.leaves.protocol.rei.payload; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.rei.REIServerProtocol; + +public record CreateItemMessagePayload(ItemStack item, String playerName) implements LeavesCustomPayload { + + private static final ResourceLocation ID = REIServerProtocol.id("ci_msg"); + + @New + public CreateItemMessagePayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { + this(buf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC), buf.readUtf()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, item); + buf.writeUtf(playerName); + } + + @Override + public ResourceLocation id() { + return ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemPayload.java new file mode 100644 index 00000000..29ea16b2 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/CreateItemPayload.java @@ -0,0 +1,28 @@ +package org.leavesmc.leaves.protocol.rei.payload; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.rei.REIServerProtocol; + +public record CreateItemPayload(ItemStack item) implements LeavesCustomPayload { + + private static final ResourceLocation ID = REIServerProtocol.id("create_item"); + + @New + public CreateItemPayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { + this(buf.readJsonWithCodec(ItemStack.OPTIONAL_CODEC)); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, item); + } + + @Override + public ResourceLocation id() { + return ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/MoveItemPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/MoveItemPayload.java new file mode 100644 index 00000000..760aae1d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/MoveItemPayload.java @@ -0,0 +1,30 @@ +package org.leavesmc.leaves.protocol.rei.payload; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.rei.REIServerProtocol; + +public record MoveItemPayload(ResourceLocation category, boolean isShift, CompoundTag nbt) implements LeavesCustomPayload { + + private static final ResourceLocation ID = REIServerProtocol.id("move_items_new"); + + @New + public MoveItemPayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { + this(buf.readResourceLocation(), buf.readBoolean(), buf.readNbt()); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeResourceLocation(category); + buf.writeBoolean(isShift); + buf.writeNbt(nbt); + } + + @Override + public ResourceLocation id() { + return ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java new file mode 100644 index 00000000..3a0e790f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java @@ -0,0 +1,117 @@ +package org.leavesmc.leaves.protocol.servux; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; + +import javax.annotation.Nullable; +import java.util.HashMap; +import java.util.Map; + +// Powered by Servux(https://github.com/sakura-ryoko/servux) + +/** + * Network packet splitter code from QuickCarpet by skyrising + * + * @author skyrising + *

+ * Updated by Sakura to work with newer versions by changing the Reading Session keys, + * and using the HANDLER interface to send packets via the Payload system + *

+ * Move to Leaves by violetc + */ +public class PacketSplitter { + public static final int MAX_TOTAL_PER_PACKET_S2C = 1048576; + public static final int MAX_PAYLOAD_PER_PACKET_S2C = MAX_TOTAL_PER_PACKET_S2C - 5; + public static final int MAX_TOTAL_PER_PACKET_C2S = 32767; + public static final int MAX_PAYLOAD_PER_PACKET_C2S = MAX_TOTAL_PER_PACKET_C2S - 5; + public static final int DEFAULT_MAX_RECEIVE_SIZE_C2S = 1048576; + public static final int DEFAULT_MAX_RECEIVE_SIZE_S2C = 67108864; + + private static final Map READING_SESSIONS = new HashMap<>(); + + public static boolean send(IPacketSplitterHandler handler, FriendlyByteBuf packet, ServerPlayer player) { + return send(handler, packet, player, MAX_PAYLOAD_PER_PACKET_S2C); + } + + private static boolean send(IPacketSplitterHandler handler, FriendlyByteBuf packet, ServerPlayer player, int payloadLimit) { + int len = packet.writerIndex(); + + packet.resetReaderIndex(); + + for (int offset = 0; offset < len; offset += payloadLimit) { + int thisLen = Math.min(len - offset, payloadLimit); + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(thisLen)); + + buf.resetWriterIndex(); + + if (offset == 0) { + buf.writeVarInt(len); + } + + buf.writeBytes(packet, thisLen); + handler.encode(player, buf); + } + + packet.release(); + + return true; + } + + public static FriendlyByteBuf receive(long key, FriendlyByteBuf buf) { + return receive(key, buf, DEFAULT_MAX_RECEIVE_SIZE_S2C); + } + + @Nullable + private static FriendlyByteBuf receive(long key, FriendlyByteBuf buf, int maxLength) { + return READING_SESSIONS.computeIfAbsent(key, ReadingSession::new).receive(buf, maxLength); + } + + /** + * I had to fix the `Pair.of` key mappings, because they were removed from MC; + * So I made it into a pre-shared random session 'key' between client and server. + * Generated using 'long key = Random.create(Util.getMeasuringTimeMs()).nextLong();' + * - + * It can be shared to the receiving end via a separate packet; or it can just be + * generated randomly on the receiving end per an expected Reading Session. + * It needs to be stored and changed for every unique session. + */ + private static class ReadingSession { + private final long key; + private int expectedSize = -1; + private FriendlyByteBuf received; + + private ReadingSession(long key) { + this.key = key; + } + + @Nullable + private FriendlyByteBuf receive(FriendlyByteBuf data, int maxLength) { + data.readerIndex(0); + // data = PacketUtils.slice(data); + + if (this.expectedSize < 0) { + this.expectedSize = data.readVarInt(); + + if (this.expectedSize > maxLength) { + throw new IllegalArgumentException("Payload too large"); + } + + this.received = new FriendlyByteBuf(Unpooled.buffer(this.expectedSize)); + } + + this.received.writeBytes(data.readBytes(data.readableBytes())); + + if (this.received.writerIndex() >= this.expectedSize) { + READING_SESSIONS.remove(this.key); + return this.received; + } + + return null; + } + } + + public interface IPacketSplitterHandler { + void encode(ServerPlayer player, FriendlyByteBuf buf); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java new file mode 100644 index 00000000..36cc207c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java @@ -0,0 +1,278 @@ +package org.leavesmc.leaves.protocol.servux; + +import io.netty.buffer.Unpooled; +import net.minecraft.Util; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.RandomSource; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.entity.BlockEntity; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +// Powered by Servux(https://github.com/sakura-ryoko/servux) + +@LeavesProtocol(namespace = "servux") +public class ServuxEntityDataProtocol { + + public static final ResourceLocation CHANNEL = ServuxProtocol.id("entity_data"); + public static final int PROTOCOL_VERSION = 1; + + private static final Map readingSessionKeys = new HashMap<>(); + + @ProtocolHandler.PlayerJoin + public static void onPlayerLoggedIn(ServerPlayer player) { + if (!LeavesConfig.protocol.servux.entityProtocol) { + return; + } + + sendMetadata(player); + } + + @ProtocolHandler.PayloadReceiver(payload = EntityDataPayload.class, payloadId = "entity_data") + public static void onPacketReceive(ServerPlayer player, EntityDataPayload payload) { + if (!LeavesConfig.protocol.servux.entityProtocol) { + return; + } + + switch (payload.packetType) { + case PACKET_C2S_METADATA_REQUEST -> sendMetadata(player); + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> onBlockEntityRequest(player, payload.pos); + case PACKET_C2S_ENTITY_REQUEST -> onEntityRequest(player, payload.entityId); + case PACKET_C2S_NBT_RESPONSE_DATA -> { + UUID uuid = player.getUUID(); + long readingSessionKey; + + if (!readingSessionKeys.containsKey(uuid)) { + readingSessionKey = RandomSource.create(Util.getMillis()).nextLong(); + readingSessionKeys.put(uuid, readingSessionKey); + } else { + readingSessionKey = readingSessionKeys.get(uuid); + } + + FriendlyByteBuf fullPacket = PacketSplitter.receive(readingSessionKey, payload.buffer); + + if (fullPacket != null) { + readingSessionKeys.remove(uuid); + LeavesLogger.LOGGER.warning("ServuxEntityDataProtocol,PACKET_C2S_NBT_RESPONSE_DATA NOT Implemented!"); + } + } + } + } + + public static void sendMetadata(ServerPlayer player) { + CompoundTag metadata = new CompoundTag(); + metadata.putString("name", "entity_data"); + metadata.putString("id", CHANNEL.toString()); + metadata.putInt("version", PROTOCOL_VERSION); + metadata.putString("servux", ServuxProtocol.SERVUX_STRING); + + EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_METADATA); + payload.nbt.merge(metadata); + sendPacket(player, payload); + } + + public static void onBlockEntityRequest(ServerPlayer player, BlockPos pos) { + MinecraftServer.getServer().execute(() -> { + BlockEntity be = player.serverLevel().getBlockEntity(pos); + CompoundTag nbt = be != null ? be.saveWithoutMetadata(player.registryAccess()) : new CompoundTag(); + + EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE); + payload.pos = pos.immutable(); + payload.nbt.merge(nbt); + sendPacket(player, payload); + }); + } + + public static void onEntityRequest(ServerPlayer player, int entityId) { + MinecraftServer.getServer().execute(() -> { + Entity entity = player.serverLevel().getEntity(entityId); + CompoundTag nbt = entity != null ? entity.saveWithoutId(new CompoundTag()) : new CompoundTag(); + + EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE); + payload.entityId = entityId; + payload.nbt.merge(nbt); + sendPacket(player, payload); + }); + } + + public static void sendPacket(ServerPlayer player, EntityDataPayload payload) { + if (!LeavesConfig.protocol.servux.entityProtocol) { + return; + } + + if (payload.packetType == EntityDataPayloadType.PACKET_S2C_NBT_RESPONSE_START) { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + buffer.writeNbt(payload.nbt); + PacketSplitter.send(ServuxEntityDataProtocol::sendWithSplitter, buffer, player); + } else { + ProtocolUtils.sendPayloadPacket(player, payload); + } + } + + private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { + EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_NBT_RESPONSE_DATA); + payload.buffer = buf; + payload.nbt = new CompoundTag(); + sendPacket(player, payload); + } + + public enum EntityDataPayloadType { + PACKET_S2C_METADATA(1), + PACKET_C2S_METADATA_REQUEST(2), + PACKET_C2S_BLOCK_ENTITY_REQUEST(3), + PACKET_C2S_ENTITY_REQUEST(4), + PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE(5), + PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE(6), + // For Packet Splitter (Oversize Packets, S2C) + PACKET_S2C_NBT_RESPONSE_START(10), + PACKET_S2C_NBT_RESPONSE_DATA(11), + // For Packet Splitter (Oversize Packets, C2S) + PACKET_C2S_NBT_RESPONSE_START(12), + PACKET_C2S_NBT_RESPONSE_DATA(13); + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + + public final int type; + + EntityDataPayloadType(int type) { + this.type = type; + EntityDataPayloadType.Helper.ID_TO_TYPE.put(type, this); + } + + public static EntityDataPayloadType fromId(int id) { + return EntityDataPayloadType.Helper.ID_TO_TYPE.get(id); + } + } + + public static class EntityDataPayload implements LeavesCustomPayload { + + private final EntityDataPayloadType packetType; + private final int transactionId; + private int entityId; + private BlockPos pos; + private CompoundTag nbt; + private FriendlyByteBuf buffer; + + private EntityDataPayload(EntityDataPayloadType type) { + this.packetType = type; + this.transactionId = -1; + this.entityId = -1; + this.pos = BlockPos.ZERO; + this.nbt = new CompoundTag(); + this.clearPacket(); + } + + private void clearPacket() { + if (this.buffer != null) { + this.buffer.clear(); + this.buffer = new FriendlyByteBuf(Unpooled.buffer()); + } + } + + @New + public static EntityDataPayload decode(ResourceLocation location, FriendlyByteBuf buffer) { + EntityDataPayloadType type = EntityDataPayloadType.fromId(buffer.readVarInt()); + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } + + EntityDataPayload payload = new EntityDataPayload(type); + switch (type) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + buffer.readVarInt(); + payload.pos = buffer.readBlockPos().immutable(); + } + + case PACKET_C2S_ENTITY_REQUEST -> { + buffer.readVarInt(); + payload.entityId = buffer.readVarInt(); + } + + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + payload.pos = buffer.readBlockPos().immutable(); + CompoundTag nbt = buffer.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + payload.entityId = buffer.readVarInt(); + CompoundTag nbt = buffer.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + + case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> { + payload.buffer = new FriendlyByteBuf(buffer.readBytes(buffer.readableBytes())); + payload.nbt = new CompoundTag(); + } + + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> { + CompoundTag nbt = buffer.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + } + + return payload; + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.packetType.type); + + switch (this.packetType) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + buf.writeVarInt(this.transactionId); + buf.writeBlockPos(this.pos); + } + + case PACKET_C2S_ENTITY_REQUEST -> { + buf.writeVarInt(this.transactionId); + buf.writeVarInt(this.entityId); + } + + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + buf.writeBlockPos(this.pos); + buf.writeNbt(this.nbt); + } + + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + buf.writeVarInt(this.entityId); + buf.writeNbt(this.nbt); + } + + case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> { + buf.writeBytes(this.buffer.readBytes(this.buffer.readableBytes())); + } + + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> { + buf.writeNbt(this.nbt); + } + } + } + + @Override + public ResourceLocation id() { + return CHANNEL; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java new file mode 100644 index 00000000..f56ac05a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java @@ -0,0 +1,16 @@ +package org.leavesmc.leaves.protocol.servux; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Contract; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +public class ServuxProtocol { + + public static final String PROTOCOL_ID = "servux"; + public static final String SERVUX_STRING = ProtocolUtils.buildProtocolVersion(PROTOCOL_ID); + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java new file mode 100644 index 00000000..fb5779d4 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java @@ -0,0 +1,452 @@ +package org.leavesmc.leaves.protocol.servux; + +import io.netty.buffer.Unpooled; +import it.unimi.dsi.fastutil.longs.LongIterator; +import it.unimi.dsi.fastutil.longs.LongOpenHashSet; +import it.unimi.dsi.fastutil.longs.LongSet; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.GameRules; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +// Powered by Servux(https://github.com/sakura-ryoko/servux) + +@LeavesProtocol(namespace = "servux") +public class ServuxStructuresProtocol { + + public static final int PROTOCOL_VERSION = 2; + + private static final int updateInterval = 40; + private static final int timeout = 30 * 20; + + public static boolean refreshSpawnMetadata = false; + private static int retainDistance; + + public static final ResourceLocation CHANNEL = ServuxProtocol.id("structures"); + + private static final Map players = new ConcurrentHashMap<>(); + private static final Map> timeouts = new HashMap<>(); + + @ProtocolHandler.PayloadReceiver(payload = StructuresPayload.class, payloadId = "structures") + public static void onPacketReceive(ServerPlayer player, StructuresPayload payload) { + if (!LeavesConfig.protocol.servux.structureProtocol) { + return; + } + + switch (payload.packetType()) { + case PACKET_C2S_STRUCTURES_REGISTER -> onPlayerSubscribed(player); + case PACKET_C2S_REQUEST_SPAWN_METADATA -> refreshSpawnMetadata(player); + case PACKET_C2S_STRUCTURES_UNREGISTER -> { + onPlayerLoggedOut(player); + refreshSpawnMetadata(player); + } + } + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerLoggedIn(ServerPlayer player) { + if (!LeavesConfig.protocol.servux.structureProtocol) { + return; + } + + onPlayerSubscribed(player); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + if (!LeavesConfig.protocol.servux.structureProtocol) { + return; + } + + players.remove(player.getId()); + } + + @ProtocolHandler.Ticker + public static void tick() { + if (!LeavesConfig.protocol.servux.structureProtocol) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + int tickCounter = server.getTickCount(); + if ((tickCounter % updateInterval) == 0) { + retainDistance = server.getPlayerList().getViewDistance() + 2; + for (ServerPlayer player : players.values()) { + if (refreshSpawnMetadata) { + refreshSpawnMetadata(player); + } + + // TODO DimensionChange + refreshTrackedChunks(player, tickCounter); + } + + if (refreshSpawnMetadata) { + refreshSpawnMetadata = false; + } + } + } + + public static void onStartedWatchingChunk(ServerPlayer player, LevelChunk chunk) { + if (!LeavesConfig.protocol.servux.structureProtocol) { + return; + } + + MinecraftServer server = player.getServer(); + + if (players.containsKey(player.getId()) && server != null) { + addChunkTimeoutIfHasReferences(player.getUUID(), chunk, server.getTickCount()); + } + } + + private static void addChunkTimeoutIfHasReferences(final UUID uuid, LevelChunk chunk, final int tickCounter) { + final ChunkPos pos = chunk.getPos(); + + if (chunkHasStructureReferences(pos.x, pos.z, chunk.getLevel())) { + final Map map = timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>()); + map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter - timeout)); + } + } + + private static boolean chunkHasStructureReferences(int chunkX, int chunkZ, Level world) { + if (!world.hasChunk(chunkX, chunkZ)) { + return false; + } + + ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); + + for (Map.Entry entry : chunk.getAllReferences().entrySet()) { + if (!entry.getValue().isEmpty()) { + return true; + } + } + + return false; + } + + + public static void onPlayerSubscribed(@NotNull ServerPlayer player) { + if (!players.containsKey(player.getId())) { + players.put(player.getId(), player); + } else { + LeavesLogger.LOGGER.warning(player.getScoreboardName() + " re-register servux:structures"); + } + + CompoundTag tag = new CompoundTag(); + tag.putString("name", "structure_bounding_boxes"); + tag.putString("id", CHANNEL.toString()); + tag.putInt("version", PROTOCOL_VERSION); + tag.putString("servux", ServuxProtocol.SERVUX_STRING); + tag.putInt("timeout", timeout); + + MinecraftServer server = MinecraftServer.getServer(); + BlockPos spawnPos = server.overworld().levelData.getSpawnPos(); + tag.putInt("spawnPosX", spawnPos.getX()); + tag.putInt("spawnPosY", spawnPos.getY()); + tag.putInt("spawnPosZ", spawnPos.getZ()); + tag.putInt("spawnChunkRadius", server.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); + + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_METADATA, tag)); + initialSyncStructures(player, player.moonrise$getViewDistanceHolder().getViewDistances().sendViewDistance() + 2, server.getTickCount()); + } + + public static void refreshSpawnMetadata(ServerPlayer player) { + CompoundTag tag = new CompoundTag(); + tag.putString("id", CHANNEL.toString()); + tag.putString("servux", ServuxProtocol.SERVUX_STRING); + + MinecraftServer server = MinecraftServer.getServer(); + BlockPos spawnPos = server.overworld().levelData.getSpawnPos(); + tag.putInt("spawnPosX", spawnPos.getX()); + tag.putInt("spawnPosY", spawnPos.getY()); + tag.putInt("spawnPosZ", spawnPos.getZ()); + tag.putInt("spawnChunkRadius", server.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); + + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_SPAWN_METADATA, tag)); + } + + public static void initialSyncStructures(ServerPlayer player, int chunkRadius, int tickCounter) { + UUID uuid = player.getUUID(); + ChunkPos center = player.getLastSectionPos().chunk(); + Map references = getStructureReferences(player.serverLevel(), center, chunkRadius); + + timeouts.remove(uuid); + + sendStructures(player, references, tickCounter); + } + + public static Map getStructureReferences(ServerLevel world, ChunkPos center, int chunkRadius) { + Map references = new HashMap<>(); + + for (int cx = center.x - chunkRadius; cx <= center.x + chunkRadius; ++cx) { + for (int cz = center.z - chunkRadius; cz <= center.z + chunkRadius; ++cz) { + getReferencesFromChunk(cx, cz, world, references); + } + } + + return references; + } + + public static void getReferencesFromChunk(int chunkX, int chunkZ, Level world, Map references) { + if (!world.hasChunk(chunkX, chunkZ)) { + return; + } + + ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); + + for (Map.Entry entry : chunk.getAllReferences().entrySet()) { + Structure feature = entry.getKey(); + LongSet startChunks = entry.getValue(); + + // TODO add an option && feature != StructureFeature.MINESHAFT (?) + if (!startChunks.isEmpty()) { + references.merge(feature, startChunks, (oldSet, entrySet) -> { + LongOpenHashSet newSet = new LongOpenHashSet(oldSet); + newSet.addAll(entrySet); + return newSet; + }); + } + } + } + + public static void sendStructures(ServerPlayer player, Map references, int tickCounter) { + ServerLevel world = player.serverLevel(); + Map starts = getStructureStarts(world, references); + + if (!starts.isEmpty()) { + addOrRefreshTimeouts(player.getUUID(), references, tickCounter); + + ListTag structureList = getStructureList(starts, world); + + if (players.containsKey(player.getId())) { + CompoundTag test = new CompoundTag(); + test.put("Structures", structureList.copy()); + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA_START, test)); + } + } + } + + public static ListTag getStructureList(Map structures, ServerLevel world) { + ListTag list = new ListTag(); + StructurePieceSerializationContext ctx = StructurePieceSerializationContext.fromLevel(world); + + for (Map.Entry entry : structures.entrySet()) { + ChunkPos pos = entry.getKey(); + list.add(entry.getValue().createTag(ctx, pos)); + } + + return list; + } + + public static Map getStructureStarts(ServerLevel world, Map references) { + Map starts = new HashMap<>(); + + for (Map.Entry entry : references.entrySet()) { + Structure structure = entry.getKey(); + LongSet startChunks = entry.getValue(); + LongIterator iter = startChunks.iterator(); + + while (iter.hasNext()) { + ChunkPos pos = new ChunkPos(iter.nextLong()); + + if (!world.hasChunk(pos.x, pos.z)) { + continue; + } + + ChunkAccess chunk = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS, false); + StructureStart start = chunk.getStartForStructure(structure); + + if (start != null) { + starts.put(pos, start); + } + } + } + + return starts; + } + + public static void refreshTrackedChunks(ServerPlayer player, int tickCounter) { + UUID uuid = player.getUUID(); + Map map = timeouts.get(uuid); + + if (map != null) { + sendAndRefreshExpiredStructures(player, map, tickCounter); + } + } + + public static void sendAndRefreshExpiredStructures(ServerPlayer player, Map map, int tickCounter) { + Set positionsToUpdate = new HashSet<>(); + + for (Map.Entry entry : map.entrySet()) { + Timeout out = entry.getValue(); + + if (out.needsUpdate(tickCounter, timeout)) { + positionsToUpdate.add(entry.getKey()); + } + } + + if (!positionsToUpdate.isEmpty()) { + ServerLevel world = player.serverLevel(); + ChunkPos center = player.getLastSectionPos().chunk(); + Map references = new HashMap<>(); + + for (ChunkPos pos : positionsToUpdate) { + if (isOutOfRange(pos, center)) { + map.remove(pos); + } else { + getReferencesFromChunk(pos.x, pos.z, world, references); + + Timeout timeout = map.get(pos); + + if (timeout != null) { + timeout.setLastSync(tickCounter); + } + } + } + + if (!references.isEmpty()) { + sendStructures(player, references, tickCounter); + } + } + } + + protected static boolean isOutOfRange(ChunkPos pos, ChunkPos center) { + return Math.abs(pos.x - center.x) > retainDistance || Math.abs(pos.z - center.z) > retainDistance; + } + + public static void addOrRefreshTimeouts(final UUID uuid, final Map references, final int tickCounter) { + Map map = timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>()); + + for (LongSet chunks : references.values()) { + for (Long chunkPosLong : chunks) { + final ChunkPos pos = new ChunkPos(chunkPosLong); + map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter)).setLastSync(tickCounter); + } + } + } + + public enum StructuresPayloadType { + PACKET_S2C_METADATA(1), + PACKET_S2C_STRUCTURE_DATA(2), + PACKET_C2S_STRUCTURES_REGISTER(3), + PACKET_C2S_STRUCTURES_UNREGISTER(4), + PACKET_S2C_STRUCTURE_DATA_START(5), + PACKET_S2C_SPAWN_METADATA(10), + PACKET_C2S_REQUEST_SPAWN_METADATA(11); + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + + public final int type; + + StructuresPayloadType(int type) { + this.type = type; + Helper.ID_TO_TYPE.put(type, this); + } + + public static StructuresPayloadType fromId(int id) { + return Helper.ID_TO_TYPE.get(id); + } + } + + public record StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt, FriendlyByteBuf buffer) implements LeavesCustomPayload { + + public StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt) { + this(packetType, nbt, null); + } + + public StructuresPayload(StructuresPayloadType packetType, FriendlyByteBuf buffer) { + this(packetType, new CompoundTag(), buffer); + } + + @New + private static StructuresPayload decode(ResourceLocation id, FriendlyByteBuf buf) { + int i = buf.readVarInt(); + StructuresPayloadType type = StructuresPayloadType.fromId(i); + + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } else if (type.equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) { + return new StructuresPayload(type, new FriendlyByteBuf(buf.readBytes(buf.readableBytes()))); + } else { + return new StructuresPayload(type, buf.readNbt()); + } + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeVarInt(this.packetType.type); + if (this.packetType.equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) { + buf.writeBytes(this.buffer.readBytes(this.buffer.readableBytes())); + } else { + buf.writeNbt(this.nbt); + } + } + + @Override + public ResourceLocation id() { + return CHANNEL; + } + } + + public static class Timeout { + private int lastSync; + + public Timeout(int currentTick) { + this.lastSync = currentTick; + } + + public boolean needsUpdate(int currentTick, int timeout) { + return currentTick - this.lastSync >= timeout; + } + + public void setLastSync(int tickCounter) { + this.lastSync = tickCounter; + } + } + + public static void sendPacket(ServerPlayer player, StructuresPayload payload) { + if (!LeavesConfig.protocol.servux.structureProtocol) { + return; + } + + if (payload.packetType() == StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA_START) { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + buffer.writeNbt(payload.nbt()); + PacketSplitter.send(ServuxStructuresProtocol::sendWithSplitter, buffer, player); + } else { + ProtocolUtils.sendPayloadPacket(player, payload); + } + } + + private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA, buf)); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java new file mode 100644 index 00000000..365b07d8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java @@ -0,0 +1,396 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.mojang.authlib.GameProfile; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.syncmatica.exchange.DownloadExchange; +import org.leavesmc.leaves.protocol.syncmatica.exchange.Exchange; +import org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget; +import org.leavesmc.leaves.protocol.syncmatica.exchange.ModifyExchangeServer; +import org.leavesmc.leaves.protocol.syncmatica.exchange.UploadExchange; +import org.leavesmc.leaves.protocol.syncmatica.exchange.VersionHandshakeServer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@LeavesProtocol(namespace = "syncmatica") +public class CommunicationManager { + + private static final Map> downloadingFile = new HashMap<>(); + private static final Map playerMap = new HashMap<>(); + + protected static final Collection broadcastTargets = new ArrayList<>(); + + protected static final Map downloadState = new HashMap<>(); + protected static final Map modifyState = new HashMap<>(); + + protected static final Rotation[] rotOrdinals = Rotation.values(); + protected static final Mirror[] mirOrdinals = Mirror.values(); + + public CommunicationManager() { + } + + public static GameProfile getGameProfile(final ExchangeTarget exchangeTarget) { + return playerMap.get(exchangeTarget).getGameProfile(); + } + + public void sendMessage(final @NotNull ExchangeTarget client, final MessageType type, final String identifier) { + if (client.getFeatureSet().hasFeature(Feature.MESSAGE)) { + final FriendlyByteBuf newPacketBuf = new FriendlyByteBuf(Unpooled.buffer()); + newPacketBuf.writeUtf(type.toString()); + newPacketBuf.writeUtf(identifier); + client.sendPacket(PacketType.MESSAGE.identifier, newPacketBuf); + } else if (playerMap.containsKey(client)) { + final ServerPlayer player = playerMap.get(client); + player.sendSystemMessage(Component.literal("Syncmatica " + type.toString() + " " + identifier)); + } + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerJoin(ServerPlayer player) { + if (!LeavesConfig.protocol.syncmatica.enable) { + return; + } + final ExchangeTarget newPlayer = player.connection.exchangeTarget; + final VersionHandshakeServer hi = new VersionHandshakeServer(newPlayer); + playerMap.put(newPlayer, player); + final GameProfile profile = player.getGameProfile(); + SyncmaticaProtocol.getPlayerIdentifierProvider().updateName(profile.getId(), profile.getName()); + startExchangeUnchecked(hi); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLeave(ServerPlayer player) { + if (!LeavesConfig.protocol.syncmatica.enable) { + return; + } + final ExchangeTarget oldPlayer = player.connection.exchangeTarget; + final Collection potentialMessageTarget = oldPlayer.getExchanges(); + if (potentialMessageTarget != null) { + for (final Exchange target : potentialMessageTarget) { + target.close(false); + handleExchange(target); + } + } + broadcastTargets.remove(oldPlayer); + playerMap.remove(oldPlayer); + } + + @ProtocolHandler.PayloadReceiver(payload = SyncmaticaPayload.class, payloadId = "main") + public static void onPacketGet(ServerPlayer player, SyncmaticaPayload payload) { + if (!LeavesConfig.protocol.syncmatica.enable) { + return; + } + onPacket(player.connection.exchangeTarget, payload.packetType(), payload.data()); + } + + public static void onPacket(final @NotNull ExchangeTarget source, final ResourceLocation id, final FriendlyByteBuf packetBuf) { + Exchange handler = null; + final Collection potentialMessageTarget = source.getExchanges(); + if (potentialMessageTarget != null) { + for (final Exchange target : potentialMessageTarget) { + if (target.checkPacket(id, packetBuf)) { + target.handle(id, packetBuf); + handler = target; + break; + } + } + } + if (handler == null) { + handle(source, id, packetBuf); + } else if (handler.isFinished()) { + notifyClose(handler); + } + } + + protected static void handle(ExchangeTarget source, @NotNull ResourceLocation id, FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.REQUEST_LITEMATIC.identifier)) { + final UUID syncmaticaId = packetBuf.readUUID(); + final ServerPlacement placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(syncmaticaId); + if (placement == null) { + return; + } + final File toUpload = SyncmaticaProtocol.getFileStorage().getLocalLitematic(placement); + final UploadExchange upload; + try { + upload = new UploadExchange(placement, toUpload, source); + } catch (final FileNotFoundException e) { + e.printStackTrace(); + return; + } + startExchange(upload); + return; + } + if (id.equals(PacketType.REGISTER_METADATA.identifier)) { + final ServerPlacement placement = receiveMetaData(packetBuf, source); + if (SyncmaticaProtocol.getSyncmaticManager().getPlacement(placement.getId()) != null) { + cancelShare(source, placement); + return; + } + + final GameProfile profile = playerMap.get(source).getGameProfile(); + final PlayerIdentifier playerIdentifier = SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet(profile); + if (!placement.getOwner().equals(playerIdentifier)) { + placement.setOwner(playerIdentifier); + placement.setLastModifiedBy(playerIdentifier); + } + + if (!SyncmaticaProtocol.getFileStorage().getLocalState(placement).isLocalFileReady()) { + if (SyncmaticaProtocol.getFileStorage().getLocalState(placement) == LocalLitematicState.DOWNLOADING_LITEMATIC) { + downloadingFile.computeIfAbsent(placement.getHash(), key -> new ArrayList<>()).add(placement); + return; + } + try { + download(placement, source); + } catch (final Exception e) { + e.printStackTrace(); + } + return; + } + + addPlacement(source, placement); + return; + } + if (id.equals(PacketType.REMOVE_SYNCMATIC.identifier)) { + final UUID placementId = packetBuf.readUUID(); + final ServerPlacement placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(placementId); + if (placement != null) { + if (!getGameProfile(source).getId().equals(placement.getOwner().uuid)) { + return; + } + + final Exchange modifier = getModifier(placement); + if (modifier != null) { + modifier.close(true); + notifyClose(modifier); + } + SyncmaticaProtocol.getSyncmaticManager().removePlacement(placement); + for (final ExchangeTarget client : broadcastTargets) { + final FriendlyByteBuf newPacketBuf = new FriendlyByteBuf(Unpooled.buffer()); + newPacketBuf.writeUUID(placement.getId()); + client.sendPacket(PacketType.REMOVE_SYNCMATIC.identifier, newPacketBuf); + } + } + } + if (id.equals(PacketType.MODIFY_REQUEST.identifier)) { + final UUID placementId = packetBuf.readUUID(); + final ModifyExchangeServer modifier = new ModifyExchangeServer(placementId, source); + startExchange(modifier); + } + } + + protected static void handleExchange(Exchange exchange) { + if (exchange instanceof DownloadExchange) { + final ServerPlacement p = ((DownloadExchange) exchange).getPlacement(); + + if (exchange.isSuccessful()) { + addPlacement(exchange.getPartner(), p); + if (downloadingFile.containsKey(p.getHash())) { + for (final ServerPlacement placement : downloadingFile.get(p.getHash())) { + addPlacement(exchange.getPartner(), placement); + } + } + } else { + cancelShare(exchange.getPartner(), p); + if (downloadingFile.containsKey(p.getHash())) { + for (final ServerPlacement placement : downloadingFile.get(p.getHash())) { + cancelShare(exchange.getPartner(), placement); + } + } + } + + downloadingFile.remove(p.getHash()); + return; + } + if (exchange instanceof VersionHandshakeServer && exchange.isSuccessful()) { + broadcastTargets.add(exchange.getPartner()); + } + if (exchange instanceof ModifyExchangeServer && exchange.isSuccessful()) { + final ServerPlacement placement = ((ModifyExchangeServer) exchange).getPlacement(); + for (final ExchangeTarget client : broadcastTargets) { + if (client.getFeatureSet().hasFeature(Feature.MODIFY)) { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(placement.getId()); + putPositionData(placement, buf, client); + if (client.getFeatureSet().hasFeature(Feature.CORE_EX)) { + buf.writeUUID(placement.getLastModifiedBy().uuid); + buf.writeUtf(placement.getLastModifiedBy().getName()); + } + client.sendPacket(PacketType.MODIFY.identifier, buf); + } else { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(placement.getId()); + client.sendPacket(PacketType.REMOVE_SYNCMATIC.identifier, buf); + sendMetaData(placement, client); + } + } + } + } + + private static void addPlacement(final ExchangeTarget t, final @NotNull ServerPlacement placement) { + if (SyncmaticaProtocol.getSyncmaticManager().getPlacement(placement.getId()) != null) { + cancelShare(t, placement); + return; + } + SyncmaticaProtocol.getSyncmaticManager().addPlacement(placement); + for (final ExchangeTarget target : broadcastTargets) { + sendMetaData(placement, target); + } + } + + private static void cancelShare(final @NotNull ExchangeTarget source, final @NotNull ServerPlacement placement) { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(placement.getId()); + source.sendPacket(PacketType.CANCEL_SHARE.identifier, FriendlyByteBuf); + } + + public static void sendMetaData(final ServerPlacement metaData, final ExchangeTarget target) { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + putMetaData(metaData, buf, target); + target.sendPacket(PacketType.REGISTER_METADATA.identifier, buf); + } + + public static void putMetaData(final @NotNull ServerPlacement metaData, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { + buf.writeUUID(metaData.getId()); + + buf.writeUtf(SyncmaticaProtocol.sanitizeFileName(metaData.getName())); + buf.writeUUID(metaData.getHash()); + + if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { + buf.writeUUID(metaData.getOwner().uuid); + buf.writeUtf(metaData.getOwner().getName()); + buf.writeUUID(metaData.getLastModifiedBy().uuid); + buf.writeUtf(metaData.getLastModifiedBy().getName()); + } + + putPositionData(metaData, buf, exchangeTarget); + } + + public static void putPositionData(final @NotNull ServerPlacement metaData, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { + buf.writeBlockPos(metaData.getPosition()); + buf.writeUtf(metaData.getDimension()); + buf.writeInt(metaData.getRotation().ordinal()); + buf.writeInt(metaData.getMirror().ordinal()); + + if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { + if (metaData.getSubRegionData().getModificationData() == null) { + buf.writeInt(0); + return; + } + + final Collection regionData = metaData.getSubRegionData().getModificationData().values(); + buf.writeInt(regionData.size()); + + for (final SubRegionPlacementModification subPlacement : regionData) { + buf.writeUtf(subPlacement.name); + buf.writeBlockPos(subPlacement.position); + buf.writeInt(subPlacement.rotation.ordinal()); + buf.writeInt(subPlacement.mirror.ordinal()); + } + } + } + + public static ServerPlacement receiveMetaData(final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { + final UUID id = buf.readUUID(); + + final String fileName = SyncmaticaProtocol.sanitizeFileName(buf.readUtf(32767)); + final UUID hash = buf.readUUID(); + + PlayerIdentifier owner = PlayerIdentifier.MISSING_PLAYER; + PlayerIdentifier lastModifiedBy = PlayerIdentifier.MISSING_PLAYER; + + if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { + final PlayerIdentifierProvider provider = SyncmaticaProtocol.getPlayerIdentifierProvider(); + owner = provider.createOrGet(buf.readUUID(), buf.readUtf(32767)); + lastModifiedBy = provider.createOrGet(buf.readUUID(), buf.readUtf(32767)); + } + + final ServerPlacement placement = new ServerPlacement(id, fileName, hash, owner); + placement.setLastModifiedBy(lastModifiedBy); + + receivePositionData(placement, buf, exchangeTarget); + + return placement; + } + + public static void receivePositionData(final @NotNull ServerPlacement placement, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { + final BlockPos pos = buf.readBlockPos(); + final String dimensionId = buf.readUtf(32767); + final Rotation rot = rotOrdinals[buf.readInt()]; + final Mirror mir = mirOrdinals[buf.readInt()]; + placement.move(dimensionId, pos, rot, mir); + + if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { + final SubRegionData subRegionData = placement.getSubRegionData(); + subRegionData.reset(); + final int limit = buf.readInt(); + for (int i = 0; i < limit; i++) { + subRegionData.modify(buf.readUtf(32767), buf.readBlockPos(), rotOrdinals[buf.readInt()], mirOrdinals[buf.readInt()]); + } + } + } + + public static void download(final ServerPlacement syncmatic, final ExchangeTarget source) throws NoSuchAlgorithmException, IOException { + if (!SyncmaticaProtocol.getFileStorage().getLocalState(syncmatic).isReadyForDownload()) { + throw new IllegalArgumentException(syncmatic.toString() + " is not ready for download local state is: " + SyncmaticaProtocol.getFileStorage().getLocalState(syncmatic).toString()); + } + final File toDownload = SyncmaticaProtocol.getFileStorage().createLocalLitematic(syncmatic); + final Exchange downloadExchange = new DownloadExchange(syncmatic, toDownload, source); + setDownloadState(syncmatic, true); + startExchange(downloadExchange); + } + + public static void setDownloadState(final @NotNull ServerPlacement syncmatic, final boolean b) { + downloadState.put(syncmatic.getHash(), b); + } + + public static boolean getDownloadState(final @NotNull ServerPlacement syncmatic) { + return downloadState.getOrDefault(syncmatic.getHash(), false); + } + + public static void setModifier(final @NotNull ServerPlacement syncmatic, final Exchange exchange) { + modifyState.put(syncmatic.getHash(), exchange); + } + + public static Exchange getModifier(final @NotNull ServerPlacement syncmatic) { + return modifyState.get(syncmatic.getHash()); + } + + public static void startExchange(final @NotNull Exchange newExchange) { + if (!broadcastTargets.contains(newExchange.getPartner())) { + throw new IllegalArgumentException(newExchange.getPartner().toString() + " is not a valid ExchangeTarget"); + } + startExchangeUnchecked(newExchange); + } + + protected static void startExchangeUnchecked(final @NotNull Exchange newExchange) { + newExchange.getPartner().getExchanges().add(newExchange); + newExchange.init(); + if (newExchange.isFinished()) { + notifyClose(newExchange); + } + } + + public static void notifyClose(final @NotNull Exchange e) { + e.getPartner().getExchanges().remove(e); + handleExchange(e); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java new file mode 100644 index 00000000..7cb3465b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java @@ -0,0 +1,23 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import org.jetbrains.annotations.Nullable; + +public enum Feature { + CORE, + FEATURE, + MODIFY, + MESSAGE, + QUOTA, + DEBUG, + CORE_EX; + + @Nullable + public static Feature fromString(final String s) { + for (final Feature f : Feature.values()) { + if (f.toString().equals(s)) { + return f; + } + } + return null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java new file mode 100644 index 00000000..ddd0f498 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java @@ -0,0 +1,67 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class FeatureSet { + + private static final Map versionFeatures; + private final Collection features; + + @Nullable + public static FeatureSet fromVersionString(@NotNull String version) { + if (version.matches("^\\d+(\\.\\d+){2,4}$")) { + final int minSize = version.indexOf("."); + while (version.length() > minSize) { + if (versionFeatures.containsKey(version)) { + return versionFeatures.get(version); + } + final int lastDot = version.lastIndexOf("."); + version = version.substring(0, lastDot); + } + } + return null; + } + + @NotNull + public static FeatureSet fromString(final @NotNull String features) { + final FeatureSet featureSet = new FeatureSet(new ArrayList<>()); + for (final String feature : features.split("\n")) { + final Feature f = Feature.fromString(feature); + if (f != null) { + featureSet.features.add(f); + } + } + return featureSet; + } + + @Override + public String toString() { + final StringBuilder output = new StringBuilder(); + boolean b = false; + for (final Feature feature : features) { + output.append(b ? "\n" + feature.toString() : feature.toString()); + b = true; + } + return output.toString(); + } + + public FeatureSet(final Collection features) { + this.features = features; + } + + public boolean hasFeature(final Feature f) { + return features.contains(f); + } + + static { + versionFeatures = new HashMap<>(); + versionFeatures.put("0.1", new FeatureSet(Collections.singletonList(Feature.CORE))); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java new file mode 100644 index 00000000..9139394e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java @@ -0,0 +1,80 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.UUID; + +public class FileStorage { + + private final HashMap buffer = new HashMap<>(); + + public LocalLitematicState getLocalState(final ServerPlacement placement) { + final File localFile = getSchematicPath(placement); + if (localFile.isFile()) { + if (isDownloading(placement)) { + return LocalLitematicState.DOWNLOADING_LITEMATIC; + } + if ((buffer.containsKey(placement) && buffer.get(placement) == localFile.lastModified()) || hashCompare(localFile, placement)) { + return LocalLitematicState.LOCAL_LITEMATIC_PRESENT; + } + return LocalLitematicState.LOCAL_LITEMATIC_DESYNC; + } + return LocalLitematicState.NO_LOCAL_LITEMATIC; + } + + private boolean isDownloading(final ServerPlacement placement) { + return SyncmaticaProtocol.getCommunicationManager().getDownloadState(placement); + } + + public File getLocalLitematic(final ServerPlacement placement) { + if (getLocalState(placement).isLocalFileReady()) { + return getSchematicPath(placement); + } else { + return null; + } + } + + public File createLocalLitematic(final ServerPlacement placement) { + if (getLocalState(placement).isLocalFileReady()) { + throw new IllegalArgumentException(""); + } + final File file = getSchematicPath(placement); + if (file.exists()) { + file.delete(); + } + try { + file.createNewFile(); + } catch (final IOException e) { + e.printStackTrace(); + } + return file; + } + + private boolean hashCompare(final File localFile, final ServerPlacement placement) { + UUID hash = null; + try { + hash = SyncmaticaProtocol.createChecksum(new FileInputStream(localFile)); + } catch (final Exception e) { + e.printStackTrace(); + } + + if (hash == null) { + return false; + } + if (hash.equals(placement.getHash())) { + buffer.put(placement, localFile.lastModified()); + return true; + } + return false; + } + + @Contract("_ -> new") + private @NotNull File getSchematicPath(final @NotNull ServerPlacement placement) { + return new File(SyncmaticaProtocol.getLitematicFolder(), placement.getHash().toString() + ".litematic"); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java new file mode 100644 index 00000000..299c5739 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java @@ -0,0 +1,24 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +public enum LocalLitematicState { + NO_LOCAL_LITEMATIC(true, false), + LOCAL_LITEMATIC_DESYNC(true, false), + DOWNLOADING_LITEMATIC(false, false), + LOCAL_LITEMATIC_PRESENT(false, true); + + private final boolean downloadReady; + private final boolean fileReady; + + LocalLitematicState(final boolean downloadReady, final boolean fileReady) { + this.downloadReady = downloadReady; + this.fileReady = fileReady; + } + + public boolean isReadyForDownload() { + return downloadReady; + } + + public boolean isLocalFileReady() { + return fileReady; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java new file mode 100644 index 00000000..b56ca12c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java @@ -0,0 +1,8 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +public enum MessageType { + SUCCESS, + INFO, + WARNING, + ERROR +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java new file mode 100644 index 00000000..d3643823 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java @@ -0,0 +1,30 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import net.minecraft.resources.ResourceLocation; + +public enum PacketType { + REGISTER_METADATA("register_metadata"), + CANCEL_SHARE("cancel_share"), + REQUEST_LITEMATIC("request_download"), + SEND_LITEMATIC("send_litematic"), + RECEIVED_LITEMATIC("received_litematic"), + FINISHED_LITEMATIC("finished_litematic"), + CANCEL_LITEMATIC("cancel_litematic"), + REMOVE_SYNCMATIC("remove_syncmatic"), + REGISTER_VERSION("register_version"), + CONFIRM_USER("confirm_user"), + FEATURE_REQUEST("feature_request"), + FEATURE("feature"), + MODIFY("modify"), + MODIFY_REQUEST("modify_request"), + MODIFY_REQUEST_DENY("modify_request_deny"), + MODIFY_REQUEST_ACCEPT("modify_request_accept"), + MODIFY_FINISH("modify_finish"), + MESSAGE("mesage"); + + public final ResourceLocation identifier; + + PacketType(final String id) { + identifier = ResourceLocation.tryBuild(SyncmaticaProtocol.PROTOCOL_ID, id); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java new file mode 100644 index 00000000..b5891b0b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java @@ -0,0 +1,37 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import java.util.UUID; + +public class PlayerIdentifier { + + public static final UUID MISSING_PLAYER_UUID = UUID.fromString("4c1b738f-56fa-4011-8273-498c972424ea"); + public static final PlayerIdentifier MISSING_PLAYER = new PlayerIdentifier(MISSING_PLAYER_UUID, "No Player"); + + public final UUID uuid; + private String bufferedPlayerName; + + PlayerIdentifier(final UUID uuid, final String bufferedPlayerName) { + this.uuid = uuid; + this.bufferedPlayerName = bufferedPlayerName; + } + + public String getName() { + return bufferedPlayerName; + } + + public void updatePlayerName(final String name) { + bufferedPlayerName = name; + } + + public JsonObject toJson() { + final JsonObject jsonObject = new JsonObject(); + + jsonObject.add("uuid", new JsonPrimitive(uuid.toString())); + jsonObject.add("name", new JsonPrimitive(bufferedPlayerName)); + + return jsonObject; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java new file mode 100644 index 00000000..df2254ed --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java @@ -0,0 +1,46 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonObject; +import com.mojang.authlib.GameProfile; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class PlayerIdentifierProvider { + + private final Map identifiers = new HashMap<>(); + + public PlayerIdentifierProvider() { + identifiers.put(PlayerIdentifier.MISSING_PLAYER_UUID, PlayerIdentifier.MISSING_PLAYER); + } + + public PlayerIdentifier createOrGet(final ExchangeTarget exchangeTarget) { + return createOrGet(SyncmaticaProtocol.getCommunicationManager().getGameProfile(exchangeTarget)); + } + + public PlayerIdentifier createOrGet(final @NotNull GameProfile gameProfile) { + return createOrGet(gameProfile.getId(), gameProfile.getName()); + } + + public PlayerIdentifier createOrGet(final UUID uuid, final String playerName) { + return identifiers.computeIfAbsent(uuid, id -> new PlayerIdentifier(uuid, playerName)); + } + + public void updateName(final UUID uuid, final String playerName) { + createOrGet(uuid, playerName).updatePlayerName(playerName); + } + + public PlayerIdentifier fromJson(final @NotNull JsonObject obj) { + if (!obj.has("uuid") || !obj.has("name")) { + return PlayerIdentifier.MISSING_PLAYER; + } + + final UUID jsonUUID = UUID.fromString(obj.get("uuid").getAsString()); + return identifiers.computeIfAbsent(jsonUUID, + key -> new PlayerIdentifier(jsonUUID, obj.get("name").getAsString()) + ); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java new file mode 100644 index 00000000..70759c9d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java @@ -0,0 +1,166 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +public class ServerPlacement { + + private final UUID id; + + private final String fileName; + private final UUID hashValue; + + private PlayerIdentifier owner; + private PlayerIdentifier lastModifiedBy; + + private ServerPosition origin; + private Rotation rotation; + private Mirror mirror; + + private SubRegionData subRegionData = new SubRegionData(); + + public ServerPlacement(final UUID id, final String fileName, final UUID hashValue, final PlayerIdentifier owner) { + this.id = id; + this.fileName = fileName; + this.hashValue = hashValue; + this.owner = owner; + lastModifiedBy = owner; + } + + public UUID getId() { + return id; + } + + public String getName() { + return fileName; + } + + public UUID getHash() { + return hashValue; + } + + public String getDimension() { + return origin.getDimensionId(); + } + + public BlockPos getPosition() { + return origin.getBlockPosition(); + } + + public ServerPosition getOrigin() { + return origin; + } + + public Rotation getRotation() { + return rotation; + } + + public Mirror getMirror() { + return mirror; + } + + public ServerPlacement move(final String dimensionId, final BlockPos origin, final Rotation rotation, final Mirror mirror) { + move(new ServerPosition(origin, dimensionId), rotation, mirror); + return this; + } + + public ServerPlacement move(final ServerPosition origin, final Rotation rotation, final Mirror mirror) { + this.origin = origin; + this.rotation = rotation; + this.mirror = mirror; + return this; + } + + public PlayerIdentifier getOwner() { + return owner; + } + + public void setOwner(final PlayerIdentifier playerIdentifier) { + owner = playerIdentifier; + } + + public PlayerIdentifier getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(final PlayerIdentifier lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public SubRegionData getSubRegionData() { + return subRegionData; + } + + public JsonObject toJson() { + final JsonObject obj = new JsonObject(); + obj.add("id", new JsonPrimitive(id.toString())); + + obj.add("file_name", new JsonPrimitive(fileName)); + obj.add("hash", new JsonPrimitive(hashValue.toString())); + + obj.add("origin", origin.toJson()); + obj.add("rotation", new JsonPrimitive(rotation.name())); + obj.add("mirror", new JsonPrimitive(mirror.name())); + + obj.add("owner", owner.toJson()); + if (!owner.equals(lastModifiedBy)) { + obj.add("lastModifiedBy", lastModifiedBy.toJson()); + } + + if (subRegionData.isModified()) { + obj.add("subregionData", subRegionData.toJson()); + } + + return obj; + } + + @Nullable + public static ServerPlacement fromJson(final @NotNull JsonObject obj) { + if (obj.has("id") + && obj.has("file_name") + && obj.has("hash") + && obj.has("origin") + && obj.has("rotation") + && obj.has("mirror")) { + final UUID id = UUID.fromString(obj.get("id").getAsString()); + final String name = obj.get("file_name").getAsString(); + final UUID hashValue = UUID.fromString(obj.get("hash").getAsString()); + + PlayerIdentifier owner = PlayerIdentifier.MISSING_PLAYER; + if (obj.has("owner")) { + owner = SyncmaticaProtocol.getPlayerIdentifierProvider().fromJson(obj.get("owner").getAsJsonObject()); + } + + final ServerPlacement newPlacement = new ServerPlacement(id, name, hashValue, owner); + final ServerPosition pos = ServerPosition.fromJson(obj.get("origin").getAsJsonObject()); + if (pos == null) { + return null; + } + newPlacement.origin = pos; + newPlacement.rotation = Rotation.valueOf(obj.get("rotation").getAsString()); + newPlacement.mirror = Mirror.valueOf(obj.get("mirror").getAsString()); + + if (obj.has("lastModifiedBy")) { + newPlacement.lastModifiedBy = SyncmaticaProtocol.getPlayerIdentifierProvider() + .fromJson(obj.get("lastModifiedBy").getAsJsonObject()); + } else { + newPlacement.lastModifiedBy = owner; + } + + if (obj.has("subregionData")) { + newPlacement.subRegionData = SubRegionData.fromJson(obj.get("subregionData")); + } + + return newPlacement; + } + + return null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java new file mode 100644 index 00000000..9775c6c4 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java @@ -0,0 +1,51 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; + +public class ServerPosition { + + private final BlockPos position; + private final String dimensionId; + + public ServerPosition(final BlockPos pos, final String dim) { + position = pos; + dimensionId = dim; + } + + public BlockPos getBlockPosition() { + return position; + } + + public String getDimensionId() { + return dimensionId; + } + + public JsonObject toJson() { + final JsonObject obj = new JsonObject(); + final JsonArray arr = new JsonArray(); + arr.add(new JsonPrimitive(position.getX())); + arr.add(new JsonPrimitive(position.getY())); + arr.add(new JsonPrimitive(position.getZ())); + obj.add("position", arr); + obj.add("dimension", new JsonPrimitive(dimensionId)); + return obj; + } + + public static ServerPosition fromJson(final JsonObject obj) { + if (obj.has("position") && obj.has("dimension")) { + final int x; + final int y; + final int z; + final JsonArray arr = obj.get("position").getAsJsonArray(); + x = arr.get(0).getAsInt(); + y = arr.get(1).getAsInt(); + z = arr.get(2).getAsInt(); + final BlockPos pos = new BlockPos(x, y, z); + return new ServerPosition(pos, obj.get("dimension").getAsString()); + } + return null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java new file mode 100644 index 00000000..22fdf92d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java @@ -0,0 +1,90 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public class SubRegionData { + + private boolean isModified; + private Map modificationData; // is null when isModified is false + + public SubRegionData() { + this(false, null); + } + + public SubRegionData(final boolean isModified, final Map modificationData) { + this.isModified = isModified; + this.modificationData = modificationData; + } + + public void reset() { + isModified = false; + modificationData = null; + } + + public void modify(final String name, final BlockPos position, final Rotation rotation, final Mirror mirror) { + modify(new SubRegionPlacementModification(name, position, rotation, mirror)); + } + + public void modify(final SubRegionPlacementModification subRegionPlacementModification) { + if (subRegionPlacementModification == null) { + return; + } + isModified = true; + if (modificationData == null) { + modificationData = new HashMap<>(); + } + modificationData.put(subRegionPlacementModification.name, subRegionPlacementModification); + } + + public boolean isModified() { + return isModified; + } + + public Map getModificationData() { + return modificationData; + } + + public JsonElement toJson() { + return modificationDataToJson(); + } + + @NotNull + private JsonElement modificationDataToJson() { + final JsonArray arr = new JsonArray(); + + for (final Map.Entry entry : modificationData.entrySet()) { + arr.add(entry.getValue().toJson()); + } + + return arr; + } + + @NotNull + public static SubRegionData fromJson(final @NotNull JsonElement obj) { + final SubRegionData newSubRegionData = new SubRegionData(); + + newSubRegionData.isModified = true; + + for (final JsonElement modification : obj.getAsJsonArray()) { + newSubRegionData.modify(SubRegionPlacementModification.fromJson(modification.getAsJsonObject())); + } + + return newSubRegionData; + } + + @Override + public String toString() { + if (!isModified) { + return "[]"; + } + return modificationData == null ? "[ERROR:null]" : modificationData.toString(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java new file mode 100644 index 00000000..a52e299b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java @@ -0,0 +1,65 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SubRegionPlacementModification { + + public final String name; + public final BlockPos position; + public final Rotation rotation; + public final Mirror mirror; + + SubRegionPlacementModification(final String name, final BlockPos position, final Rotation rotation, final Mirror mirror) { + this.name = name; + this.position = position; + this.rotation = rotation; + this.mirror = mirror; + } + + public JsonObject toJson() { + final JsonObject obj = new JsonObject(); + + final JsonArray arr = new JsonArray(); + arr.add(position.getX()); + arr.add(position.getY()); + arr.add(position.getZ()); + obj.add("position", arr); + + obj.add("name", new JsonPrimitive(name)); + obj.add("rotation", new JsonPrimitive(rotation.name())); + obj.add("mirror", new JsonPrimitive(mirror.name())); + + return obj; + } + + @Nullable + public static SubRegionPlacementModification fromJson(final @NotNull JsonObject obj) { + if (!obj.has("name") || !obj.has("position") || !obj.has("rotation") || !obj.has("mirror")) { + return null; + } + + final String name = obj.get("name").getAsString(); + final JsonArray arr = obj.get("position").getAsJsonArray(); + if (arr.size() != 3) { + return null; + } + + final BlockPos position = new BlockPos(arr.get(0).getAsInt(), arr.get(1).getAsInt(), arr.get(2).getAsInt()); + final Rotation rotation = Rotation.valueOf(obj.get("rotation").getAsString()); + final Mirror mirror = Mirror.valueOf(obj.get("mirror").getAsString()); + + return new SubRegionPlacementModification(name, position, rotation, mirror); + } + + @Override + public String toString() { + return String.format("[name=%s, position=%s, rotation=%s, mirror=%s]", name, position, rotation, mirror); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java new file mode 100644 index 00000000..27a056b3 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java @@ -0,0 +1,108 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class SyncmaticManager { + + public static final String PLACEMENTS_JSON_KEY = "placements"; + private final Map schematics = new HashMap<>(); + + public void addPlacement(final ServerPlacement placement) { + schematics.put(placement.getId(), placement); + updateServerPlacement(); + } + + public ServerPlacement getPlacement(final UUID id) { + return schematics.get(id); + } + + public Collection getAll() { + return schematics.values(); + } + + public void removePlacement(final @NotNull ServerPlacement placement) { + schematics.remove(placement.getId()); + updateServerPlacement(); + } + + public void updateServerPlacement() { + saveServer(); + } + + public void startup() { + loadServer(); + } + + private void saveServer() { + final JsonObject obj = new JsonObject(); + final JsonArray arr = new JsonArray(); + + for (final ServerPlacement p : getAll()) { + arr.add(p.toJson()); + } + + obj.add(PLACEMENTS_JSON_KEY, arr); + final File backup = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json.bak"); + final File incoming = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json.new"); + final File current = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json"); + + try (final FileWriter writer = new FileWriter(incoming)) { + writer.write(new GsonBuilder().setPrettyPrinting().create().toJson(obj)); + } catch (final IOException e) { + e.printStackTrace(); + return; + } + + SyncmaticaProtocol.backupAndReplace(backup.toPath(), current.toPath(), incoming.toPath()); + } + + private void loadServer() { + final File f = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json"); + if (f.exists() && f.isFile() && f.canRead()) { + JsonElement element = null; + try { + final JsonParser parser = new JsonParser(); + final FileReader reader = new FileReader(f); + + element = parser.parse(reader); + reader.close(); + + } catch (final Exception e) { + e.printStackTrace(); + } + if (element == null) { + return; + } + try { + final JsonObject obj = element.getAsJsonObject(); + if (obj == null || !obj.has(PLACEMENTS_JSON_KEY)) { + return; + } + final JsonArray arr = obj.getAsJsonArray(PLACEMENTS_JSON_KEY); + for (final JsonElement elem : arr) { + final ServerPlacement placement = ServerPlacement.fromJson(elem.getAsJsonObject()); + if (placement != null) { + schematics.put(placement.getId(), placement); + } + } + + } catch (final IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java new file mode 100644 index 00000000..37710a78 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java @@ -0,0 +1,26 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; + +public record SyncmaticaPayload(ResourceLocation packetType, FriendlyByteBuf data) implements LeavesCustomPayload { + + private static final ResourceLocation NETWORK_ID = ResourceLocation.tryBuild(SyncmaticaProtocol.PROTOCOL_ID, "main"); + + @New + public static SyncmaticaPayload decode(ResourceLocation location, FriendlyByteBuf buf) { + return new SyncmaticaPayload(buf.readResourceLocation(), new FriendlyByteBuf(buf.readBytes(buf.readableBytes()))); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeResourceLocation(this.packetType); + buf.writeBytes(this.data.readBytes(this.data.readableBytes())); + } + + @Override + public ResourceLocation id() { + return NETWORK_ID; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java new file mode 100644 index 00000000..055da192 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java @@ -0,0 +1,127 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.UUID; + +public class SyncmaticaProtocol { + + public static final String PROTOCOL_ID = "syncmatica"; + public static final String PROTOCOL_VERSION = "leaves-syncmatica-1.1.0"; + + private static boolean loaded = false; + private static final File litematicFolder = new File("." + File.separator + "syncmatics"); + private static final PlayerIdentifierProvider playerIdentifierProvider = new PlayerIdentifierProvider(); + private static final CommunicationManager communicationManager = new CommunicationManager(); + private static final FeatureSet featureSet = new FeatureSet(Arrays.asList(Feature.values())); + private static final SyncmaticManager syncmaticManager = new SyncmaticManager(); + private static final FileStorage fileStorage = new FileStorage(); + + public static File getLitematicFolder() { + return litematicFolder; + } + + public static PlayerIdentifierProvider getPlayerIdentifierProvider() { + return playerIdentifierProvider; + } + + public static CommunicationManager getCommunicationManager() { + return communicationManager; + } + + public static FeatureSet getFeatureSet() { + return featureSet; + } + + public static SyncmaticManager getSyncmaticManager() { + return syncmaticManager; + } + + public static FileStorage getFileStorage() { + return fileStorage; + } + + public static void init() { + if (!loaded) { + litematicFolder.mkdirs(); + syncmaticManager.startup(); + loaded = true; + } + } + + @NotNull + public static UUID createChecksum(final @NotNull InputStream fis) throws NoSuchAlgorithmException, IOException { + final byte[] buffer = new byte[4096]; + final MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + int numRead; + + do { + numRead = fis.read(buffer); + if (numRead > 0) { + messageDigest.update(buffer, 0, numRead); + } + } while (numRead != -1); + + fis.close(); + return UUID.nameUUIDFromBytes(messageDigest.digest()); + } + + private static final int[] ILLEGAL_CHARS = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47}; + private static final String ILLEGAL_PATTERNS = "(^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\\..*)?$)|(^\\.\\.*$)"; + + @NotNull + public static String sanitizeFileName(final @NotNull String badFileName) { + final StringBuilder sanitized = new StringBuilder(); + final int len = badFileName.codePointCount(0, badFileName.length()); + + for (int i = 0; i < len; i++) { + final int c = badFileName.codePointAt(i); + if (Arrays.binarySearch(ILLEGAL_CHARS, c) < 0) { + sanitized.appendCodePoint(c); + if (sanitized.length() == 255) { + break; + } + } + } + + return sanitized.toString().replaceAll(ILLEGAL_PATTERNS, "_"); + } + + public static boolean isOverQuota(int sent) { + return LeavesConfig.protocol.syncmatica.useQuota && sent > LeavesConfig.protocol.syncmatica.quotaLimit; + } + + public static void backupAndReplace(final Path backup, final Path current, final Path incoming) { + if (!Files.exists(incoming)) { + return; + } + if (overwrite(backup, current, 2) && !overwrite(current, incoming, 4)) { + overwrite(current, backup, 8); + } + } + + private static boolean overwrite(final Path backup, final Path current, final int tries) { + if (!Files.exists(current)) { + return true; + } + try { + Files.deleteIfExists(backup); + Files.move(current, backup); + } catch (final IOException exception) { + if (tries <= 0) { + return false; + } + return overwrite(backup, current, tries - 1); + } + return true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java new file mode 100644 index 00000000..b06ffeac --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java @@ -0,0 +1,66 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.util.UUID; + +public abstract class AbstractExchange implements Exchange { + + private boolean success = false; + private boolean finished = false; + private final ExchangeTarget partner; + + protected AbstractExchange(final ExchangeTarget partner) { + this.partner = partner; + } + + @Override + public ExchangeTarget getPartner() { + return partner; + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public boolean isSuccessful() { + return success; + } + + @Override + public void close(final boolean notifyPartner) { + finished = true; + success = false; + onClose(); + if (notifyPartner) { + sendCancelPacket(); + } + } + + public CommunicationManager getManager() { + return SyncmaticaProtocol.getCommunicationManager(); + } + + protected void sendCancelPacket() { + } + + protected void onClose() { + } + + protected void succeed() { + finished = true; + success = true; + onClose(); + } + + protected static boolean checkUUID(final FriendlyByteBuf sourceBuf, final UUID targetId) { + final int r = sourceBuf.readerIndex(); + final UUID sourceId = sourceBuf.readUUID(); + sourceBuf.readerIndex(r); + return sourceId.equals(targetId); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java new file mode 100644 index 00000000..b0463dc8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java @@ -0,0 +1,125 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.MessageType; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.UUID; + +public class DownloadExchange extends AbstractExchange { + + private final ServerPlacement toDownload; + private final OutputStream outputStream; + private final MessageDigest md5; + private final File downloadFile; + private int bytesSent; + + public DownloadExchange(final ServerPlacement syncmatic, final File downloadFile, final ExchangeTarget partner) throws IOException, NoSuchAlgorithmException { + super(partner); + this.downloadFile = downloadFile; + final OutputStream os = new FileOutputStream(downloadFile); + toDownload = syncmatic; + md5 = MessageDigest.getInstance("MD5"); + outputStream = new DigestOutputStream(os, md5); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.SEND_LITEMATIC.identifier) + || id.equals(PacketType.FINISHED_LITEMATIC.identifier) + || id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + return checkUUID(packetBuf, toDownload.getId()); + } + return false; + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.SEND_LITEMATIC.identifier)) { + final int size = packetBuf.readInt(); + bytesSent += size; + if (SyncmaticaProtocol.isOverQuota(bytesSent)) { + close(true); + SyncmaticaProtocol.getCommunicationManager().sendMessage( + getPartner(), + MessageType.ERROR, + "syncmatica.error.cancelled_transmit_exceed_quota" + ); + } + try { + packetBuf.readBytes(outputStream, size); + } catch (final IOException e) { + close(true); + e.printStackTrace(); + return; + } + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toDownload.getId()); + getPartner().sendPacket(PacketType.RECEIVED_LITEMATIC.identifier, FriendlyByteBuf); + return; + } + if (id.equals(PacketType.FINISHED_LITEMATIC.identifier)) { + try { + outputStream.flush(); + } catch (final IOException e) { + close(false); + e.printStackTrace(); + return; + } + final UUID downloadHash = UUID.nameUUIDFromBytes(md5.digest()); + if (downloadHash.equals(toDownload.getHash())) { + succeed(); + } else { + close(false); + } + return; + } + if (id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + close(false); + } + } + + @Override + public void init() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toDownload.getId()); + getPartner().sendPacket(PacketType.REQUEST_LITEMATIC.identifier, FriendlyByteBuf); + } + + @Override + protected void onClose() { + getManager().setDownloadState(toDownload, false); + try { + outputStream.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + if (!isSuccessful() && downloadFile.exists()) { + downloadFile.delete(); + } + } + + @Override + protected void sendCancelPacket() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toDownload.getId()); + getPartner().sendPacket(PacketType.CANCEL_LITEMATIC.identifier, FriendlyByteBuf); + } + + public ServerPlacement getPlacement() { + return toDownload; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java new file mode 100644 index 00000000..0f45ef7f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java @@ -0,0 +1,20 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +public interface Exchange { + ExchangeTarget getPartner(); + + boolean checkPacket(ResourceLocation id, FriendlyByteBuf packetBuf); + + void handle(ResourceLocation id, FriendlyByteBuf packetBuf); + + boolean isFinished(); + + boolean isSuccessful(); + + void close(boolean notifyPartner); + + void init(); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java new file mode 100644 index 00000000..b0da7abf --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java @@ -0,0 +1,39 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaPayload; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ExchangeTarget { + + private final List ongoingExchanges = new ArrayList<>(); + private final ServerGamePacketListenerImpl client; + private FeatureSet features; + + public ExchangeTarget(final ServerGamePacketListenerImpl client) { + this.client = client; + } + + public void sendPacket(final ResourceLocation id, final FriendlyByteBuf packetBuf) { + ProtocolUtils.sendPayloadPacket(client.player, new SyncmaticaPayload(id, packetBuf)); + } + + public FeatureSet getFeatureSet() { + return features; + } + + public void setFeatureSet(final FeatureSet f) { + features = f; + } + + public Collection getExchanges() { + return ongoingExchanges; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java new file mode 100644 index 00000000..45fc0191 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java @@ -0,0 +1,48 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +public abstract class FeatureExchange extends AbstractExchange { + + protected FeatureExchange(final ExchangeTarget partner) { + super(partner); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.FEATURE_REQUEST.identifier) + || id.equals(PacketType.FEATURE.identifier); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.FEATURE_REQUEST.identifier)) { + sendFeatures(); + } else if (id.equals(PacketType.FEATURE.identifier)) { + final FeatureSet fs = FeatureSet.fromString(packetBuf.readUtf(32767)); + getPartner().setFeatureSet(fs); + onFeatureSetReceive(); + } + } + + protected void onFeatureSetReceive() { + succeed(); + } + + public void requestFeatureSet() { + getPartner().sendPacket(PacketType.FEATURE_REQUEST.identifier, new FriendlyByteBuf(Unpooled.buffer())); + } + + private void sendFeatures() { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + final FeatureSet fs = SyncmaticaProtocol.getFeatureSet(); + buf.writeUtf(fs.toString(), 32767); + getPartner().sendPacket(PacketType.FEATURE.identifier, buf); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java new file mode 100644 index 00000000..c691201f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java @@ -0,0 +1,81 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.PlayerIdentifier; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.util.UUID; + +public class ModifyExchangeServer extends AbstractExchange { + + private final ServerPlacement placement; + UUID placementId; + + public ModifyExchangeServer(final UUID placeId, final ExchangeTarget partner) { + super(partner); + placementId = placeId; + placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(placementId); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.MODIFY_FINISH.identifier) && checkUUID(packetBuf, placement.getId()); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.MODIFY_FINISH.identifier)) { + SyncmaticaProtocol.getCommunicationManager().receivePositionData(placement, packetBuf, getPartner()); + final PlayerIdentifier identifier = SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet( + getPartner() + ); + placement.setLastModifiedBy(identifier); + SyncmaticaProtocol.getSyncmaticManager().updateServerPlacement(); + succeed(); + } + } + + @Override + public void init() { + if (getPlacement() == null || SyncmaticaProtocol.getCommunicationManager().getModifier(placement) != null) { + close(true); + } else { + if (SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet(this.getPartner()).uuid.equals(placement.getOwner().uuid)) { + accept(); + } else { + close(true); + } + } + } + + private void accept() { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(placement.getId()); + getPartner().sendPacket(PacketType.MODIFY_REQUEST_ACCEPT.identifier, buf); + SyncmaticaProtocol.getCommunicationManager().setModifier(placement, this); + } + + @Override + protected void sendCancelPacket() { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(placementId); + getPartner().sendPacket(PacketType.MODIFY_REQUEST_DENY.identifier, buf); + } + + public ServerPlacement getPlacement() { + return placement; + } + + @Override + protected void onClose() { + if (SyncmaticaProtocol.getCommunicationManager().getModifier(placement) == this) { + SyncmaticaProtocol.getCommunicationManager().setModifier(placement, null); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java new file mode 100644 index 00000000..065a3994 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java @@ -0,0 +1,101 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class UploadExchange extends AbstractExchange { + + private static final int BUFFER_SIZE = 16384; + + private final ServerPlacement toUpload; + private final InputStream inputStream; + private final byte[] buffer = new byte[BUFFER_SIZE]; + + public UploadExchange(final ServerPlacement syncmatic, final File uploadFile, final ExchangeTarget partner) throws FileNotFoundException { + super(partner); + toUpload = syncmatic; + inputStream = new FileInputStream(uploadFile); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.RECEIVED_LITEMATIC.identifier) + || id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + return checkUUID(packetBuf, toUpload.getId()); + } + return false; + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.RECEIVED_LITEMATIC.identifier)) { + send(); + } + if (id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + close(false); + } + } + + private void send() { + final int bytesRead; + try { + bytesRead = inputStream.read(buffer); + } catch (final IOException e) { + close(true); + e.printStackTrace(); + return; + } + if (bytesRead == -1) { + sendFinish(); + } else { + sendData(bytesRead); + } + } + + private void sendData(final int bytesRead) { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + FriendlyByteBuf.writeInt(bytesRead); + FriendlyByteBuf.writeBytes(buffer, 0, bytesRead); + getPartner().sendPacket(PacketType.SEND_LITEMATIC.identifier, FriendlyByteBuf); + } + + private void sendFinish() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + getPartner().sendPacket(PacketType.FINISHED_LITEMATIC.identifier, FriendlyByteBuf); + succeed(); + } + + @Override + public void init() { + send(); + } + + @Override + protected void onClose() { + try { + inputStream.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + } + + @Override + protected void sendCancelPacket() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + getPartner().sendPacket(PacketType.CANCEL_LITEMATIC.identifier, FriendlyByteBuf); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java new file mode 100644 index 00000000..cb7aed6c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java @@ -0,0 +1,66 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; +import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.util.Collection; + +public class VersionHandshakeServer extends FeatureExchange { + + public VersionHandshakeServer(final ExchangeTarget partner) { + super(partner); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.REGISTER_VERSION.identifier) + || super.checkPacket(id, packetBuf); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.REGISTER_VERSION.identifier)) { + String partnerVersion = packetBuf.readUtf(); + if (partnerVersion.equals("0.0.1")) { + close(false); + return; + } + final FeatureSet fs = FeatureSet.fromVersionString(partnerVersion); + if (fs == null) { + requestFeatureSet(); + } else { + getPartner().setFeatureSet(fs); + onFeatureSetReceive(); + } + } else { + super.handle(id, packetBuf); + } + + } + + @Override + public void onFeatureSetReceive() { + final FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); + final Collection l = SyncmaticaProtocol.getSyncmaticManager().getAll(); + newBuf.writeInt(l.size()); + for (final ServerPlacement p : l) { + CommunicationManager.putMetaData(p, newBuf, getPartner()); + } + getPartner().sendPacket(PacketType.CONFIRM_USER.identifier, newBuf); + succeed(); + } + + @Override + public void init() { + final FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); + newBuf.writeUtf(SyncmaticaProtocol.PROTOCOL_VERSION); + getPartner().sendPacket(PacketType.REGISTER_VERSION.identifier, newBuf); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/region/IRegionFile.java b/leaves-server/src/main/java/org/leavesmc/leaves/region/IRegionFile.java new file mode 100644 index 00000000..d4f6ed71 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/region/IRegionFile.java @@ -0,0 +1,40 @@ +package org.leavesmc.leaves.region; + +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; + + DataOutputStream getChunkDataOutputStream(ChunkPos pos) throws IOException; + + CompoundTag getOversizedData(int x, int z) throws IOException; + + void setOversized(int x, int z, boolean oversized) throws IOException; + + boolean isOversized(int x, int z); + + void write(ChunkPos pos, ByteBuffer buf) throws IOException; + + void clear(ChunkPos pos) throws IOException; + + void flush() throws IOException; + + @Override + void close() throws IOException; + + boolean doesChunkExist(ChunkPos pos) throws Exception; + + boolean hasChunk(ChunkPos pos); + + boolean recalculateHeader() throws IOException; +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/region/IRegionFileFactory.java b/leaves-server/src/main/java/org/leavesmc/leaves/region/IRegionFileFactory.java new file mode 100644 index 00000000..5ccb7420 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/region/IRegionFileFactory.java @@ -0,0 +1,85 @@ +package org.leavesmc.leaves.region; + +import net.minecraft.world.level.chunk.storage.RegionFile; +import net.minecraft.world.level.chunk.storage.RegionStorageInfo; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.region.linear.LinearRegionFile; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import static net.minecraft.world.level.chunk.storage.RegionFileStorage.REGION_SHIFT; + +public class IRegionFileFactory { + + private static final List regionArguments; + + static { + regionArguments = new ArrayList<>(); + for (RegionFileFormat format : RegionFileFormat.values()) { + String arg = "." + format.getArgument(); + if (!regionArguments.contains(arg)) { + regionArguments.add(arg); + } + } + } + + public static void initFirstRegion(@NotNull RegionFileFormat firstFormat) { + regionArguments.remove("." + firstFormat.getArgument()); + regionArguments.addFirst("." + firstFormat.getArgument()); + } + + @NotNull + public static Pattern getRegionFileRegex() { + String extensionsPattern = String.join("|", regionArguments.stream() + .map(extension -> extension.replace(".", "")) + .toList()); + return Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.(" + extensionsPattern + ")$"); + } + + @NotNull + public static String getFirstRegionFileName(final int chunkX, final int chunkZ) { + return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + regionArguments.getFirst(); + } + + @NotNull + public static List getRegionFileName(final int chunkX, final int chunkZ) { + List fileNames = new ArrayList<>(); + String regionName = "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT); + for (String argument : regionArguments) { + fileNames.add(regionName + argument); + } + return fileNames; + } + + public static boolean isRegionFile(String fileName) { + for (String extension : regionArguments) { + if (fileName.endsWith(extension)) { + return true; + } + } + return false; + } + + @NotNull + @Contract("_, _, _, _ -> new") + public static IRegionFile createRegionFile(RegionStorageInfo info, Path filePath, @NotNull Path folder, boolean sync) throws IOException { + String extension = filePath.getFileName().toString().split("\\.")[3]; + + switch (extension) { + case "mca" -> { + return new RegionFile(info, filePath, folder, sync); + } + case "linear" -> { + return new LinearRegionFile(filePath, LeavesConfig.region.linear.version, LeavesConfig.region.linear.compressionLevel); + } + } + + throw new IOException("Unsupported region file format: " + extension); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/region/LeavesHooks.java b/leaves-server/src/main/java/org/leavesmc/leaves/region/LeavesHooks.java new file mode 100644 index 00000000..59fe70c9 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/region/LeavesHooks.java @@ -0,0 +1,20 @@ +package org.leavesmc.leaves.region; + +import ca.spottedleaf.moonrise.paper.PaperHooks; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.LevelChunk; + +public final class LeavesHooks extends PaperHooks { + + @Override + public String getBrand() { + return "Leaves"; + } + + @Override + public void onChunkWatch(ServerLevel world, LevelChunk chunk, ServerPlayer player) { + super.onChunkWatch(world, chunk, player); + org.leavesmc.leaves.protocol.servux.ServuxStructuresProtocol.onStartedWatchingChunk(player, chunk); // servux + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/region/RegionFileFormat.java b/leaves-server/src/main/java/org/leavesmc/leaves/region/RegionFileFormat.java new file mode 100644 index 00000000..d126daaf --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/region/RegionFileFormat.java @@ -0,0 +1,16 @@ +package org.leavesmc.leaves.region; + +public enum RegionFileFormat { + ANVIL("mca"), + LINEAR("linear"); + + private final String argument; + + RegionFileFormat(String argument) { + this.argument = argument; + } + + public String getArgument() { + return argument; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/region/linear/LinearRegionFile.java b/leaves-server/src/main/java/org/leavesmc/leaves/region/linear/LinearRegionFile.java new file mode 100644 index 00000000..c002d6f1 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/region/linear/LinearRegionFile.java @@ -0,0 +1,651 @@ +package org.leavesmc.leaves.region.linear; + +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.openhft.hashing.LongHashFunction; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.region.IRegionFile; +import org.slf4j.Logger; + +import javax.annotation.Nullable; +import java.io.BufferedOutputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +// LinearRegionFile_implementation_version_0_5byXymb +// Just gonna use this string to inform other forks about updates ;-) +public class LinearRegionFile implements IRegionFile { + + public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; + + private static final Object saveLock = new Object(); + private static final long SUPERBLOCK = 0xc3ff13183cca9d9aL; + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final byte V1_VERSION = 2; + private static final byte VERSION = 3; + + 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; + + public boolean regionFileOpen = false; + private boolean markedToSave = false; + private boolean close = false; + + public Path regionFile; + + private int gridSize = 8; + private int bucketSize = 4; + private final int compressionLevel; + private final LinearVersion linearVersion; + private final Thread bindThread; + + private static int activeSaveThreads = 0; + + public LinearRegionFile(Path path, LinearVersion linearVersion, int compressionLevel) { + Runnable flushCheck = () -> { + while (!close) { + synchronized (saveLock) { + if (markedToSave && activeSaveThreads < LeavesConfig.region.linear.getLinearFlushThreads()) { + activeSaveThreads++; + Runnable flushOperation = () -> { + try { + flush(); + } catch (IOException ex) { + LOGGER.error("Region file {} flush failed", this.regionFile.toAbsolutePath(), ex); + } finally { + synchronized (saveLock) { + activeSaveThreads--; + } + } + }; + + Thread saveThread = LeavesConfig.region.linear.useVirtualThread ? 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(LeavesConfig.region.linear.flushDelayMs)); + } + }; + this.bindThread = LeavesConfig.region.linear.useVirtualThread ? Thread.ofVirtual().unstarted(flushCheck) : Thread.ofPlatform().unstarted(flushCheck); + this.bindThread.setName("Linear IO Schedule - " + this.hashCode()); + this.compressionLevel = compressionLevel; + this.regionFile = path; + this.linearVersion = linearVersion; + + this.compressor = LZ4Factory.fastestInstance().fastCompressor(); + this.decompressor = LZ4Factory.fastestInstance().fastDecompressor(); + } + + private synchronized void openRegionFile() { + if (regionFileOpen) { + return; + } else { + 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(); + } + + 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); + } + + private synchronized void markToSave() { + synchronized (markedToSaveLock) { + markedToSave = true; + } + } + + private synchronized boolean isMarkedToSave() { + synchronized (markedToSaveLock) { + if (markedToSave) { + markedToSave = false; + return true; + } + return false; + } + } + + public synchronized boolean doesChunkExist(ChunkPos pos) { + openRegionFile(); + return hasChunk(pos); + } + + public synchronized boolean hasChunk(ChunkPos pos) { + openRegionFile(); + openBucket(pos.x, pos.z); + return this.bufferUncompressedSize[getChunkIndex(pos.x, pos.z)] > 0; + } + + public synchronized void flush() throws IOException { + if (!isMarkedToSave()) { + return; + } + + openRegionFile(); + if (linearVersion == LinearVersion.V1) { + flushLinearV1(); + } else if (linearVersion == LinearVersion.V2) { + flushLinearV2(); + } + } + + private synchronized void flushLinearV1() throws IOException { + long timestamp = getTimestamp(); + short chunkCount = 0; + + File tempFile = new File(regionFile.toString() + ".tmp"); + + try (FileOutputStream fileStream = new FileOutputStream(tempFile); + ByteArrayOutputStream zstdByteArray = new ByteArrayOutputStream(); + ZstdOutputStream zstdStream = new ZstdOutputStream(zstdByteArray, this.compressionLevel); + DataOutputStream zstdDataStream = new DataOutputStream(zstdStream); + DataOutputStream dataStream = new DataOutputStream(fileStream)) { + + dataStream.writeLong(SUPERBLOCK); + dataStream.writeByte(V1_VERSION); + dataStream.writeLong(timestamp); + dataStream.writeByte(this.compressionLevel); + + ArrayList byteBuffers = new ArrayList<>(); + for (int i = 0; i < 1024; i++) { + if (this.bufferUncompressedSize[i] != 0) { + chunkCount += 1; + byte[] content = new byte[bufferUncompressedSize[i]]; + this.decompressor.decompress(buffer[i], 0, content, 0, bufferUncompressedSize[i]); + + byteBuffers.add(content); + } else { + byteBuffers.add(null); + } + } + for (int i = 0; i < 1024; i++) { + zstdDataStream.writeInt(this.bufferUncompressedSize[i]); // Write uncompressed size + zstdDataStream.writeInt((int) this.chunkTimestamps[i]); // Write timestamp + } + for (int i = 0; i < 1024; i++) { + if (byteBuffers.get(i) != null) { + zstdDataStream.write(byteBuffers.get(i), 0, byteBuffers.get(i).length); + } + } + zstdDataStream.close(); + + dataStream.writeShort(chunkCount); + + byte[] compressed = zstdByteArray.toByteArray(); + + dataStream.writeInt(compressed.length); + dataStream.writeLong(0); + + dataStream.write(compressed, 0, compressed.length); + dataStream.writeLong(SUPERBLOCK); + + dataStream.flush(); + fileStream.getFD().sync(); + fileStream.getChannel().force(true); // Ensure atomicity on Btrfs + } + + Files.move(tempFile.toPath(), this.regionFile, StandardCopyOption.REPLACE_EXISTING); + } + + private synchronized void flushLinearV2() throws IOException { + long timestamp = getTimestamp(); + + 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); + + 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; + } + + 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); + } + + private void writeNBTFeatures(DataOutputStream dataStream) throws IOException { + 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); + } + + 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 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) { + 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() { + 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 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); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/region/linear/LinearVersion.java b/leaves-server/src/main/java/org/leavesmc/leaves/region/linear/LinearVersion.java new file mode 100644 index 00000000..99acf20e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/region/linear/LinearVersion.java @@ -0,0 +1,5 @@ +package org.leavesmc.leaves.region.linear; + +public enum LinearVersion { + V1, V2 +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/DigestOutputStream.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/DigestOutputStream.java new file mode 100644 index 00000000..e67ff063 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/DigestOutputStream.java @@ -0,0 +1,46 @@ +package org.leavesmc.leaves.replay; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.zip.Checksum; + +public class DigestOutputStream extends OutputStream { + + private final Checksum sum; + private final OutputStream out; + + public DigestOutputStream(OutputStream out, Checksum sum) { + this.out = out; + this.sum = sum; + } + + @Override + public void close() throws IOException { + out.close(); + } + + @Override + public void flush() throws IOException { + out.flush(); + } + + @Override + public void write(int b) throws IOException { + sum.update(b); + out.write(b); + } + + @Override + public void write(byte @NotNull [] b) throws IOException { + sum.update(b); + out.write(b); + } + + @Override + public void write(byte @NotNull [] b, int off, int len) throws IOException { + sum.update(b, off, len); + out.write(b, off, len); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/RecordMetaData.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/RecordMetaData.java new file mode 100644 index 00000000..0e8cd7e8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/RecordMetaData.java @@ -0,0 +1,23 @@ +package org.leavesmc.leaves.replay; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +public class RecordMetaData { + + public static final int CURRENT_FILE_FORMAT_VERSION = 14; + + public boolean singleplayer = false; + public String serverName = "Leaves"; + public int duration = 0; + public long date; + public String mcversion; + public String fileFormat = "MCPR"; + public int fileFormatVersion; + public int protocol; + public String generator; + public int selfId = -1; + + public Set players = new HashSet<>(); +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/Recorder.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/Recorder.java new file mode 100644 index 00000000..d1fb2f08 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/Recorder.java @@ -0,0 +1,285 @@ +package org.leavesmc.leaves.replay; + +import com.mojang.serialization.DynamicOps; +import io.netty.channel.local.LocalChannel; +import net.minecraft.SharedConstants; +import net.minecraft.core.LayeredRegistryAccess; +import net.minecraft.core.RegistrySynchronization; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.network.Connection; +import net.minecraft.network.ConnectionProtocol; +import net.minecraft.network.PacketSendListener; +import net.minecraft.network.protocol.BundlePacket; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.PacketFlow; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; +import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket; +import net.minecraft.network.protocol.common.ClientboundServerLinksPacket; +import net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket; +import net.minecraft.network.protocol.common.custom.BrandPayload; +import net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket; +import net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket; +import net.minecraft.network.protocol.configuration.ClientboundSelectKnownPacks; +import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket; +import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; +import net.minecraft.network.protocol.game.ClientboundGameEventPacket; +import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket; +import net.minecraft.network.protocol.game.ClientboundSetTimePacket; +import net.minecraft.network.protocol.game.ClientboundSystemChatPacket; +import net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.RegistryLayer; +import net.minecraft.server.packs.repository.KnownPack; +import net.minecraft.tags.TagNetworkSerialization; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.flag.FeatureFlags; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesLogger; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class Recorder extends Connection { + + private static final LeavesLogger LOGGER = LeavesLogger.LOGGER; + + private final ReplayFile replayFile; + private final ServerPhotographer photographer; + private final RecorderOption recorderOption; + private final RecordMetaData metaData; + + private final ExecutorService saveService = Executors.newSingleThreadExecutor(); + + private boolean stopped = false; + private boolean paused = false; + private boolean resumeOnNextPacket = true; + + private long startTime; + private long lastPacket; + private long timeShift = 0; + + private boolean isSaved; + private boolean isSaving; + private ConnectionProtocol state = ConnectionProtocol.LOGIN; + + public Recorder(ServerPhotographer photographer, RecorderOption recorderOption, File replayFile) throws IOException { + super(PacketFlow.CLIENTBOUND); + + this.photographer = photographer; + this.recorderOption = recorderOption; + this.metaData = new RecordMetaData(); + this.replayFile = new ReplayFile(replayFile); + this.channel = new LocalChannel(); + } + + public void start() { + startTime = System.currentTimeMillis(); + + metaData.singleplayer = false; + metaData.serverName = recorderOption.serverName; + metaData.date = startTime; + metaData.mcversion = SharedConstants.getCurrentVersion().getName(); + + // TODO start event + this.savePacket(new ClientboundLoginFinishedPacket(photographer.getGameProfile()), ConnectionProtocol.LOGIN); + this.startConfiguration(); + + if (recorderOption.forceWeather != null) { + setWeather(recorderOption.forceWeather); + } + } + + public void startConfiguration() { + this.state = ConnectionProtocol.CONFIGURATION; + MinecraftServer server = MinecraftServer.getServer(); + + this.savePacket(new ClientboundCustomPayloadPacket(new BrandPayload(server.getServerModName())), ConnectionProtocol.CONFIGURATION); + this.savePacket(new ClientboundServerLinksPacket(server.serverLinks().untrust()), ConnectionProtocol.CONFIGURATION); + this.savePacket(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(server.getWorldData().enabledFeatures())), ConnectionProtocol.CONFIGURATION); + + List knownPackslist = server.getResourceManager().listPacks().flatMap((iresourcepack) -> iresourcepack.location().knownPackInfo().stream()).toList(); + this.savePacket(new ClientboundSelectKnownPacks(knownPackslist), ConnectionProtocol.CONFIGURATION); + + server.getServerResourcePack().ifPresent((info) -> this.savePacket(new ClientboundResourcePackPushPacket( + info.id(), info.url(), info.hash(), info.isRequired(), Optional.ofNullable(info.prompt()) + ))); + + LayeredRegistryAccess layeredregistryaccess = server.registries(); + DynamicOps dynamicOps = layeredregistryaccess.compositeAccess().createSerializationContext(NbtOps.INSTANCE); + RegistrySynchronization.packRegistries(dynamicOps, layeredregistryaccess.getAccessFrom(RegistryLayer.WORLDGEN), Set.copyOf(knownPackslist), + (key, entries) -> + this.savePacket(new ClientboundRegistryDataPacket(key, entries), ConnectionProtocol.CONFIGURATION) + ); + this.savePacket(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(layeredregistryaccess)), ConnectionProtocol.CONFIGURATION); + + this.savePacket(ClientboundFinishConfigurationPacket.INSTANCE, ConnectionProtocol.CONFIGURATION); + state = ConnectionProtocol.PLAY; + } + + @Override + public void flushChannel() { + } + + public void stop() { + stopped = true; + } + + public void pauseRecording() { + resumeOnNextPacket = false; + paused = true; + } + + public void resumeRecording() { + resumeOnNextPacket = true; + } + + public void setWeather(RecorderOption.RecordWeather weather) { + weather.getPackets().forEach(this::savePacket); + } + + public long getRecordedTime() { + final long base = System.currentTimeMillis() - startTime; + return base - timeShift; + } + + private synchronized long getCurrentTimeAndUpdate() { + long now = getRecordedTime(); + if (paused) { + if (resumeOnNextPacket) { + paused = false; + } + timeShift += now - lastPacket; + return lastPacket; + } + return lastPacket = now; + } + + @Override + public boolean isConnected() { + return true; + } + + @Override + public void send(@NotNull Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { + if (!stopped) { + if (packet instanceof BundlePacket packet1) { + packet1.subPackets().forEach(subPacket -> send(subPacket, null)); + return; + } + + if (packet instanceof ClientboundAddEntityPacket packet1) { + if (packet1.getType() == EntityType.PLAYER) { + metaData.players.add(packet1.getUUID()); + saveMetadata(); + } + } + + if (packet instanceof ClientboundDisconnectPacket) { + return; + } + + if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) { + packet = new ClientboundSetTimePacket(packet1.dayTime(), recorderOption.forceDayTime, false); + } + + if (recorderOption.forceWeather != null && packet instanceof ClientboundGameEventPacket packet1) { + ClientboundGameEventPacket.Type type = packet1.getEvent(); + if (type == ClientboundGameEventPacket.START_RAINING || type == ClientboundGameEventPacket.STOP_RAINING || type == ClientboundGameEventPacket.RAIN_LEVEL_CHANGE || type == ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) { + return; + } + } + + if (recorderOption.ignoreChat && (packet instanceof ClientboundSystemChatPacket || packet instanceof ClientboundPlayerChatPacket)) { + return; + } + + savePacket(packet); + } + } + + private void saveMetadata() { + saveService.submit(() -> { + try { + replayFile.saveMetaData(metaData); + } catch (IOException e) { + e.printStackTrace(); + } + }); + } + + private void savePacket(Packet packet) { + this.savePacket(packet, state); + } + + private void savePacket(Packet packet, final ConnectionProtocol protocol) { + try { + final long timestamp = getCurrentTimeAndUpdate(); + saveService.submit(() -> { + try { + replayFile.savePacket(timestamp, packet, protocol); + } catch (Exception e) { + LOGGER.severe("Error saving packet"); + e.printStackTrace(); + } + }); + } catch (Exception e) { + LOGGER.severe("Error saving packet"); + e.printStackTrace(); + } + } + + public boolean isSaved() { + return isSaved; + } + + public CompletableFuture saveRecording(File dest, boolean save) { + isSaved = true; + if (!isSaving) { + isSaving = true; + metaData.duration = (int) lastPacket; + return CompletableFuture.runAsync(() -> { + saveMetadata(); + saveService.shutdown(); + boolean interrupted = false; + try { + saveService.awaitTermination(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + interrupted = true; + } + try { + if (save) { + replayFile.closeAndSave(dest); + } else { + replayFile.closeNotSave(); + } + } catch (IOException e) { + e.printStackTrace(); + throw new CompletionException(e); + } finally { + if (interrupted) { + Thread.currentThread().interrupt(); + } + } + }, runnable -> { + final Thread thread = new Thread(runnable, "Recording file save thread"); + thread.start(); + }); + } else { + LOGGER.warning("saveRecording() called twice"); + return CompletableFuture.supplyAsync(() -> { + throw new IllegalStateException("saveRecording() called twice"); + }); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/RecorderOption.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/RecorderOption.java new file mode 100644 index 00000000..8978fe0c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/RecorderOption.java @@ -0,0 +1,57 @@ +package org.leavesmc.leaves.replay; + +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.game.ClientboundGameEventPacket; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public class RecorderOption { + + public int recordDistance = -1; + public String serverName = "Leaves"; + public RecordWeather forceWeather = null; + public int forceDayTime = -1; + public boolean ignoreChat = false; + public boolean ignoreItem = false; + + @NotNull + @Contract(" -> new") + public static RecorderOption createDefaultOption() { + return new RecorderOption(); + } + + @NotNull + public static RecorderOption createFromBukkit(@NotNull BukkitRecorderOption bukkitRecorderOption) { + RecorderOption recorderOption = new RecorderOption(); + // recorderOption.recordDistance = bukkitRecorderOption.recordDistance; + // recorderOption.ignoreItem = bukkitRecorderOption.ignoreItem; + recorderOption.serverName = bukkitRecorderOption.serverName; + recorderOption.ignoreChat = bukkitRecorderOption.ignoreChat; + recorderOption.forceDayTime = bukkitRecorderOption.forceDayTime; + recorderOption.forceWeather = switch (bukkitRecorderOption.forceWeather) { + case RAIN -> RecordWeather.RAIN; + case CLEAR -> RecordWeather.CLEAR; + case THUNDER -> RecordWeather.THUNDER; + case NULL -> null; + }; + return recorderOption; + } + + public enum RecordWeather { + CLEAR(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)), + RAIN(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)), + THUNDER(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 1)); + + private final List> packets; + + private RecordWeather(Packet... packets) { + this.packets = List.of(packets); + } + + public List> getPackets() { + return packets; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java new file mode 100644 index 00000000..8d96445f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java @@ -0,0 +1,198 @@ +package org.leavesmc.leaves.replay; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import net.minecraft.SharedConstants; +import net.minecraft.network.ConnectionProtocol; +import net.minecraft.network.ProtocolInfo; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.Packet; +import net.minecraft.network.protocol.configuration.ConfigurationProtocols; +import net.minecraft.network.protocol.game.GameProtocols; +import net.minecraft.network.protocol.login.LoginProtocols; +import net.minecraft.network.protocol.status.StatusProtocols; +import net.minecraft.server.MinecraftServer; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.util.UUIDSerializer; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +public class ReplayFile { + + private static final String RECORDING_FILE = "recording.tmcpr"; + private static final String RECORDING_FILE_CRC32 = "recording.tmcpr.crc32"; + private static final String MARKER_FILE = "markers.json"; + private static final String META_FILE = "metaData.json"; + + private static final Gson MARKER_GSON = new GsonBuilder().registerTypeAdapter(ReplayMarker.class, new ReplayMarker.Serializer()).create(); + private static final Gson META_GSON = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDSerializer()).create(); + + private final File tmpDir; + private final DataOutputStream packetStream; + private final CRC32 crc32 = new CRC32(); + + private final File markerFile; + private final File metaFile; + + private final Map> protocols; + + public ReplayFile(@NotNull File name) throws IOException { + this.tmpDir = new File(name.getParentFile(), name.getName() + ".tmp"); + if (tmpDir.exists()) { + if (!ReplayFile.deleteDir(tmpDir)) { + throw new IOException("Recording file " + name + " already exists!"); + } + } + + if (!tmpDir.mkdirs()) { + throw new IOException("Failed to create temp directory for recording " + tmpDir); + } + + File packetFile = new File(tmpDir, RECORDING_FILE); + this.metaFile = new File(tmpDir, META_FILE); + this.markerFile = new File(tmpDir, MARKER_FILE); + + this.packetStream = new DataOutputStream(new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(packetFile)), crc32)); + + this.protocols = Map.of( + ConnectionProtocol.STATUS, StatusProtocols.CLIENTBOUND, + ConnectionProtocol.LOGIN, LoginProtocols.CLIENTBOUND, + ConnectionProtocol.CONFIGURATION, ConfigurationProtocols.CLIENTBOUND, + ConnectionProtocol.PLAY, GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(MinecraftServer.getServer().registryAccess())) + ); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private byte @NotNull [] getPacketBytes(Packet packet, ConnectionProtocol state) { + ProtocolInfo protocol = this.protocols.get(state); + if (protocol == null) { + throw new IllegalArgumentException("Unknown protocol state " + state); + } + + ByteBuf buf = Unpooled.buffer(); + protocol.codec().encode(buf, packet); + + buf.readerIndex(0); + byte[] ret = new byte[buf.readableBytes()]; + buf.readBytes(ret); + buf.release(); + return ret; + } + + public void saveMarkers(List markers) throws IOException { + try (Writer writer = new OutputStreamWriter(new FileOutputStream(markerFile), StandardCharsets.UTF_8)) { + writer.write(MARKER_GSON.toJson(markers)); + } + } + + public void saveMetaData(@NotNull RecordMetaData data) throws IOException { + data.fileFormat = "MCPR"; + data.fileFormatVersion = RecordMetaData.CURRENT_FILE_FORMAT_VERSION; + data.protocol = SharedConstants.getCurrentVersion().getProtocolVersion(); + data.generator = ProtocolUtils.buildProtocolVersion("replay"); + + try (Writer writer = new OutputStreamWriter(new FileOutputStream(metaFile), StandardCharsets.UTF_8)) { + writer.write(META_GSON.toJson(data)); + } + } + + public void savePacket(long timestamp, Packet packet, ConnectionProtocol protocol) throws Exception { + byte[] data = getPacketBytes(packet, protocol); + packetStream.writeInt((int) timestamp); + packetStream.writeInt(data.length); + packetStream.write(data); + } + + public synchronized void closeAndSave(File file) throws IOException { + packetStream.close(); + + String[] files = tmpDir.list(); + if (files == null) { + return; + } + + try (ZipOutputStream os = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) { + for (String fileName : files) { + os.putNextEntry(new ZipEntry(fileName)); + File f = new File(tmpDir, fileName); + copy(new FileInputStream(f), os); + } + + os.putNextEntry(new ZipEntry(RECORDING_FILE_CRC32)); + Writer writer = new OutputStreamWriter(os); + writer.write(Long.toString(crc32.getValue())); + writer.flush(); + } + + for (String fileName : files) { + File f = new File(tmpDir, fileName); + Files.delete(f.toPath()); + } + Files.delete(tmpDir.toPath()); + } + + public synchronized void closeNotSave() throws IOException { + packetStream.close(); + + String[] files = tmpDir.list(); + if (files == null) { + return; + } + + for (String fileName : files) { + File f = new File(tmpDir, fileName); + Files.delete(f.toPath()); + } + Files.delete(tmpDir.toPath()); + } + + private void copy(@NotNull InputStream in, OutputStream out) throws IOException { + byte[] buffer = new byte[8192]; + int len; + while ((len = in.read(buffer)) > -1) { + out.write(buffer, 0, len); + } + in.close(); + } + + private static boolean deleteDir(File dir) { + if (dir == null || !dir.exists()) { + return false; + } + + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDir(file); + } else { + if (!file.delete()) { + return false; + } + } + } + } + + return dir.delete(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayMarker.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayMarker.java new file mode 100644 index 00000000..1568f692 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ReplayMarker.java @@ -0,0 +1,43 @@ +package org.leavesmc.leaves.replay; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +import java.lang.reflect.Type; + +public class ReplayMarker { + + public int time; + public String name; + public double x = 0; + public double y = 0; + public double z = 0; + public float phi = 0; + public float theta = 0; + public float varphi = 0; + + public static class Serializer implements JsonSerializer { + @Override + public JsonElement serialize(ReplayMarker src, Type typeOfSrc, JsonSerializationContext context) { + JsonObject ret = new JsonObject(); + JsonObject value = new JsonObject(); + JsonObject position = new JsonObject(); + ret.add("realTimestamp", new JsonPrimitive(src.time)); + ret.add("value", value); + + value.add("name", new JsonPrimitive(src.name)); + value.add("position", position); + + position.add("x", new JsonPrimitive(src.x)); + position.add("y", new JsonPrimitive(src.y)); + position.add("z", new JsonPrimitive(src.z)); + position.add("yaw", new JsonPrimitive(src.phi)); + position.add("pitch", new JsonPrimitive(src.theta)); + position.add("roll", new JsonPrimitive(src.varphi)); + return ret; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java new file mode 100644 index 00000000..d1eb8b9e --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java @@ -0,0 +1,222 @@ +package org.leavesmc.leaves.replay; + +import com.mojang.authlib.GameProfile; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ClientInformation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.stats.ServerStatsCounter; +import net.minecraft.world.damagesource.DamageSource; +import net.minecraft.world.phys.Vec3; +import org.bukkit.Bukkit; +import org.bukkit.Location; +import org.bukkit.craftbukkit.CraftWorld; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.bot.BotStatsCounter; +import org.leavesmc.leaves.entity.CraftPhotographer; +import org.leavesmc.leaves.entity.Photographer; + +import java.io.File; +import java.io.IOException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; + +public class ServerPhotographer extends ServerPlayer { + + private static final List photographers = new CopyOnWriteArrayList<>(); + + public PhotographerCreateState createState; + private ServerPlayer followPlayer; + private Recorder recorder; + private File saveFile; + private Vec3 lastPos; + + private final ServerStatsCounter stats; + + private ServerPhotographer(MinecraftServer server, ServerLevel world, GameProfile profile) { + super(server, world, profile, ClientInformation.createDefault()); + this.gameMode = new ServerPhotographerGameMode(this); + this.followPlayer = null; + this.stats = new BotStatsCounter(server); + this.lastPos = this.position(); + } + + public static ServerPhotographer createPhotographer(@NotNull PhotographerCreateState state) throws IOException { + if (!isCreateLegal(state.id)) { + throw new IllegalArgumentException(state.id + " is a invalid photographer id"); + } + + MinecraftServer server = MinecraftServer.getServer(); + + ServerLevel world = ((CraftWorld) state.loc.getWorld()).getHandle(); + GameProfile profile = new GameProfile(UUID.randomUUID(), state.id); + + ServerPhotographer photographer = new ServerPhotographer(server, world, profile); + photographer.recorder = new Recorder(photographer, state.option, new File("replay", state.id)); + photographer.saveFile = new File("replay", state.id + ".mcpr"); + photographer.createState = state; + + photographer.recorder.start(); + MinecraftServer.getServer().getPlayerList().placeNewPhotographer(photographer.recorder, photographer, world); + photographer.serverLevel().chunkSource.move(photographer); + photographer.setInvisible(true); + photographers.add(photographer); + + LeavesLogger.LOGGER.info("Photographer " + state.id + " created"); + + // TODO record distance + + return photographer; + } + + @Override + public void tick() { + super.tick(); + super.doTick(); + + if (this.server.getTickCount() % 10 == 0) { + connection.resetPosition(); + this.serverLevel().chunkSource.move(this); + } + + if (this.followPlayer != null) { + if (this.getCamera() == this || this.getCamera().level() != this.level()) { + this.getBukkitPlayer().teleport(this.getCamera().getBukkitEntity().getLocation()); + this.setCamera(followPlayer); + } + if (lastPos.distanceToSqr(this.position()) > 1024D) { + this.getBukkitPlayer().teleport(this.getCamera().getBukkitEntity().getLocation()); + } + } + + lastPos = this.position(); + } + + @Override + public void die(@NotNull DamageSource damageSource) { + super.die(damageSource); + remove(true); + } + + @Override + public boolean isInvulnerableTo(@NotNull ServerLevel world, @NotNull DamageSource damageSource) { + return true; + } + + @Override + public boolean hurtServer(@NotNull ServerLevel world, @NotNull DamageSource source, float amount) { + return false; + } + + @Override + public void setHealth(float health) { + } + + @NotNull + @Override + public ServerStatsCounter getStats() { + return stats; + } + + public void remove(boolean async) { + this.remove(async, true); + } + + public void remove(boolean async, boolean save) { + super.remove(RemovalReason.KILLED); + photographers.remove(this); + this.recorder.stop(); + this.server.getPlayerList().removePhotographer(this); + + LeavesLogger.LOGGER.info("Photographer " + createState.id + " removed"); + + if (!recorder.isSaved()) { + CompletableFuture future = recorder.saveRecording(saveFile, save); + if (!async) { + future.join(); + } + } + } + + public void setFollowPlayer(ServerPlayer followPlayer) { + this.setCamera(followPlayer); + this.followPlayer = followPlayer; + } + + public void setSaveFile(File saveFile) { + this.saveFile = saveFile; + } + + public void pauseRecording() { + this.recorder.pauseRecording(); + } + + public void resumeRecording() { + this.recorder.resumeRecording(); + } + + public static ServerPhotographer getPhotographer(String id) { + for (ServerPhotographer photographer : photographers) { + if (photographer.createState.id.equals(id)) { + return photographer; + } + } + return null; + } + + public static ServerPhotographer getPhotographer(UUID uuid) { + for (ServerPhotographer photographer : photographers) { + if (photographer.getUUID().equals(uuid)) { + return photographer; + } + } + return null; + } + + public static List getPhotographers() { + return photographers; + } + + public Photographer getBukkitPlayer() { + return getBukkitEntity(); + } + + @Override + @NotNull + public CraftPhotographer getBukkitEntity() { + return (CraftPhotographer) super.getBukkitEntity(); + } + + public static boolean isCreateLegal(@NotNull String name) { + if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { + return false; + } + + return Bukkit.getPlayerExact(name) == null && ServerPhotographer.getPhotographer(name) == null; + } + + public static class PhotographerCreateState { + + public RecorderOption option; + public Location loc; + public final String id; + + public PhotographerCreateState(Location loc, String id, RecorderOption option) { + this.loc = loc; + this.id = id; + this.option = option; + } + + public ServerPhotographer createSync() { + try { + return createPhotographer(this); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographerGameMode.java b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographerGameMode.java new file mode 100644 index 00000000..c612215b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/replay/ServerPhotographerGameMode.java @@ -0,0 +1,35 @@ +package org.leavesmc.leaves.replay; + +import net.kyori.adventure.text.Component; +import net.minecraft.server.level.ServerPlayerGameMode; +import net.minecraft.world.level.GameType; +import org.bukkit.event.player.PlayerGameModeChangeEvent; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class ServerPhotographerGameMode extends ServerPlayerGameMode { + + public ServerPhotographerGameMode(ServerPhotographer photographer) { + super(photographer); + super.setGameModeForPlayer(GameType.SPECTATOR, null); + } + + @Override + public boolean changeGameModeForPlayer(@NotNull GameType gameMode) { + return false; + } + + @Nullable + @Override + public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) { + return null; + } + + @Override + protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) { + } + + @Override + public void tick() { + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/spark/LeavesServerConfigProvider.java b/leaves-server/src/main/java/org/leavesmc/leaves/spark/LeavesServerConfigProvider.java new file mode 100644 index 00000000..79e0c55c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/spark/LeavesServerConfigProvider.java @@ -0,0 +1,137 @@ +package org.leavesmc.leaves.spark; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializer; +import me.lucko.spark.paper.common.platform.serverconfig.ConfigParser; +import me.lucko.spark.paper.common.platform.serverconfig.ExcludedConfigFilter; +import me.lucko.spark.paper.common.platform.serverconfig.PropertiesConfigParser; +import me.lucko.spark.paper.common.platform.serverconfig.ServerConfigProvider; +import org.bukkit.Bukkit; +import org.bukkit.World; +import org.bukkit.configuration.MemorySection; +import org.bukkit.configuration.file.YamlConfiguration; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +// copy form https://github.com/lucko/spark/blob/master/spark-paper/src/main/java/me/lucko/spark/paper/PaperServerConfigProvider.java +public class LeavesServerConfigProvider extends ServerConfigProvider { + private static final Map FILES; + private static final Collection HIDDEN_PATHS; + + public LeavesServerConfigProvider() { + super(FILES, HIDDEN_PATHS); + } + + private static class YamlConfigParser implements ConfigParser { + public static final YamlConfigParser INSTANCE = new YamlConfigParser(); + protected static final Gson GSON = new GsonBuilder() + .registerTypeAdapter(MemorySection.class, (JsonSerializer) (obj, type, ctx) -> ctx.serialize(obj.getValues(false))) + .create(); + + @Override + public JsonElement load(String file, ExcludedConfigFilter filter) throws IOException { + Map values = this.parse(Paths.get(file)); + if (values == null) { + return null; + } + + return filter.apply(GSON.toJsonTree(values)); + } + + @Override + public Map parse(BufferedReader reader) { + YamlConfiguration config = YamlConfiguration.loadConfiguration(reader); + return config.getValues(false); + } + } + + private static class SplitYamlConfigParser extends YamlConfigParser { + public static final SplitYamlConfigParser INSTANCE = new SplitYamlConfigParser(); + + @Override + @Nullable + public JsonElement load(@NotNull String group, ExcludedConfigFilter filter) throws IOException { + String prefix = group.replace("/", ""); + + Path configDir = Paths.get("config"); + if (!Files.exists(configDir)) { + return null; + } + + JsonObject root = new JsonObject(); + + for (Map.Entry entry : getNestedFiles(configDir, prefix).entrySet()) { + String fileName = entry.getKey(); + Path path = entry.getValue(); + + Map values = this.parse(path); + if (values == null) { + continue; + } + + // apply the filter individually to each nested file + root.add(fileName, filter.apply(GSON.toJsonTree(values))); + } + + return root; + } + + @NotNull + private static Map getNestedFiles(@NotNull Path configDir, String prefix) { + Map files = new LinkedHashMap<>(); + files.put("global.yml", configDir.resolve(prefix + "-global.yml")); + files.put("world-defaults.yml", configDir.resolve(prefix + "-world-defaults.yml")); + for (World world : Bukkit.getWorlds()) { + files.put(world.getName() + ".yml", world.getWorldFolder().toPath().resolve(prefix + "-world.yml")); + } + return files; + } + } + + static { + ImmutableMap.Builder files = ImmutableMap.builder() + .put("server.properties", PropertiesConfigParser.INSTANCE) + .put("bukkit.yml", YamlConfigParser.INSTANCE) + .put("spigot.yml", YamlConfigParser.INSTANCE) + .put("paper/", SplitYamlConfigParser.INSTANCE) + .put("leaves.yml", YamlConfigParser.INSTANCE); + + for (String config : getSystemPropertyList("spark.serverconfigs.extra")) { + files.put(config, YamlConfigParser.INSTANCE); + } + + ImmutableSet.Builder hiddenPaths = ImmutableSet.builder() + .add("database") + .add("settings.bungeecord-addresses") + .add("settings.velocity-support.secret") + .add("proxies.velocity.secret") + .add("server-ip") + .add("motd") + .add("resource-pack") + .add("rconpassword") + .add("rconip") + .add("level-seed") + .add("world-settings.*.feature-seeds") + .add("world-settings.*.seed-*") + .add("feature-seeds") + .add("seed-*") + .addAll(getSystemPropertyList("spark.serverconfigs.hiddenpaths")); + + FILES = files.build(); + HIDDEN_PATHS = hiddenPaths.build(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/spark/LeavesSparkPlugin.java b/leaves-server/src/main/java/org/leavesmc/leaves/spark/LeavesSparkPlugin.java new file mode 100644 index 00000000..def59fb8 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/spark/LeavesSparkPlugin.java @@ -0,0 +1,28 @@ +package org.leavesmc.leaves.spark; + +import me.lucko.spark.paper.PaperSparkPlugin; +import me.lucko.spark.paper.api.Compatibility; +import me.lucko.spark.paper.api.PaperClassLookup; +import me.lucko.spark.paper.api.PaperScheduler; +import me.lucko.spark.paper.common.platform.serverconfig.ServerConfigProvider; +import org.bukkit.Server; +import org.jetbrains.annotations.NotNull; + +import java.util.logging.Logger; + +public class LeavesSparkPlugin extends PaperSparkPlugin { + + @NotNull + public static LeavesSparkPlugin create(Compatibility ignoredCompatibility, Server server, Logger logger, PaperScheduler scheduler, PaperClassLookup classLookup) { + return new LeavesSparkPlugin(server, logger, scheduler, classLookup); + } + + public LeavesSparkPlugin(Server server, Logger logger, PaperScheduler scheduler, PaperClassLookup classLookup) { + super(server, logger, scheduler, classLookup); + } + + @Override + public ServerConfigProvider createServerConfigProvider() { + return new LeavesServerConfigProvider(); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/ArrayConstants.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/ArrayConstants.java new file mode 100644 index 00000000..99d6484c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/ArrayConstants.java @@ -0,0 +1,21 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.server.level.ServerLevel; + +// Powered by Gale(https://github.com/GaleMC/Gale) + +public class ArrayConstants { + + private ArrayConstants() {} + + public static final Object[] emptyObjectArray = new Object[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 ServerLevel[] emptyServerLevelArray = new ServerLevel[0]; + +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java new file mode 100644 index 00000000..567cd33a --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/BlockPatternHelper.java @@ -0,0 +1,28 @@ +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/leaves-server/src/main/java/org/leavesmc/leaves/util/BreakBedrockList.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/BreakBedrockList.java new file mode 100644 index 00000000..fb10e783 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/BreakBedrockList.java @@ -0,0 +1,114 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.DirectionalBlock; +import net.minecraft.world.scores.Objective; +import net.minecraft.world.scores.ScoreHolder; +import net.minecraft.world.scores.criteria.ObjectiveCriteria; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class BreakBedrockList { + + private static final Map> pistonCache = new HashMap<>(); + private static final List BBL = new ArrayList<>(); + private static final List MBB = new ArrayList<>(); + private static final List LBL = new ArrayList<>(); + + public static void endTick() { + for (var map : pistonCache.values()) { + if (!map.isEmpty()) { + map.clear(); + } + } + } + + public static void onPlayerPlacePiston(Level level, Player player, BlockPos pos) { + if (LeavesConfig.modify.bedrockBreakList) { + Direction pistonFacing = level.getBlockState(pos).getValue(DirectionalBlock.FACING); + BlockPos bedrockPos = pos.relative(pistonFacing); + if (level.getBlockState(bedrockPos).getBlock() == Blocks.BEDROCK) { + pistonCache.computeIfAbsent(level, k -> new HashMap<>()).put(bedrockPos, player); + } + } + } + + public static void onPistonBreakBedrock(Level level, BlockPos bedrock) { + if (LeavesConfig.modify.bedrockBreakList) { + Map map = pistonCache.get(level); + + boolean flag = map != null && map.get(bedrock) != null; + + if (flag) { + if (!BBL.isEmpty()) { + Player player = map.get(bedrock); + for (Objective objective : BBL) { + level.getScoreboard().getOrCreatePlayerScore(player, objective).increment(); + } + } + } else { + if (!MBB.isEmpty()) { + ScoreHolder world = ScoreHolder.forNameOnly("$" + level.dimension().location()); + for (Objective objective : MBB) { + level.getScoreboard().getOrCreatePlayerScore(world, objective).increment(); + level.getScoreboard().getOrCreatePlayerScore(ScoreHolder.forNameOnly("$total"), objective).increment(); + } + } + } + + if (!LBL.isEmpty() && !level.players().isEmpty()) { + Player closestPlayer = level.getNearestPlayer(bedrock.getX(), bedrock.getY(), bedrock.getZ(), 10.5, null); + if (closestPlayer != null) { + for (Objective objective : LBL) { + level.getScoreboard().getOrCreatePlayerScore(closestPlayer, objective).increment(); + } + } + } + } + } + + public static void onScoreboardAdd(@NotNull Objective objective) { + if (LeavesConfig.modify.bedrockBreakList) { + if (objective.getCriteria() == ObjectiveCriteria.DUMMY) { + String name = objective.getName(); + + int i = name.length() - 4; + if (i >= 0) { + String suffix = name.substring(i); + switch (suffix) { + case ".bbl" -> BBL.add(objective); + case ".mbb" -> MBB.add(objective); + case ".lbl" -> LBL.add(objective); + } + } + } + } + } + + public static void onScoreboardRemove(@NotNull Objective objective) { + if (LeavesConfig.modify.bedrockBreakList) { + if (objective.getCriteria() == ObjectiveCriteria.DUMMY) { + String name = objective.getName(); + + int i = name.length() - 4; + if (i >= 0) { + String suffix = name.substring(i); + switch (suffix) { + case ".bbl" -> BBL.remove(objective); + case ".mbb" -> MBB.remove(objective); + case ".lbl" -> LBL.remove(objective); + } + } + } + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/ElytraAeronauticsHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/ElytraAeronauticsHelper.java new file mode 100644 index 00000000..00fb833c --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/ElytraAeronauticsHelper.java @@ -0,0 +1,40 @@ +package org.leavesmc.leaves.util; + +import ca.spottedleaf.moonrise.common.PlatformHooks; +import net.minecraft.core.SectionPos; +import net.minecraft.network.chat.Component; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.Vec3; +import org.leavesmc.leaves.LeavesConfig; + +public class ElytraAeronauticsHelper { + + public static void flightBehaviour(Player player, Vec3 velocity) { + if (LeavesConfig.modify.elytraAeronautics.noChunk) { + if ((LeavesConfig.modify.elytraAeronautics.noChunkSpeed <= 0.0D || velocity.horizontalDistanceSqr() >= LeavesConfig.modify.elytraAeronautics.noChunkSpeed) + && (LeavesConfig.modify.elytraAeronautics.noChunkHeight <= 0.0D || player.getY() >= LeavesConfig.modify.elytraAeronautics.noChunkHeight)) { + if (!player.elytraAeronauticsNoChunk) { + player.elytraAeronauticsNoChunk = true; + ServerPlayer serverPlayer = (ServerPlayer) player; + if (LeavesConfig.modify.elytraAeronautics.noChunkMes) { + serverPlayer.sendSystemMessage(Component.literal(LeavesConfig.modify.elytraAeronautics.noChunkStartMes), true); + } + PlatformHooks.get().removePlayerFromDistanceMaps(serverPlayer.serverLevel(), serverPlayer); + serverPlayer.serverLevel().chunkSource.chunkMap.getDistanceManager().removePlayer(serverPlayer.getLastSectionPos(), serverPlayer); + } + } else { + if (player.elytraAeronauticsNoChunk) { + player.elytraAeronauticsNoChunk = false; + ServerPlayer serverPlayer = (ServerPlayer) player; + if (LeavesConfig.modify.elytraAeronautics.noChunkMes) { + serverPlayer.sendSystemMessage(Component.literal(LeavesConfig.modify.elytraAeronautics.noChunkEndMes), true); + } + PlatformHooks.get().addPlayerToDistanceMaps((ServerLevel) serverPlayer.level(), serverPlayer); + ((ServerLevel) serverPlayer.level()).chunkSource.chunkMap.getDistanceManager().addPlayer(SectionPos.of(serverPlayer), serverPlayer); + } + } + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/FertilizableCoral.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/FertilizableCoral.java new file mode 100644 index 00000000..ec5aa65d --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/FertilizableCoral.java @@ -0,0 +1,72 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.registries.Registries; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.BaseCoralPlantTypeBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.BonemealableBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.levelgen.feature.CoralClawFeature; +import net.minecraft.world.level.levelgen.feature.CoralFeature; +import net.minecraft.world.level.levelgen.feature.CoralMushroomFeature; +import net.minecraft.world.level.levelgen.feature.CoralTreeFeature; +import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; +import net.minecraft.world.level.material.MapColor; +import org.jetbrains.annotations.NotNull; + +// Powered by fabric-carpet/src/main/java/carpet/helpers/FertilizableCoral.java +public interface FertilizableCoral extends BonemealableBlock { + + boolean isEnabled(); + + @Override + default boolean isValidBonemealTarget(@NotNull LevelReader world, @NotNull BlockPos pos, @NotNull BlockState state) { + return isEnabled() && state.getValue(BaseCoralPlantTypeBlock.WATERLOGGED) && world.getFluidState(pos.above()).is(FluidTags.WATER); + } + + @Override + default boolean isBonemealSuccess(@NotNull Level world, RandomSource random, @NotNull BlockPos pos, @NotNull BlockState state) { + return random.nextFloat() < 0.15D; + } + + @Override + default void performBonemeal(@NotNull ServerLevel worldIn, RandomSource random, @NotNull BlockPos pos, @NotNull BlockState blockUnder) { + int variant = random.nextInt(3); + CoralFeature coral = switch (variant) { + case 0 -> new CoralClawFeature(NoneFeatureConfiguration.CODEC); + case 1 -> new CoralTreeFeature(NoneFeatureConfiguration.CODEC); + default -> new CoralMushroomFeature(NoneFeatureConfiguration.CODEC); + }; + + MapColor color = blockUnder.getMapColor(worldIn, pos); + BlockState properBlock = blockUnder; + HolderSet.Named coralBlocks = worldIn.registryAccess().lookupOrThrow(Registries.BLOCK).getOrThrow(BlockTags.CORAL_BLOCKS); + for (Holder block : coralBlocks) { + properBlock = block.value().defaultBlockState(); + if (properBlock.getMapColor(worldIn, pos) == color) { + break; + } + } + worldIn.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_NONE); + + if (!coral.placeFeature(worldIn, random, pos, properBlock)) { + worldIn.setBlock(pos, blockUnder, 3); + } else { + if (worldIn.random.nextInt(10) == 0) { + BlockPos randomPos = pos.offset(worldIn.random.nextInt(16) - 8, worldIn.random.nextInt(8), worldIn.random.nextInt(16) - 8); + if (coralBlocks.contains(worldIn.getBlockState(randomPos).getBlockHolder())) { + worldIn.setBlock(randomPos, Blocks.WET_SPONGE.defaultBlockState(), Block.UPDATE_ALL); + } + } + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/HopperCounter.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/HopperCounter.java new file mode 100644 index 00000000..f87dfa76 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/HopperCounter.java @@ -0,0 +1,337 @@ +package org.leavesmc.leaves.util; + +import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2LongMap; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.TextComponent; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.TextColor; +import net.kyori.adventure.text.format.TextDecoration; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.item.DyeItem; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeManager; +import net.minecraft.world.item.crafting.display.RecipeDisplay; +import net.minecraft.world.item.crafting.display.SlotDisplayContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.AbstractBannerBlock; +import net.minecraft.world.level.block.BeaconBeamBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.MapColor; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesConfig; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import static java.util.Map.entry; + +// Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet) + +public class HopperCounter { + + private static boolean enabled = false; + private static final Map COUNTERS; + + static { + EnumMap counterMap = new EnumMap<>(DyeColor.class); + for (DyeColor color : DyeColor.values()) { + counterMap.put(color, new HopperCounter(color)); + } + COUNTERS = Collections.unmodifiableMap(counterMap); + } + + public final DyeColor color; + private final TextComponent coloredName; + private final Object2LongMap counter = new Object2LongLinkedOpenHashMap<>(); + private long startTick; + private long startMillis; + + private HopperCounter(DyeColor color) { + this.startTick = -1; + this.color = color; + this.coloredName = Component.text(color.getName(), TextColor.color(color.getTextColor())); + } + + public void add(MinecraftServer server, ItemStack stack) { + if (startTick < 0) { + startTick = server.overworld().getGameTime(); + startMillis = System.currentTimeMillis(); + } + Item item = stack.getItem(); + counter.put(item, counter.getLong(item) + stack.getCount()); + } + + public void reset(MinecraftServer server) { + counter.clear(); + startTick = server.overworld().getGameTime(); + startMillis = System.currentTimeMillis(); + } + + public static void resetAll(MinecraftServer server, boolean fresh) { + for (HopperCounter counter : COUNTERS.values()) { + counter.reset(server); + if (fresh) { + counter.startTick = -1; + } + } + } + + public List format(MinecraftServer server, boolean realTime) { + long ticks = Math.max(realTime ? (System.currentTimeMillis() - startMillis) / 50 : server.overworld().getGameTime() - startTick, -1); + + if (startTick < 0 || ticks == -1) { + return Collections.singletonList(Component.text().append(coloredName, Component.text(" hasn't started counting yet")).build()); + } + + long total = getTotalItems(); + if (total <= 0) { + return Collections.singletonList(Component.text() + .append(Component.text("No items for "), coloredName) + .append(Component.text(" yet ("), Component.text(String.format("%.2f ", ticks / (20.0 * 60.0)), Style.style(TextDecoration.BOLD))) + .append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text(")")) + .build()); + } + + List items = new ArrayList<>(); + items.add(Component.text() + .append(Component.text("Items for "), coloredName, Component.text(" ")) + .append(Component.text("("), Component.text(String.format("%.2f ", ticks * 1.0 / (20 * 60)), Style.style(TextDecoration.BOLD))) + .append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text("), ")) + .append(Component.text("total: "), Component.text(total, Style.style(TextDecoration.BOLD)), Component.text(", ")) + .append(Component.text("("), Component.text(String.format("%.1f", total * 1.0 * (20 * 60 * 60) / ticks), Style.style(TextDecoration.BOLD))) + .append(Component.text("/h):")) + .build()); + + items.addAll(counter.object2LongEntrySet().stream().sorted((e, f) -> Long.compare(f.getLongValue(), e.getLongValue())).map(entry -> { + Item item = entry.getKey(); + Component name = Component.translatable(item.getDescriptionId()); + TextColor textColor = guessColor(server, item); + + if (textColor != null) { + name = name.style(name.style().merge(Style.style(textColor))); + } else { + name = name.style(name.style().merge(Style.style(TextDecoration.ITALIC))); + } + + long count = entry.getLongValue(); + return Component.text() + .append(Component.text("- ", NamedTextColor.GRAY)) + .append(name) + .append(Component.text(": ", NamedTextColor.GRAY)) + .append(Component.text(count, Style.style(TextDecoration.BOLD)), Component.text(", ", NamedTextColor.GRAY)) + .append(Component.text(String.format("%.1f", count * (20.0 * 60.0 * 60.0) / ticks), Style.style(TextDecoration.BOLD))) + .append(Component.text("/h")) + .build(); + }).toList()); + return items; + } + + private static final Map DEFAULTS = Map.ofEntries( + entry(Items.DANDELION, Blocks.YELLOW_WOOL), + entry(Items.POPPY, Blocks.RED_WOOL), + entry(Items.BLUE_ORCHID, Blocks.LIGHT_BLUE_WOOL), + entry(Items.ALLIUM, Blocks.MAGENTA_WOOL), + entry(Items.AZURE_BLUET, Blocks.SNOW_BLOCK), + entry(Items.RED_TULIP, Blocks.RED_WOOL), + entry(Items.ORANGE_TULIP, Blocks.ORANGE_WOOL), + entry(Items.WHITE_TULIP, Blocks.SNOW_BLOCK), + entry(Items.PINK_TULIP, Blocks.PINK_WOOL), + entry(Items.OXEYE_DAISY, Blocks.SNOW_BLOCK), + entry(Items.CORNFLOWER, Blocks.BLUE_WOOL), + entry(Items.WITHER_ROSE, Blocks.BLACK_WOOL), + entry(Items.LILY_OF_THE_VALLEY, Blocks.WHITE_WOOL), + entry(Items.BROWN_MUSHROOM, Blocks.BROWN_MUSHROOM_BLOCK), + entry(Items.RED_MUSHROOM, Blocks.RED_MUSHROOM_BLOCK), + entry(Items.STICK, Blocks.OAK_PLANKS), + entry(Items.GOLD_INGOT, Blocks.GOLD_BLOCK), + entry(Items.IRON_INGOT, Blocks.IRON_BLOCK), + entry(Items.DIAMOND, Blocks.DIAMOND_BLOCK), + entry(Items.NETHERITE_INGOT, Blocks.NETHERITE_BLOCK), + entry(Items.SUNFLOWER, Blocks.YELLOW_WOOL), + entry(Items.LILAC, Blocks.MAGENTA_WOOL), + entry(Items.ROSE_BUSH, Blocks.RED_WOOL), + entry(Items.PEONY, Blocks.PINK_WOOL), + entry(Items.CARROT, Blocks.ORANGE_WOOL), + entry(Items.APPLE, Blocks.RED_WOOL), + entry(Items.WHEAT, Blocks.HAY_BLOCK), + entry(Items.PORKCHOP, Blocks.PINK_WOOL), + entry(Items.RABBIT, Blocks.PINK_WOOL), + entry(Items.CHICKEN, Blocks.WHITE_TERRACOTTA), + entry(Items.BEEF, Blocks.NETHERRACK), + entry(Items.ENCHANTED_GOLDEN_APPLE, Blocks.GOLD_BLOCK), + entry(Items.COD, Blocks.WHITE_TERRACOTTA), + entry(Items.SALMON, Blocks.ACACIA_PLANKS), + entry(Items.ROTTEN_FLESH, Blocks.BROWN_WOOL), + entry(Items.PUFFERFISH, Blocks.YELLOW_TERRACOTTA), + entry(Items.TROPICAL_FISH, Blocks.ORANGE_WOOL), + entry(Items.POTATO, Blocks.WHITE_TERRACOTTA), + entry(Items.MUTTON, Blocks.RED_WOOL), + entry(Items.BEETROOT, Blocks.NETHERRACK), + entry(Items.MELON_SLICE, Blocks.MELON), + entry(Items.POISONOUS_POTATO, Blocks.SLIME_BLOCK), + entry(Items.SPIDER_EYE, Blocks.NETHERRACK), + entry(Items.GUNPOWDER, Blocks.GRAY_WOOL), + entry(Items.TURTLE_SCUTE, Blocks.LIME_WOOL), + entry(Items.ARMADILLO_SCUTE, Blocks.ANCIENT_DEBRIS), + entry(Items.FEATHER, Blocks.WHITE_WOOL), + entry(Items.FLINT, Blocks.BLACK_WOOL), + entry(Items.LEATHER, Blocks.SPRUCE_PLANKS), + entry(Items.GLOWSTONE_DUST, Blocks.GLOWSTONE), + entry(Items.PAPER, Blocks.WHITE_WOOL), + entry(Items.BRICK, Blocks.BRICKS), + entry(Items.INK_SAC, Blocks.BLACK_WOOL), + entry(Items.SNOWBALL, Blocks.SNOW_BLOCK), + entry(Items.WATER_BUCKET, Blocks.WATER), + entry(Items.LAVA_BUCKET, Blocks.LAVA), + entry(Items.MILK_BUCKET, Blocks.WHITE_WOOL), + entry(Items.CLAY_BALL, Blocks.CLAY), + entry(Items.COCOA_BEANS, Blocks.COCOA), + entry(Items.BONE, Blocks.BONE_BLOCK), + entry(Items.COD_BUCKET, Blocks.BROWN_TERRACOTTA), + entry(Items.PUFFERFISH_BUCKET, Blocks.YELLOW_TERRACOTTA), + entry(Items.SALMON_BUCKET, Blocks.PINK_TERRACOTTA), + entry(Items.TROPICAL_FISH_BUCKET, Blocks.ORANGE_TERRACOTTA), + entry(Items.SUGAR, Blocks.WHITE_WOOL), + entry(Items.BLAZE_POWDER, Blocks.GOLD_BLOCK), + entry(Items.ENDER_PEARL, Blocks.WARPED_PLANKS), + entry(Items.NETHER_STAR, Blocks.DIAMOND_BLOCK), + entry(Items.PRISMARINE_CRYSTALS, Blocks.SEA_LANTERN), + entry(Items.PRISMARINE_SHARD, Blocks.PRISMARINE), + entry(Items.RABBIT_HIDE, Blocks.OAK_PLANKS), + entry(Items.CHORUS_FRUIT, Blocks.PURPUR_BLOCK), + entry(Items.SHULKER_SHELL, Blocks.SHULKER_BOX), + entry(Items.NAUTILUS_SHELL, Blocks.BONE_BLOCK), + entry(Items.HEART_OF_THE_SEA, Blocks.CONDUIT), + entry(Items.HONEYCOMB, Blocks.HONEYCOMB_BLOCK), + entry(Items.NAME_TAG, Blocks.BONE_BLOCK), + entry(Items.TOTEM_OF_UNDYING, Blocks.YELLOW_TERRACOTTA), + entry(Items.TRIDENT, Blocks.PRISMARINE), + entry(Items.GHAST_TEAR, Blocks.WHITE_WOOL), + entry(Items.PHANTOM_MEMBRANE, Blocks.BONE_BLOCK), + entry(Items.EGG, Blocks.BONE_BLOCK), + entry(Items.COPPER_INGOT, Blocks.COPPER_BLOCK), + entry(Items.AMETHYST_SHARD, Blocks.AMETHYST_BLOCK) + ); + + @Nullable + public static TextColor guessColor(@NotNull MinecraftServer server, Item item) { + RegistryAccess registryAccess = server.registryAccess(); + TextColor direct = fromItem(item, registryAccess); + if (direct != null) { + return direct; + } + + ResourceLocation id = registryAccess.lookupOrThrow(Registries.ITEM).getKey(item); + if (id == null) { + return null; + } + + + for (Recipe recipe : getRecipesForOutput(server.getRecipeManager(), id, server.overworld())) { + for (Ingredient ingredient : recipe.placementInfo().ingredients()) { + Optional> match = ingredient.items().filter(stack -> fromItem(stack.value(), registryAccess) != null).findFirst(); + if (match.isPresent()) { + return fromItem(match.get().value(), registryAccess); + } + } + } + return null; + } + + @NotNull + public static List> getRecipesForOutput(@NotNull RecipeManager recipeManager, ResourceLocation id, Level level) { + List> results = new ArrayList<>(); + ContextMap context = SlotDisplayContext.fromLevel(level); + recipeManager.getRecipes().forEach(recipe -> { + for (RecipeDisplay recipeDisplay : recipe.value().display()) { + recipeDisplay.result().resolveForStacks(context).forEach(stack -> { + if (BuiltInRegistries.ITEM.wrapAsHolder(stack.getItem()).unwrapKey().map(ResourceKey::location).orElseThrow(IllegalStateException::new).equals(id)) { + results.add(recipe.value()); + } + }); + } + }); + return results; + } + + @Nullable + public static TextColor fromItem(Item item, RegistryAccess registryAccess) { + if (DEFAULTS.containsKey(item)) { + return TextColor.color(appropriateColor(DEFAULTS.get(item).defaultMapColor().col)); + } + if (item instanceof DyeItem dye) { + return TextColor.color(appropriateColor(dye.getDyeColor().getMapColor().col)); + } + + Block block = null; + final Registry itemRegistry = registryAccess.lookupOrThrow(Registries.ITEM); + final Registry blockRegistry = registryAccess.lookupOrThrow(Registries.BLOCK); + ResourceLocation id = itemRegistry.getKey(item); + if (item instanceof BlockItem blockItem) { + block = blockItem.getBlock(); + } else if (blockRegistry.getOptional(id).isPresent()) { + block = blockRegistry.getValue(id); + } + + if (block != null) { + if (block instanceof AbstractBannerBlock) { + return TextColor.color(appropriateColor(((AbstractBannerBlock) block).getColor().getMapColor().col)); + } else if (block instanceof BeaconBeamBlock) { + return TextColor.color(appropriateColor(((BeaconBeamBlock) block).getColor().getMapColor().col)); + } + return TextColor.color(appropriateColor(block.defaultMapColor().col)); + } + return null; + } + + public static int appropriateColor(int color) { + if (color == 0) { + return MapColor.SNOW.col; + } + int r = (color >> 16 & 255); + int g = (color >> 8 & 255); + int b = (color & 255); + if (r < 70) r = 70; + if (g < 70) g = 70; + if (b < 70) b = 70; + return (r << 16) + (g << 8) + b; + } + + public long getTotalItems() { + return counter.isEmpty() ? 0 : counter.values().longStream().sum(); + } + + public static HopperCounter getCounter(DyeColor color) { + return COUNTERS.get(color); + } + + public static void setEnabled(boolean is) { + enabled = is; + } + + public static boolean isEnabled() { + return LeavesConfig.modify.hopperCounter && enabled; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java new file mode 100644 index 00000000..0b48f91f --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java @@ -0,0 +1,249 @@ +package org.leavesmc.leaves.util; + +import com.google.common.base.Charsets; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import io.papermc.paper.ServerBuildInfo; +import net.minecraft.Util; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.channels.Channels; +import java.nio.channels.FileChannel; +import java.nio.channels.ReadableByteChannel; +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.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; + +import static java.nio.file.StandardOpenOption.CREATE; +import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; +import static java.nio.file.StandardOpenOption.WRITE; + +public class LeavesUpdateHelper { + + private final static String autoUpdateDir = "auto_update"; + private final static String corePathFileName = autoUpdateDir + File.separator + "core.path"; + + private final static ReentrantLock updateLock = new ReentrantLock(); + private static boolean updateTaskStarted = false; + + private static final ScheduledExecutorService autoUpdateExecutor = Executors.newScheduledThreadPool(1); + + public static void init() { + File workingDirFile = new File(autoUpdateDir); + if (!workingDirFile.exists()) { + if (!workingDirFile.mkdir()) { + LeavesLogger.LOGGER.warning("Failed to create working directory: " + autoUpdateDir); + } + } + + File corePathFile = new File(corePathFileName); + if (!corePathFile.exists()) { + try { + if (!corePathFile.createNewFile()) { + throw new IOException(); + } + } catch (IOException e) { + LeavesLogger.LOGGER.severe("Failed to create core path file: " + corePathFileName, e); + } + } + + File leavesUpdateDir = new File(autoUpdateDir + File.separator + "leaves"); + if (!leavesUpdateDir.exists()) { + if (!leavesUpdateDir.mkdir()) { + LeavesLogger.LOGGER.warning("Failed to create leaves update directory: " + leavesUpdateDir); + } + } + + if (LeavesConfig.mics.autoUpdate.enable) { + LocalTime currentTime = LocalTime.now(); + long dailyTaskPeriod = 24 * 60 * 60 * 1000; + + for (String time : LeavesConfig.mics.autoUpdate.updateTime) { + try { + LocalTime taskTime = LocalTime.of(Integer.parseInt(time.split(":")[0]), Integer.parseInt(time.split(":")[1])); + Duration task = Duration.between(currentTime, taskTime); + if (task.isNegative()) { + task = task.plusDays(1); + } + autoUpdateExecutor.scheduleAtFixedRate(LeavesUpdateHelper::tryUpdateLeaves, task.toMillis(), dailyTaskPeriod, TimeUnit.MILLISECONDS); + } catch (Exception ignored) { + LeavesLogger.LOGGER.warning("Illegal auto-update time ignored: " + time); + } + } + } + } + + public static void tryUpdateLeaves() { + updateLock.lock(); + try { + if (!updateTaskStarted) { + updateTaskStarted = true; + new Thread(LeavesUpdateHelper::downloadLeaves).start(); + } + } finally { + updateLock.unlock(); + } + } + + private static void downloadLeaves() { + ServerBuildInfo version = ServerBuildInfo.buildInfo(); + if (version.gitCommit().isEmpty() || version.buildNumber().isEmpty()) { + LeavesLogger.LOGGER.info("IDE, custom build? Can not update!"); + updateTaskStarted = false; + return; + } + + LeavesLogger.LOGGER.info("Now gitHash: " + version.gitCommit().get()); + LeavesLogger.LOGGER.info("Trying to get latest build info."); + LeavesBuildInfo buildInfo = getLatestBuildInfo(version.minecraftVersionId(), version.gitCommit().get()); + + if (buildInfo != LeavesBuildInfo.ERROR) { + if (!buildInfo.needUpdate) { + LeavesLogger.LOGGER.warning("You are running the latest version, stopping update."); + updateTaskStarted = false; + return; + } + + LeavesLogger.LOGGER.info("Got build info, trying to download " + buildInfo.fileName); + try { + Path outFile = Path.of(autoUpdateDir, "leaves", buildInfo.fileName + ".cache"); + Files.deleteIfExists(outFile); + + try ( + final ReadableByteChannel source = Channels.newChannel(new URI( + buildInfo.url + LeavesConfig.mics.autoUpdate.source).toURL().openStream() + ); + final FileChannel fileChannel = FileChannel.open(outFile, CREATE, WRITE, TRUNCATE_EXISTING) + ) { + fileChannel.transferFrom(source, 0, Long.MAX_VALUE); + LeavesLogger.LOGGER.info("Download " + buildInfo.fileName + " completed."); + } catch (final IOException e) { + LeavesLogger.LOGGER.warning("Download " + buildInfo.fileName + " failed.", e); + Files.deleteIfExists(outFile); + updateTaskStarted = false; + return; + } + + if (!isFileValid(outFile, buildInfo.sha256)) { + LeavesLogger.LOGGER.warning("Hash check failed for downloaded file " + buildInfo.fileName); + Files.deleteIfExists(outFile); + updateTaskStarted = false; + return; + } + + File nowServerCore = new File(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName); + File backupServerCore = new File(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName + ".old"); + Util.safeReplaceFile(nowServerCore.toPath(), outFile, backupServerCore.toPath()); + + try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(corePathFileName))) { + bufferedWriter.write(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName); + } catch (IOException e) { + LeavesLogger.LOGGER.warning("Fail to download leaves core", e); + updateTaskStarted = false; + return; + } + + LeavesLogger.LOGGER.info("Leaves update completed, please restart your server."); + } catch (Exception e) { + LeavesLogger.LOGGER.severe("Leaves update failed", e); + } + } else { + LeavesLogger.LOGGER.warning("Stopping update."); + } + updateTaskStarted = false; + } + + private static boolean isFileValid(Path file, String hash) { + try (FileInputStream inputStream = new FileInputStream(file.toFile())) { + byte[] buffer = new byte[1024]; + MessageDigest md5 = MessageDigest.getInstance("SHA-256"); + + for (int numRead; (numRead = inputStream.read(buffer)) > 0; ) { + md5.update(buffer, 0, numRead); + } + + return toHexString(md5.digest()).equals(hash); + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Fail to validate file " + file, e); + } + return false; + } + + @NotNull + private static String toHexString(byte @NotNull [] bytes) { + StringBuilder builder = new StringBuilder(); + for (byte b : bytes) { + builder.append(String.format("%02x", b)); + } + return builder.toString(); + } + + private static LeavesBuildInfo getLatestBuildInfo(String mcVersion, String gitHash) { + try { + HttpURLConnection connection = (HttpURLConnection) new URI( + "https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/latest" + ).toURL().openConnection(); + connection.connect(); + if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { + return LeavesBuildInfo.ERROR; + } + try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) { + JsonObject obj = new Gson().fromJson(reader, JsonObject.class); + String channel = obj.get("channel").getAsString(); + if ("experimental".equals(channel) && !LeavesConfig.mics.autoUpdate.allowExperimental) { + LeavesLogger.LOGGER.warning("Experimental version is not allowed to update for default, if you really want to update, please set misc.auto-update.allow-experimental to true in leaves.yml"); + return LeavesBuildInfo.ERROR; + } + int build = obj.get("build").getAsInt(); + + JsonArray changes = obj.get("changes").getAsJsonArray(); + boolean needUpdate = true; + for (JsonElement change : changes) { + if (change.getAsJsonObject().get("commit").getAsString().startsWith(gitHash)) { + needUpdate = false; + break; + } + } + + JsonObject downloadInfo = obj.get("downloads").getAsJsonObject().get("application").getAsJsonObject(); + String fileName = downloadInfo.get("name").getAsString(); + String sha256 = downloadInfo.get("sha256").getAsString(); + String url = "https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/" + build + "/downloads/"; + return new LeavesBuildInfo(build, fileName, sha256, needUpdate, url); + } catch (JsonSyntaxException | NumberFormatException e) { + LeavesLogger.LOGGER.warning("Fail to get latest build info", e); + return LeavesBuildInfo.ERROR; + } + } catch (IOException | URISyntaxException e) { + LeavesLogger.LOGGER.warning("Fail to get latest build info", e); + return LeavesBuildInfo.ERROR; + } + } + + private record LeavesBuildInfo(int build, String fileName, String sha256, boolean needUpdate, String url) { + public static LeavesBuildInfo ERROR = null; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/LeavesVersionFetcher.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/LeavesVersionFetcher.java new file mode 100644 index 00000000..83f9bf94 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/LeavesVersionFetcher.java @@ -0,0 +1,128 @@ +package org.leavesmc.leaves.util; + +import com.destroystokyo.paper.PaperVersionFetcher; +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; +import com.mojang.logging.LogUtils; +import io.papermc.paper.ServerBuildInfo; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.event.ClickEvent; +import net.kyori.adventure.text.format.NamedTextColor; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; + +import java.io.BufferedReader; +import java.io.IOException; +import java.net.URI; +import java.util.Optional; +import java.util.OptionalInt; +import java.util.stream.StreamSupport; + +import static net.kyori.adventure.text.Component.text; +import static net.kyori.adventure.text.format.TextColor.color; + +public class LeavesVersionFetcher extends PaperVersionFetcher { + + private static final Logger LOGGER = LogUtils.getClassLogger(); + + private static final int DISTANCE_ERROR = -1; + private static final int DISTANCE_UNKNOWN = -2; + private static final String DOWNLOAD_PAGE = "https://leavesmc.org/downloads/leaves"; + + @NotNull + @Override + public Component getVersionMessage(@NotNull String serverVersion) { + final Component updateMessage; + final ServerBuildInfo build = ServerBuildInfo.buildInfo(); + if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) { + updateMessage = text("You are running a development version without access to version information", color(0xFF5300)); + } else if (build.buildNumber().isEmpty()) { + updateMessage = text("You are running a development version form CI", color(0xFF5300)); + } else { + updateMessage = getUpdateStatusMessage("LeavesMC/Leaves", build); + } + final @Nullable Component history = this.getHistory(); + + return history != null ? Component.textOfChildren(updateMessage, Component.newline(), history) : updateMessage; + } + + private static Component getUpdateStatusMessage(@NotNull final String repo, @NotNull final ServerBuildInfo build) { + int distance = fetchDistanceFromLeavesApiV2Build(build); + + if (distance == DISTANCE_ERROR) { + distance = fetchDistanceFromLeavesApiV2Hash(build); + } + + if (distance == DISTANCE_ERROR) { + final Optional gitBranch = build.gitBranch(); + final Optional gitCommit = build.gitCommit(); + if (gitBranch.isPresent() && gitCommit.isPresent()) { + distance = fetchDistanceFromGitHub(repo, gitBranch.get(), gitCommit.get()); + } + } + + return switch (distance) { + case DISTANCE_ERROR -> Component.text("Error obtaining version information", NamedTextColor.YELLOW); + case 0 -> Component.text("You are running the latest version", NamedTextColor.GREEN); + case DISTANCE_UNKNOWN -> Component.text("Unknown version", NamedTextColor.YELLOW); + default -> Component.text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW) + .append(Component.newline()) + .append(Component.text("Download the new version at: ") + .append(Component.text(DOWNLOAD_PAGE, NamedTextColor.GOLD) + .hoverEvent(Component.text("Click to open", NamedTextColor.WHITE)) + .clickEvent(ClickEvent.openUrl(DOWNLOAD_PAGE)))); + }; + } + + private static int fetchDistanceFromLeavesApiV2Build(final ServerBuildInfo build) { + OptionalInt buildNumber = build.buildNumber(); + if (buildNumber.isEmpty()) { + return DISTANCE_ERROR; + } + + try { + try (final BufferedReader reader = Resources.asCharSource( + URI.create("https://api.leavesmc.org/v2/projects/leaves/versions/" + build.minecraftVersionId()).toURL(), + Charsets.UTF_8 + ).openBufferedStream()) { + final JsonObject json = new Gson().fromJson(reader, JsonObject.class); + final JsonArray builds = json.getAsJsonArray("builds"); + final int latest = StreamSupport.stream(builds.spliterator(), false) + .mapToInt(JsonElement::getAsInt) + .max() + .orElseThrow(); + return latest - buildNumber.getAsInt(); + } catch (final JsonSyntaxException ex) { + LOGGER.error("Error parsing json from Leaves's downloads API", ex); + return DISTANCE_ERROR; + } + } catch (final IOException e) { + LOGGER.error("Error while parsing version", e); + return DISTANCE_ERROR; + } + } + + private static int fetchDistanceFromLeavesApiV2Hash(final ServerBuildInfo build) { + if (build.gitCommit().isEmpty()) { + return DISTANCE_ERROR; + } + + try { + try (BufferedReader reader = Resources.asCharSource( + URI.create("https://api.leavesmc.org/v2/projects/leaves/versions/" + build.minecraftVersionId() + "/differ/" + build.gitCommit().get()).toURL(), + Charsets.UTF_8 + ).openBufferedStream()) { + return Integer.parseInt(reader.readLine()); + } + } catch (IOException e) { + LOGGER.error("Error while parsing version", e); + return DISTANCE_ERROR; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/MathUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/MathUtils.java new file mode 100644 index 00000000..56050158 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/MathUtils.java @@ -0,0 +1,96 @@ +package org.leavesmc.leaves.util; + +import org.bukkit.util.NumberConversions; +import org.bukkit.util.Vector; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Pattern; + +public class MathUtils { + + private static final Pattern numericPattern = Pattern.compile("^-?[1-9]\\d*$|^0$"); + + public static boolean isNumeric(String str) { + return numericPattern.matcher(str).matches(); + } + + public static float @NotNull [] fetchYawPitch(@NotNull Vector dir) { + double x = dir.getX(); + double z = dir.getZ(); + + float[] out = new float[2]; + + if (x == 0.0D && z == 0.0D) { + out[1] = (float) (dir.getY() > 0.0D ? -90 : 90); + } else { + double theta = Math.atan2(-x, z); + out[0] = (float) Math.toDegrees((theta + 6.283185307179586D) % 6.283185307179586D); + + double x2 = NumberConversions.square(x); + double z2 = NumberConversions.square(z); + double xz = Math.sqrt(x2 + z2); + out[1] = (float) Math.toDegrees(Math.atan(-dir.getY() / xz)); + } + + return out; + } + + public static float fetchPitch(@NotNull Vector dir) { + double x = dir.getX(); + double z = dir.getZ(); + + float result; + + if (x == 0.0D && z == 0.0D) { + result = (float) (dir.getY() > 0.0D ? -90 : 90); + } else { + double x2 = NumberConversions.square(x); + double z2 = NumberConversions.square(z); + double xz = Math.sqrt(x2 + z2); + result = (float) Math.toDegrees(Math.atan(-dir.getY() / xz)); + } + + return result; + } + + @NotNull + public static Vector getDirection(double rotX, double rotY) { + Vector vector = new Vector(); + + rotX = Math.toRadians(rotX); + rotY = Math.toRadians(rotY); + + double xz = Math.abs(Math.cos(rotY)); + + vector.setX(-Math.sin(rotX) * xz); + vector.setZ(Math.cos(rotX) * xz); + vector.setY(-Math.sin(rotY)); + + return vector; + } + + private static final int[] MULTIPLY_DE_BRUIJN_BIT_POSITION = new int[]{0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; + + public static int floorLog2(int value) { + return ceilLog2(value) - (isPowerOfTwo(value) ? 0 : 1); + } + + public static int ceilLog2(int value) { + value = isPowerOfTwo(value) ? value : smallestEncompassingPowerOfTwo(value); + return MULTIPLY_DE_BRUIJN_BIT_POSITION[(int) ((long) value * 125613361L >> 27) & 31]; + } + + public static boolean isPowerOfTwo(int value) { + return value != 0 && (value & value - 1) == 0; + } + + public static int smallestEncompassingPowerOfTwo(int value) { + int i = value - 1; + i |= i >> 1; + i |= i >> 2; + i |= i >> 4; + i |= i >> 8; + i |= i >> 16; + return i + 1; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/McTechnicalModeHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/McTechnicalModeHelper.java new file mode 100644 index 00000000..0d8e5bf5 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/McTechnicalModeHelper.java @@ -0,0 +1,27 @@ +package org.leavesmc.leaves.util; + +import io.papermc.paper.configuration.GlobalConfiguration; +import org.leavesmc.leaves.LeavesConfig; + +import java.util.Map; + +public class McTechnicalModeHelper { + + public static void doMcTechnicalModeIf() { + if (LeavesConfig.modify.mcTechnicalMode) { + doMcTechnicalMode(); + } + } + + public static void doMcTechnicalMode() { + GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication = true; + GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons = true; + GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits = true; + GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation = true; + GlobalConfiguration.get().packetLimiter.allPackets = new GlobalConfiguration.PacketLimiter.PacketLimit(GlobalConfiguration.get().packetLimiter.allPackets.interval(), + 5000.0, GlobalConfiguration.get().packetLimiter.allPackets.action()); + GlobalConfiguration.get().packetLimiter.overrides = Map.of(); + GlobalConfiguration.get().itemValidation.resolveSelectorsInBooks = true; + GlobalConfiguration.get().scoreboards.saveEmptyScoreboardTeams = true; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/ReturnPortalManager.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/ReturnPortalManager.java new file mode 100644 index 00000000..67eb48e5 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/ReturnPortalManager.java @@ -0,0 +1,98 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.portal.PortalForcer; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +// Powered by NetherPortalFix(https://github.com/TwelveIterationMods/NetherPortalFix) +public class ReturnPortalManager { + + private static final int MAX_PORTAL_DISTANCE_SQ = 16; + private static final String RETURN_PORTAL_LIST = "ReturnPortalList"; + private static final String RETURN_PORTAL_UID = "UID"; + private static final String FROM_DIM = "FromDim"; + private static final String FROM_POS = "FromPos"; + private static final String TO_POS = "ToPos"; + + public static BlockPos findPortalAt(Player player, ResourceKey dim, BlockPos pos) { + MinecraftServer server = player.level().getServer(); + if (server != null) { + ServerLevel fromWorld = server.getLevel(dim); + if (fromWorld != null) { + PortalForcer portalForcer = fromWorld.getPortalForcer(); + return portalForcer.findClosestPortalPosition(pos, false, fromWorld.getWorldBorder()).orElse(null); + } + } + + return null; + } + + public static ListTag getPlayerPortalList(Player player) { + CompoundTag data = player.getLeavesData(); + ListTag list = data.getList(RETURN_PORTAL_LIST, Tag.TAG_COMPOUND); + data.put(RETURN_PORTAL_LIST, list); + return list; + } + + @Nullable + public static ReturnPortal findReturnPortal(ServerPlayer player, ResourceKey fromDim, BlockPos fromPos) { + ListTag portalList = getPlayerPortalList(player); + for (Tag entry : portalList) { + CompoundTag portal = (CompoundTag) entry; + ResourceKey entryFromDim = ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(portal.getString(FROM_DIM))); + if (entryFromDim == fromDim) { + BlockPos portalTrigger = BlockPos.of(portal.getLong(FROM_POS)); + if (portalTrigger.distSqr(fromPos) <= MAX_PORTAL_DISTANCE_SQ) { + final var uid = portal.hasUUID(RETURN_PORTAL_UID) ? portal.getUUID(RETURN_PORTAL_UID) : UUID.randomUUID(); + final var pos = BlockPos.of(portal.getLong(TO_POS)); + return new ReturnPortal(uid, pos); + } + } + } + + return null; + } + + public static void storeReturnPortal(ServerPlayer player, ResourceKey fromDim, BlockPos fromPos, BlockPos toPos) { + ListTag portalList = getPlayerPortalList(player); + ReturnPortal returnPortal = findReturnPortal(player, fromDim, fromPos); + if (returnPortal != null) { + removeReturnPortal(player, returnPortal); + } + + CompoundTag portalCompound = new CompoundTag(); + portalCompound.putUUID(RETURN_PORTAL_UID, UUID.randomUUID()); + portalCompound.putString(FROM_DIM, String.valueOf(fromDim.location())); + portalCompound.putLong(FROM_POS, fromPos.asLong()); + portalCompound.putLong(TO_POS, toPos.asLong()); + portalList.add(portalCompound); + } + + public static void removeReturnPortal(ServerPlayer player, ReturnPortal portal) { + // This doesn't check if it's the right toDim, but it's probably so rare for positions to actually overlap that I don't care + ListTag portalList = getPlayerPortalList(player); + for (int i = 0; i < portalList.size(); i++) { + CompoundTag entry = (CompoundTag) portalList.get(i); + if (entry.hasUUID(RETURN_PORTAL_UID) && entry.getUUID(RETURN_PORTAL_UID).equals(portal.uid)) { + portalList.remove(i); + break; + } + } + } + + public record ReturnPortal(UUID uid, BlockPos pos) { + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/ShulkerBoxUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/ShulkerBoxUtils.java new file mode 100644 index 00000000..07ed58fb --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/ShulkerBoxUtils.java @@ -0,0 +1,39 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.ItemContainerContents; +import net.minecraft.world.level.block.ShulkerBoxBlock; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; + +public class ShulkerBoxUtils { + + public static boolean shulkerBoxNoItem(@NotNull ItemStack stack) { + return stack.getComponents().getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY).stream().findAny().isEmpty(); + } + + public static int getItemStackMaxCount(ItemStack stack) { + if (LeavesConfig.modify.shulkerBoxStackSize > 1 && stack.getItem() instanceof BlockItem bi && + bi.getBlock() instanceof ShulkerBoxBlock && shulkerBoxNoItem(stack)) { + return LeavesConfig.modify.shulkerBoxStackSize; + } + return stack.getMaxStackSize(); + } + + public static ItemStack correctItemStackMaxStackSize(ItemStack itemStack) { + int trulyMaxStackSize = getItemStackMaxCount(itemStack); + if (itemStack.getMaxStackSize() != trulyMaxStackSize) { + org.bukkit.inventory.ItemStack bkStack = CraftItemStack.asBukkitCopy(itemStack); + bkStack.editMeta(meta -> meta.setMaxStackSize(trulyMaxStackSize)); + itemStack = CraftItemStack.asNMSCopy(bkStack); + } + return itemStack; + } + + public static boolean isStackable(ItemStack itemStack) { + return getItemStackMaxCount(itemStack) > 1 && (!itemStack.isDamageableItem() || !itemStack.isDamaged()); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/TicketHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/TicketHelper.java new file mode 100644 index 00000000..a47c3236 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/TicketHelper.java @@ -0,0 +1,175 @@ +package org.leavesmc.leaves.util; + +import ca.spottedleaf.moonrise.patches.chunk_system.player.RegionizedPlayerChunkLoader; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.Registries; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.DistanceManager; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.Ticket; +import net.minecraft.server.level.TicketType; +import net.minecraft.util.SortedArraySet; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.storage.LevelResource; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.LeavesLogger; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Set; + +public class TicketHelper { + + private static final Set> NEED_SAVED = Set.of(TicketType.PLAYER, TicketType.PORTAL, RegionizedPlayerChunkLoader.PLAYER_TICKET); + + public static void tryToLoadTickets() { + if (!LeavesConfig.modify.fastResume) { + return; + } + + File file = MinecraftServer.getServer().getWorldPath(LevelResource.ROOT).resolve("chunk_tickets.leaves.json").toFile(); + if (file.isFile()) { + try (BufferedReader bfr = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { + JsonObject json = new Gson().fromJson(bfr, JsonObject.class); + loadSavedChunkTickets(json); + if (!file.delete()) { + throw new IOException(); + } + } catch (IOException e) { + LeavesLogger.LOGGER.severe("Failed to load saved chunk tickets file", e); + } + } + } + + public static void tryToSaveTickets() { + if (!LeavesConfig.modify.fastResume) { + return; + } + + File file = MinecraftServer.getServer().getWorldPath(LevelResource.ROOT).resolve("chunk_tickets.leaves.json").toFile(); + try (BufferedWriter bfw = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { + bfw.write(new Gson().toJson(getSavedChunkTickets())); + } catch (IOException e) { + LeavesLogger.LOGGER.severe("Failed to save chunk tickets file", e); + } + } + + public static void loadSavedChunkTickets(JsonObject json) { + MinecraftServer server = MinecraftServer.getServer(); + for (String worldKey : json.keySet()) { + ResourceLocation dimensionKey = ResourceLocation.tryParse(worldKey); + if (dimensionKey == null) { + continue; + } + + ServerLevel level = server.getLevel(ResourceKey.create(Registries.DIMENSION, dimensionKey)); + if (level == null) { + continue; + } + + DistanceManager chunkDistanceManager = level.getChunkSource().chunkMap.distanceManager; + for (JsonElement chunkElement : json.get(worldKey).getAsJsonArray()) { + JsonObject chunkJson = (JsonObject) chunkElement; + long chunkKey = chunkJson.get("key").getAsLong(); + + for (JsonElement ticketElement : chunkJson.get("tickets").getAsJsonArray()) { + Ticket ticket = tickFormJson((JsonObject) ticketElement); + chunkDistanceManager.moonrise$getChunkHolderManager().addTicketAtLevelCustom(ticket, chunkKey, true); + } + } + } + } + + public static JsonObject getSavedChunkTickets() { + JsonObject json = new JsonObject(); + + for (ServerLevel level : MinecraftServer.getServer().getAllLevels()) { + JsonArray levelArray = new JsonArray(); + DistanceManager chunkDistanceManager = level.getChunkSource().chunkMap.distanceManager; + + for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { + long chunkKey = chunkTickets.getLongKey(); + JsonArray ticketArray = new JsonArray(); + SortedArraySet> tickets = chunkTickets.getValue(); + + for (Ticket ticket : tickets) { + if (!NEED_SAVED.contains(ticket.getType())) { + continue; + } + + ticketArray.add(ticketToJson(ticket)); + } + + if (!ticketArray.isEmpty()) { + JsonObject chunkJson = new JsonObject(); + chunkJson.addProperty("key", chunkKey); + chunkJson.add("tickets", ticketArray); + levelArray.add(chunkJson); + } + } + + if (!levelArray.isEmpty()) { + json.add(level.dimension().location().toString(), levelArray); + } + } + + return json; + } + + private static JsonObject ticketToJson(Ticket ticket) { + JsonObject json = new JsonObject(); + json.addProperty("type", ticket.getType().toString()); + json.addProperty("ticketLevel", ticket.getTicketLevel()); + json.addProperty("removeDelay", ticket.moonrise$getRemoveDelay()); + if (ticket.key instanceof BlockPos pos) { + json.addProperty("key", pos.asLong()); + } else if (ticket.key instanceof ChunkPos pos) { + json.addProperty("key", pos.toLong()); + } else if (ticket.key instanceof Long l) { + json.addProperty("key", l); + } + return json; + } + + private static Ticket tickFormJson(JsonObject json) { + TicketType ticketType = null; + Object key = null; + switch (json.get("type").getAsString()) { + case "player" -> { + ticketType = TicketType.PLAYER; + key = new ChunkPos(json.get("key").getAsLong()); + } + case "portal" -> { + ticketType = TicketType.PORTAL; + key = BlockPos.of(json.get("key").getAsLong()); + } + case "chunk_system:player_ticket" -> { + ticketType = RegionizedPlayerChunkLoader.PLAYER_TICKET; + key = json.get("key").getAsLong(); + } + } + + if (ticketType == null) { + throw new IllegalArgumentException("Cant convert " + json.get("type").getAsString() + ", report it ???"); + } + + int ticketLevel = json.get("ticketLevel").getAsInt(); + long removeDelay = json.get("removeDelay").getAsLong(); + @SuppressWarnings("unchecked") + Ticket ticket = new Ticket<>((TicketType) ticketType, ticketLevel, (T) key); + ticket.moonrise$setRemoveDelay(removeDelay); + + return ticket; + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/UUIDSerializer.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/UUIDSerializer.java new file mode 100644 index 00000000..b0834f4b --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/UUIDSerializer.java @@ -0,0 +1,17 @@ +package org.leavesmc.leaves.util; + +import com.google.gson.JsonElement; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Type; +import java.util.UUID; + +public class UUIDSerializer implements JsonSerializer { + @Override + public JsonElement serialize(@NotNull UUID src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(src.toString()); + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/UpdateSuppressionException.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/UpdateSuppressionException.java new file mode 100644 index 00000000..150a0813 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/UpdateSuppressionException.java @@ -0,0 +1,32 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Block; + +public class UpdateSuppressionException extends RuntimeException { + + private final BlockPos pos; + private final Block source; + + public UpdateSuppressionException(BlockPos pos, Block source) { + super("Update suppression"); + this.pos = pos; + this.source = source; + } + + public BlockPos getPos() { + return pos; + } + + public Block getSource() { + return source; + } + + public String getMessage() { + if (pos != null) { + return "An update suppression processed, form [%s] to [x:%d,y:%d,z:%d]".formatted(source.getName(), pos.getX(), pos.getY(), pos.getZ()); + } else { + return "An update suppression processed, form [%s]".formatted(source.getName()); + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/VillagerInfiniteDiscountHelper.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/VillagerInfiniteDiscountHelper.java new file mode 100644 index 00000000..83a6ef92 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/VillagerInfiniteDiscountHelper.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.world.entity.ai.gossip.GossipType; + +public class VillagerInfiniteDiscountHelper { + + public static void doVillagerInfiniteDiscount(boolean value) { + if (value) { + GossipType.MAJOR_POSITIVE.max = 100; + GossipType.MAJOR_POSITIVE.decayPerTransfer = 100; + GossipType.MINOR_POSITIVE.max = 200; + } else { + GossipType.MAJOR_POSITIVE.max = 20; + GossipType.MAJOR_POSITIVE.decayPerTransfer = 20; + GossipType.MINOR_POSITIVE.max = 25; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/util/WoolUtils.java b/leaves-server/src/main/java/org/leavesmc/leaves/util/WoolUtils.java new file mode 100644 index 00000000..a4cd6317 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/util/WoolUtils.java @@ -0,0 +1,38 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.item.DyeColor; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Map; + +import static java.util.Map.entry; + +public class WoolUtils { + private static final Map WOOL_BLOCK_TO_DYE = Map.ofEntries( + entry(Blocks.WHITE_WOOL, DyeColor.WHITE), + entry(Blocks.ORANGE_WOOL, DyeColor.ORANGE), + entry(Blocks.MAGENTA_WOOL, DyeColor.MAGENTA), + entry(Blocks.LIGHT_BLUE_WOOL, DyeColor.LIGHT_BLUE), + entry(Blocks.YELLOW_WOOL, DyeColor.YELLOW), + entry(Blocks.LIME_WOOL, DyeColor.LIME), + entry(Blocks.PINK_WOOL, DyeColor.PINK), + entry(Blocks.GRAY_WOOL, DyeColor.GRAY), + entry(Blocks.LIGHT_GRAY_WOOL, DyeColor.LIGHT_GRAY), + entry(Blocks.CYAN_WOOL, DyeColor.CYAN), + entry(Blocks.PURPLE_WOOL, DyeColor.PURPLE), + entry(Blocks.BLUE_WOOL, DyeColor.BLUE), + entry(Blocks.BROWN_WOOL, DyeColor.BROWN), + entry(Blocks.GREEN_WOOL, DyeColor.GREEN), + entry(Blocks.RED_WOOL, DyeColor.RED), + entry(Blocks.BLACK_WOOL, DyeColor.BLACK) + ); + + public static DyeColor getWoolColorAtPosition(Level worldIn, BlockPos pos) { + BlockState state = worldIn.getBlockState(pos); + return WOOL_BLOCK_TO_DYE.get(state.getBlock()); + } +} diff --git a/leaves-server/src/main/resources/assets/minecraft/lang/zh_cn.json b/leaves-server/src/main/resources/assets/minecraft/lang/zh_cn.json new file mode 100644 index 00000000..be39e96f --- /dev/null +++ b/leaves-server/src/main/resources/assets/minecraft/lang/zh_cn.json @@ -0,0 +1,7109 @@ +{ + "accessibility.onboarding.accessibility.button": "辅助功能设置…", + "accessibility.onboarding.screen.narrator": "按下Enter键启用复述功能", + "accessibility.onboarding.screen.title": "欢迎进入Minecraft!\n\n需要启用复述功能或访问辅助功能设置吗?", + "addServer.add": "完成", + "addServer.enterIp": "服务器地址", + "addServer.enterName": "服务器名称", + "addServer.resourcePack": "服务器资源包", + "addServer.resourcePack.disabled": "禁用", + "addServer.resourcePack.enabled": "启用", + "addServer.resourcePack.prompt": "询问", + "addServer.title": "编辑服务器信息", + "advMode.command": "控制台命令", + "advMode.mode": "模式", + "advMode.mode.auto": "循环", + "advMode.mode.autoexec.bat": "保持开启", + "advMode.mode.conditional": "条件制约", + "advMode.mode.redstone": "脉冲", + "advMode.mode.redstoneTriggered": "红石控制", + "advMode.mode.sequence": "连锁", + "advMode.mode.unconditional": "不受制约", + "advMode.notAllowed": "必须是处于创造模式的管理员", + "advMode.notEnabled": "此服务器未启用命令方块", + "advMode.previousOutput": "上一个输出", + "advMode.setCommand": "设置此方块的控制台命令", + "advMode.setCommand.success": "成功设置命令:%s", + "advMode.trackOutput": "记录输出", + "advMode.triggering": "触发方式", + "advMode.type": "类型", + "advancement.advancementNotFound": "未知的进度:%s", + "advancements.adventure.adventuring_time.description": "发现所有的生物群系", + "advancements.adventure.adventuring_time.title": "探索的时光", + "advancements.adventure.arbalistic.description": "用弩一发击杀五只不同的生物", + "advancements.adventure.arbalistic.title": "劲弩手", + "advancements.adventure.avoid_vibration.description": "在幽匿感测体或监守者周围潜行以防被它们探测到", + "advancements.adventure.avoid_vibration.title": "潜行100级", + "advancements.adventure.blowback.description": "反弹旋风人的风弹来击杀旋风人", + "advancements.adventure.blowback.title": "逆风翻盘", + "advancements.adventure.brush_armadillo.description": "用刷子从犰狳身上获得犰狳鳞甲", + "advancements.adventure.brush_armadillo.title": "这不是鳞甲么?", + "advancements.adventure.bullseye.description": "从至少30米外射中标靶的靶心", + "advancements.adventure.bullseye.title": "正中靶心", + "advancements.adventure.craft_decorated_pot_using_only_sherds.description": "用4个陶片制作饰纹陶罐", + "advancements.adventure.craft_decorated_pot_using_only_sherds.title": "精修细补", + "advancements.adventure.crafters_crafting_crafters.description": "靠近一个正在合成合成器的合成器", + "advancements.adventure.crafters_crafting_crafters.title": "合成器合成合成器", + "advancements.adventure.fall_from_world_height.description": "从世界顶部(建筑高度限制处)自由落体,坠至世界底部并存活下来", + "advancements.adventure.fall_from_world_height.title": "上天入地", + "advancements.adventure.hero_of_the_village.description": "成功在袭击中保卫村庄", + "advancements.adventure.hero_of_the_village.title": "村庄英雄", + "advancements.adventure.honey_block_slide.description": "跳入蜂蜜块以缓冲摔落", + "advancements.adventure.honey_block_slide.title": "胶着状态", + "advancements.adventure.kill_a_mob.description": "杀死任意敌对性怪物", + "advancements.adventure.kill_a_mob.title": "怪物猎人", + "advancements.adventure.kill_all_mobs.description": "杀死每一种敌对性怪物", + "advancements.adventure.kill_all_mobs.title": "资深怪物猎人", + "advancements.adventure.kill_mob_near_sculk_catalyst.description": "在幽匿催发体附近杀死生物", + "advancements.adventure.kill_mob_near_sculk_catalyst.title": "它蔓延了", + "advancements.adventure.lighten_up.description": "用斧刮削铜灯来让它变得更亮", + "advancements.adventure.lighten_up.title": "铜光焕发", + "advancements.adventure.lightning_rod_with_villager_no_fire.description": "在不引发火灾的前提下保护村民免受雷击", + "advancements.adventure.lightning_rod_with_villager_no_fire.title": "电涌保护器", + "advancements.adventure.minecraft_trials_edition.description": "踏入试炼密室", + "advancements.adventure.minecraft_trials_edition.title": "Minecraft:试炼版", + "advancements.adventure.ol_betsy.description": "用弩进行一次射击", + "advancements.adventure.ol_betsy.title": "扣下悬刀", + "advancements.adventure.overoverkill.description": "使用重锤一击造成50颗心的伤害", + "advancements.adventure.overoverkill.title": "天赐良击", + "advancements.adventure.play_jukebox_in_meadows.description": "用唱片机的音乐声为草甸增添生机", + "advancements.adventure.play_jukebox_in_meadows.title": "音乐之声", + "advancements.adventure.read_power_from_chiseled_bookshelf.description": "使用红石比较器获取雕纹书架的信号强度", + "advancements.adventure.read_power_from_chiseled_bookshelf.title": "知识就是力量", + "advancements.adventure.revaulting.description": "用不祥试炼钥匙解锁不祥宝库", + "advancements.adventure.revaulting.title": "宝经磨炼", + "advancements.adventure.root.description": "冒险、探索与战斗", + "advancements.adventure.root.title": "冒险", + "advancements.adventure.salvage_sherd.description": "刷扫可疑的方块来获得陶片", + "advancements.adventure.salvage_sherd.title": "探古寻源", + "advancements.adventure.shoot_arrow.description": "用弓箭射点什么", + "advancements.adventure.shoot_arrow.title": "瞄准目标", + "advancements.adventure.sleep_in_bed.description": "在床上睡觉以改变你的重生点", + "advancements.adventure.sleep_in_bed.title": "甜蜜的梦", + "advancements.adventure.sniper_duel.description": "从至少50米外射杀一只骷髅", + "advancements.adventure.sniper_duel.title": "狙击手的对决", + "advancements.adventure.spyglass_at_dragon.description": "透过望远镜观察末影龙", + "advancements.adventure.spyglass_at_dragon.title": "那是飞机吗?", + "advancements.adventure.spyglass_at_ghast.description": "透过望远镜观察恶魂", + "advancements.adventure.spyglass_at_ghast.title": "那是气球吗?", + "advancements.adventure.spyglass_at_parrot.description": "透过望远镜观察鹦鹉", + "advancements.adventure.spyglass_at_parrot.title": "那是鸟吗?", + "advancements.adventure.summon_iron_golem.description": "召唤一只铁傀儡来帮忙守卫村庄", + "advancements.adventure.summon_iron_golem.title": "招募援兵", + "advancements.adventure.throw_trident.description": "往什么东西扔出三叉戟。\n注:别把你唯一的武器也抖掉了。", + "advancements.adventure.throw_trident.title": "抖包袱", + "advancements.adventure.totem_of_undying.description": "利用不死图腾逃离死神", + "advancements.adventure.totem_of_undying.title": "超越生死", + "advancements.adventure.trade.description": "成功与一名村民进行交易", + "advancements.adventure.trade.title": "成交!", + "advancements.adventure.trade_at_world_height.description": "在建筑高度限制处与村民交易", + "advancements.adventure.trade_at_world_height.title": "星际商人", + "advancements.adventure.trim_with_all_exclusive_armor_patterns.description": "将下列锻造模板都至少使用一次:尖塔、猪鼻、肋骨、监守、幽静、恼鬼、潮汐、向导", + "advancements.adventure.trim_with_all_exclusive_armor_patterns.title": "匠心独具", + "advancements.adventure.trim_with_any_armor_pattern.description": "在锻造台中合成带有纹饰的盔甲", + "advancements.adventure.trim_with_any_armor_pattern.title": "旧貌锻新颜", + "advancements.adventure.two_birds_one_arrow.description": "用一支穿透箭射杀两只幻翼", + "advancements.adventure.two_birds_one_arrow.title": "一箭双雕", + "advancements.adventure.under_lock_and_key.description": "用试炼钥匙解锁宝库", + "advancements.adventure.under_lock_and_key.title": "珍藏密敛", + "advancements.adventure.very_very_frightening.description": "雷击一名村民", + "advancements.adventure.very_very_frightening.title": "魔女审判", + "advancements.adventure.voluntary_exile.description": "杀死一名袭击队长。\n或许该考虑暂时远离村庄……", + "advancements.adventure.voluntary_exile.title": "自我放逐", + "advancements.adventure.walk_on_powder_snow_with_leather_boots.description": "在细雪上行走……并且不陷进去", + "advancements.adventure.walk_on_powder_snow_with_leather_boots.title": "轻功雪上飘", + "advancements.adventure.who_needs_rockets.description": "使用风弹将自己向上弹射8格", + "advancements.adventure.who_needs_rockets.title": "还要啥火箭啊?", + "advancements.adventure.whos_the_pillager_now.description": "让掠夺者也尝尝弩的滋味", + "advancements.adventure.whos_the_pillager_now.title": "现在谁才是掠夺者?", + "advancements.empty": "这里好像什么都没有……", + "advancements.end.dragon_breath.description": "用玻璃瓶收集一些龙息", + "advancements.end.dragon_breath.title": "你需要来点薄荷糖", + "advancements.end.dragon_egg.description": "获得龙蛋", + "advancements.end.dragon_egg.title": "下一世代", + "advancements.end.elytra.description": "找到鞘翅", + "advancements.end.elytra.title": "天空即为极限", + "advancements.end.enter_end_gateway.description": "逃离这座岛屿", + "advancements.end.enter_end_gateway.title": "远程折跃", + "advancements.end.find_end_city.description": "进去吧,又能发生什么呢?", + "advancements.end.find_end_city.title": "在游戏尽头的城市", + "advancements.end.kill_dragon.description": "祝君好运", + "advancements.end.kill_dragon.title": "解放末地", + "advancements.end.levitate.description": "利用潜影贝的攻击向上漂浮50个方块", + "advancements.end.levitate.title": "这上面的风景不错", + "advancements.end.respawn_dragon.description": "复活末影龙", + "advancements.end.respawn_dragon.title": "结束了…再一次…", + "advancements.end.root.description": "抑或是起点?", + "advancements.end.root.title": "末地", + "advancements.husbandry.allay_deliver_cake_to_note_block.description": "让悦灵向音符盒投掷一块蛋糕", + "advancements.husbandry.allay_deliver_cake_to_note_block.title": "生日快乐歌", + "advancements.husbandry.allay_deliver_item_to_player.description": "让悦灵向你投掷物品", + "advancements.husbandry.allay_deliver_item_to_player.title": "找到一个好朋友", + "advancements.husbandry.axolotl_in_a_bucket.description": "用铁桶捕获一只美西螈", + "advancements.husbandry.axolotl_in_a_bucket.title": "最萌捕食者", + "advancements.husbandry.balanced_diet.description": "尝遍天下食材,即便是对身体不好的", + "advancements.husbandry.balanced_diet.title": "均衡饮食", + "advancements.husbandry.breed_all_animals.description": "繁殖每种动物!", + "advancements.husbandry.breed_all_animals.title": "成双成对", + "advancements.husbandry.breed_an_animal.description": "繁殖一对动物", + "advancements.husbandry.breed_an_animal.title": "我从哪儿来?", + "advancements.husbandry.complete_catalogue.description": "驯服所有种类的猫!", + "advancements.husbandry.complete_catalogue.title": "百猫全书", + "advancements.husbandry.feed_snifflet.description": "喂食一只幼年嗅探兽", + "advancements.husbandry.feed_snifflet.title": "小小嗅探兽", + "advancements.husbandry.fishy_business.description": "钓到一条鱼", + "advancements.husbandry.fishy_business.title": "腥味十足的生意", + "advancements.husbandry.froglights.description": "在你的物品栏中集齐所有种类的蛙明灯", + "advancements.husbandry.froglights.title": "相映生辉!", + "advancements.husbandry.kill_axolotl_target.description": "与美西螈并肩作战并赢得胜利", + "advancements.husbandry.kill_axolotl_target.title": "友谊的治愈力!", + "advancements.husbandry.leash_all_frog_variants.description": "用拴绳拴住所有种类的青蛙", + "advancements.husbandry.leash_all_frog_variants.title": "呱呱队出动", + "advancements.husbandry.make_a_sign_glow.description": "让任意种类告示牌上的文本发光", + "advancements.husbandry.make_a_sign_glow.title": "眼前一亮!", + "advancements.husbandry.netherite_hoe.description": "用下界合金锭升级一把锄,然后重新考虑你的人生抉择", + "advancements.husbandry.netherite_hoe.title": "终极奉献", + "advancements.husbandry.obtain_sniffer_egg.description": "获得嗅探兽蛋", + "advancements.husbandry.obtain_sniffer_egg.title": "怪味蛋", + "advancements.husbandry.plant_any_sniffer_seed.description": "种植任意嗅探兽种子", + "advancements.husbandry.plant_any_sniffer_seed.title": "播种往事", + "advancements.husbandry.plant_seed.description": "种下种子,见证它的成长", + "advancements.husbandry.plant_seed.title": "开荒垦地", + "advancements.husbandry.remove_wolf_armor.description": "用剪刀移除狼身上的狼铠", + "advancements.husbandry.remove_wolf_armor.title": "华丽一剪", + "advancements.husbandry.repair_wolf_armor.description": "用犰狳鳞甲修复损坏的狼铠", + "advancements.husbandry.repair_wolf_armor.title": "完好如初", + "advancements.husbandry.ride_a_boat_with_a_goat.description": "与山羊同船共渡", + "advancements.husbandry.ride_a_boat_with_a_goat.title": "羊帆起航!", + "advancements.husbandry.root.description": "世界无处没有朋友与美食", + "advancements.husbandry.root.title": "农牧业", + "advancements.husbandry.safely_harvest_honey.description": "利用营火在不惊动蜜蜂的情况下从蜂巢收集蜂蜜", + "advancements.husbandry.safely_harvest_honey.title": "与蜂共舞", + "advancements.husbandry.silk_touch_nest.description": "用精准采集移动住着3只蜜蜂的蜂巢或蜂箱", + "advancements.husbandry.silk_touch_nest.title": "举巢搬迁", + "advancements.husbandry.tactical_fishing.description": "不用钓鱼竿抓住一条鱼!", + "advancements.husbandry.tactical_fishing.title": "战术性钓鱼", + "advancements.husbandry.tadpole_in_a_bucket.description": "用铁桶捕获一只蝌蚪", + "advancements.husbandry.tadpole_in_a_bucket.title": "蚪到桶里来", + "advancements.husbandry.tame_an_animal.description": "驯服一只动物", + "advancements.husbandry.tame_an_animal.title": "永恒的伙伴", + "advancements.husbandry.wax_off.description": "给铜质方块脱蜡!", + "advancements.husbandry.wax_off.title": "脱蜡", + "advancements.husbandry.wax_on.description": "将蜜脾涂到铜质方块上!", + "advancements.husbandry.wax_on.title": "涂蜡", + "advancements.husbandry.whole_pack.description": "驯服所有种类的狼", + "advancements.husbandry.whole_pack.title": "群狼聚首", + "advancements.nether.all_effects.description": "同时拥有所有状态效果", + "advancements.nether.all_effects.title": "为什么会变成这样呢?", + "advancements.nether.all_potions.description": "同时拥有所有药水效果", + "advancements.nether.all_potions.title": "狂乱的鸡尾酒", + "advancements.nether.brew_potion.description": "酿造一瓶药水", + "advancements.nether.brew_potion.title": "本地酿造厂", + "advancements.nether.charge_respawn_anchor.description": "为重生锚充满能量", + "advancements.nether.charge_respawn_anchor.title": "锚没有九条命", + "advancements.nether.create_beacon.description": "建造并放置一个信标", + "advancements.nether.create_beacon.title": "带信标回家", + "advancements.nether.create_full_beacon.description": "让一座信标发挥最大功效", + "advancements.nether.create_full_beacon.title": "信标工程师", + "advancements.nether.distract_piglin.description": "用金质物品让猪灵分神", + "advancements.nether.distract_piglin.title": "金光闪闪", + "advancements.nether.explore_nether.description": "探索所有下界生物群系", + "advancements.nether.explore_nether.title": "热门景点", + "advancements.nether.fast_travel.description": "利用下界移动对应主世界7千米的距离", + "advancements.nether.fast_travel.title": "曲速泡", + "advancements.nether.find_bastion.description": "进入堡垒遗迹", + "advancements.nether.find_bastion.title": "光辉岁月", + "advancements.nether.find_fortress.description": "用你的方式进入下界要塞", + "advancements.nether.find_fortress.title": "阴森的要塞", + "advancements.nether.get_wither_skull.description": "获得凋灵骷髅的头颅", + "advancements.nether.get_wither_skull.title": "惊悚恐怖骷髅头", + "advancements.nether.loot_bastion.description": "掠夺堡垒遗迹里的箱子", + "advancements.nether.loot_bastion.title": "战猪", + "advancements.nether.netherite_armor.description": "获得一整套下界合金盔甲", + "advancements.nether.netherite_armor.title": "残骸裹身", + "advancements.nether.obtain_ancient_debris.description": "获得远古残骸", + "advancements.nether.obtain_ancient_debris.title": "深藏不露", + "advancements.nether.obtain_blaze_rod.description": "让烈焰人从烈焰棒中解放吧", + "advancements.nether.obtain_blaze_rod.title": "与火共舞", + "advancements.nether.obtain_crying_obsidian.description": "获得哭泣的黑曜石", + "advancements.nether.obtain_crying_obsidian.title": "谁在切洋葱?", + "advancements.nether.return_to_sender.description": "用一团火球干掉一只恶魂", + "advancements.nether.return_to_sender.title": "见鬼去吧", + "advancements.nether.ride_strider.description": "手持诡异菌钓竿骑乘炽足兽", + "advancements.nether.ride_strider.title": "画船添足", + "advancements.nether.ride_strider_in_overworld_lava.description": "带炽足兽在主世界的熔岩湖上来一场长——途旅行", + "advancements.nether.ride_strider_in_overworld_lava.title": "温暖如家", + "advancements.nether.root.description": "记得带夏装", + "advancements.nether.root.title": "下界", + "advancements.nether.summon_wither.description": "召唤凋灵", + "advancements.nether.summon_wither.title": "凋零山庄", + "advancements.nether.uneasy_alliance.description": "从下界救出一只恶魂,将其安全地带到主世界……然后干掉它", + "advancements.nether.uneasy_alliance.title": "脆弱的同盟", + "advancements.nether.use_lodestone.description": "对着磁石使用指南针", + "advancements.nether.use_lodestone.title": "天涯共此石", + "advancements.progress": "%s/%s", + "advancements.sad_label": ":(", + "advancements.story.cure_zombie_villager.description": "弱化并治疗一名僵尸村民", + "advancements.story.cure_zombie_villager.title": "僵尸科医生", + "advancements.story.deflect_arrow.description": "用盾牌反弹一个弹射物", + "advancements.story.deflect_arrow.title": "不吃这套,谢谢", + "advancements.story.enchant_item.description": "用附魔台附魔一样物品", + "advancements.story.enchant_item.title": "附魔师", + "advancements.story.enter_the_end.description": "进入末地传送门", + "advancements.story.enter_the_end.title": "结束了?", + "advancements.story.enter_the_nether.description": "建造、激活并进入一座下界传送门", + "advancements.story.enter_the_nether.title": "勇往直下", + "advancements.story.follow_ender_eye.description": "跟随末影之眼", + "advancements.story.follow_ender_eye.title": "隔墙有眼", + "advancements.story.form_obsidian.description": "获得一块黑曜石", + "advancements.story.form_obsidian.title": "冰桶挑战", + "advancements.story.iron_tools.description": "升级你的镐", + "advancements.story.iron_tools.title": "这不是铁镐么", + "advancements.story.lava_bucket.description": "用铁桶装点熔岩", + "advancements.story.lava_bucket.title": "热腾腾的", + "advancements.story.mine_diamond.description": "获得钻石", + "advancements.story.mine_diamond.title": "钻石!", + "advancements.story.mine_stone.description": "用你的新镐挖掘石头", + "advancements.story.mine_stone.title": "石器时代", + "advancements.story.obtain_armor.description": "用铁盔甲来保护你自己", + "advancements.story.obtain_armor.title": "整装上阵", + "advancements.story.root.description": "游戏的核心与故事", + "advancements.story.root.title": "Minecraft", + "advancements.story.shiny_gear.description": "钻石盔甲能救人", + "advancements.story.shiny_gear.title": "钻石护体", + "advancements.story.smelt_iron.description": "冶炼出一块铁锭", + "advancements.story.smelt_iron.title": "来硬的", + "advancements.story.upgrade_tools.description": "制作一把更好的镐", + "advancements.story.upgrade_tools.title": "获得升级", + "advancements.toast.challenge": "挑战已完成!", + "advancements.toast.goal": "目标已达成!", + "advancements.toast.task": "进度已达成!", + "argument.anchor.invalid": "无效的实体锚点%s", + "argument.angle.incomplete": "不完整(应有1个角度)", + "argument.angle.invalid": "无效的角度", + "argument.block.id.invalid": "未知的方块类型“%s”", + "argument.block.property.duplicate": "“%s”属性只能给%s设置一次", + "argument.block.property.invalid": "%1$s的%3$s属性不能被设为“%2$s”", + "argument.block.property.novalue": "方块%s上的属性“%s”必须要有值", + "argument.block.property.unclosed": "方块属性应以]结束", + "argument.block.property.unknown": "方块%s没有属性“%s”", + "argument.block.tag.disallowed": "无法在此使用标签,只允许使用实际的方块", + "argument.color.invalid": "未知的颜色“%s”", + "argument.component.invalid": "无效的聊天组件: %s", + "argument.criteria.invalid": "未知的准则“%s”", + "argument.dimension.invalid": "未知的维度“%s”", + "argument.double.big": "双精度浮点型数据不能大于%s,但发现了%s", + "argument.double.low": "双精度浮点型数据不能小于%s,但发现了%s", + "argument.entity.invalid": "无效的名称或UUID", + "argument.entity.notfound.entity": "未找到实体", + "argument.entity.notfound.player": "未找到玩家", + "argument.entity.options.advancements.description": "玩家拥有的进度", + "argument.entity.options.distance.description": "与实体间的距离", + "argument.entity.options.distance.negative": "距离不能为负", + "argument.entity.options.dx.description": "位于x与x+dx之间的实体", + "argument.entity.options.dy.description": "位于y与y+dy之间的实体", + "argument.entity.options.dz.description": "位于z与z+dz之间的实体", + "argument.entity.options.gamemode.description": "玩家的游戏模式", + "argument.entity.options.inapplicable": "“%s”选项不适用于这里", + "argument.entity.options.level.description": "经验等级", + "argument.entity.options.level.negative": "等级不应该为负数", + "argument.entity.options.limit.description": "最大返回实体数", + "argument.entity.options.limit.toosmall": "限制必须至少为1", + "argument.entity.options.mode.invalid": "无效或未知的游戏模式“%s”", + "argument.entity.options.name.description": "实体名称", + "argument.entity.options.nbt.description": "实体所带的NBT", + "argument.entity.options.predicate.description": "自定义谓词", + "argument.entity.options.scores.description": "实体的分数", + "argument.entity.options.sort.description": "对实体排序", + "argument.entity.options.sort.irreversible": "无效或未知的排序类型“%s”", + "argument.entity.options.tag.description": "实体所带的标签", + "argument.entity.options.team.description": "实体所在的队伍", + "argument.entity.options.type.description": "实体类型", + "argument.entity.options.type.invalid": "无效或未知的实体类型“%s”", + "argument.entity.options.unknown": "未知的选项“%s”", + "argument.entity.options.unterminated": "选项的方括号不成对", + "argument.entity.options.valueless": "选项“%s”应有值", + "argument.entity.options.x.description": "X轴位置", + "argument.entity.options.x_rotation.description": "实体的X轴旋转角度", + "argument.entity.options.y.description": "Y轴位置", + "argument.entity.options.y_rotation.description": "实体的Y轴旋转角度", + "argument.entity.options.z.description": "Z轴位置", + "argument.entity.selector.allEntities": "所有实体", + "argument.entity.selector.allPlayers": "所有玩家", + "argument.entity.selector.missing": "缺少选择器类型", + "argument.entity.selector.nearestEntity": "距离最近的实体", + "argument.entity.selector.nearestPlayer": "距离最近的玩家", + "argument.entity.selector.not_allowed": "不能使用选择器", + "argument.entity.selector.randomPlayer": "随机玩家", + "argument.entity.selector.self": "当前实体", + "argument.entity.selector.unknown": "未知的选择器类型“%s”", + "argument.entity.toomany": "只允许一个实体,但提供的选择器允许多个实体", + "argument.enum.invalid": "无效的值“%s”", + "argument.float.big": "浮点型数据不能大于%s,但发现了%s", + "argument.float.low": "浮点型数据不能小于%s,但发现了%s", + "argument.gamemode.invalid": "未知的游戏模式:%s", + "argument.id.invalid": "无效的ID", + "argument.id.unknown": "未知的ID:%s", + "argument.integer.big": "整型数据不能大于%s,但发现了%s", + "argument.integer.low": "整型数据不能小于%s,但发现了%s", + "argument.item.id.invalid": "未知的物品“%s”", + "argument.item.tag.disallowed": "无法在此使用标签,只允许使用实际的物品", + "argument.literal.incorrect": "应为字面量%s", + "argument.long.big": "长整型数据不能大于%s,但发现了%s", + "argument.long.low": "长整型数据不能小于%s,但发现了%s", + "argument.message.too_long": "聊天消息过长(%s大于上限%s个字符)", + "argument.nbt.array.invalid": "无效的数组类型“%s”", + "argument.nbt.array.mixed": "无法将%s插入%s", + "argument.nbt.expected.compound": "应为复合标签", + "argument.nbt.expected.key": "需要键", + "argument.nbt.expected.value": "需要值", + "argument.nbt.list.mixed": "无法将%s插入%s的列表", + "argument.nbt.trailing": "多余的尾随数据", + "argument.player.entities": "只有玩家会受此命令的影响,但提供的选择器包括其他实体", + "argument.player.toomany": "只允许一名玩家,但提供的选择器允许多名玩家", + "argument.player.unknown": "该玩家不存在", + "argument.pos.missing.double": "应为一个坐标", + "argument.pos.missing.int": "应为一个方块的位置", + "argument.pos.mixed": "不能混用世界与局部坐标(必须全部用^或都不使用)", + "argument.pos.outofbounds": "该位置超出了允许的范围。", + "argument.pos.outofworld": "该位置已超出此世界!", + "argument.pos.unloaded": "该位置尚未被加载", + "argument.pos2d.incomplete": "不完整(应有2个坐标)", + "argument.pos3d.incomplete": "不完整(应有3个坐标)", + "argument.range.empty": "应为值或取值范围", + "argument.range.ints": "只允许整数,不允许小数", + "argument.range.swapped": "最小值不能大于最大值", + "argument.resource.invalid_type": "元素“%s”的类型“%s”错误(应为“%s”)", + "argument.resource.not_found": "无法找到类型为“%2$s”的元素“%1$s”", + "argument.resource_or_id.failed_to_parse": "解析结构失败:%s", + "argument.resource_or_id.invalid": "无效的ID或标签", + "argument.resource_selector.not_found": "选择器“%s”和类型“%s”没有匹配项", + "argument.resource_tag.invalid_type": "标签“%s”的类型“%s”错误(应为“%s”)", + "argument.resource_tag.not_found": "无法找到类型为“%2$s”的标签“%1$s”", + "argument.rotation.incomplete": "不完整(应有2个坐标)", + "argument.scoreHolder.empty": "找不到与分数关联的持有者", + "argument.scoreboardDisplaySlot.invalid": "未知的显示位置“%s”", + "argument.style.invalid": "无效的样式:%s", + "argument.time.invalid_tick_count": "刻的计数必须为非负数", + "argument.time.invalid_unit": "无效的单位", + "argument.time.tick_count_too_low": "刻数不能小于%s,却发现了%s", + "argument.uuid.invalid": "无效的UUID", + "arguments.block.tag.unknown": "未知的方块标签“%s”", + "arguments.function.tag.unknown": "未知的函数标签“%s”", + "arguments.function.unknown": "未知的函数%s", + "arguments.item.component.expected": "应为物品组件", + "arguments.item.component.malformed": "错误的“%s”组件:“%s”", + "arguments.item.component.repeated": "物品组件“%s”重复,只能指定一个值", + "arguments.item.component.unknown": "未知的物品组件“%s”", + "arguments.item.malformed": "错误的物品:“%s”", + "arguments.item.overstacked": "%s只可以堆叠到%s", + "arguments.item.predicate.malformed": "错误的“%s”谓词:“%s”", + "arguments.item.predicate.unknown": "未知的物品谓词“%s”", + "arguments.item.tag.unknown": "未知的物品标签“%s”", + "arguments.nbtpath.node.invalid": "无效的NBT路径元素", + "arguments.nbtpath.nothing_found": "没有与%s相匹配的元素", + "arguments.nbtpath.too_deep": "生成的NBT嵌套过深", + "arguments.nbtpath.too_large": "生成的NBT过大", + "arguments.objective.notFound": "未知的记分项“%s”", + "arguments.objective.readonly": "记分项“%s”为只读类型", + "arguments.operation.div0": "不能除以零", + "arguments.operation.invalid": "无效的操作", + "arguments.swizzle.invalid": "无效的坐标组合,需要“x”、“y”和“z”的组合", + "attribute.modifier.equals.0": "%s %s", + "attribute.modifier.equals.1": "%s%% %s", + "attribute.modifier.equals.2": "%s%% %s", + "attribute.modifier.plus.0": "+%s %s", + "attribute.modifier.plus.1": "+%s%% %s", + "attribute.modifier.plus.2": "+%s%% %s", + "attribute.modifier.take.0": "-%s %s", + "attribute.modifier.take.1": "-%s%% %s", + "attribute.modifier.take.2": "-%s%% %s", + "attribute.name.armor": "护甲值", + "attribute.name.armor_toughness": "盔甲韧性", + "attribute.name.attack_damage": "攻击伤害", + "attribute.name.attack_knockback": "击退", + "attribute.name.attack_speed": "攻击速度", + "attribute.name.block_break_speed": "方块破坏速度", + "attribute.name.block_interaction_range": "方块交互距离", + "attribute.name.burning_time": "着火时间", + "attribute.name.entity_interaction_range": "实体交互距离", + "attribute.name.explosion_knockback_resistance": "爆炸击退抗性", + "attribute.name.fall_damage_multiplier": "摔落伤害倍数", + "attribute.name.flying_speed": "飞行速度", + "attribute.name.follow_range": "生物跟随距离", + "attribute.name.generic.armor": "护甲值", + "attribute.name.generic.armor_toughness": "盔甲韧性", + "attribute.name.generic.attack_damage": "攻击伤害", + "attribute.name.generic.attack_knockback": "击退", + "attribute.name.generic.attack_speed": "攻击速度", + "attribute.name.generic.block_interaction_range": "方块交互距离", + "attribute.name.generic.burning_time": "着火时间", + "attribute.name.generic.entity_interaction_range": "实体交互距离", + "attribute.name.generic.explosion_knockback_resistance": "爆炸击退抗性", + "attribute.name.generic.fall_damage_multiplier": "摔落伤害倍数", + "attribute.name.generic.flying_speed": "飞行速度", + "attribute.name.generic.follow_range": "生物跟随距离", + "attribute.name.generic.gravity": "重力", + "attribute.name.generic.jump_strength": "跳跃力度", + "attribute.name.generic.knockback_resistance": "击退抗性", + "attribute.name.generic.luck": "幸运值", + "attribute.name.generic.max_absorption": "最大伤害吸收值", + "attribute.name.generic.max_health": "最大生命值", + "attribute.name.generic.movement_efficiency": "移动效率", + "attribute.name.generic.movement_speed": "速度", + "attribute.name.generic.oxygen_bonus": "额外氧气", + "attribute.name.generic.safe_fall_distance": "安全摔落高度", + "attribute.name.generic.scale": "尺寸", + "attribute.name.generic.step_height": "最大行走高度", + "attribute.name.generic.water_movement_efficiency": "水中移动效率", + "attribute.name.gravity": "重力", + "attribute.name.horse.jump_strength": "马匹跳跃能力", + "attribute.name.jump_strength": "跳跃力度", + "attribute.name.knockback_resistance": "击退抗性", + "attribute.name.luck": "幸运值", + "attribute.name.max_absorption": "最大伤害吸收值", + "attribute.name.max_health": "最大生命值", + "attribute.name.mining_efficiency": "挖掘效率", + "attribute.name.movement_efficiency": "移动效率", + "attribute.name.movement_speed": "速度", + "attribute.name.oxygen_bonus": "额外氧气", + "attribute.name.player.block_break_speed": "方块破坏速度", + "attribute.name.player.block_interaction_range": "方块交互距离", + "attribute.name.player.entity_interaction_range": "实体交互距离", + "attribute.name.player.mining_efficiency": "挖掘效率", + "attribute.name.player.sneaking_speed": "潜行速度", + "attribute.name.player.submerged_mining_speed": "水下挖掘速度", + "attribute.name.player.sweeping_damage_ratio": "横扫伤害比率", + "attribute.name.safe_fall_distance": "安全摔落高度", + "attribute.name.scale": "尺寸", + "attribute.name.sneaking_speed": "潜行速度", + "attribute.name.spawn_reinforcements": "僵尸增援", + "attribute.name.step_height": "最大行走高度", + "attribute.name.submerged_mining_speed": "水下挖掘速度", + "attribute.name.sweeping_damage_ratio": "横扫伤害比率", + "attribute.name.tempt_range": "生物引诱范围", + "attribute.name.water_movement_efficiency": "水中移动效率", + "attribute.name.zombie.spawn_reinforcements": "僵尸增援", + "biome.minecraft.badlands": "恶地", + "biome.minecraft.bamboo_jungle": "竹林", + "biome.minecraft.basalt_deltas": "玄武岩三角洲", + "biome.minecraft.beach": "沙滩", + "biome.minecraft.birch_forest": "桦木森林", + "biome.minecraft.cherry_grove": "樱花树林", + "biome.minecraft.cold_ocean": "冷水海洋", + "biome.minecraft.crimson_forest": "绯红森林", + "biome.minecraft.dark_forest": "黑森林", + "biome.minecraft.deep_cold_ocean": "冷水深海", + "biome.minecraft.deep_dark": "深暗之域", + "biome.minecraft.deep_frozen_ocean": "冰冻深海", + "biome.minecraft.deep_lukewarm_ocean": "温水深海", + "biome.minecraft.deep_ocean": "深海", + "biome.minecraft.desert": "沙漠", + "biome.minecraft.dripstone_caves": "溶洞", + "biome.minecraft.end_barrens": "末地荒地", + "biome.minecraft.end_highlands": "末地高地", + "biome.minecraft.end_midlands": "末地内陆", + "biome.minecraft.eroded_badlands": "风蚀恶地", + "biome.minecraft.flower_forest": "繁花森林", + "biome.minecraft.forest": "森林", + "biome.minecraft.frozen_ocean": "冻洋", + "biome.minecraft.frozen_peaks": "冰封山峰", + "biome.minecraft.frozen_river": "冻河", + "biome.minecraft.grove": "雪林", + "biome.minecraft.ice_spikes": "冰刺之地", + "biome.minecraft.jagged_peaks": "尖峭山峰", + "biome.minecraft.jungle": "丛林", + "biome.minecraft.lukewarm_ocean": "温水海洋", + "biome.minecraft.lush_caves": "繁茂洞穴", + "biome.minecraft.mangrove_swamp": "红树林沼泽", + "biome.minecraft.meadow": "草甸", + "biome.minecraft.mushroom_fields": "蘑菇岛", + "biome.minecraft.nether_wastes": "下界荒地", + "biome.minecraft.ocean": "海洋", + "biome.minecraft.old_growth_birch_forest": "原始桦木森林", + "biome.minecraft.old_growth_pine_taiga": "原始松木针叶林", + "biome.minecraft.old_growth_spruce_taiga": "原始云杉针叶林", + "biome.minecraft.pale_garden": "苍白之园", + "biome.minecraft.plains": "平原", + "biome.minecraft.river": "河流", + "biome.minecraft.savanna": "热带草原", + "biome.minecraft.savanna_plateau": "热带高原", + "biome.minecraft.small_end_islands": "末地小型岛屿", + "biome.minecraft.snowy_beach": "积雪沙滩", + "biome.minecraft.snowy_plains": "雪原", + "biome.minecraft.snowy_slopes": "积雪山坡", + "biome.minecraft.snowy_taiga": "积雪针叶林", + "biome.minecraft.soul_sand_valley": "灵魂沙峡谷", + "biome.minecraft.sparse_jungle": "稀疏丛林", + "biome.minecraft.stony_peaks": "裸岩山峰", + "biome.minecraft.stony_shore": "石岸", + "biome.minecraft.sunflower_plains": "向日葵平原", + "biome.minecraft.swamp": "沼泽", + "biome.minecraft.taiga": "针叶林", + "biome.minecraft.the_end": "末地", + "biome.minecraft.the_void": "虚空", + "biome.minecraft.warm_ocean": "暖水海洋", + "biome.minecraft.warped_forest": "诡异森林", + "biome.minecraft.windswept_forest": "风袭森林", + "biome.minecraft.windswept_gravelly_hills": "风袭沙砾丘陵", + "biome.minecraft.windswept_hills": "风袭丘陵", + "biome.minecraft.windswept_savanna": "风袭热带草原", + "biome.minecraft.wooded_badlands": "疏林恶地", + "block.minecraft.acacia_button": "金合欢木按钮", + "block.minecraft.acacia_door": "金合欢木门", + "block.minecraft.acacia_fence": "金合欢木栅栏", + "block.minecraft.acacia_fence_gate": "金合欢木栅栏门", + "block.minecraft.acacia_hanging_sign": "悬挂式金合欢木告示牌", + "block.minecraft.acacia_leaves": "金合欢树叶", + "block.minecraft.acacia_log": "金合欢原木", + "block.minecraft.acacia_planks": "金合欢木板", + "block.minecraft.acacia_pressure_plate": "金合欢木压力板", + "block.minecraft.acacia_sapling": "金合欢树苗", + "block.minecraft.acacia_sign": "金合欢木告示牌", + "block.minecraft.acacia_slab": "金合欢木台阶", + "block.minecraft.acacia_stairs": "金合欢木楼梯", + "block.minecraft.acacia_trapdoor": "金合欢木活板门", + "block.minecraft.acacia_wall_hanging_sign": "墙上的悬挂式金合欢木告示牌", + "block.minecraft.acacia_wall_sign": "墙上的金合欢木告示牌", + "block.minecraft.acacia_wood": "金合欢木", + "block.minecraft.activator_rail": "激活铁轨", + "block.minecraft.air": "空气", + "block.minecraft.allium": "绒球葱", + "block.minecraft.amethyst_block": "紫水晶块", + "block.minecraft.amethyst_cluster": "紫水晶簇", + "block.minecraft.ancient_debris": "远古残骸", + "block.minecraft.andesite": "安山岩", + "block.minecraft.andesite_slab": "安山岩台阶", + "block.minecraft.andesite_stairs": "安山岩楼梯", + "block.minecraft.andesite_wall": "安山岩墙", + "block.minecraft.anvil": "铁砧", + "block.minecraft.attached_melon_stem": "结果的西瓜茎", + "block.minecraft.attached_pumpkin_stem": "结果的南瓜茎", + "block.minecraft.azalea": "杜鹃花丛", + "block.minecraft.azalea_leaves": "杜鹃树叶", + "block.minecraft.azure_bluet": "蓝花美耳草", + "block.minecraft.bamboo": "竹子", + "block.minecraft.bamboo_block": "竹块", + "block.minecraft.bamboo_button": "竹按钮", + "block.minecraft.bamboo_door": "竹门", + "block.minecraft.bamboo_fence": "竹栅栏", + "block.minecraft.bamboo_fence_gate": "竹栅栏门", + "block.minecraft.bamboo_hanging_sign": "悬挂式竹告示牌", + "block.minecraft.bamboo_mosaic": "竹马赛克", + "block.minecraft.bamboo_mosaic_slab": "竹马赛克台阶", + "block.minecraft.bamboo_mosaic_stairs": "竹马赛克楼梯", + "block.minecraft.bamboo_planks": "竹板", + "block.minecraft.bamboo_pressure_plate": "竹压力板", + "block.minecraft.bamboo_sapling": "竹笋", + "block.minecraft.bamboo_sign": "竹告示牌", + "block.minecraft.bamboo_slab": "竹台阶", + "block.minecraft.bamboo_stairs": "竹楼梯", + "block.minecraft.bamboo_trapdoor": "竹活板门", + "block.minecraft.bamboo_wall_hanging_sign": "墙上的悬挂式竹告示牌", + "block.minecraft.bamboo_wall_sign": "墙上的竹告示牌", + "block.minecraft.banner.base.black": "黑底", + "block.minecraft.banner.base.blue": "蓝底", + "block.minecraft.banner.base.brown": "棕底", + "block.minecraft.banner.base.cyan": "青底", + "block.minecraft.banner.base.gray": "灰底", + "block.minecraft.banner.base.green": "绿底", + "block.minecraft.banner.base.light_blue": "淡蓝底", + "block.minecraft.banner.base.light_gray": "淡灰底", + "block.minecraft.banner.base.lime": "黄绿底", + "block.minecraft.banner.base.magenta": "品红底", + "block.minecraft.banner.base.orange": "橙底", + "block.minecraft.banner.base.pink": "粉红底", + "block.minecraft.banner.base.purple": "紫底", + "block.minecraft.banner.base.red": "红底", + "block.minecraft.banner.base.white": "白底", + "block.minecraft.banner.base.yellow": "黄底", + "block.minecraft.banner.border.black": "黑色方框边", + "block.minecraft.banner.border.blue": "蓝色方框边", + "block.minecraft.banner.border.brown": "棕色方框边", + "block.minecraft.banner.border.cyan": "青色方框边", + "block.minecraft.banner.border.gray": "灰色方框边", + "block.minecraft.banner.border.green": "绿色方框边", + "block.minecraft.banner.border.light_blue": "淡蓝色方框边", + "block.minecraft.banner.border.light_gray": "淡灰色方框边", + "block.minecraft.banner.border.lime": "黄绿色方框边", + "block.minecraft.banner.border.magenta": "品红色方框边", + "block.minecraft.banner.border.orange": "橙色方框边", + "block.minecraft.banner.border.pink": "粉红色方框边", + "block.minecraft.banner.border.purple": "紫色方框边", + "block.minecraft.banner.border.red": "红色方框边", + "block.minecraft.banner.border.white": "白色方框边", + "block.minecraft.banner.border.yellow": "黄色方框边", + "block.minecraft.banner.bricks.black": "黑色砖纹", + "block.minecraft.banner.bricks.blue": "蓝色砖纹", + "block.minecraft.banner.bricks.brown": "棕色砖纹", + "block.minecraft.banner.bricks.cyan": "青色砖纹", + "block.minecraft.banner.bricks.gray": "灰色砖纹", + "block.minecraft.banner.bricks.green": "绿色砖纹", + "block.minecraft.banner.bricks.light_blue": "淡蓝色砖纹", + "block.minecraft.banner.bricks.light_gray": "淡灰色砖纹", + "block.minecraft.banner.bricks.lime": "黄绿色砖纹", + "block.minecraft.banner.bricks.magenta": "品红色砖纹", + "block.minecraft.banner.bricks.orange": "橙色砖纹", + "block.minecraft.banner.bricks.pink": "粉红色砖纹", + "block.minecraft.banner.bricks.purple": "紫色砖纹", + "block.minecraft.banner.bricks.red": "红色砖纹", + "block.minecraft.banner.bricks.white": "白色砖纹", + "block.minecraft.banner.bricks.yellow": "黄色砖纹", + "block.minecraft.banner.circle.black": "黑色圆形", + "block.minecraft.banner.circle.blue": "蓝色圆形", + "block.minecraft.banner.circle.brown": "棕色圆形", + "block.minecraft.banner.circle.cyan": "青色圆形", + "block.minecraft.banner.circle.gray": "灰色圆形", + "block.minecraft.banner.circle.green": "绿色圆形", + "block.minecraft.banner.circle.light_blue": "淡蓝色圆形", + "block.minecraft.banner.circle.light_gray": "淡灰色圆形", + "block.minecraft.banner.circle.lime": "黄绿色圆形", + "block.minecraft.banner.circle.magenta": "品红色圆形", + "block.minecraft.banner.circle.orange": "橙色圆形", + "block.minecraft.banner.circle.pink": "粉红色圆形", + "block.minecraft.banner.circle.purple": "紫色圆形", + "block.minecraft.banner.circle.red": "红色圆形", + "block.minecraft.banner.circle.white": "白色圆形", + "block.minecraft.banner.circle.yellow": "黄色圆形", + "block.minecraft.banner.creeper.black": "黑色苦力怕盾徽", + "block.minecraft.banner.creeper.blue": "蓝色苦力怕盾徽", + "block.minecraft.banner.creeper.brown": "棕色苦力怕盾徽", + "block.minecraft.banner.creeper.cyan": "青色苦力怕盾徽", + "block.minecraft.banner.creeper.gray": "灰色苦力怕盾徽", + "block.minecraft.banner.creeper.green": "绿色苦力怕盾徽", + "block.minecraft.banner.creeper.light_blue": "淡蓝色苦力怕盾徽", + "block.minecraft.banner.creeper.light_gray": "淡灰色苦力怕盾徽", + "block.minecraft.banner.creeper.lime": "黄绿色苦力怕盾徽", + "block.minecraft.banner.creeper.magenta": "品红色苦力怕盾徽", + "block.minecraft.banner.creeper.orange": "橙色苦力怕盾徽", + "block.minecraft.banner.creeper.pink": "粉红色苦力怕盾徽", + "block.minecraft.banner.creeper.purple": "紫色苦力怕盾徽", + "block.minecraft.banner.creeper.red": "红色苦力怕盾徽", + "block.minecraft.banner.creeper.white": "白色苦力怕盾徽", + "block.minecraft.banner.creeper.yellow": "黄色苦力怕盾徽", + "block.minecraft.banner.cross.black": "黑斜十字", + "block.minecraft.banner.cross.blue": "蓝斜十字", + "block.minecraft.banner.cross.brown": "棕斜十字", + "block.minecraft.banner.cross.cyan": "青斜十字", + "block.minecraft.banner.cross.gray": "灰斜十字", + "block.minecraft.banner.cross.green": "绿斜十字", + "block.minecraft.banner.cross.light_blue": "淡蓝斜十字", + "block.minecraft.banner.cross.light_gray": "淡灰斜十字", + "block.minecraft.banner.cross.lime": "黄绿斜十字", + "block.minecraft.banner.cross.magenta": "品红斜十字", + "block.minecraft.banner.cross.orange": "橙斜十字", + "block.minecraft.banner.cross.pink": "粉红斜十字", + "block.minecraft.banner.cross.purple": "紫斜十字", + "block.minecraft.banner.cross.red": "红斜十字", + "block.minecraft.banner.cross.white": "白斜十字", + "block.minecraft.banner.cross.yellow": "黄斜十字", + "block.minecraft.banner.curly_border.black": "黑色波纹边", + "block.minecraft.banner.curly_border.blue": "蓝色波纹边", + "block.minecraft.banner.curly_border.brown": "棕色波纹边", + "block.minecraft.banner.curly_border.cyan": "青色波纹边", + "block.minecraft.banner.curly_border.gray": "灰色波纹边", + "block.minecraft.banner.curly_border.green": "绿色波纹边", + "block.minecraft.banner.curly_border.light_blue": "淡蓝色波纹边", + "block.minecraft.banner.curly_border.light_gray": "淡灰色波纹边", + "block.minecraft.banner.curly_border.lime": "黄绿色波纹边", + "block.minecraft.banner.curly_border.magenta": "品红色波纹边", + "block.minecraft.banner.curly_border.orange": "橙色波纹边", + "block.minecraft.banner.curly_border.pink": "粉红色波纹边", + "block.minecraft.banner.curly_border.purple": "紫色波纹边", + "block.minecraft.banner.curly_border.red": "红色波纹边", + "block.minecraft.banner.curly_border.white": "白色波纹边", + "block.minecraft.banner.curly_border.yellow": "黄色波纹边", + "block.minecraft.banner.diagonal_left.black": "黑色右上三角", + "block.minecraft.banner.diagonal_left.blue": "蓝色右上三角", + "block.minecraft.banner.diagonal_left.brown": "棕色右上三角", + "block.minecraft.banner.diagonal_left.cyan": "青色右上三角", + "block.minecraft.banner.diagonal_left.gray": "灰色右上三角", + "block.minecraft.banner.diagonal_left.green": "绿色右上三角", + "block.minecraft.banner.diagonal_left.light_blue": "淡蓝色右上三角", + "block.minecraft.banner.diagonal_left.light_gray": "淡灰色右上三角", + "block.minecraft.banner.diagonal_left.lime": "黄绿色右上三角", + "block.minecraft.banner.diagonal_left.magenta": "品红色右上三角", + "block.minecraft.banner.diagonal_left.orange": "橙色右上三角", + "block.minecraft.banner.diagonal_left.pink": "粉红色右上三角", + "block.minecraft.banner.diagonal_left.purple": "紫色右上三角", + "block.minecraft.banner.diagonal_left.red": "红色右上三角", + "block.minecraft.banner.diagonal_left.white": "白色右上三角", + "block.minecraft.banner.diagonal_left.yellow": "黄色右上三角", + "block.minecraft.banner.diagonal_right.black": "黑色左上三角", + "block.minecraft.banner.diagonal_right.blue": "蓝色左上三角", + "block.minecraft.banner.diagonal_right.brown": "棕色左上三角", + "block.minecraft.banner.diagonal_right.cyan": "青色左上三角", + "block.minecraft.banner.diagonal_right.gray": "灰色左上三角", + "block.minecraft.banner.diagonal_right.green": "绿色左上三角", + "block.minecraft.banner.diagonal_right.light_blue": "淡蓝色左上三角", + "block.minecraft.banner.diagonal_right.light_gray": "淡灰色左上三角", + "block.minecraft.banner.diagonal_right.lime": "黄绿色左上三角", + "block.minecraft.banner.diagonal_right.magenta": "品红色左上三角", + "block.minecraft.banner.diagonal_right.orange": "橙色左上三角", + "block.minecraft.banner.diagonal_right.pink": "粉红色左上三角", + "block.minecraft.banner.diagonal_right.purple": "紫色左上三角", + "block.minecraft.banner.diagonal_right.red": "红色左上三角", + "block.minecraft.banner.diagonal_right.white": "白色左上三角", + "block.minecraft.banner.diagonal_right.yellow": "黄色左上三角", + "block.minecraft.banner.diagonal_up_left.black": "黑色右下三角", + "block.minecraft.banner.diagonal_up_left.blue": "蓝色右下三角", + "block.minecraft.banner.diagonal_up_left.brown": "棕色右下三角", + "block.minecraft.banner.diagonal_up_left.cyan": "青色右下三角", + "block.minecraft.banner.diagonal_up_left.gray": "灰色右下三角", + "block.minecraft.banner.diagonal_up_left.green": "绿色右下三角", + "block.minecraft.banner.diagonal_up_left.light_blue": "淡蓝色右下三角", + "block.minecraft.banner.diagonal_up_left.light_gray": "淡灰色右下三角", + "block.minecraft.banner.diagonal_up_left.lime": "黄绿色右下三角", + "block.minecraft.banner.diagonal_up_left.magenta": "品红色右下三角", + "block.minecraft.banner.diagonal_up_left.orange": "橙色右下三角", + "block.minecraft.banner.diagonal_up_left.pink": "粉红色右下三角", + "block.minecraft.banner.diagonal_up_left.purple": "紫色右下三角", + "block.minecraft.banner.diagonal_up_left.red": "红色右下三角", + "block.minecraft.banner.diagonal_up_left.white": "白色右下三角", + "block.minecraft.banner.diagonal_up_left.yellow": "黄色右下三角", + "block.minecraft.banner.diagonal_up_right.black": "黑色左下三角", + "block.minecraft.banner.diagonal_up_right.blue": "蓝色左下三角", + "block.minecraft.banner.diagonal_up_right.brown": "棕色左下三角", + "block.minecraft.banner.diagonal_up_right.cyan": "青色左下三角", + "block.minecraft.banner.diagonal_up_right.gray": "灰色左下三角", + "block.minecraft.banner.diagonal_up_right.green": "绿色左下三角", + "block.minecraft.banner.diagonal_up_right.light_blue": "淡蓝色左下三角", + "block.minecraft.banner.diagonal_up_right.light_gray": "淡灰色左下三角", + "block.minecraft.banner.diagonal_up_right.lime": "黄绿色左下三角", + "block.minecraft.banner.diagonal_up_right.magenta": "品红色左下三角", + "block.minecraft.banner.diagonal_up_right.orange": "橙色左下三角", + "block.minecraft.banner.diagonal_up_right.pink": "粉红色左下三角", + "block.minecraft.banner.diagonal_up_right.purple": "紫色左下三角", + "block.minecraft.banner.diagonal_up_right.red": "红色左下三角", + "block.minecraft.banner.diagonal_up_right.white": "白色左下三角", + "block.minecraft.banner.diagonal_up_right.yellow": "黄色左下三角", + "block.minecraft.banner.flow.black": "黑色涡流", + "block.minecraft.banner.flow.blue": "蓝色涡流", + "block.minecraft.banner.flow.brown": "棕色涡流", + "block.minecraft.banner.flow.cyan": "青色涡流", + "block.minecraft.banner.flow.gray": "灰色涡流", + "block.minecraft.banner.flow.green": "绿色涡流", + "block.minecraft.banner.flow.light_blue": "淡蓝色涡流", + "block.minecraft.banner.flow.light_gray": "淡灰色涡流", + "block.minecraft.banner.flow.lime": "黄绿色涡流", + "block.minecraft.banner.flow.magenta": "品红色涡流", + "block.minecraft.banner.flow.orange": "橙色涡流", + "block.minecraft.banner.flow.pink": "粉红色涡流", + "block.minecraft.banner.flow.purple": "紫色涡流", + "block.minecraft.banner.flow.red": "红色涡流", + "block.minecraft.banner.flow.white": "白色涡流", + "block.minecraft.banner.flow.yellow": "黄色涡流", + "block.minecraft.banner.flower.black": "黑色花朵盾徽", + "block.minecraft.banner.flower.blue": "蓝色花朵盾徽", + "block.minecraft.banner.flower.brown": "棕色花朵盾徽", + "block.minecraft.banner.flower.cyan": "青色花朵盾徽", + "block.minecraft.banner.flower.gray": "灰色花朵盾徽", + "block.minecraft.banner.flower.green": "绿色花朵盾徽", + "block.minecraft.banner.flower.light_blue": "淡蓝色花朵盾徽", + "block.minecraft.banner.flower.light_gray": "淡灰色花朵盾徽", + "block.minecraft.banner.flower.lime": "黄绿色花朵盾徽", + "block.minecraft.banner.flower.magenta": "品红色花朵盾徽", + "block.minecraft.banner.flower.orange": "橙色花朵盾徽", + "block.minecraft.banner.flower.pink": "粉红色花朵盾徽", + "block.minecraft.banner.flower.purple": "紫色花朵盾徽", + "block.minecraft.banner.flower.red": "红色花朵盾徽", + "block.minecraft.banner.flower.white": "白色花朵盾徽", + "block.minecraft.banner.flower.yellow": "黄色花朵盾徽", + "block.minecraft.banner.globe.black": "黑色地球", + "block.minecraft.banner.globe.blue": "蓝色地球", + "block.minecraft.banner.globe.brown": "棕色地球", + "block.minecraft.banner.globe.cyan": "青色地球", + "block.minecraft.banner.globe.gray": "灰色地球", + "block.minecraft.banner.globe.green": "绿色地球", + "block.minecraft.banner.globe.light_blue": "淡蓝色地球", + "block.minecraft.banner.globe.light_gray": "淡灰色地球", + "block.minecraft.banner.globe.lime": "黄绿色地球", + "block.minecraft.banner.globe.magenta": "品红色地球", + "block.minecraft.banner.globe.orange": "橙色地球", + "block.minecraft.banner.globe.pink": "粉红色地球", + "block.minecraft.banner.globe.purple": "紫色地球", + "block.minecraft.banner.globe.red": "红色地球", + "block.minecraft.banner.globe.white": "白色地球", + "block.minecraft.banner.globe.yellow": "黄色地球", + "block.minecraft.banner.gradient.black": "黑色自上渐淡", + "block.minecraft.banner.gradient.blue": "蓝色自上渐淡", + "block.minecraft.banner.gradient.brown": "棕色自上渐淡", + "block.minecraft.banner.gradient.cyan": "青色自上渐淡", + "block.minecraft.banner.gradient.gray": "灰色自上渐淡", + "block.minecraft.banner.gradient.green": "绿色自上渐淡", + "block.minecraft.banner.gradient.light_blue": "淡蓝色自上渐淡", + "block.minecraft.banner.gradient.light_gray": "淡灰色自上渐淡", + "block.minecraft.banner.gradient.lime": "黄绿色自上渐淡", + "block.minecraft.banner.gradient.magenta": "品红色自上渐淡", + "block.minecraft.banner.gradient.orange": "橙色自上渐淡", + "block.minecraft.banner.gradient.pink": "粉红色自上渐淡", + "block.minecraft.banner.gradient.purple": "紫色自上渐淡", + "block.minecraft.banner.gradient.red": "红色自上渐淡", + "block.minecraft.banner.gradient.white": "白色自上渐淡", + "block.minecraft.banner.gradient.yellow": "黄色自上渐淡", + "block.minecraft.banner.gradient_up.black": "黑色自下渐淡", + "block.minecraft.banner.gradient_up.blue": "蓝色自下渐淡", + "block.minecraft.banner.gradient_up.brown": "棕色自下渐淡", + "block.minecraft.banner.gradient_up.cyan": "青色自下渐淡", + "block.minecraft.banner.gradient_up.gray": "灰色自下渐淡", + "block.minecraft.banner.gradient_up.green": "绿色自下渐淡", + "block.minecraft.banner.gradient_up.light_blue": "淡蓝色自下渐淡", + "block.minecraft.banner.gradient_up.light_gray": "淡灰色自下渐淡", + "block.minecraft.banner.gradient_up.lime": "黄绿色自下渐淡", + "block.minecraft.banner.gradient_up.magenta": "品红色自下渐淡", + "block.minecraft.banner.gradient_up.orange": "橙色自下渐淡", + "block.minecraft.banner.gradient_up.pink": "粉红色自下渐淡", + "block.minecraft.banner.gradient_up.purple": "紫色自下渐淡", + "block.minecraft.banner.gradient_up.red": "红色自下渐淡", + "block.minecraft.banner.gradient_up.white": "白色自下渐淡", + "block.minecraft.banner.gradient_up.yellow": "黄色自下渐淡", + "block.minecraft.banner.guster.black": "黑色旋风", + "block.minecraft.banner.guster.blue": "蓝色旋风", + "block.minecraft.banner.guster.brown": "棕色旋风", + "block.minecraft.banner.guster.cyan": "青色旋风", + "block.minecraft.banner.guster.gray": "灰色旋风", + "block.minecraft.banner.guster.green": "绿色旋风", + "block.minecraft.banner.guster.light_blue": "淡蓝色旋风", + "block.minecraft.banner.guster.light_gray": "淡灰色旋风", + "block.minecraft.banner.guster.lime": "黄绿色旋风", + "block.minecraft.banner.guster.magenta": "品红色旋风", + "block.minecraft.banner.guster.orange": "橙色旋风", + "block.minecraft.banner.guster.pink": "粉红色旋风", + "block.minecraft.banner.guster.purple": "紫色旋风", + "block.minecraft.banner.guster.red": "红色旋风", + "block.minecraft.banner.guster.white": "白色旋风", + "block.minecraft.banner.guster.yellow": "黄色旋风", + "block.minecraft.banner.half_horizontal.black": "黑色上半方形", + "block.minecraft.banner.half_horizontal.blue": "蓝色上半方形", + "block.minecraft.banner.half_horizontal.brown": "棕色上半方形", + "block.minecraft.banner.half_horizontal.cyan": "青色上半方形", + "block.minecraft.banner.half_horizontal.gray": "灰色上半方形", + "block.minecraft.banner.half_horizontal.green": "绿色上半方形", + "block.minecraft.banner.half_horizontal.light_blue": "淡蓝色上半方形", + "block.minecraft.banner.half_horizontal.light_gray": "淡灰色上半方形", + "block.minecraft.banner.half_horizontal.lime": "黄绿色上半方形", + "block.minecraft.banner.half_horizontal.magenta": "品红色上半方形", + "block.minecraft.banner.half_horizontal.orange": "橙色上半方形", + "block.minecraft.banner.half_horizontal.pink": "粉红色上半方形", + "block.minecraft.banner.half_horizontal.purple": "紫色上半方形", + "block.minecraft.banner.half_horizontal.red": "红色上半方形", + "block.minecraft.banner.half_horizontal.white": "白色上半方形", + "block.minecraft.banner.half_horizontal.yellow": "黄色上半方形", + "block.minecraft.banner.half_horizontal_bottom.black": "黑色下半方形", + "block.minecraft.banner.half_horizontal_bottom.blue": "蓝色下半方形", + "block.minecraft.banner.half_horizontal_bottom.brown": "棕色下半方形", + "block.minecraft.banner.half_horizontal_bottom.cyan": "青色下半方形", + "block.minecraft.banner.half_horizontal_bottom.gray": "灰色下半方形", + "block.minecraft.banner.half_horizontal_bottom.green": "绿色下半方形", + "block.minecraft.banner.half_horizontal_bottom.light_blue": "淡蓝色下半方形", + "block.minecraft.banner.half_horizontal_bottom.light_gray": "淡灰色下半方形", + "block.minecraft.banner.half_horizontal_bottom.lime": "黄绿色下半方形", + "block.minecraft.banner.half_horizontal_bottom.magenta": "品红色下半方形", + "block.minecraft.banner.half_horizontal_bottom.orange": "橙色下半方形", + "block.minecraft.banner.half_horizontal_bottom.pink": "粉红色下半方形", + "block.minecraft.banner.half_horizontal_bottom.purple": "紫色下半方形", + "block.minecraft.banner.half_horizontal_bottom.red": "红色下半方形", + "block.minecraft.banner.half_horizontal_bottom.white": "白色下半方形", + "block.minecraft.banner.half_horizontal_bottom.yellow": "黄色下半方形", + "block.minecraft.banner.half_vertical.black": "黑色右半方形", + "block.minecraft.banner.half_vertical.blue": "蓝色右半方形", + "block.minecraft.banner.half_vertical.brown": "棕色右半方形", + "block.minecraft.banner.half_vertical.cyan": "青色右半方形", + "block.minecraft.banner.half_vertical.gray": "灰色右半方形", + "block.minecraft.banner.half_vertical.green": "绿色右半方形", + "block.minecraft.banner.half_vertical.light_blue": "淡蓝色右半方形", + "block.minecraft.banner.half_vertical.light_gray": "淡灰色右半方形", + "block.minecraft.banner.half_vertical.lime": "黄绿色右半方形", + "block.minecraft.banner.half_vertical.magenta": "品红色右半方形", + "block.minecraft.banner.half_vertical.orange": "橙色右半方形", + "block.minecraft.banner.half_vertical.pink": "粉红色右半方形", + "block.minecraft.banner.half_vertical.purple": "紫色右半方形", + "block.minecraft.banner.half_vertical.red": "红色右半方形", + "block.minecraft.banner.half_vertical.white": "白色右半方形", + "block.minecraft.banner.half_vertical.yellow": "黄色右半方形", + "block.minecraft.banner.half_vertical_right.black": "黑色左半方形", + "block.minecraft.banner.half_vertical_right.blue": "蓝色左半方形", + "block.minecraft.banner.half_vertical_right.brown": "棕色左半方形", + "block.minecraft.banner.half_vertical_right.cyan": "青色左半方形", + "block.minecraft.banner.half_vertical_right.gray": "灰色左半方形", + "block.minecraft.banner.half_vertical_right.green": "绿色左半方形", + "block.minecraft.banner.half_vertical_right.light_blue": "淡蓝色左半方形", + "block.minecraft.banner.half_vertical_right.light_gray": "淡灰色左半方形", + "block.minecraft.banner.half_vertical_right.lime": "黄绿色左半方形", + "block.minecraft.banner.half_vertical_right.magenta": "品红色左半方形", + "block.minecraft.banner.half_vertical_right.orange": "橙色左半方形", + "block.minecraft.banner.half_vertical_right.pink": "粉红色左半方形", + "block.minecraft.banner.half_vertical_right.purple": "紫色左半方形", + "block.minecraft.banner.half_vertical_right.red": "红色左半方形", + "block.minecraft.banner.half_vertical_right.white": "白色左半方形", + "block.minecraft.banner.half_vertical_right.yellow": "黄色左半方形", + "block.minecraft.banner.mojang.black": "黑色Mojang徽标", + "block.minecraft.banner.mojang.blue": "蓝色Mojang徽标", + "block.minecraft.banner.mojang.brown": "棕色Mojang徽标", + "block.minecraft.banner.mojang.cyan": "青色Mojang徽标", + "block.minecraft.banner.mojang.gray": "灰色Mojang徽标", + "block.minecraft.banner.mojang.green": "绿色Mojang徽标", + "block.minecraft.banner.mojang.light_blue": "淡蓝色Mojang徽标", + "block.minecraft.banner.mojang.light_gray": "淡灰色Mojang徽标", + "block.minecraft.banner.mojang.lime": "黄绿色Mojang徽标", + "block.minecraft.banner.mojang.magenta": "品红色Mojang徽标", + "block.minecraft.banner.mojang.orange": "橙色Mojang徽标", + "block.minecraft.banner.mojang.pink": "粉红色Mojang徽标", + "block.minecraft.banner.mojang.purple": "紫色Mojang徽标", + "block.minecraft.banner.mojang.red": "红色Mojang徽标", + "block.minecraft.banner.mojang.white": "白色Mojang徽标", + "block.minecraft.banner.mojang.yellow": "黄色Mojang徽标", + "block.minecraft.banner.piglin.black": "黑色猪鼻", + "block.minecraft.banner.piglin.blue": "蓝色猪鼻", + "block.minecraft.banner.piglin.brown": "棕色猪鼻", + "block.minecraft.banner.piglin.cyan": "青色猪鼻", + "block.minecraft.banner.piglin.gray": "灰色猪鼻", + "block.minecraft.banner.piglin.green": "绿色猪鼻", + "block.minecraft.banner.piglin.light_blue": "淡蓝色猪鼻", + "block.minecraft.banner.piglin.light_gray": "淡灰色猪鼻", + "block.minecraft.banner.piglin.lime": "黄绿色猪鼻", + "block.minecraft.banner.piglin.magenta": "品红色猪鼻", + "block.minecraft.banner.piglin.orange": "橙色猪鼻", + "block.minecraft.banner.piglin.pink": "粉红色猪鼻", + "block.minecraft.banner.piglin.purple": "紫色猪鼻", + "block.minecraft.banner.piglin.red": "红色猪鼻", + "block.minecraft.banner.piglin.white": "白色猪鼻", + "block.minecraft.banner.piglin.yellow": "黄色猪鼻", + "block.minecraft.banner.rhombus.black": "黑色菱形", + "block.minecraft.banner.rhombus.blue": "蓝色菱形", + "block.minecraft.banner.rhombus.brown": "棕色菱形", + "block.minecraft.banner.rhombus.cyan": "青色菱形", + "block.minecraft.banner.rhombus.gray": "灰色菱形", + "block.minecraft.banner.rhombus.green": "绿色菱形", + "block.minecraft.banner.rhombus.light_blue": "淡蓝色菱形", + "block.minecraft.banner.rhombus.light_gray": "淡灰色菱形", + "block.minecraft.banner.rhombus.lime": "黄绿色菱形", + "block.minecraft.banner.rhombus.magenta": "品红色菱形", + "block.minecraft.banner.rhombus.orange": "橙色菱形", + "block.minecraft.banner.rhombus.pink": "粉红色菱形", + "block.minecraft.banner.rhombus.purple": "紫色菱形", + "block.minecraft.banner.rhombus.red": "红色菱形", + "block.minecraft.banner.rhombus.white": "白色菱形", + "block.minecraft.banner.rhombus.yellow": "黄色菱形", + "block.minecraft.banner.skull.black": "黑色头颅盾徽", + "block.minecraft.banner.skull.blue": "蓝色头颅盾徽", + "block.minecraft.banner.skull.brown": "棕色头颅盾徽", + "block.minecraft.banner.skull.cyan": "青色头颅盾徽", + "block.minecraft.banner.skull.gray": "灰色头颅盾徽", + "block.minecraft.banner.skull.green": "绿色头颅盾徽", + "block.minecraft.banner.skull.light_blue": "淡蓝色头颅盾徽", + "block.minecraft.banner.skull.light_gray": "淡灰色头颅盾徽", + "block.minecraft.banner.skull.lime": "黄绿色头颅盾徽", + "block.minecraft.banner.skull.magenta": "品红色头颅盾徽", + "block.minecraft.banner.skull.orange": "橙色头颅盾徽", + "block.minecraft.banner.skull.pink": "粉红色头颅盾徽", + "block.minecraft.banner.skull.purple": "紫色头颅盾徽", + "block.minecraft.banner.skull.red": "红色头颅盾徽", + "block.minecraft.banner.skull.white": "白色头颅盾徽", + "block.minecraft.banner.skull.yellow": "黄色头颅盾徽", + "block.minecraft.banner.small_stripes.black": "黑竖条纹", + "block.minecraft.banner.small_stripes.blue": "蓝竖条纹", + "block.minecraft.banner.small_stripes.brown": "棕竖条纹", + "block.minecraft.banner.small_stripes.cyan": "青竖条纹", + "block.minecraft.banner.small_stripes.gray": "灰竖条纹", + "block.minecraft.banner.small_stripes.green": "绿竖条纹", + "block.minecraft.banner.small_stripes.light_blue": "淡蓝竖条纹", + "block.minecraft.banner.small_stripes.light_gray": "淡灰竖条纹", + "block.minecraft.banner.small_stripes.lime": "黄绿竖条纹", + "block.minecraft.banner.small_stripes.magenta": "品红竖条纹", + "block.minecraft.banner.small_stripes.orange": "橙竖条纹", + "block.minecraft.banner.small_stripes.pink": "粉红竖条纹", + "block.minecraft.banner.small_stripes.purple": "紫竖条纹", + "block.minecraft.banner.small_stripes.red": "红竖条纹", + "block.minecraft.banner.small_stripes.white": "白竖条纹", + "block.minecraft.banner.small_stripes.yellow": "黄竖条纹", + "block.minecraft.banner.square_bottom_left.black": "右底黑方", + "block.minecraft.banner.square_bottom_left.blue": "右底蓝方", + "block.minecraft.banner.square_bottom_left.brown": "右底棕方", + "block.minecraft.banner.square_bottom_left.cyan": "右底青方", + "block.minecraft.banner.square_bottom_left.gray": "右底灰方", + "block.minecraft.banner.square_bottom_left.green": "右底绿方", + "block.minecraft.banner.square_bottom_left.light_blue": "右底淡蓝方", + "block.minecraft.banner.square_bottom_left.light_gray": "右底淡灰方", + "block.minecraft.banner.square_bottom_left.lime": "右底黄绿方", + "block.minecraft.banner.square_bottom_left.magenta": "右底品红方", + "block.minecraft.banner.square_bottom_left.orange": "右底橙方", + "block.minecraft.banner.square_bottom_left.pink": "右底粉红方", + "block.minecraft.banner.square_bottom_left.purple": "右底紫方", + "block.minecraft.banner.square_bottom_left.red": "右底红方", + "block.minecraft.banner.square_bottom_left.white": "右底白方", + "block.minecraft.banner.square_bottom_left.yellow": "右底黄方", + "block.minecraft.banner.square_bottom_right.black": "左底黑方", + "block.minecraft.banner.square_bottom_right.blue": "左底蓝方", + "block.minecraft.banner.square_bottom_right.brown": "左底棕方", + "block.minecraft.banner.square_bottom_right.cyan": "左底青方", + "block.minecraft.banner.square_bottom_right.gray": "左底灰方", + "block.minecraft.banner.square_bottom_right.green": "左底绿方", + "block.minecraft.banner.square_bottom_right.light_blue": "左底淡蓝方", + "block.minecraft.banner.square_bottom_right.light_gray": "左底淡灰方", + "block.minecraft.banner.square_bottom_right.lime": "左底黄绿方", + "block.minecraft.banner.square_bottom_right.magenta": "左底品红方", + "block.minecraft.banner.square_bottom_right.orange": "左底橙方", + "block.minecraft.banner.square_bottom_right.pink": "左底粉红方", + "block.minecraft.banner.square_bottom_right.purple": "左底紫方", + "block.minecraft.banner.square_bottom_right.red": "左底红方", + "block.minecraft.banner.square_bottom_right.white": "左底白方", + "block.minecraft.banner.square_bottom_right.yellow": "左底黄方", + "block.minecraft.banner.square_top_left.black": "右顶黑方", + "block.minecraft.banner.square_top_left.blue": "右顶蓝方", + "block.minecraft.banner.square_top_left.brown": "右顶棕方", + "block.minecraft.banner.square_top_left.cyan": "右顶青方", + "block.minecraft.banner.square_top_left.gray": "右顶灰方", + "block.minecraft.banner.square_top_left.green": "右顶绿方", + "block.minecraft.banner.square_top_left.light_blue": "右顶淡蓝方", + "block.minecraft.banner.square_top_left.light_gray": "右顶淡灰方", + "block.minecraft.banner.square_top_left.lime": "右顶黄绿方", + "block.minecraft.banner.square_top_left.magenta": "右顶品红方", + "block.minecraft.banner.square_top_left.orange": "右顶橙方", + "block.minecraft.banner.square_top_left.pink": "右顶粉红方", + "block.minecraft.banner.square_top_left.purple": "右顶紫方", + "block.minecraft.banner.square_top_left.red": "右顶红方", + "block.minecraft.banner.square_top_left.white": "右顶白方", + "block.minecraft.banner.square_top_left.yellow": "右顶黄方", + "block.minecraft.banner.square_top_right.black": "左顶黑方", + "block.minecraft.banner.square_top_right.blue": "左顶蓝方", + "block.minecraft.banner.square_top_right.brown": "左顶棕方", + "block.minecraft.banner.square_top_right.cyan": "左顶青方", + "block.minecraft.banner.square_top_right.gray": "左顶灰方", + "block.minecraft.banner.square_top_right.green": "左顶绿方", + "block.minecraft.banner.square_top_right.light_blue": "左顶淡蓝方", + "block.minecraft.banner.square_top_right.light_gray": "左顶淡灰方", + "block.minecraft.banner.square_top_right.lime": "左顶黄绿方", + "block.minecraft.banner.square_top_right.magenta": "左顶品红方", + "block.minecraft.banner.square_top_right.orange": "左顶橙方", + "block.minecraft.banner.square_top_right.pink": "左顶粉红方", + "block.minecraft.banner.square_top_right.purple": "左顶紫方", + "block.minecraft.banner.square_top_right.red": "左顶红方", + "block.minecraft.banner.square_top_right.white": "左顶白方", + "block.minecraft.banner.square_top_right.yellow": "左顶黄方", + "block.minecraft.banner.straight_cross.black": "黑正十字", + "block.minecraft.banner.straight_cross.blue": "蓝正十字", + "block.minecraft.banner.straight_cross.brown": "棕正十字", + "block.minecraft.banner.straight_cross.cyan": "青正十字", + "block.minecraft.banner.straight_cross.gray": "灰正十字", + "block.minecraft.banner.straight_cross.green": "绿正十字", + "block.minecraft.banner.straight_cross.light_blue": "淡蓝正十字", + "block.minecraft.banner.straight_cross.light_gray": "淡灰正十字", + "block.minecraft.banner.straight_cross.lime": "黄绿正十字", + "block.minecraft.banner.straight_cross.magenta": "品红正十字", + "block.minecraft.banner.straight_cross.orange": "橙正十字", + "block.minecraft.banner.straight_cross.pink": "粉红正十字", + "block.minecraft.banner.straight_cross.purple": "紫正十字", + "block.minecraft.banner.straight_cross.red": "红正十字", + "block.minecraft.banner.straight_cross.white": "白正十字", + "block.minecraft.banner.straight_cross.yellow": "黄正十字", + "block.minecraft.banner.stripe_bottom.black": "底黑横条", + "block.minecraft.banner.stripe_bottom.blue": "底蓝横条", + "block.minecraft.banner.stripe_bottom.brown": "底棕横条", + "block.minecraft.banner.stripe_bottom.cyan": "底青横条", + "block.minecraft.banner.stripe_bottom.gray": "底灰横条", + "block.minecraft.banner.stripe_bottom.green": "底绿横条", + "block.minecraft.banner.stripe_bottom.light_blue": "底淡蓝横条", + "block.minecraft.banner.stripe_bottom.light_gray": "底淡灰横条", + "block.minecraft.banner.stripe_bottom.lime": "底黄绿横条", + "block.minecraft.banner.stripe_bottom.magenta": "底品红横条", + "block.minecraft.banner.stripe_bottom.orange": "底橙横条", + "block.minecraft.banner.stripe_bottom.pink": "底粉红横条", + "block.minecraft.banner.stripe_bottom.purple": "底紫横条", + "block.minecraft.banner.stripe_bottom.red": "底红横条", + "block.minecraft.banner.stripe_bottom.white": "底白横条", + "block.minecraft.banner.stripe_bottom.yellow": "底黄横条", + "block.minecraft.banner.stripe_center.black": "中黑竖条", + "block.minecraft.banner.stripe_center.blue": "中蓝竖条", + "block.minecraft.banner.stripe_center.brown": "中棕竖条", + "block.minecraft.banner.stripe_center.cyan": "中青竖条", + "block.minecraft.banner.stripe_center.gray": "中灰竖条", + "block.minecraft.banner.stripe_center.green": "中绿竖条", + "block.minecraft.banner.stripe_center.light_blue": "中淡蓝竖条", + "block.minecraft.banner.stripe_center.light_gray": "中淡灰竖条", + "block.minecraft.banner.stripe_center.lime": "中黄绿竖条", + "block.minecraft.banner.stripe_center.magenta": "中品红竖条", + "block.minecraft.banner.stripe_center.orange": "中橙竖条", + "block.minecraft.banner.stripe_center.pink": "中粉红竖条", + "block.minecraft.banner.stripe_center.purple": "中紫竖条", + "block.minecraft.banner.stripe_center.red": "中红竖条", + "block.minecraft.banner.stripe_center.white": "中白竖条", + "block.minecraft.banner.stripe_center.yellow": "中黄竖条", + "block.minecraft.banner.stripe_downleft.black": "左黑斜条", + "block.minecraft.banner.stripe_downleft.blue": "左蓝斜条", + "block.minecraft.banner.stripe_downleft.brown": "左棕斜条", + "block.minecraft.banner.stripe_downleft.cyan": "左青斜条", + "block.minecraft.banner.stripe_downleft.gray": "左灰斜条", + "block.minecraft.banner.stripe_downleft.green": "左绿斜条", + "block.minecraft.banner.stripe_downleft.light_blue": "左淡蓝斜条", + "block.minecraft.banner.stripe_downleft.light_gray": "左淡灰斜条", + "block.minecraft.banner.stripe_downleft.lime": "左黄绿斜条", + "block.minecraft.banner.stripe_downleft.magenta": "左品红斜条", + "block.minecraft.banner.stripe_downleft.orange": "左橙斜条", + "block.minecraft.banner.stripe_downleft.pink": "左粉红斜条", + "block.minecraft.banner.stripe_downleft.purple": "左紫斜条", + "block.minecraft.banner.stripe_downleft.red": "左红斜条", + "block.minecraft.banner.stripe_downleft.white": "左白斜条", + "block.minecraft.banner.stripe_downleft.yellow": "左黄斜条", + "block.minecraft.banner.stripe_downright.black": "右黑斜条", + "block.minecraft.banner.stripe_downright.blue": "右蓝斜条", + "block.minecraft.banner.stripe_downright.brown": "右棕斜条", + "block.minecraft.banner.stripe_downright.cyan": "右青斜条", + "block.minecraft.banner.stripe_downright.gray": "右灰斜条", + "block.minecraft.banner.stripe_downright.green": "右绿斜条", + "block.minecraft.banner.stripe_downright.light_blue": "右淡蓝斜条", + "block.minecraft.banner.stripe_downright.light_gray": "右淡灰斜条", + "block.minecraft.banner.stripe_downright.lime": "右黄绿斜条", + "block.minecraft.banner.stripe_downright.magenta": "右品红斜条", + "block.minecraft.banner.stripe_downright.orange": "右橙斜条", + "block.minecraft.banner.stripe_downright.pink": "右粉红斜条", + "block.minecraft.banner.stripe_downright.purple": "右紫斜条", + "block.minecraft.banner.stripe_downright.red": "右红斜条", + "block.minecraft.banner.stripe_downright.white": "右白斜条", + "block.minecraft.banner.stripe_downright.yellow": "右黄斜条", + "block.minecraft.banner.stripe_left.black": "右黑竖条", + "block.minecraft.banner.stripe_left.blue": "右蓝竖条", + "block.minecraft.banner.stripe_left.brown": "右棕竖条", + "block.minecraft.banner.stripe_left.cyan": "右青竖条", + "block.minecraft.banner.stripe_left.gray": "右灰竖条", + "block.minecraft.banner.stripe_left.green": "右绿竖条", + "block.minecraft.banner.stripe_left.light_blue": "右淡蓝竖条", + "block.minecraft.banner.stripe_left.light_gray": "右淡灰竖条", + "block.minecraft.banner.stripe_left.lime": "右黄绿竖条", + "block.minecraft.banner.stripe_left.magenta": "右品红竖条", + "block.minecraft.banner.stripe_left.orange": "右橙竖条", + "block.minecraft.banner.stripe_left.pink": "右粉红竖条", + "block.minecraft.banner.stripe_left.purple": "右紫竖条", + "block.minecraft.banner.stripe_left.red": "右红竖条", + "block.minecraft.banner.stripe_left.white": "右白竖条", + "block.minecraft.banner.stripe_left.yellow": "右黄竖条", + "block.minecraft.banner.stripe_middle.black": "中黑横条", + "block.minecraft.banner.stripe_middle.blue": "中蓝横条", + "block.minecraft.banner.stripe_middle.brown": "中棕横条", + "block.minecraft.banner.stripe_middle.cyan": "中青横条", + "block.minecraft.banner.stripe_middle.gray": "中灰横条", + "block.minecraft.banner.stripe_middle.green": "中绿横条", + "block.minecraft.banner.stripe_middle.light_blue": "中淡蓝横条", + "block.minecraft.banner.stripe_middle.light_gray": "中淡灰横条", + "block.minecraft.banner.stripe_middle.lime": "中黄绿横条", + "block.minecraft.banner.stripe_middle.magenta": "中品红横条", + "block.minecraft.banner.stripe_middle.orange": "中橙横条", + "block.minecraft.banner.stripe_middle.pink": "中粉红横条", + "block.minecraft.banner.stripe_middle.purple": "中紫横条", + "block.minecraft.banner.stripe_middle.red": "中红横条", + "block.minecraft.banner.stripe_middle.white": "中白横条", + "block.minecraft.banner.stripe_middle.yellow": "中黄横条", + "block.minecraft.banner.stripe_right.black": "左黑竖条", + "block.minecraft.banner.stripe_right.blue": "左蓝竖条", + "block.minecraft.banner.stripe_right.brown": "左棕竖条", + "block.minecraft.banner.stripe_right.cyan": "左青竖条", + "block.minecraft.banner.stripe_right.gray": "左灰竖条", + "block.minecraft.banner.stripe_right.green": "左绿竖条", + "block.minecraft.banner.stripe_right.light_blue": "左淡蓝竖条", + "block.minecraft.banner.stripe_right.light_gray": "左淡灰竖条", + "block.minecraft.banner.stripe_right.lime": "左黄绿竖条", + "block.minecraft.banner.stripe_right.magenta": "左品红竖条", + "block.minecraft.banner.stripe_right.orange": "左橙竖条", + "block.minecraft.banner.stripe_right.pink": "左粉红竖条", + "block.minecraft.banner.stripe_right.purple": "左紫竖条", + "block.minecraft.banner.stripe_right.red": "左红竖条", + "block.minecraft.banner.stripe_right.white": "左白竖条", + "block.minecraft.banner.stripe_right.yellow": "左黄竖条", + "block.minecraft.banner.stripe_top.black": "顶黑横条", + "block.minecraft.banner.stripe_top.blue": "顶蓝横条", + "block.minecraft.banner.stripe_top.brown": "顶棕横条", + "block.minecraft.banner.stripe_top.cyan": "顶青横条", + "block.minecraft.banner.stripe_top.gray": "顶灰横条", + "block.minecraft.banner.stripe_top.green": "顶绿横条", + "block.minecraft.banner.stripe_top.light_blue": "顶淡蓝横条", + "block.minecraft.banner.stripe_top.light_gray": "顶淡灰横条", + "block.minecraft.banner.stripe_top.lime": "顶黄绿横条", + "block.minecraft.banner.stripe_top.magenta": "顶品红横条", + "block.minecraft.banner.stripe_top.orange": "顶橙横条", + "block.minecraft.banner.stripe_top.pink": "顶粉红横条", + "block.minecraft.banner.stripe_top.purple": "顶紫横条", + "block.minecraft.banner.stripe_top.red": "顶红横条", + "block.minecraft.banner.stripe_top.white": "顶白横条", + "block.minecraft.banner.stripe_top.yellow": "顶黄横条", + "block.minecraft.banner.triangle_bottom.black": "底黑三角", + "block.minecraft.banner.triangle_bottom.blue": "底蓝三角", + "block.minecraft.banner.triangle_bottom.brown": "底棕三角", + "block.minecraft.banner.triangle_bottom.cyan": "底青三角", + "block.minecraft.banner.triangle_bottom.gray": "底灰三角", + "block.minecraft.banner.triangle_bottom.green": "底绿三角", + "block.minecraft.banner.triangle_bottom.light_blue": "底淡蓝三角", + "block.minecraft.banner.triangle_bottom.light_gray": "底淡灰三角", + "block.minecraft.banner.triangle_bottom.lime": "底黄绿三角", + "block.minecraft.banner.triangle_bottom.magenta": "底品红三角", + "block.minecraft.banner.triangle_bottom.orange": "底橙三角", + "block.minecraft.banner.triangle_bottom.pink": "底粉红三角", + "block.minecraft.banner.triangle_bottom.purple": "底紫三角", + "block.minecraft.banner.triangle_bottom.red": "底红三角", + "block.minecraft.banner.triangle_bottom.white": "底白三角", + "block.minecraft.banner.triangle_bottom.yellow": "底黄三角", + "block.minecraft.banner.triangle_top.black": "顶黑三角", + "block.minecraft.banner.triangle_top.blue": "顶蓝三角", + "block.minecraft.banner.triangle_top.brown": "顶棕三角", + "block.minecraft.banner.triangle_top.cyan": "顶青三角", + "block.minecraft.banner.triangle_top.gray": "顶灰三角", + "block.minecraft.banner.triangle_top.green": "顶绿三角", + "block.minecraft.banner.triangle_top.light_blue": "顶淡蓝三角", + "block.minecraft.banner.triangle_top.light_gray": "顶淡灰三角", + "block.minecraft.banner.triangle_top.lime": "顶黄绿三角", + "block.minecraft.banner.triangle_top.magenta": "顶品红三角", + "block.minecraft.banner.triangle_top.orange": "顶橙三角", + "block.minecraft.banner.triangle_top.pink": "顶粉红三角", + "block.minecraft.banner.triangle_top.purple": "顶紫三角", + "block.minecraft.banner.triangle_top.red": "顶红三角", + "block.minecraft.banner.triangle_top.white": "顶白三角", + "block.minecraft.banner.triangle_top.yellow": "顶黄三角", + "block.minecraft.banner.triangles_bottom.black": "黑色底波纹", + "block.minecraft.banner.triangles_bottom.blue": "蓝色底波纹", + "block.minecraft.banner.triangles_bottom.brown": "棕色底波纹", + "block.minecraft.banner.triangles_bottom.cyan": "青色底波纹", + "block.minecraft.banner.triangles_bottom.gray": "灰色底波纹", + "block.minecraft.banner.triangles_bottom.green": "绿色底波纹", + "block.minecraft.banner.triangles_bottom.light_blue": "淡蓝色底波纹", + "block.minecraft.banner.triangles_bottom.light_gray": "淡灰色底波纹", + "block.minecraft.banner.triangles_bottom.lime": "黄绿色底波纹", + "block.minecraft.banner.triangles_bottom.magenta": "品红色底波纹", + "block.minecraft.banner.triangles_bottom.orange": "橙色底波纹", + "block.minecraft.banner.triangles_bottom.pink": "粉红色底波纹", + "block.minecraft.banner.triangles_bottom.purple": "紫色底波纹", + "block.minecraft.banner.triangles_bottom.red": "红色底波纹", + "block.minecraft.banner.triangles_bottom.white": "白色底波纹", + "block.minecraft.banner.triangles_bottom.yellow": "黄色底波纹", + "block.minecraft.banner.triangles_top.black": "黑色顶波纹", + "block.minecraft.banner.triangles_top.blue": "蓝色顶波纹", + "block.minecraft.banner.triangles_top.brown": "棕色顶波纹", + "block.minecraft.banner.triangles_top.cyan": "青色顶波纹", + "block.minecraft.banner.triangles_top.gray": "灰色顶波纹", + "block.minecraft.banner.triangles_top.green": "绿色顶波纹", + "block.minecraft.banner.triangles_top.light_blue": "淡蓝色顶波纹", + "block.minecraft.banner.triangles_top.light_gray": "淡灰色顶波纹", + "block.minecraft.banner.triangles_top.lime": "黄绿色顶波纹", + "block.minecraft.banner.triangles_top.magenta": "品红色顶波纹", + "block.minecraft.banner.triangles_top.orange": "橙色顶波纹", + "block.minecraft.banner.triangles_top.pink": "粉红色顶波纹", + "block.minecraft.banner.triangles_top.purple": "紫色顶波纹", + "block.minecraft.banner.triangles_top.red": "红色顶波纹", + "block.minecraft.banner.triangles_top.white": "白色顶波纹", + "block.minecraft.banner.triangles_top.yellow": "黄色顶波纹", + "block.minecraft.barrel": "木桶", + "block.minecraft.barrier": "屏障", + "block.minecraft.basalt": "玄武岩", + "block.minecraft.beacon": "信标", + "block.minecraft.beacon.primary": "主效果", + "block.minecraft.beacon.secondary": "辅助效果", + "block.minecraft.bed.no_sleep": "你只能在夜间或雷暴中入眠", + "block.minecraft.bed.not_safe": "你现在不能休息,周围有怪物在游荡", + "block.minecraft.bed.obstructed": "这张床已被阻挡", + "block.minecraft.bed.occupied": "这张床已被占用", + "block.minecraft.bed.too_far_away": "你现在不能休息,床太远了", + "block.minecraft.bedrock": "基岩", + "block.minecraft.bee_nest": "蜂巢", + "block.minecraft.beehive": "蜂箱", + "block.minecraft.beetroots": "甜菜根", + "block.minecraft.bell": "钟", + "block.minecraft.big_dripleaf": "大型垂滴叶", + "block.minecraft.big_dripleaf_stem": "大型垂滴叶茎", + "block.minecraft.birch_button": "白桦木按钮", + "block.minecraft.birch_door": "白桦木门", + "block.minecraft.birch_fence": "白桦木栅栏", + "block.minecraft.birch_fence_gate": "白桦木栅栏门", + "block.minecraft.birch_hanging_sign": "悬挂式白桦木告示牌", + "block.minecraft.birch_leaves": "白桦树叶", + "block.minecraft.birch_log": "白桦原木", + "block.minecraft.birch_planks": "白桦木板", + "block.minecraft.birch_pressure_plate": "白桦木压力板", + "block.minecraft.birch_sapling": "白桦树苗", + "block.minecraft.birch_sign": "白桦木告示牌", + "block.minecraft.birch_slab": "白桦木台阶", + "block.minecraft.birch_stairs": "白桦木楼梯", + "block.minecraft.birch_trapdoor": "白桦木活板门", + "block.minecraft.birch_wall_hanging_sign": "墙上的悬挂式白桦木告示牌", + "block.minecraft.birch_wall_sign": "墙上的白桦木告示牌", + "block.minecraft.birch_wood": "白桦木", + "block.minecraft.black_banner": "黑色旗帜", + "block.minecraft.black_bed": "黑色床", + "block.minecraft.black_candle": "黑色蜡烛", + "block.minecraft.black_candle_cake": "插上黑色蜡烛的蛋糕", + "block.minecraft.black_carpet": "黑色地毯", + "block.minecraft.black_concrete": "黑色混凝土", + "block.minecraft.black_concrete_powder": "黑色混凝土粉末", + "block.minecraft.black_glazed_terracotta": "黑色带釉陶瓦", + "block.minecraft.black_shulker_box": "黑色潜影盒", + "block.minecraft.black_stained_glass": "黑色染色玻璃", + "block.minecraft.black_stained_glass_pane": "黑色染色玻璃板", + "block.minecraft.black_terracotta": "黑色陶瓦", + "block.minecraft.black_wool": "黑色羊毛", + "block.minecraft.blackstone": "黑石", + "block.minecraft.blackstone_slab": "黑石台阶", + "block.minecraft.blackstone_stairs": "黑石楼梯", + "block.minecraft.blackstone_wall": "黑石墙", + "block.minecraft.blast_furnace": "高炉", + "block.minecraft.blue_banner": "蓝色旗帜", + "block.minecraft.blue_bed": "蓝色床", + "block.minecraft.blue_candle": "蓝色蜡烛", + "block.minecraft.blue_candle_cake": "插上蓝色蜡烛的蛋糕", + "block.minecraft.blue_carpet": "蓝色地毯", + "block.minecraft.blue_concrete": "蓝色混凝土", + "block.minecraft.blue_concrete_powder": "蓝色混凝土粉末", + "block.minecraft.blue_glazed_terracotta": "蓝色带釉陶瓦", + "block.minecraft.blue_ice": "蓝冰", + "block.minecraft.blue_orchid": "兰花", + "block.minecraft.blue_shulker_box": "蓝色潜影盒", + "block.minecraft.blue_stained_glass": "蓝色染色玻璃", + "block.minecraft.blue_stained_glass_pane": "蓝色染色玻璃板", + "block.minecraft.blue_terracotta": "蓝色陶瓦", + "block.minecraft.blue_wool": "蓝色羊毛", + "block.minecraft.bone_block": "骨块", + "block.minecraft.bookshelf": "书架", + "block.minecraft.brain_coral": "脑纹珊瑚", + "block.minecraft.brain_coral_block": "脑纹珊瑚块", + "block.minecraft.brain_coral_fan": "脑纹珊瑚扇", + "block.minecraft.brain_coral_wall_fan": "墙上的脑纹珊瑚扇", + "block.minecraft.brewing_stand": "酿造台", + "block.minecraft.brick_slab": "红砖台阶", + "block.minecraft.brick_stairs": "红砖楼梯", + "block.minecraft.brick_wall": "红砖墙", + "block.minecraft.bricks": "红砖块", + "block.minecraft.brown_banner": "棕色旗帜", + "block.minecraft.brown_bed": "棕色床", + "block.minecraft.brown_candle": "棕色蜡烛", + "block.minecraft.brown_candle_cake": "插上棕色蜡烛的蛋糕", + "block.minecraft.brown_carpet": "棕色地毯", + "block.minecraft.brown_concrete": "棕色混凝土", + "block.minecraft.brown_concrete_powder": "棕色混凝土粉末", + "block.minecraft.brown_glazed_terracotta": "棕色带釉陶瓦", + "block.minecraft.brown_mushroom": "棕色蘑菇", + "block.minecraft.brown_mushroom_block": "棕色蘑菇方块", + "block.minecraft.brown_shulker_box": "棕色潜影盒", + "block.minecraft.brown_stained_glass": "棕色染色玻璃", + "block.minecraft.brown_stained_glass_pane": "棕色染色玻璃板", + "block.minecraft.brown_terracotta": "棕色陶瓦", + "block.minecraft.brown_wool": "棕色羊毛", + "block.minecraft.bubble_column": "气泡柱", + "block.minecraft.bubble_coral": "气泡珊瑚", + "block.minecraft.bubble_coral_block": "气泡珊瑚块", + "block.minecraft.bubble_coral_fan": "气泡珊瑚扇", + "block.minecraft.bubble_coral_wall_fan": "墙上的气泡珊瑚扇", + "block.minecraft.budding_amethyst": "紫水晶母岩", + "block.minecraft.cactus": "仙人掌", + "block.minecraft.cake": "蛋糕", + "block.minecraft.calcite": "方解石", + "block.minecraft.calibrated_sculk_sensor": "校频幽匿感测体", + "block.minecraft.campfire": "营火", + "block.minecraft.candle": "蜡烛", + "block.minecraft.candle_cake": "插上蜡烛的蛋糕", + "block.minecraft.carrots": "胡萝卜", + "block.minecraft.cartography_table": "制图台", + "block.minecraft.carved_pumpkin": "雕刻南瓜", + "block.minecraft.cauldron": "炼药锅", + "block.minecraft.cave_air": "洞穴空气", + "block.minecraft.cave_vines": "洞穴藤蔓", + "block.minecraft.cave_vines_plant": "洞穴藤蔓植株", + "block.minecraft.chain": "锁链", + "block.minecraft.chain_command_block": "连锁型命令方块", + "block.minecraft.cherry_button": "樱花木按钮", + "block.minecraft.cherry_door": "樱花木门", + "block.minecraft.cherry_fence": "樱花木栅栏", + "block.minecraft.cherry_fence_gate": "樱花木栅栏门", + "block.minecraft.cherry_hanging_sign": "悬挂式樱花木告示牌", + "block.minecraft.cherry_leaves": "樱花树叶", + "block.minecraft.cherry_log": "樱花原木", + "block.minecraft.cherry_planks": "樱花木板", + "block.minecraft.cherry_pressure_plate": "樱花木压力板", + "block.minecraft.cherry_sapling": "樱花树苗", + "block.minecraft.cherry_sign": "樱花木告示牌", + "block.minecraft.cherry_slab": "樱花木台阶", + "block.minecraft.cherry_stairs": "樱花木楼梯", + "block.minecraft.cherry_trapdoor": "樱花木活板门", + "block.minecraft.cherry_wall_hanging_sign": "墙上的悬挂式樱花木告示牌", + "block.minecraft.cherry_wall_sign": "墙上的樱花木告示牌", + "block.minecraft.cherry_wood": "樱花木", + "block.minecraft.chest": "箱子", + "block.minecraft.chipped_anvil": "开裂的铁砧", + "block.minecraft.chiseled_bookshelf": "雕纹书架", + "block.minecraft.chiseled_copper": "雕纹铜块", + "block.minecraft.chiseled_deepslate": "雕纹深板岩", + "block.minecraft.chiseled_nether_bricks": "雕纹下界砖块", + "block.minecraft.chiseled_polished_blackstone": "雕纹磨制黑石", + "block.minecraft.chiseled_quartz_block": "雕纹石英块", + "block.minecraft.chiseled_red_sandstone": "雕纹红砂岩", + "block.minecraft.chiseled_resin_bricks": "雕纹树脂砖块", + "block.minecraft.chiseled_sandstone": "雕纹砂岩", + "block.minecraft.chiseled_stone_bricks": "雕纹石砖", + "block.minecraft.chiseled_tuff": "雕纹凝灰岩", + "block.minecraft.chiseled_tuff_bricks": "雕纹凝灰岩砖", + "block.minecraft.chorus_flower": "紫颂花", + "block.minecraft.chorus_plant": "紫颂植株", + "block.minecraft.clay": "黏土", + "block.minecraft.closed_eyeblossom": "闭合的眼眸花", + "block.minecraft.coal_block": "煤炭块", + "block.minecraft.coal_ore": "煤矿石", + "block.minecraft.coarse_dirt": "砂土", + "block.minecraft.cobbled_deepslate": "深板岩圆石", + "block.minecraft.cobbled_deepslate_slab": "深板岩圆石台阶", + "block.minecraft.cobbled_deepslate_stairs": "深板岩圆石楼梯", + "block.minecraft.cobbled_deepslate_wall": "深板岩圆石墙", + "block.minecraft.cobblestone": "圆石", + "block.minecraft.cobblestone_slab": "圆石台阶", + "block.minecraft.cobblestone_stairs": "圆石楼梯", + "block.minecraft.cobblestone_wall": "圆石墙", + "block.minecraft.cobweb": "蜘蛛网", + "block.minecraft.cocoa": "可可果", + "block.minecraft.command_block": "命令方块", + "block.minecraft.comparator": "红石比较器", + "block.minecraft.composter": "堆肥桶", + "block.minecraft.conduit": "潮涌核心", + "block.minecraft.copper_block": "铜块", + "block.minecraft.copper_bulb": "铜灯", + "block.minecraft.copper_door": "铜门", + "block.minecraft.copper_grate": "铜格栅", + "block.minecraft.copper_ore": "铜矿石", + "block.minecraft.copper_trapdoor": "铜活板门", + "block.minecraft.cornflower": "矢车菊", + "block.minecraft.cracked_deepslate_bricks": "裂纹深板岩砖", + "block.minecraft.cracked_deepslate_tiles": "裂纹深板岩瓦", + "block.minecraft.cracked_nether_bricks": "裂纹下界砖块", + "block.minecraft.cracked_polished_blackstone_bricks": "裂纹磨制黑石砖", + "block.minecraft.cracked_stone_bricks": "裂纹石砖", + "block.minecraft.crafter": "合成器", + "block.minecraft.crafting_table": "工作台", + "block.minecraft.creaking_heart": "嘎枝之心", + "block.minecraft.creeper_head": "苦力怕的头", + "block.minecraft.creeper_wall_head": "墙上的苦力怕的头", + "block.minecraft.crimson_button": "绯红木按钮", + "block.minecraft.crimson_door": "绯红木门", + "block.minecraft.crimson_fence": "绯红木栅栏", + "block.minecraft.crimson_fence_gate": "绯红木栅栏门", + "block.minecraft.crimson_fungus": "绯红菌", + "block.minecraft.crimson_hanging_sign": "悬挂式绯红木告示牌", + "block.minecraft.crimson_hyphae": "绯红菌核", + "block.minecraft.crimson_nylium": "绯红菌岩", + "block.minecraft.crimson_planks": "绯红木板", + "block.minecraft.crimson_pressure_plate": "绯红木压力板", + "block.minecraft.crimson_roots": "绯红菌索", + "block.minecraft.crimson_sign": "绯红木告示牌", + "block.minecraft.crimson_slab": "绯红木台阶", + "block.minecraft.crimson_stairs": "绯红木楼梯", + "block.minecraft.crimson_stem": "绯红菌柄", + "block.minecraft.crimson_trapdoor": "绯红木活板门", + "block.minecraft.crimson_wall_hanging_sign": "墙上的悬挂式绯红木告示牌", + "block.minecraft.crimson_wall_sign": "墙上的绯红木告示牌", + "block.minecraft.crying_obsidian": "哭泣的黑曜石", + "block.minecraft.cut_copper": "切制铜块", + "block.minecraft.cut_copper_slab": "切制铜台阶", + "block.minecraft.cut_copper_stairs": "切制铜楼梯", + "block.minecraft.cut_red_sandstone": "切制红砂岩", + "block.minecraft.cut_red_sandstone_slab": "切制红砂岩台阶", + "block.minecraft.cut_sandstone": "切制砂岩", + "block.minecraft.cut_sandstone_slab": "切制砂岩台阶", + "block.minecraft.cyan_banner": "青色旗帜", + "block.minecraft.cyan_bed": "青色床", + "block.minecraft.cyan_candle": "青色蜡烛", + "block.minecraft.cyan_candle_cake": "插上青色蜡烛的蛋糕", + "block.minecraft.cyan_carpet": "青色地毯", + "block.minecraft.cyan_concrete": "青色混凝土", + "block.minecraft.cyan_concrete_powder": "青色混凝土粉末", + "block.minecraft.cyan_glazed_terracotta": "青色带釉陶瓦", + "block.minecraft.cyan_shulker_box": "青色潜影盒", + "block.minecraft.cyan_stained_glass": "青色染色玻璃", + "block.minecraft.cyan_stained_glass_pane": "青色染色玻璃板", + "block.minecraft.cyan_terracotta": "青色陶瓦", + "block.minecraft.cyan_wool": "青色羊毛", + "block.minecraft.damaged_anvil": "损坏的铁砧", + "block.minecraft.dandelion": "蒲公英", + "block.minecraft.dark_oak_button": "深色橡木按钮", + "block.minecraft.dark_oak_door": "深色橡木门", + "block.minecraft.dark_oak_fence": "深色橡木栅栏", + "block.minecraft.dark_oak_fence_gate": "深色橡木栅栏门", + "block.minecraft.dark_oak_hanging_sign": "悬挂式深色橡木告示牌", + "block.minecraft.dark_oak_leaves": "深色橡树树叶", + "block.minecraft.dark_oak_log": "深色橡木原木", + "block.minecraft.dark_oak_planks": "深色橡木木板", + "block.minecraft.dark_oak_pressure_plate": "深色橡木压力板", + "block.minecraft.dark_oak_sapling": "深色橡树树苗", + "block.minecraft.dark_oak_sign": "深色橡木告示牌", + "block.minecraft.dark_oak_slab": "深色橡木台阶", + "block.minecraft.dark_oak_stairs": "深色橡木楼梯", + "block.minecraft.dark_oak_trapdoor": "深色橡木活板门", + "block.minecraft.dark_oak_wall_hanging_sign": "墙上的悬挂式深色橡木告示牌", + "block.minecraft.dark_oak_wall_sign": "墙上的深色橡木告示牌", + "block.minecraft.dark_oak_wood": "深色橡木", + "block.minecraft.dark_prismarine": "暗海晶石", + "block.minecraft.dark_prismarine_slab": "暗海晶石台阶", + "block.minecraft.dark_prismarine_stairs": "暗海晶石楼梯", + "block.minecraft.daylight_detector": "阳光探测器", + "block.minecraft.dead_brain_coral": "失活的脑纹珊瑚", + "block.minecraft.dead_brain_coral_block": "失活的脑纹珊瑚块", + "block.minecraft.dead_brain_coral_fan": "失活的脑纹珊瑚扇", + "block.minecraft.dead_brain_coral_wall_fan": "墙上的失活脑纹珊瑚扇", + "block.minecraft.dead_bubble_coral": "失活的气泡珊瑚", + "block.minecraft.dead_bubble_coral_block": "失活的气泡珊瑚块", + "block.minecraft.dead_bubble_coral_fan": "失活的气泡珊瑚扇", + "block.minecraft.dead_bubble_coral_wall_fan": "墙上的失活气泡珊瑚扇", + "block.minecraft.dead_bush": "枯萎的灌木", + "block.minecraft.dead_fire_coral": "失活的火珊瑚", + "block.minecraft.dead_fire_coral_block": "失活的火珊瑚块", + "block.minecraft.dead_fire_coral_fan": "失活的火珊瑚扇", + "block.minecraft.dead_fire_coral_wall_fan": "墙上的失活火珊瑚扇", + "block.minecraft.dead_horn_coral": "失活的鹿角珊瑚", + "block.minecraft.dead_horn_coral_block": "失活的鹿角珊瑚块", + "block.minecraft.dead_horn_coral_fan": "失活的鹿角珊瑚扇", + "block.minecraft.dead_horn_coral_wall_fan": "墙上的失活鹿角珊瑚扇", + "block.minecraft.dead_tube_coral": "失活的管珊瑚", + "block.minecraft.dead_tube_coral_block": "失活的管珊瑚块", + "block.minecraft.dead_tube_coral_fan": "失活的管珊瑚扇", + "block.minecraft.dead_tube_coral_wall_fan": "墙上的失活管珊瑚扇", + "block.minecraft.decorated_pot": "饰纹陶罐", + "block.minecraft.deepslate": "深板岩", + "block.minecraft.deepslate_brick_slab": "深板岩砖台阶", + "block.minecraft.deepslate_brick_stairs": "深板岩砖楼梯", + "block.minecraft.deepslate_brick_wall": "深板岩砖墙", + "block.minecraft.deepslate_bricks": "深板岩砖", + "block.minecraft.deepslate_coal_ore": "深层煤矿石", + "block.minecraft.deepslate_copper_ore": "深层铜矿石", + "block.minecraft.deepslate_diamond_ore": "深层钻石矿石", + "block.minecraft.deepslate_emerald_ore": "深层绿宝石矿石", + "block.minecraft.deepslate_gold_ore": "深层金矿石", + "block.minecraft.deepslate_iron_ore": "深层铁矿石", + "block.minecraft.deepslate_lapis_ore": "深层青金石矿石", + "block.minecraft.deepslate_redstone_ore": "深层红石矿石", + "block.minecraft.deepslate_tile_slab": "深板岩瓦台阶", + "block.minecraft.deepslate_tile_stairs": "深板岩瓦楼梯", + "block.minecraft.deepslate_tile_wall": "深板岩瓦墙", + "block.minecraft.deepslate_tiles": "深板岩瓦", + "block.minecraft.detector_rail": "探测铁轨", + "block.minecraft.diamond_block": "钻石块", + "block.minecraft.diamond_ore": "钻石矿石", + "block.minecraft.diorite": "闪长岩", + "block.minecraft.diorite_slab": "闪长岩台阶", + "block.minecraft.diorite_stairs": "闪长岩楼梯", + "block.minecraft.diorite_wall": "闪长岩墙", + "block.minecraft.dirt": "泥土", + "block.minecraft.dirt_path": "土径", + "block.minecraft.dispenser": "发射器", + "block.minecraft.dragon_egg": "龙蛋", + "block.minecraft.dragon_head": "龙首", + "block.minecraft.dragon_wall_head": "墙上的龙首", + "block.minecraft.dried_kelp_block": "干海带块", + "block.minecraft.dripstone_block": "滴水石块", + "block.minecraft.dropper": "投掷器", + "block.minecraft.emerald_block": "绿宝石块", + "block.minecraft.emerald_ore": "绿宝石矿石", + "block.minecraft.enchanting_table": "附魔台", + "block.minecraft.end_gateway": "末地折跃门", + "block.minecraft.end_portal": "末地传送门", + "block.minecraft.end_portal_frame": "末地传送门框架", + "block.minecraft.end_rod": "末地烛", + "block.minecraft.end_stone": "末地石", + "block.minecraft.end_stone_brick_slab": "末地石砖台阶", + "block.minecraft.end_stone_brick_stairs": "末地石砖楼梯", + "block.minecraft.end_stone_brick_wall": "末地石砖墙", + "block.minecraft.end_stone_bricks": "末地石砖", + "block.minecraft.ender_chest": "末影箱", + "block.minecraft.exposed_chiseled_copper": "斑驳的雕纹铜块", + "block.minecraft.exposed_copper": "斑驳的铜块", + "block.minecraft.exposed_copper_bulb": "斑驳的铜灯", + "block.minecraft.exposed_copper_door": "斑驳的铜门", + "block.minecraft.exposed_copper_grate": "斑驳的铜格栅", + "block.minecraft.exposed_copper_trapdoor": "斑驳的铜活板门", + "block.minecraft.exposed_cut_copper": "斑驳的切制铜块", + "block.minecraft.exposed_cut_copper_slab": "斑驳的切制铜台阶", + "block.minecraft.exposed_cut_copper_stairs": "斑驳的切制铜楼梯", + "block.minecraft.farmland": "耕地", + "block.minecraft.fern": "蕨", + "block.minecraft.fire": "火", + "block.minecraft.fire_coral": "火珊瑚", + "block.minecraft.fire_coral_block": "火珊瑚块", + "block.minecraft.fire_coral_fan": "火珊瑚扇", + "block.minecraft.fire_coral_wall_fan": "墙上的火珊瑚扇", + "block.minecraft.fletching_table": "制箭台", + "block.minecraft.flower_pot": "花盆", + "block.minecraft.flowering_azalea": "盛开的杜鹃花丛", + "block.minecraft.flowering_azalea_leaves": "盛开的杜鹃树叶", + "block.minecraft.frogspawn": "青蛙卵", + "block.minecraft.frosted_ice": "霜冰", + "block.minecraft.furnace": "熔炉", + "block.minecraft.gilded_blackstone": "镶金黑石", + "block.minecraft.glass": "玻璃", + "block.minecraft.glass_pane": "玻璃板", + "block.minecraft.glow_lichen": "发光地衣", + "block.minecraft.glowstone": "荧石", + "block.minecraft.gold_block": "金块", + "block.minecraft.gold_ore": "金矿石", + "block.minecraft.granite": "花岗岩", + "block.minecraft.granite_slab": "花岗岩台阶", + "block.minecraft.granite_stairs": "花岗岩楼梯", + "block.minecraft.granite_wall": "花岗岩墙", + "block.minecraft.grass": "草", + "block.minecraft.grass_block": "草方块", + "block.minecraft.gravel": "沙砾", + "block.minecraft.gray_banner": "灰色旗帜", + "block.minecraft.gray_bed": "灰色床", + "block.minecraft.gray_candle": "灰色蜡烛", + "block.minecraft.gray_candle_cake": "插上灰色蜡烛的蛋糕", + "block.minecraft.gray_carpet": "灰色地毯", + "block.minecraft.gray_concrete": "灰色混凝土", + "block.minecraft.gray_concrete_powder": "灰色混凝土粉末", + "block.minecraft.gray_glazed_terracotta": "灰色带釉陶瓦", + "block.minecraft.gray_shulker_box": "灰色潜影盒", + "block.minecraft.gray_stained_glass": "灰色染色玻璃", + "block.minecraft.gray_stained_glass_pane": "灰色染色玻璃板", + "block.minecraft.gray_terracotta": "灰色陶瓦", + "block.minecraft.gray_wool": "灰色羊毛", + "block.minecraft.green_banner": "绿色旗帜", + "block.minecraft.green_bed": "绿色床", + "block.minecraft.green_candle": "绿色蜡烛", + "block.minecraft.green_candle_cake": "插上绿色蜡烛的蛋糕", + "block.minecraft.green_carpet": "绿色地毯", + "block.minecraft.green_concrete": "绿色混凝土", + "block.minecraft.green_concrete_powder": "绿色混凝土粉末", + "block.minecraft.green_glazed_terracotta": "绿色带釉陶瓦", + "block.minecraft.green_shulker_box": "绿色潜影盒", + "block.minecraft.green_stained_glass": "绿色染色玻璃", + "block.minecraft.green_stained_glass_pane": "绿色染色玻璃板", + "block.minecraft.green_terracotta": "绿色陶瓦", + "block.minecraft.green_wool": "绿色羊毛", + "block.minecraft.grindstone": "砂轮", + "block.minecraft.hanging_roots": "垂根", + "block.minecraft.hay_block": "干草捆", + "block.minecraft.heavy_core": "沉重核心", + "block.minecraft.heavy_weighted_pressure_plate": "重质测重压力板", + "block.minecraft.honey_block": "蜂蜜块", + "block.minecraft.honeycomb_block": "蜜脾块", + "block.minecraft.hopper": "漏斗", + "block.minecraft.horn_coral": "鹿角珊瑚", + "block.minecraft.horn_coral_block": "鹿角珊瑚块", + "block.minecraft.horn_coral_fan": "鹿角珊瑚扇", + "block.minecraft.horn_coral_wall_fan": "墙上的鹿角珊瑚扇", + "block.minecraft.ice": "冰", + "block.minecraft.infested_chiseled_stone_bricks": "虫蚀雕纹石砖", + "block.minecraft.infested_cobblestone": "虫蚀圆石", + "block.minecraft.infested_cracked_stone_bricks": "虫蚀裂纹石砖", + "block.minecraft.infested_deepslate": "虫蚀深板岩", + "block.minecraft.infested_mossy_stone_bricks": "虫蚀苔石砖", + "block.minecraft.infested_stone": "虫蚀石头", + "block.minecraft.infested_stone_bricks": "虫蚀石砖", + "block.minecraft.iron_bars": "铁栏杆", + "block.minecraft.iron_block": "铁块", + "block.minecraft.iron_door": "铁门", + "block.minecraft.iron_ore": "铁矿石", + "block.minecraft.iron_trapdoor": "铁活板门", + "block.minecraft.jack_o_lantern": "南瓜灯", + "block.minecraft.jigsaw": "拼图方块", + "block.minecraft.jukebox": "唱片机", + "block.minecraft.jungle_button": "丛林木按钮", + "block.minecraft.jungle_door": "丛林木门", + "block.minecraft.jungle_fence": "丛林木栅栏", + "block.minecraft.jungle_fence_gate": "丛林木栅栏门", + "block.minecraft.jungle_hanging_sign": "悬挂式丛林木告示牌", + "block.minecraft.jungle_leaves": "丛林树叶", + "block.minecraft.jungle_log": "丛林原木", + "block.minecraft.jungle_planks": "丛林木板", + "block.minecraft.jungle_pressure_plate": "丛林木压力板", + "block.minecraft.jungle_sapling": "丛林树苗", + "block.minecraft.jungle_sign": "丛林木告示牌", + "block.minecraft.jungle_slab": "丛林木台阶", + "block.minecraft.jungle_stairs": "丛林木楼梯", + "block.minecraft.jungle_trapdoor": "丛林木活板门", + "block.minecraft.jungle_wall_hanging_sign": "墙上的悬挂式丛林木告示牌", + "block.minecraft.jungle_wall_sign": "墙上的丛林木告示牌", + "block.minecraft.jungle_wood": "丛林木", + "block.minecraft.kelp": "海带", + "block.minecraft.kelp_plant": "海带植株", + "block.minecraft.ladder": "梯子", + "block.minecraft.lantern": "灯笼", + "block.minecraft.lapis_block": "青金石块", + "block.minecraft.lapis_ore": "青金石矿石", + "block.minecraft.large_amethyst_bud": "大型紫晶芽", + "block.minecraft.large_fern": "大型蕨", + "block.minecraft.lava": "熔岩", + "block.minecraft.lava_cauldron": "装有熔岩的炼药锅", + "block.minecraft.leaf_litter": "枯叶", + "block.minecraft.lectern": "讲台", + "block.minecraft.lever": "拉杆", + "block.minecraft.light": "光源方块", + "block.minecraft.light_blue_banner": "淡蓝色旗帜", + "block.minecraft.light_blue_bed": "淡蓝色床", + "block.minecraft.light_blue_candle": "淡蓝色蜡烛", + "block.minecraft.light_blue_candle_cake": "插上淡蓝色蜡烛的蛋糕", + "block.minecraft.light_blue_carpet": "淡蓝色地毯", + "block.minecraft.light_blue_concrete": "淡蓝色混凝土", + "block.minecraft.light_blue_concrete_powder": "淡蓝色混凝土粉末", + "block.minecraft.light_blue_glazed_terracotta": "淡蓝色带釉陶瓦", + "block.minecraft.light_blue_shulker_box": "淡蓝色潜影盒", + "block.minecraft.light_blue_stained_glass": "淡蓝色染色玻璃", + "block.minecraft.light_blue_stained_glass_pane": "淡蓝色染色玻璃板", + "block.minecraft.light_blue_terracotta": "淡蓝色陶瓦", + "block.minecraft.light_blue_wool": "淡蓝色羊毛", + "block.minecraft.light_gray_banner": "淡灰色旗帜", + "block.minecraft.light_gray_bed": "淡灰色床", + "block.minecraft.light_gray_candle": "淡灰色蜡烛", + "block.minecraft.light_gray_candle_cake": "插上淡灰色蜡烛的蛋糕", + "block.minecraft.light_gray_carpet": "淡灰色地毯", + "block.minecraft.light_gray_concrete": "淡灰色混凝土", + "block.minecraft.light_gray_concrete_powder": "淡灰色混凝土粉末", + "block.minecraft.light_gray_glazed_terracotta": "淡灰色带釉陶瓦", + "block.minecraft.light_gray_shulker_box": "淡灰色潜影盒", + "block.minecraft.light_gray_stained_glass": "淡灰色染色玻璃", + "block.minecraft.light_gray_stained_glass_pane": "淡灰色染色玻璃板", + "block.minecraft.light_gray_terracotta": "淡灰色陶瓦", + "block.minecraft.light_gray_wool": "淡灰色羊毛", + "block.minecraft.light_weighted_pressure_plate": "轻质测重压力板", + "block.minecraft.lightning_rod": "避雷针", + "block.minecraft.lilac": "丁香", + "block.minecraft.lily_of_the_valley": "铃兰", + "block.minecraft.lily_pad": "睡莲", + "block.minecraft.lime_banner": "黄绿色旗帜", + "block.minecraft.lime_bed": "黄绿色床", + "block.minecraft.lime_candle": "黄绿色蜡烛", + "block.minecraft.lime_candle_cake": "插上黄绿色蜡烛的蛋糕", + "block.minecraft.lime_carpet": "黄绿色地毯", + "block.minecraft.lime_concrete": "黄绿色混凝土", + "block.minecraft.lime_concrete_powder": "黄绿色混凝土粉末", + "block.minecraft.lime_glazed_terracotta": "黄绿色带釉陶瓦", + "block.minecraft.lime_shulker_box": "黄绿色潜影盒", + "block.minecraft.lime_stained_glass": "黄绿色染色玻璃", + "block.minecraft.lime_stained_glass_pane": "黄绿色染色玻璃板", + "block.minecraft.lime_terracotta": "黄绿色陶瓦", + "block.minecraft.lime_wool": "黄绿色羊毛", + "block.minecraft.lodestone": "磁石", + "block.minecraft.loom": "织布机", + "block.minecraft.magenta_banner": "品红色旗帜", + "block.minecraft.magenta_bed": "品红色床", + "block.minecraft.magenta_candle": "品红色蜡烛", + "block.minecraft.magenta_candle_cake": "插上品红色蜡烛的蛋糕", + "block.minecraft.magenta_carpet": "品红色地毯", + "block.minecraft.magenta_concrete": "品红色混凝土", + "block.minecraft.magenta_concrete_powder": "品红色混凝土粉末", + "block.minecraft.magenta_glazed_terracotta": "品红色带釉陶瓦", + "block.minecraft.magenta_shulker_box": "品红色潜影盒", + "block.minecraft.magenta_stained_glass": "品红色染色玻璃", + "block.minecraft.magenta_stained_glass_pane": "品红色染色玻璃板", + "block.minecraft.magenta_terracotta": "品红色陶瓦", + "block.minecraft.magenta_wool": "品红色羊毛", + "block.minecraft.magma_block": "岩浆块", + "block.minecraft.mangrove_button": "红树木按钮", + "block.minecraft.mangrove_door": "红树木门", + "block.minecraft.mangrove_fence": "红树木栅栏", + "block.minecraft.mangrove_fence_gate": "红树木栅栏门", + "block.minecraft.mangrove_hanging_sign": "悬挂式红树木告示牌", + "block.minecraft.mangrove_leaves": "红树树叶", + "block.minecraft.mangrove_log": "红树原木", + "block.minecraft.mangrove_planks": "红树木板", + "block.minecraft.mangrove_pressure_plate": "红树木压力板", + "block.minecraft.mangrove_propagule": "红树胎生苗", + "block.minecraft.mangrove_roots": "红树根", + "block.minecraft.mangrove_sign": "红树木告示牌", + "block.minecraft.mangrove_slab": "红树木台阶", + "block.minecraft.mangrove_stairs": "红树木楼梯", + "block.minecraft.mangrove_trapdoor": "红树木活板门", + "block.minecraft.mangrove_wall_hanging_sign": "墙上的悬挂式红树木告示牌", + "block.minecraft.mangrove_wall_sign": "墙上的红树木告示牌", + "block.minecraft.mangrove_wood": "红树木", + "block.minecraft.medium_amethyst_bud": "中型紫晶芽", + "block.minecraft.melon": "西瓜", + "block.minecraft.melon_stem": "西瓜茎", + "block.minecraft.moss_block": "苔藓块", + "block.minecraft.moss_carpet": "覆地苔藓", + "block.minecraft.mossy_cobblestone": "苔石", + "block.minecraft.mossy_cobblestone_slab": "苔石台阶", + "block.minecraft.mossy_cobblestone_stairs": "苔石楼梯", + "block.minecraft.mossy_cobblestone_wall": "苔石墙", + "block.minecraft.mossy_stone_brick_slab": "苔石砖台阶", + "block.minecraft.mossy_stone_brick_stairs": "苔石砖楼梯", + "block.minecraft.mossy_stone_brick_wall": "苔石砖墙", + "block.minecraft.mossy_stone_bricks": "苔石砖", + "block.minecraft.moving_piston": "移动的活塞", + "block.minecraft.mud": "泥巴", + "block.minecraft.mud_brick_slab": "泥砖台阶", + "block.minecraft.mud_brick_stairs": "泥砖楼梯", + "block.minecraft.mud_brick_wall": "泥砖墙", + "block.minecraft.mud_bricks": "泥砖", + "block.minecraft.muddy_mangrove_roots": "沾泥的红树根", + "block.minecraft.mushroom_stem": "蘑菇柄", + "block.minecraft.mycelium": "菌丝体", + "block.minecraft.nether_brick_fence": "下界砖栅栏", + "block.minecraft.nether_brick_slab": "下界砖台阶", + "block.minecraft.nether_brick_stairs": "下界砖楼梯", + "block.minecraft.nether_brick_wall": "下界砖墙", + "block.minecraft.nether_bricks": "下界砖块", + "block.minecraft.nether_gold_ore": "下界金矿石", + "block.minecraft.nether_portal": "下界传送门", + "block.minecraft.nether_quartz_ore": "下界石英矿石", + "block.minecraft.nether_sprouts": "下界苗", + "block.minecraft.nether_wart": "下界疣", + "block.minecraft.nether_wart_block": "下界疣块", + "block.minecraft.netherite_block": "下界合金块", + "block.minecraft.netherrack": "下界岩", + "block.minecraft.note_block": "音符盒", + "block.minecraft.oak_button": "橡木按钮", + "block.minecraft.oak_door": "橡木门", + "block.minecraft.oak_fence": "橡木栅栏", + "block.minecraft.oak_fence_gate": "橡木栅栏门", + "block.minecraft.oak_hanging_sign": "悬挂式橡木告示牌", + "block.minecraft.oak_leaves": "橡树树叶", + "block.minecraft.oak_log": "橡木原木", + "block.minecraft.oak_planks": "橡木木板", + "block.minecraft.oak_pressure_plate": "橡木压力板", + "block.minecraft.oak_sapling": "橡树树苗", + "block.minecraft.oak_sign": "橡木告示牌", + "block.minecraft.oak_slab": "橡木台阶", + "block.minecraft.oak_stairs": "橡木楼梯", + "block.minecraft.oak_trapdoor": "橡木活板门", + "block.minecraft.oak_wall_hanging_sign": "墙上的悬挂式橡木告示牌", + "block.minecraft.oak_wall_sign": "墙上的橡木告示牌", + "block.minecraft.oak_wood": "橡木", + "block.minecraft.observer": "侦测器", + "block.minecraft.obsidian": "黑曜石", + "block.minecraft.ochre_froglight": "赭黄蛙明灯", + "block.minecraft.ominous_banner": "不祥旗帜", + "block.minecraft.open_eyeblossom": "张开的眼眸花", + "block.minecraft.orange_banner": "橙色旗帜", + "block.minecraft.orange_bed": "橙色床", + "block.minecraft.orange_candle": "橙色蜡烛", + "block.minecraft.orange_candle_cake": "插上橙色蜡烛的蛋糕", + "block.minecraft.orange_carpet": "橙色地毯", + "block.minecraft.orange_concrete": "橙色混凝土", + "block.minecraft.orange_concrete_powder": "橙色混凝土粉末", + "block.minecraft.orange_glazed_terracotta": "橙色带釉陶瓦", + "block.minecraft.orange_shulker_box": "橙色潜影盒", + "block.minecraft.orange_stained_glass": "橙色染色玻璃", + "block.minecraft.orange_stained_glass_pane": "橙色染色玻璃板", + "block.minecraft.orange_terracotta": "橙色陶瓦", + "block.minecraft.orange_tulip": "橙色郁金香", + "block.minecraft.orange_wool": "橙色羊毛", + "block.minecraft.oxeye_daisy": "滨菊", + "block.minecraft.oxidized_chiseled_copper": "氧化的雕纹铜块", + "block.minecraft.oxidized_copper": "氧化的铜块", + "block.minecraft.oxidized_copper_bulb": "氧化的铜灯", + "block.minecraft.oxidized_copper_door": "氧化的铜门", + "block.minecraft.oxidized_copper_grate": "氧化的铜格栅", + "block.minecraft.oxidized_copper_trapdoor": "氧化的铜活板门", + "block.minecraft.oxidized_cut_copper": "氧化的切制铜块", + "block.minecraft.oxidized_cut_copper_slab": "氧化的切制铜台阶", + "block.minecraft.oxidized_cut_copper_stairs": "氧化的切制铜楼梯", + "block.minecraft.packed_ice": "浮冰", + "block.minecraft.packed_mud": "泥坯", + "block.minecraft.pale_hanging_moss": "苍白垂须", + "block.minecraft.pale_moss_block": "苍白苔藓块", + "block.minecraft.pale_moss_carpet": "苍白覆地苔藓", + "block.minecraft.pale_oak_button": "苍白橡木按钮", + "block.minecraft.pale_oak_door": "苍白橡木门", + "block.minecraft.pale_oak_fence": "苍白橡木栅栏", + "block.minecraft.pale_oak_fence_gate": "苍白橡木栅栏门", + "block.minecraft.pale_oak_hanging_sign": "悬挂式苍白橡木告示牌", + "block.minecraft.pale_oak_leaves": "苍白橡树树叶", + "block.minecraft.pale_oak_log": "苍白橡木原木", + "block.minecraft.pale_oak_planks": "苍白橡木木板", + "block.minecraft.pale_oak_pressure_plate": "苍白橡木压力板", + "block.minecraft.pale_oak_sapling": "苍白橡树树苗", + "block.minecraft.pale_oak_sign": "苍白橡木告示牌", + "block.minecraft.pale_oak_slab": "苍白橡木台阶", + "block.minecraft.pale_oak_stairs": "苍白橡木楼梯", + "block.minecraft.pale_oak_trapdoor": "苍白橡木活板门", + "block.minecraft.pale_oak_wall_hanging_sign": "墙上的悬挂式苍白橡木告示牌", + "block.minecraft.pale_oak_wall_sign": "墙上的苍白橡木告示牌", + "block.minecraft.pale_oak_wood": "苍白橡木", + "block.minecraft.pearlescent_froglight": "珠光蛙明灯", + "block.minecraft.peony": "牡丹", + "block.minecraft.petrified_oak_slab": "石化橡木台阶", + "block.minecraft.piglin_head": "猪灵的头", + "block.minecraft.piglin_wall_head": "墙上的猪灵的头", + "block.minecraft.pink_banner": "粉红色旗帜", + "block.minecraft.pink_bed": "粉红色床", + "block.minecraft.pink_candle": "粉红色蜡烛", + "block.minecraft.pink_candle_cake": "插上粉红色蜡烛的蛋糕", + "block.minecraft.pink_carpet": "粉红色地毯", + "block.minecraft.pink_concrete": "粉红色混凝土", + "block.minecraft.pink_concrete_powder": "粉红色混凝土粉末", + "block.minecraft.pink_glazed_terracotta": "粉红色带釉陶瓦", + "block.minecraft.pink_petals": "粉红色花簇", + "block.minecraft.pink_shulker_box": "粉红色潜影盒", + "block.minecraft.pink_stained_glass": "粉红色染色玻璃", + "block.minecraft.pink_stained_glass_pane": "粉红色染色玻璃板", + "block.minecraft.pink_terracotta": "粉红色陶瓦", + "block.minecraft.pink_tulip": "粉红色郁金香", + "block.minecraft.pink_wool": "粉红色羊毛", + "block.minecraft.piston": "活塞", + "block.minecraft.piston_head": "活塞头", + "block.minecraft.pitcher_crop": "瓶子草植株", + "block.minecraft.pitcher_plant": "瓶子草", + "block.minecraft.player_head": "玩家的头", + "block.minecraft.player_head.named": "%s的头", + "block.minecraft.player_wall_head": "墙上的玩家的头", + "block.minecraft.podzol": "灰化土", + "block.minecraft.pointed_dripstone": "滴水石锥", + "block.minecraft.polished_andesite": "磨制安山岩", + "block.minecraft.polished_andesite_slab": "磨制安山岩台阶", + "block.minecraft.polished_andesite_stairs": "磨制安山岩楼梯", + "block.minecraft.polished_basalt": "磨制玄武岩", + "block.minecraft.polished_blackstone": "磨制黑石", + "block.minecraft.polished_blackstone_brick_slab": "磨制黑石砖台阶", + "block.minecraft.polished_blackstone_brick_stairs": "磨制黑石砖楼梯", + "block.minecraft.polished_blackstone_brick_wall": "磨制黑石砖墙", + "block.minecraft.polished_blackstone_bricks": "磨制黑石砖", + "block.minecraft.polished_blackstone_button": "磨制黑石按钮", + "block.minecraft.polished_blackstone_pressure_plate": "磨制黑石压力板", + "block.minecraft.polished_blackstone_slab": "磨制黑石台阶", + "block.minecraft.polished_blackstone_stairs": "磨制黑石楼梯", + "block.minecraft.polished_blackstone_wall": "磨制黑石墙", + "block.minecraft.polished_deepslate": "磨制深板岩", + "block.minecraft.polished_deepslate_slab": "磨制深板岩台阶", + "block.minecraft.polished_deepslate_stairs": "磨制深板岩楼梯", + "block.minecraft.polished_deepslate_wall": "磨制深板岩墙", + "block.minecraft.polished_diorite": "磨制闪长岩", + "block.minecraft.polished_diorite_slab": "磨制闪长岩台阶", + "block.minecraft.polished_diorite_stairs": "磨制闪长岩楼梯", + "block.minecraft.polished_granite": "磨制花岗岩", + "block.minecraft.polished_granite_slab": "磨制花岗岩台阶", + "block.minecraft.polished_granite_stairs": "磨制花岗岩楼梯", + "block.minecraft.polished_tuff": "磨制凝灰岩", + "block.minecraft.polished_tuff_slab": "磨制凝灰岩台阶", + "block.minecraft.polished_tuff_stairs": "磨制凝灰岩楼梯", + "block.minecraft.polished_tuff_wall": "磨制凝灰岩墙", + "block.minecraft.poppy": "虞美人", + "block.minecraft.potatoes": "马铃薯", + "block.minecraft.potted_acacia_sapling": "金合欢树苗盆栽", + "block.minecraft.potted_allium": "绒球葱盆栽", + "block.minecraft.potted_azalea_bush": "杜鹃花丛盆栽", + "block.minecraft.potted_azure_bluet": "蓝花美耳草盆栽", + "block.minecraft.potted_bamboo": "竹子盆栽", + "block.minecraft.potted_birch_sapling": "白桦树苗盆栽", + "block.minecraft.potted_blue_orchid": "兰花盆栽", + "block.minecraft.potted_brown_mushroom": "棕色蘑菇盆栽", + "block.minecraft.potted_cactus": "仙人掌盆栽", + "block.minecraft.potted_cherry_sapling": "樱花树苗盆栽", + "block.minecraft.potted_closed_eyeblossom": "闭合的眼眸花盆栽", + "block.minecraft.potted_cornflower": "矢车菊盆栽", + "block.minecraft.potted_crimson_fungus": "绯红菌盆栽", + "block.minecraft.potted_crimson_roots": "绯红菌索盆栽", + "block.minecraft.potted_dandelion": "蒲公英盆栽", + "block.minecraft.potted_dark_oak_sapling": "深色橡树树苗盆栽", + "block.minecraft.potted_dead_bush": "枯萎的灌木盆栽", + "block.minecraft.potted_fern": "蕨盆栽", + "block.minecraft.potted_flowering_azalea_bush": "盛开的杜鹃花丛盆栽", + "block.minecraft.potted_jungle_sapling": "丛林树苗盆栽", + "block.minecraft.potted_lily_of_the_valley": "铃兰盆栽", + "block.minecraft.potted_mangrove_propagule": "红树胎生苗盆栽", + "block.minecraft.potted_oak_sapling": "橡树树苗盆栽", + "block.minecraft.potted_open_eyeblossom": "张开的眼眸花盆栽", + "block.minecraft.potted_orange_tulip": "橙色郁金香盆栽", + "block.minecraft.potted_oxeye_daisy": "滨菊盆栽", + "block.minecraft.potted_pale_oak_sapling": "苍白橡树树苗盆栽", + "block.minecraft.potted_pink_tulip": "粉红色郁金香盆栽", + "block.minecraft.potted_poppy": "虞美人盆栽", + "block.minecraft.potted_red_mushroom": "红色蘑菇盆栽", + "block.minecraft.potted_red_tulip": "红色郁金香盆栽", + "block.minecraft.potted_spruce_sapling": "云杉树苗盆栽", + "block.minecraft.potted_torchflower": "火把花盆栽", + "block.minecraft.potted_warped_fungus": "诡异菌盆栽", + "block.minecraft.potted_warped_roots": "诡异菌索盆栽", + "block.minecraft.potted_white_tulip": "白色郁金香盆栽", + "block.minecraft.potted_wither_rose": "凋灵玫瑰盆栽", + "block.minecraft.powder_snow": "细雪", + "block.minecraft.powder_snow_cauldron": "装有细雪的炼药锅", + "block.minecraft.powered_rail": "动力铁轨", + "block.minecraft.prismarine": "海晶石", + "block.minecraft.prismarine_brick_slab": "海晶石砖台阶", + "block.minecraft.prismarine_brick_stairs": "海晶石砖楼梯", + "block.minecraft.prismarine_bricks": "海晶石砖", + "block.minecraft.prismarine_slab": "海晶石台阶", + "block.minecraft.prismarine_stairs": "海晶石楼梯", + "block.minecraft.prismarine_wall": "海晶石墙", + "block.minecraft.pumpkin": "南瓜", + "block.minecraft.pumpkin_stem": "南瓜茎", + "block.minecraft.purple_banner": "紫色旗帜", + "block.minecraft.purple_bed": "紫色床", + "block.minecraft.purple_candle": "紫色蜡烛", + "block.minecraft.purple_candle_cake": "插上紫色蜡烛的蛋糕", + "block.minecraft.purple_carpet": "紫色地毯", + "block.minecraft.purple_concrete": "紫色混凝土", + "block.minecraft.purple_concrete_powder": "紫色混凝土粉末", + "block.minecraft.purple_glazed_terracotta": "紫色带釉陶瓦", + "block.minecraft.purple_shulker_box": "紫色潜影盒", + "block.minecraft.purple_stained_glass": "紫色染色玻璃", + "block.minecraft.purple_stained_glass_pane": "紫色染色玻璃板", + "block.minecraft.purple_terracotta": "紫色陶瓦", + "block.minecraft.purple_wool": "紫色羊毛", + "block.minecraft.purpur_block": "紫珀块", + "block.minecraft.purpur_pillar": "紫珀柱", + "block.minecraft.purpur_slab": "紫珀台阶", + "block.minecraft.purpur_stairs": "紫珀楼梯", + "block.minecraft.quartz_block": "石英块", + "block.minecraft.quartz_bricks": "石英砖", + "block.minecraft.quartz_pillar": "石英柱", + "block.minecraft.quartz_slab": "石英台阶", + "block.minecraft.quartz_stairs": "石英楼梯", + "block.minecraft.rail": "铁轨", + "block.minecraft.raw_copper_block": "粗铜块", + "block.minecraft.raw_gold_block": "粗金块", + "block.minecraft.raw_iron_block": "粗铁块", + "block.minecraft.red_banner": "红色旗帜", + "block.minecraft.red_bed": "红色床", + "block.minecraft.red_candle": "红色蜡烛", + "block.minecraft.red_candle_cake": "插上红色蜡烛的蛋糕", + "block.minecraft.red_carpet": "红色地毯", + "block.minecraft.red_concrete": "红色混凝土", + "block.minecraft.red_concrete_powder": "红色混凝土粉末", + "block.minecraft.red_glazed_terracotta": "红色带釉陶瓦", + "block.minecraft.red_mushroom": "红色蘑菇", + "block.minecraft.red_mushroom_block": "红色蘑菇方块", + "block.minecraft.red_nether_brick_slab": "红色下界砖台阶", + "block.minecraft.red_nether_brick_stairs": "红色下界砖楼梯", + "block.minecraft.red_nether_brick_wall": "红色下界砖墙", + "block.minecraft.red_nether_bricks": "红色下界砖块", + "block.minecraft.red_sand": "红沙", + "block.minecraft.red_sandstone": "红砂岩", + "block.minecraft.red_sandstone_slab": "红砂岩台阶", + "block.minecraft.red_sandstone_stairs": "红砂岩楼梯", + "block.minecraft.red_sandstone_wall": "红砂岩墙", + "block.minecraft.red_shulker_box": "红色潜影盒", + "block.minecraft.red_stained_glass": "红色染色玻璃", + "block.minecraft.red_stained_glass_pane": "红色染色玻璃板", + "block.minecraft.red_terracotta": "红色陶瓦", + "block.minecraft.red_tulip": "红色郁金香", + "block.minecraft.red_wool": "红色羊毛", + "block.minecraft.redstone_block": "红石块", + "block.minecraft.redstone_lamp": "红石灯", + "block.minecraft.redstone_ore": "红石矿石", + "block.minecraft.redstone_torch": "红石火把", + "block.minecraft.redstone_wall_torch": "墙上的红石火把", + "block.minecraft.redstone_wire": "红石线", + "block.minecraft.reinforced_deepslate": "强化深板岩", + "block.minecraft.repeater": "红石中继器", + "block.minecraft.repeating_command_block": "循环型命令方块", + "block.minecraft.resin_block": "树脂块", + "block.minecraft.resin_brick_slab": "树脂砖台阶", + "block.minecraft.resin_brick_stairs": "树脂砖楼梯", + "block.minecraft.resin_brick_wall": "树脂砖墙", + "block.minecraft.resin_bricks": "树脂砖块", + "block.minecraft.resin_clump": "树脂团", + "block.minecraft.respawn_anchor": "重生锚", + "block.minecraft.rooted_dirt": "缠根泥土", + "block.minecraft.rose_bush": "玫瑰丛", + "block.minecraft.sand": "沙子", + "block.minecraft.sandstone": "砂岩", + "block.minecraft.sandstone_slab": "砂岩台阶", + "block.minecraft.sandstone_stairs": "砂岩楼梯", + "block.minecraft.sandstone_wall": "砂岩墙", + "block.minecraft.scaffolding": "脚手架", + "block.minecraft.sculk": "幽匿块", + "block.minecraft.sculk_catalyst": "幽匿催发体", + "block.minecraft.sculk_sensor": "幽匿感测体", + "block.minecraft.sculk_shrieker": "幽匿尖啸体", + "block.minecraft.sculk_vein": "幽匿脉络", + "block.minecraft.sea_lantern": "海晶灯", + "block.minecraft.sea_pickle": "海泡菜", + "block.minecraft.seagrass": "海草", + "block.minecraft.set_spawn": "已设置重生点", + "block.minecraft.short_grass": "矮草丛", + "block.minecraft.shroomlight": "菌光体", + "block.minecraft.shulker_box": "潜影盒", + "block.minecraft.skeleton_skull": "骷髅头颅", + "block.minecraft.skeleton_wall_skull": "墙上的骷髅头颅", + "block.minecraft.slime_block": "黏液块", + "block.minecraft.small_amethyst_bud": "小型紫晶芽", + "block.minecraft.small_dripleaf": "小型垂滴叶", + "block.minecraft.smithing_table": "锻造台", + "block.minecraft.smoker": "烟熏炉", + "block.minecraft.smooth_basalt": "平滑玄武岩", + "block.minecraft.smooth_quartz": "平滑石英块", + "block.minecraft.smooth_quartz_slab": "平滑石英台阶", + "block.minecraft.smooth_quartz_stairs": "平滑石英楼梯", + "block.minecraft.smooth_red_sandstone": "平滑红砂岩", + "block.minecraft.smooth_red_sandstone_slab": "平滑红砂岩台阶", + "block.minecraft.smooth_red_sandstone_stairs": "平滑红砂岩楼梯", + "block.minecraft.smooth_sandstone": "平滑砂岩", + "block.minecraft.smooth_sandstone_slab": "平滑砂岩台阶", + "block.minecraft.smooth_sandstone_stairs": "平滑砂岩楼梯", + "block.minecraft.smooth_stone": "平滑石头", + "block.minecraft.smooth_stone_slab": "平滑石头台阶", + "block.minecraft.sniffer_egg": "嗅探兽蛋", + "block.minecraft.snow": "雪", + "block.minecraft.snow_block": "雪块", + "block.minecraft.soul_campfire": "灵魂营火", + "block.minecraft.soul_fire": "灵魂火", + "block.minecraft.soul_lantern": "灵魂灯笼", + "block.minecraft.soul_sand": "灵魂沙", + "block.minecraft.soul_soil": "灵魂土", + "block.minecraft.soul_torch": "灵魂火把", + "block.minecraft.soul_wall_torch": "墙上的灵魂火把", + "block.minecraft.spawn.not_valid": "你的床或已充能的重生锚不存在或已被阻挡", + "block.minecraft.spawner": "刷怪笼", + "block.minecraft.spawner.desc1": "用刷怪蛋交互时:", + "block.minecraft.spawner.desc2": "设置生物种类", + "block.minecraft.sponge": "海绵", + "block.minecraft.spore_blossom": "孢子花", + "block.minecraft.spruce_button": "云杉木按钮", + "block.minecraft.spruce_door": "云杉木门", + "block.minecraft.spruce_fence": "云杉木栅栏", + "block.minecraft.spruce_fence_gate": "云杉木栅栏门", + "block.minecraft.spruce_hanging_sign": "悬挂式云杉木告示牌", + "block.minecraft.spruce_leaves": "云杉树叶", + "block.minecraft.spruce_log": "云杉原木", + "block.minecraft.spruce_planks": "云杉木板", + "block.minecraft.spruce_pressure_plate": "云杉木压力板", + "block.minecraft.spruce_sapling": "云杉树苗", + "block.minecraft.spruce_sign": "云杉木告示牌", + "block.minecraft.spruce_slab": "云杉木台阶", + "block.minecraft.spruce_stairs": "云杉木楼梯", + "block.minecraft.spruce_trapdoor": "云杉木活板门", + "block.minecraft.spruce_wall_hanging_sign": "墙上的悬挂式云杉木告示牌", + "block.minecraft.spruce_wall_sign": "墙上的云杉木告示牌", + "block.minecraft.spruce_wood": "云杉木", + "block.minecraft.sticky_piston": "黏性活塞", + "block.minecraft.stone": "石头", + "block.minecraft.stone_brick_slab": "石砖台阶", + "block.minecraft.stone_brick_stairs": "石砖楼梯", + "block.minecraft.stone_brick_wall": "石砖墙", + "block.minecraft.stone_bricks": "石砖", + "block.minecraft.stone_button": "石头按钮", + "block.minecraft.stone_pressure_plate": "石头压力板", + "block.minecraft.stone_slab": "石头台阶", + "block.minecraft.stone_stairs": "石头楼梯", + "block.minecraft.stonecutter": "切石机", + "block.minecraft.stripped_acacia_log": "去皮金合欢原木", + "block.minecraft.stripped_acacia_wood": "去皮金合欢木", + "block.minecraft.stripped_bamboo_block": "去皮竹块", + "block.minecraft.stripped_birch_log": "去皮白桦原木", + "block.minecraft.stripped_birch_wood": "去皮白桦木", + "block.minecraft.stripped_cherry_log": "去皮樱花原木", + "block.minecraft.stripped_cherry_wood": "去皮樱花木", + "block.minecraft.stripped_crimson_hyphae": "去皮绯红菌核", + "block.minecraft.stripped_crimson_stem": "去皮绯红菌柄", + "block.minecraft.stripped_dark_oak_log": "去皮深色橡木原木", + "block.minecraft.stripped_dark_oak_wood": "去皮深色橡木", + "block.minecraft.stripped_jungle_log": "去皮丛林原木", + "block.minecraft.stripped_jungle_wood": "去皮丛林木", + "block.minecraft.stripped_mangrove_log": "去皮红树原木", + "block.minecraft.stripped_mangrove_wood": "去皮红树木", + "block.minecraft.stripped_oak_log": "去皮橡木原木", + "block.minecraft.stripped_oak_wood": "去皮橡木", + "block.minecraft.stripped_pale_oak_log": "去皮苍白橡木原木", + "block.minecraft.stripped_pale_oak_wood": "去皮苍白橡木", + "block.minecraft.stripped_spruce_log": "去皮云杉原木", + "block.minecraft.stripped_spruce_wood": "去皮云杉木", + "block.minecraft.stripped_warped_hyphae": "去皮诡异菌核", + "block.minecraft.stripped_warped_stem": "去皮诡异菌柄", + "block.minecraft.structure_block": "结构方块", + "block.minecraft.structure_void": "结构空位", + "block.minecraft.sugar_cane": "甘蔗", + "block.minecraft.sunflower": "向日葵", + "block.minecraft.suspicious_gravel": "可疑的沙砾", + "block.minecraft.suspicious_sand": "可疑的沙子", + "block.minecraft.sweet_berry_bush": "甜浆果丛", + "block.minecraft.tall_grass": "高草丛", + "block.minecraft.tall_seagrass": "高海草", + "block.minecraft.target": "标靶", + "block.minecraft.terracotta": "陶瓦", + "block.minecraft.test_block": "测试方块", + "block.minecraft.test_instance_block": "测试实例方块", + "block.minecraft.tinted_glass": "遮光玻璃", + "block.minecraft.tnt": "TNT", + "block.minecraft.torch": "火把", + "block.minecraft.torchflower": "火把花", + "block.minecraft.torchflower_crop": "火把花植株", + "block.minecraft.trapped_chest": "陷阱箱", + "block.minecraft.trial_spawner": "试炼刷怪笼", + "block.minecraft.tripwire": "绊线", + "block.minecraft.tripwire_hook": "绊线钩", + "block.minecraft.tube_coral": "管珊瑚", + "block.minecraft.tube_coral_block": "管珊瑚块", + "block.minecraft.tube_coral_fan": "管珊瑚扇", + "block.minecraft.tube_coral_wall_fan": "墙上的管珊瑚扇", + "block.minecraft.tuff": "凝灰岩", + "block.minecraft.tuff_brick_slab": "凝灰岩砖台阶", + "block.minecraft.tuff_brick_stairs": "凝灰岩砖楼梯", + "block.minecraft.tuff_brick_wall": "凝灰岩砖墙", + "block.minecraft.tuff_bricks": "凝灰岩砖", + "block.minecraft.tuff_slab": "凝灰岩台阶", + "block.minecraft.tuff_stairs": "凝灰岩楼梯", + "block.minecraft.tuff_wall": "凝灰岩墙", + "block.minecraft.turtle_egg": "海龟蛋", + "block.minecraft.twisting_vines": "缠怨藤", + "block.minecraft.twisting_vines_plant": "缠怨藤植株", + "block.minecraft.vault": "宝库", + "block.minecraft.verdant_froglight": "青翠蛙明灯", + "block.minecraft.vine": "藤蔓", + "block.minecraft.void_air": "虚空空气", + "block.minecraft.wall_torch": "墙上的火把", + "block.minecraft.warped_button": "诡异木按钮", + "block.minecraft.warped_door": "诡异木门", + "block.minecraft.warped_fence": "诡异木栅栏", + "block.minecraft.warped_fence_gate": "诡异木栅栏门", + "block.minecraft.warped_fungus": "诡异菌", + "block.minecraft.warped_hanging_sign": "悬挂式诡异木告示牌", + "block.minecraft.warped_hyphae": "诡异菌核", + "block.minecraft.warped_nylium": "诡异菌岩", + "block.minecraft.warped_planks": "诡异木板", + "block.minecraft.warped_pressure_plate": "诡异木压力板", + "block.minecraft.warped_roots": "诡异菌索", + "block.minecraft.warped_sign": "诡异木告示牌", + "block.minecraft.warped_slab": "诡异木台阶", + "block.minecraft.warped_stairs": "诡异木楼梯", + "block.minecraft.warped_stem": "诡异菌柄", + "block.minecraft.warped_trapdoor": "诡异木活板门", + "block.minecraft.warped_wall_hanging_sign": "墙上的悬挂式诡异木告示牌", + "block.minecraft.warped_wall_sign": "墙上的诡异木告示牌", + "block.minecraft.warped_wart_block": "诡异疣块", + "block.minecraft.water": "水", + "block.minecraft.water_cauldron": "装有水的炼药锅", + "block.minecraft.waxed_chiseled_copper": "涂蜡的雕纹铜块", + "block.minecraft.waxed_copper_block": "涂蜡的铜块", + "block.minecraft.waxed_copper_bulb": "涂蜡的铜灯", + "block.minecraft.waxed_copper_door": "涂蜡的铜门", + "block.minecraft.waxed_copper_grate": "涂蜡的铜格栅", + "block.minecraft.waxed_copper_trapdoor": "涂蜡的铜活板门", + "block.minecraft.waxed_cut_copper": "涂蜡的切制铜块", + "block.minecraft.waxed_cut_copper_slab": "涂蜡的切制铜台阶", + "block.minecraft.waxed_cut_copper_stairs": "涂蜡的切制铜楼梯", + "block.minecraft.waxed_exposed_chiseled_copper": "涂蜡的斑驳雕纹铜块", + "block.minecraft.waxed_exposed_copper": "涂蜡的斑驳铜块", + "block.minecraft.waxed_exposed_copper_bulb": "涂蜡的斑驳铜灯", + "block.minecraft.waxed_exposed_copper_door": "涂蜡的斑驳铜门", + "block.minecraft.waxed_exposed_copper_grate": "涂蜡的斑驳铜格栅", + "block.minecraft.waxed_exposed_copper_trapdoor": "涂蜡的斑驳铜活板门", + "block.minecraft.waxed_exposed_cut_copper": "涂蜡的斑驳切制铜块", + "block.minecraft.waxed_exposed_cut_copper_slab": "涂蜡的斑驳切制铜台阶", + "block.minecraft.waxed_exposed_cut_copper_stairs": "涂蜡的斑驳切制铜楼梯", + "block.minecraft.waxed_oxidized_chiseled_copper": "涂蜡的氧化雕纹铜块", + "block.minecraft.waxed_oxidized_copper": "涂蜡的氧化铜块", + "block.minecraft.waxed_oxidized_copper_bulb": "涂蜡的氧化铜灯", + "block.minecraft.waxed_oxidized_copper_door": "涂蜡的氧化铜门", + "block.minecraft.waxed_oxidized_copper_grate": "涂蜡的氧化铜格栅", + "block.minecraft.waxed_oxidized_copper_trapdoor": "涂蜡的氧化铜活板门", + "block.minecraft.waxed_oxidized_cut_copper": "涂蜡的氧化切制铜块", + "block.minecraft.waxed_oxidized_cut_copper_slab": "涂蜡的氧化切制铜台阶", + "block.minecraft.waxed_oxidized_cut_copper_stairs": "涂蜡的氧化切制铜楼梯", + "block.minecraft.waxed_weathered_chiseled_copper": "涂蜡的锈蚀雕纹铜块", + "block.minecraft.waxed_weathered_copper": "涂蜡的锈蚀铜块", + "block.minecraft.waxed_weathered_copper_bulb": "涂蜡的锈蚀铜灯", + "block.minecraft.waxed_weathered_copper_door": "涂蜡的锈蚀铜门", + "block.minecraft.waxed_weathered_copper_grate": "涂蜡的锈蚀铜格栅", + "block.minecraft.waxed_weathered_copper_trapdoor": "涂蜡的锈蚀铜活板门", + "block.minecraft.waxed_weathered_cut_copper": "涂蜡的锈蚀切制铜块", + "block.minecraft.waxed_weathered_cut_copper_slab": "涂蜡的锈蚀切制铜台阶", + "block.minecraft.waxed_weathered_cut_copper_stairs": "涂蜡的锈蚀切制铜楼梯", + "block.minecraft.weathered_chiseled_copper": "锈蚀的雕纹铜块", + "block.minecraft.weathered_copper": "锈蚀的铜块", + "block.minecraft.weathered_copper_bulb": "锈蚀的铜灯", + "block.minecraft.weathered_copper_door": "锈蚀的铜门", + "block.minecraft.weathered_copper_grate": "锈蚀的铜格栅", + "block.minecraft.weathered_copper_trapdoor": "锈蚀的铜活板门", + "block.minecraft.weathered_cut_copper": "锈蚀的切制铜块", + "block.minecraft.weathered_cut_copper_slab": "锈蚀的切制铜台阶", + "block.minecraft.weathered_cut_copper_stairs": "锈蚀的切制铜楼梯", + "block.minecraft.weeping_vines": "垂泪藤", + "block.minecraft.weeping_vines_plant": "垂泪藤植株", + "block.minecraft.wet_sponge": "湿海绵", + "block.minecraft.wheat": "小麦植株", + "block.minecraft.white_banner": "白色旗帜", + "block.minecraft.white_bed": "白色床", + "block.minecraft.white_candle": "白色蜡烛", + "block.minecraft.white_candle_cake": "插上白色蜡烛的蛋糕", + "block.minecraft.white_carpet": "白色地毯", + "block.minecraft.white_concrete": "白色混凝土", + "block.minecraft.white_concrete_powder": "白色混凝土粉末", + "block.minecraft.white_glazed_terracotta": "白色带釉陶瓦", + "block.minecraft.white_shulker_box": "白色潜影盒", + "block.minecraft.white_stained_glass": "白色染色玻璃", + "block.minecraft.white_stained_glass_pane": "白色染色玻璃板", + "block.minecraft.white_terracotta": "白色陶瓦", + "block.minecraft.white_tulip": "白色郁金香", + "block.minecraft.white_wool": "白色羊毛", + "block.minecraft.wildflowers": "野花簇", + "block.minecraft.wither_rose": "凋灵玫瑰", + "block.minecraft.wither_skeleton_skull": "凋灵骷髅头颅", + "block.minecraft.wither_skeleton_wall_skull": "墙上的凋灵骷髅头颅", + "block.minecraft.yellow_banner": "黄色旗帜", + "block.minecraft.yellow_bed": "黄色床", + "block.minecraft.yellow_candle": "黄色蜡烛", + "block.minecraft.yellow_candle_cake": "插上黄色蜡烛的蛋糕", + "block.minecraft.yellow_carpet": "黄色地毯", + "block.minecraft.yellow_concrete": "黄色混凝土", + "block.minecraft.yellow_concrete_powder": "黄色混凝土粉末", + "block.minecraft.yellow_glazed_terracotta": "黄色带釉陶瓦", + "block.minecraft.yellow_shulker_box": "黄色潜影盒", + "block.minecraft.yellow_stained_glass": "黄色染色玻璃", + "block.minecraft.yellow_stained_glass_pane": "黄色染色玻璃板", + "block.minecraft.yellow_terracotta": "黄色陶瓦", + "block.minecraft.yellow_wool": "黄色羊毛", + "block.minecraft.zombie_head": "僵尸的头", + "block.minecraft.zombie_wall_head": "墙上的僵尸的头", + "book.byAuthor": "%1$s 著", + "book.editTitle": "输入书名:", + "book.finalizeButton": "署名并关闭", + "book.finalizeWarning": "注意!在你署名后,它将不能再被修改。", + "book.generation.0": "原稿", + "book.generation.1": "原稿的副本", + "book.generation.2": "副本的副本", + "book.generation.3": "破烂不堪", + "book.invalid.tag": "* 无效的书本标签 *", + "book.pageIndicator": "第%1$s页/共%2$s页", + "book.signButton": "署名", + "build.tooHigh": "建筑高度限制是%s", + "chat.cannotSend": "无法发送聊天消息", + "chat.coordinates": "%s, %s, %s", + "chat.coordinates.tooltip": "点此传送", + "chat.copy": "复制到剪贴板", + "chat.copy.click": "单击复制到剪贴板", + "chat.deleted_marker": "此聊天消息已被服务器删除。", + "chat.disabled.chain_broken": "由于消息链损坏,聊天已被禁用。请尝试重新连接。", + "chat.disabled.expiredProfileKey": "由于个人信息公钥过期,聊天已被禁用。请尝试重新连接。", + "chat.disabled.invalid_command_signature": "命令包含意外或缺失的命令参数签名。", + "chat.disabled.invalid_signature": "聊天签名无效。请尝试重新连接。", + "chat.disabled.launcher": "聊天在启动器选项中被禁用。无法发送消息。", + "chat.disabled.missingProfileKey": "由于个人信息公钥丢失,聊天已被禁用。请尝试重新连接。", + "chat.disabled.options": "聊天在客户端选项中被禁用。", + "chat.disabled.out_of_order_chat": "接收到了乱序的聊天消息。你的系统时间是否被更改过?", + "chat.disabled.profile": "聊天在账户设置中被禁用。再按一次“%s”以获取更多信息。", + "chat.disabled.profile.moreInfo": "聊天在账户设置中被禁用。无法发送或查看消息。", + "chat.editBox": "聊天", + "chat.filtered": "被服务器过滤。", + "chat.filtered_full": "服务器已向部分玩家隐藏了你的消息。", + "chat.link.confirm": "你确定要打开以下网页?", + "chat.link.confirmTrusted": "你想要打开这个链接或将其复制到你的剪贴板吗?", + "chat.link.open": "在浏览器中打开", + "chat.link.warning": "永远不要打开你不信任的人提供的链接!", + "chat.queue": "[+%s行待发送]", + "chat.square_brackets": "[%s]", + "chat.tag.error": "服务器发送了无效消息。", + "chat.tag.modified": "消息被服务器修改。原文:", + "chat.tag.not_secure": "未经验证的消息。无法举报。", + "chat.tag.system": "服务器消息。无法举报。", + "chat.tag.system_single_player": "服务器消息。", + "chat.type.admin": "[%s: %s]", + "chat.type.advancement.challenge": "%s完成了挑战%s", + "chat.type.advancement.goal": "%s达成了目标%s", + "chat.type.advancement.task": "%s取得了进度%s", + "chat.type.announcement": "[%s] %s", + "chat.type.emote": "* %s %s", + "chat.type.team.hover": "发送队伍消息", + "chat.type.team.sent": "-> %s <%s> %s", + "chat.type.team.text": "%s <%s> %s", + "chat.type.text": "<%s> %s", + "chat.type.text.narrate": "%s说%s", + "chat.validation_error": "聊天验证错误", + "chat_screen.message": "要发送的消息:%s", + "chat_screen.title": "聊天屏幕", + "chat_screen.usage": "输入消息并按Enter键发送", + "chunk.toast.checkLog": "查看日志获取详情", + "chunk.toast.loadFailure": "无法加载%s处的区块", + "chunk.toast.lowDiskSpace": "磁盘空间不足!", + "chunk.toast.lowDiskSpace.description": "可能无法保存世界。", + "chunk.toast.saveFailure": "无法保存%s处的区块", + "clear.failed.multiple": "未能从%s名玩家找到任何物品", + "clear.failed.single": "未能从玩家%s找到任何物品", + "color.minecraft.black": "黑色", + "color.minecraft.blue": "蓝色", + "color.minecraft.brown": "棕色", + "color.minecraft.cyan": "青色", + "color.minecraft.gray": "灰色", + "color.minecraft.green": "绿色", + "color.minecraft.light_blue": "淡蓝色", + "color.minecraft.light_gray": "淡灰色", + "color.minecraft.lime": "黄绿色", + "color.minecraft.magenta": "品红色", + "color.minecraft.orange": "橙色", + "color.minecraft.pink": "粉红色", + "color.minecraft.purple": "紫色", + "color.minecraft.red": "红色", + "color.minecraft.white": "白色", + "color.minecraft.yellow": "黄色", + "command.context.here": "<--[此处]", + "command.context.parse_error": "%s,位于第%s个字符:%s", + "command.exception": "无法解析命令:%s", + "command.expected.separator": "参数后应有空格分隔,但发现了紧邻的数据", + "command.failed": "试图执行该命令时出现意外错误", + "command.forkLimit": "上下文数量已达上限值(%s)", + "command.unknown.argument": "错误的命令参数", + "command.unknown.command": "未知或不完整的命令,错误见下", + "commands.advancement.criterionNotFound": "进度%1$s并不包含条件“%2$s”", + "commands.advancement.grant.criterion.to.many.failure": "无法将进度%2$s的达成条件“%1$s”赋予%3$s名玩家,因为他们已达成此条件", + "commands.advancement.grant.criterion.to.many.success": "已将进度%2$s的达成条件“%1$s”赋予%3$s名玩家", + "commands.advancement.grant.criterion.to.one.failure": "无法将进度%2$s的达成条件“%1$s”赋予%3$s,因为该玩家已达成此条件", + "commands.advancement.grant.criterion.to.one.success": "已将进度%2$s的达成条件“%1$s”赋予%3$s", + "commands.advancement.grant.many.to.many.failure": "无法将%s项进度赋予%s名玩家,因为他们已达成这些进度", + "commands.advancement.grant.many.to.many.success": "已将%s项进度赋予%s名玩家", + "commands.advancement.grant.many.to.one.failure": "无法将%s项进度赋予%s,因为该玩家已达成这些进度", + "commands.advancement.grant.many.to.one.success": "已将%s项进度赋予%s", + "commands.advancement.grant.one.to.many.failure": "无法将进度%s赋予%s名玩家,因为他们已达成此进度", + "commands.advancement.grant.one.to.many.success": "已将进度%s赋予%s名玩家", + "commands.advancement.grant.one.to.one.failure": "无法将进度%s赋予%s,因为该玩家已达成此进度", + "commands.advancement.grant.one.to.one.success": "已将进度%s赋予%s", + "commands.advancement.revoke.criterion.to.many.failure": "无法撤销%3$s名玩家关于进度%2$s的达成条件“%1$s”,因为他们并未达成此条件", + "commands.advancement.revoke.criterion.to.many.success": "已撤销%3$s名玩家关于进度%2$s的达成条件“%1$s”", + "commands.advancement.revoke.criterion.to.one.failure": "无法撤销%3$s关于进度%2$s的达成条件“%1$s”,因为该玩家并未达成此条件", + "commands.advancement.revoke.criterion.to.one.success": "已撤销%3$s关于进度%2$s的达成条件“%1$s”", + "commands.advancement.revoke.many.to.many.failure": "无法撤销%2$s名玩家的%1$s项进度,因为他们并未达成此进度", + "commands.advancement.revoke.many.to.many.success": "已撤销%2$s名玩家的%1$s项进度", + "commands.advancement.revoke.many.to.one.failure": "无法撤销%2$s的%1$s项进度,因为该玩家并未达成这些进度", + "commands.advancement.revoke.many.to.one.success": "已撤销%2$s的%1$s项进度", + "commands.advancement.revoke.one.to.many.failure": "无法撤销%2$s名玩家的进度%1$s,因为他们并未达成此进度", + "commands.advancement.revoke.one.to.many.success": "已撤销%2$s名玩家的进度%1$s", + "commands.advancement.revoke.one.to.one.failure": "无法撤销%2$s的进度%1$s,因为该玩家并未达成此进度", + "commands.advancement.revoke.one.to.one.success": "已撤销%2$s的进度%1$s", + "commands.attribute.base_value.get.success": "实体%2$s的属性%1$s的基值为%3$s", + "commands.attribute.base_value.reset.success": "实体%2$s的属性%1$s的基值已重置为默认值%3$s", + "commands.attribute.base_value.set.success": "实体%2$s的属性%1$s的基值已设置为%3$s", + "commands.attribute.failed.entity": "%s不是此命令的有效实体", + "commands.attribute.failed.modifier_already_present": "实体%3$s的属性%2$s已存在修饰符%1$s", + "commands.attribute.failed.no_attribute": "实体%s没有属性%s", + "commands.attribute.failed.no_modifier": "实体%2$s的属性%1$s无修饰符%3$s", + "commands.attribute.modifier.add.success": "为实体%3$s的属性%2$s添加了修饰符%1$s", + "commands.attribute.modifier.remove.success": "为实体%3$s的属性%2$s移除了修饰符%1$s", + "commands.attribute.modifier.value.get.success": "实体%3$s的属性%2$s中修饰符%1$s值为%4$s", + "commands.attribute.value.get.success": "实体%2$s的属性%1$s的值为%3$s", + "commands.ban.failed": "无变化,该玩家已被封禁", + "commands.ban.success": "已封禁%s:%s", + "commands.banip.failed": "无变化,该IP地址已被封禁", + "commands.banip.info": "此次封禁涉及%s名玩家:%s", + "commands.banip.invalid": "无效的IP地址或未知的玩家", + "commands.banip.success": "已封禁IP地址%s:%s", + "commands.banlist.entry": "%s被%s封禁:%s", + "commands.banlist.entry.unknown": "(未知)", + "commands.banlist.list": "共有%s条封禁:", + "commands.banlist.none": "没有玩家被封禁", + "commands.bossbar.create.failed": "ID为“%s”的Boss栏已经存在", + "commands.bossbar.create.success": "已创建自定义Boss栏%s", + "commands.bossbar.get.max": "自定义Boss栏%s的最大值为%s", + "commands.bossbar.get.players.none": "自定义Boss栏%s目前没有在线玩家", + "commands.bossbar.get.players.some": "自定义Boss栏%s当前在线的%s名玩家有:%s", + "commands.bossbar.get.value": "自定义Boss栏%s的数值为%s", + "commands.bossbar.get.visible.hidden": "自定义Boss栏%s现为隐藏", + "commands.bossbar.get.visible.visible": "自定义Boss栏%s现为可见", + "commands.bossbar.list.bars.none": "无运行中的自定义Boss栏", + "commands.bossbar.list.bars.some": "有%s项运行中的自定义Boss栏:%s", + "commands.bossbar.remove.success": "已移除自定义Boss栏%s", + "commands.bossbar.set.color.success": "已更改自定义Boss栏%s的颜色", + "commands.bossbar.set.color.unchanged": "无变化,这本就是这个Boss栏的颜色", + "commands.bossbar.set.max.success": "自定义Boss栏%s的最大值已改为%s", + "commands.bossbar.set.max.unchanged": "无变化,这本就是这个Boss栏的最大值", + "commands.bossbar.set.name.success": "已重命名自定义Boss栏%s", + "commands.bossbar.set.name.unchanged": "无变化,这本就是这个Boss栏的名称", + "commands.bossbar.set.players.success.none": "自定义Boss栏%s在当下没有在线玩家", + "commands.bossbar.set.players.success.some": "自定义Boss栏%s在当下在线的%s名玩家有:%s", + "commands.bossbar.set.players.unchanged": "无变化,这些玩家已经在Boss栏上,没有玩家需要被添加或移除", + "commands.bossbar.set.style.success": "已改变自定义Boss栏%s的样式", + "commands.bossbar.set.style.unchanged": "无变化,这本就是这个Boss栏的样式", + "commands.bossbar.set.value.success": "自定义Boss栏%s的值已改为%s", + "commands.bossbar.set.value.unchanged": "无变化,这本就是这个Boss栏的值", + "commands.bossbar.set.visibility.unchanged.hidden": "无变化,该Boss栏原已隐藏", + "commands.bossbar.set.visibility.unchanged.visible": "无变化,该Boss栏原已可见", + "commands.bossbar.set.visible.success.hidden": "已将自定义Boss栏%s改为隐藏", + "commands.bossbar.set.visible.success.visible": "已将自定义Boss栏%s改为可见", + "commands.bossbar.unknown": "不存在ID为“%s”的Boss栏", + "commands.clear.success.multiple": "已移除%2$s名玩家的%1$s个物品", + "commands.clear.success.single": "已移除玩家%2$s的%1$s个物品", + "commands.clear.test.multiple": "已在%2$s名玩家身上找到%1$s个匹配的物品", + "commands.clear.test.single": "已在玩家%2$s身上找到%1$s个匹配的物品", + "commands.clone.failed": "未复制任何方块", + "commands.clone.overlap": "源区域和目标区域不能重叠", + "commands.clone.success": "已成功复制%s个方块", + "commands.clone.toobig": "指定区域内的方块太多(最大值为%s,指定值为%s)", + "commands.damage.invulnerable": "对象免疫指定的伤害类型", + "commands.damage.success": "已将%s伤害施加于%s", + "commands.data.block.get": "位于%2$s, %3$s, %4$s的方块的%1$s乘以%5$s倍率后的值为%6$s", + "commands.data.block.invalid": "目标方块不是方块实体", + "commands.data.block.modified": "已修改%s, %s, %s处的方块数据", + "commands.data.block.query": "%s, %s, %s拥有以下方块数据:%s", + "commands.data.entity.get": "%2$s的%1$s在乘以倍率%3$s后的值是%4$s", + "commands.data.entity.invalid": "无法修改玩家数据", + "commands.data.entity.modified": "已修改%s的实体数据", + "commands.data.entity.query": "%s拥有以下实体数据:%s", + "commands.data.get.invalid": "无法获取%s,只接受数字标签", + "commands.data.get.multiple": "该参数只接受单个NBT值", + "commands.data.get.unknown": "无法获取%s,标签不存在", + "commands.data.merge.failed": "无变化,所指定的属性已有这些值", + "commands.data.modify.expected_list": "应为列表,实际为:%s", + "commands.data.modify.expected_object": "应为对象,实际为:%s", + "commands.data.modify.expected_value": "应为值,实际为:%s", + "commands.data.modify.invalid_index": "无效的列表索引:%s", + "commands.data.modify.invalid_substring": "无效的子串索引:%s到%s", + "commands.data.storage.get": "在存储%2$s中的%1$s在乘以倍率%3$s后的值是%4$s", + "commands.data.storage.modified": "已修改存储%s", + "commands.data.storage.query": "存储%s含有以下内容:%s", + "commands.datapack.disable.failed": "数据包“%s”并未启用!", + "commands.datapack.disable.failed.feature": "无法禁用数据包“%s”,因为它属于已启用功能!", + "commands.datapack.enable.failed": "数据包“%s”已经启用!", + "commands.datapack.enable.failed.no_flags": "无法启用数据包“%s”,因为所需的功能未在此世界启用:%s!", + "commands.datapack.list.available.none": "已无更多可用的数据包", + "commands.datapack.list.available.success": "共有%s个数据包可用:%s", + "commands.datapack.list.enabled.none": "没有启用中的数据包", + "commands.datapack.list.enabled.success": "已启用%s个数据包:%s", + "commands.datapack.modify.disable": "正在禁用数据包%s", + "commands.datapack.modify.enable": "正在启用数据包%s", + "commands.datapack.unknown": "未知的数据包 “%s”", + "commands.debug.alreadyRunning": "刻分析器原已开启", + "commands.debug.function.noRecursion": "无法从函数内部开始追踪", + "commands.debug.function.noReturnRun": "使用/return run时无法追踪", + "commands.debug.function.success.multiple": "已追踪%2$s个函数内的%1$s条命令至输出文件%3$s", + "commands.debug.function.success.single": "已追踪函数“%2$s”内的%1$s条命令至输出文件%3$s", + "commands.debug.function.traceFailed": "追踪函数失败", + "commands.debug.notRunning": "尚未启动刻分析器", + "commands.debug.started": "已开始刻分析", + "commands.debug.stopped": "已停止刻分析,用时%s秒和%s刻(每秒%s刻)", + "commands.defaultgamemode.success": "默认游戏模式现在为%s", + "commands.deop.failed": "无变化,此玩家不是管理员", + "commands.deop.success": "%s不再是服务器管理员了", + "commands.difficulty.failure": "难度未变,它原来就是%s", + "commands.difficulty.query": "目前难度为%s", + "commands.difficulty.success": "难度已被设置为%s", + "commands.drop.no_held_items": "该实体无法持有任何物品", + "commands.drop.no_loot_table": "实体%s没有战利品表", + "commands.drop.no_loot_table.block": "方块%s没有战利品表", + "commands.drop.success.multiple": "掉落了%s个物品", + "commands.drop.success.multiple_with_table": "从战利品表%2$s中掉落了%1$s个物品", + "commands.drop.success.single": "掉落了%s个%s", + "commands.drop.success.single_with_table": "从战利品表%3$s中掉落了%1$s个%2$s", + "commands.effect.clear.everything.failed": "对象没有可以移除的效果", + "commands.effect.clear.everything.success.multiple": "已移除%s个对象的所有效果", + "commands.effect.clear.everything.success.single": "已移除%s的所有效果", + "commands.effect.clear.specific.failed": "对象没有指定效果", + "commands.effect.clear.specific.success.multiple": "已移除%2$s个对象的%1$s效果", + "commands.effect.clear.specific.success.single": "已移除%2$s的%1$s效果", + "commands.effect.give.failed": "无法应用此效果(对象对效果免疫,或已有更强的效果)", + "commands.effect.give.success.multiple": "已将%s效果应用于%s个对象", + "commands.effect.give.success.single": "已将%s效果应用于%s", + "commands.enchant.failed": "无变化,对象未持有物品或其不支持此魔咒", + "commands.enchant.failed.entity": "%s不是此命令的有效实体", + "commands.enchant.failed.incompatible": "%s不支持此魔咒", + "commands.enchant.failed.itemless": "%s未手持任何物品", + "commands.enchant.failed.level": "%s高于该魔咒支持的最高等级%s", + "commands.enchant.success.multiple": "已将%s魔咒应用于%s个实体", + "commands.enchant.success.single": "已将%s魔咒应用于%s的物品上", + "commands.execute.blocks.toobig": "指定区域内的方块太多(最大值为%s,指定值为%s)", + "commands.execute.conditional.fail": "测试失败", + "commands.execute.conditional.fail_count": "测试失败,计数:%s", + "commands.execute.conditional.pass": "测试通过", + "commands.execute.conditional.pass_count": "测试通过,计数:%s", + "commands.execute.function.instantiationFailure": "无法实例化函数%s:%s", + "commands.experience.add.levels.success.multiple": "已给予%2$s名玩家%1$s级经验", + "commands.experience.add.levels.success.single": "已给予%2$s %1$s级经验", + "commands.experience.add.points.success.multiple": "已给予%2$s名玩家%1$s点经验", + "commands.experience.add.points.success.single": "已给予%2$s %1$s点经验", + "commands.experience.query.levels": "%s拥有%s级经验", + "commands.experience.query.points": "%s拥有%s点经验值", + "commands.experience.set.levels.success.multiple": "已将%2$s名玩家的经验等级设为%1$s", + "commands.experience.set.levels.success.single": "已将%2$s的经验等级设为%1$s", + "commands.experience.set.points.invalid": "无法将此玩家的经验值设置为超过现有等级的最大值", + "commands.experience.set.points.success.multiple": "已将%2$s名玩家的经验值设为%1$s", + "commands.experience.set.points.success.single": "已将%2$s的经验值设为%1$s", + "commands.fill.failed": "没有方块被填充", + "commands.fill.success": "已成功填充%s个方块", + "commands.fill.toobig": "指定区域内的方块太多(最大值为%s,指定值为%s)", + "commands.fillbiome.success": "已在%s, %s, %s与%s, %s, %s之间设置生物群系", + "commands.fillbiome.success.count": "已在%2$s, %3$s, %4$s与%5$s, %6$s, %7$s之间设置%1$s个生物群系单元", + "commands.fillbiome.toobig": "指定范围内的方块过多(最大值为%s,指定值为%s)", + "commands.forceload.added.failure": "没有被标记为强制加载的区块", + "commands.forceload.added.multiple": "已将%2$s中的%3$s至%4$s间的%1$s个区块标记为强制加载", + "commands.forceload.added.none": "在%s中未找到强制加载的区块", + "commands.forceload.added.single": "已将%2$s中的区块%1$s标记为强制加载", + "commands.forceload.list.multiple": "在%2$s内找到%1$s个强制加载的区块:%3$s", + "commands.forceload.list.single": "在%s内找到一个强制加载的区块:%s", + "commands.forceload.query.failure": "在%2$s中%1$s内的区块未被标记为强制加载", + "commands.forceload.query.success": "在%2$s中%1$s内的区块被标记为强制加载", + "commands.forceload.removed.all": "已解除标记%s内所有的强制加载区块", + "commands.forceload.removed.failure": "没有强制加载的区块被移除", + "commands.forceload.removed.multiple": "已将%2$s中的%3$s至%4$s间的%1$s个区块取消强制加载", + "commands.forceload.removed.single": "已将%2$s中的区块%1$s解除强制加载", + "commands.forceload.toobig": "指定区域内区块过多(最大值为%s,指定值为%s)", + "commands.function.error.argument_not_compound": "无效的参数类型:%s,应为Compound类型", + "commands.function.error.missing_argument": "函数%1$s缺少传入参数%2$s", + "commands.function.error.missing_arguments": "函数%s缺少传入参数", + "commands.function.error.parse": "实例化宏%s时:命令“%s”发生错误:%s", + "commands.function.instantiationFailure": "无法实例化函数%s:%s", + "commands.function.result": "函数%s返回了%s", + "commands.function.scheduled.multiple": "正在运行函数%s", + "commands.function.scheduled.no_functions": "无法找到名称为%s的函数", + "commands.function.scheduled.single": "正在运行函数%s", + "commands.function.success.multiple": "已执行%2$s个函数中的%1$s条命令", + "commands.function.success.multiple.result": "执行了%s个函数", + "commands.function.success.single": "已执行函数%2$s中的%1$s条命令", + "commands.function.success.single.result": "函数“%2$s”返回了%1$s", + "commands.gamemode.success.other": "已将%s的游戏模式改为%s", + "commands.gamemode.success.self": "已将自己的游戏模式设置为%s", + "commands.gamerule.query": "游戏规则%s目前为:%s", + "commands.gamerule.set": "游戏规则%s已被设为:%s", + "commands.give.failed.toomanyitems": "最多只能给予%s个%s", + "commands.give.success.multiple": "已将%s个%s给予%s名玩家", + "commands.give.success.single": "已将%s个%s给予%s", + "commands.help.failed": "未知的命令或权限不足", + "commands.item.block.set.success": "已用%4$s替换了位于%1$s, %2$s, %3$s的槽位", + "commands.item.entity.set.success.multiple": "已用%2$s替换了%1$s个实体的槽位", + "commands.item.entity.set.success.single": "已用%2$s替换了%1$s的槽位", + "commands.item.source.no_such_slot": "来源没有%s槽位", + "commands.item.source.not_a_container": "在来源位置%s, %s, %s上的不是一个容器", + "commands.item.target.no_changed.known_item": "没有对象在%2$s槽位接受了物品%1$s", + "commands.item.target.no_changes": "没有对象在%s槽位接受了物品", + "commands.item.target.no_such_slot": "对象没有%s槽位", + "commands.item.target.not_a_container": "在目标位置%s, %s, %s上的不是一个容器", + "commands.jfr.dump.failed": "无法转储JFR记录:%s", + "commands.jfr.start.failed": "无法开始JFR分析", + "commands.jfr.started": "已开始JFR分析", + "commands.jfr.stopped": "JFR分析已结束,已转储至%s", + "commands.kick.owner.failed": "无法在局域网游戏中踢出服务器所有者", + "commands.kick.singleplayer.failed": "无法在离线单人游戏中踢出玩家", + "commands.kick.success": "已踢出%s:%s", + "commands.kill.success.multiple": "杀死了%s个实体", + "commands.kill.success.single": "杀死了%s", + "commands.list.nameAndId": "%s(%s)", + "commands.list.players": "当前共有%s名玩家在线(最大玩家数为%s):%s", + "commands.locate.biome.not_found": "无法在合理距离内找到类型为“%s”的生物群系", + "commands.locate.biome.success": "最近的%s位于%s(%s个方块外)", + "commands.locate.poi.not_found": "无法在合理距离内找到类型为“%s”的兴趣点", + "commands.locate.poi.success": "最近的%s位于%s(%s个方块外)", + "commands.locate.structure.invalid": "没有类型为“%s”的结构", + "commands.locate.structure.not_found": "无法在附近找到类型为“%s”的结构", + "commands.locate.structure.success": "最近的%s位于%s(%s个方块外)", + "commands.message.display.incoming": "%s悄悄地对你说:%s", + "commands.message.display.outgoing": "你悄悄地对%s说:%s", + "commands.op.failed": "无变化,该玩家已是管理员", + "commands.op.success": "已将%s设为服务器管理员", + "commands.pardon.failed": "无变化,该玩家未被封禁", + "commands.pardon.success": "已解封%s", + "commands.pardonip.failed": "无变化,该IP地址未被封禁", + "commands.pardonip.invalid": "无效的IP地址", + "commands.pardonip.success": "已解封IP地址%s", + "commands.particle.failed": "该粒子无法被任何玩家看见", + "commands.particle.success": "正在显示粒子%s", + "commands.perf.alreadyRunning": "性能分析器已在运行", + "commands.perf.notRunning": "性能分析器尚未启动", + "commands.perf.reportFailed": "生成调试报告失败", + "commands.perf.reportSaved": "已在%s生成调试报告", + "commands.perf.started": "已开始时长为10秒的性能分析测试(使用“/perf stop”以提前结束)", + "commands.perf.stopped": "已停止性能分析,用时%s秒和%s刻(每秒%s刻)", + "commands.place.feature.failed": "放置地物失败", + "commands.place.feature.invalid": "没有类型为“%s”的地物", + "commands.place.feature.success": "已在%2$s, %3$s, %4$s处放置“%1$s”", + "commands.place.jigsaw.failed": "生成拼图失败", + "commands.place.jigsaw.invalid": "没有类型为“%s”的模板池", + "commands.place.jigsaw.success": "已在%s, %s, %s处生成拼图", + "commands.place.structure.failed": "放置结构失败", + "commands.place.structure.invalid": "没有类型为“%s”的结构", + "commands.place.structure.success": "已在%2$s, %3$s, %4$s处生成结构“%1$s”", + "commands.place.template.failed": "放置模板失败", + "commands.place.template.invalid": "没有ID为“%s”的模板", + "commands.place.template.success": "已在%2$s, %3$s, %4$s处加载模板“%1$s”", + "commands.playsound.failed": "声音过远而无法被听见", + "commands.playsound.success.multiple": "已将声音%s播放给%s名玩家", + "commands.playsound.success.single": "已将声音%s播放给%s", + "commands.publish.alreadyPublished": "已存在开放于%s端口的多人游戏", + "commands.publish.failed": "无法建立本地游戏", + "commands.publish.started": "本地游戏已在端口%s上开启", + "commands.publish.success": "多人游戏已在%s端口上开启", + "commands.random.error.range_too_large": "随机值的范围最大为2147483646", + "commands.random.error.range_too_small": "随机值的范围最小为2", + "commands.random.reset.all.success": "已重置%s个随机序列", + "commands.random.reset.success": "已重置随机序列%s", + "commands.random.roll": "%s掷出了%s(从%s到%s)", + "commands.random.sample.success": "随机值:%s", + "commands.recipe.give.failed": "没有新配方被解锁", + "commands.recipe.give.success.multiple": "已为%2$s名玩家解锁了%1$s条配方", + "commands.recipe.give.success.single": "已为%2$s解锁了%1$s条配方", + "commands.recipe.take.failed": "没有可遗忘的配方", + "commands.recipe.take.success.multiple": "已剥夺%2$s名玩家的%1$s条配方", + "commands.recipe.take.success.single": "已剥夺%2$s的%1$s条配方", + "commands.reload.failure": "重新加载失败,保留原有数据", + "commands.reload.success": "重新加载中!", + "commands.ride.already_riding": "%s已经在骑乘%s", + "commands.ride.dismount.success": "%s已停止骑乘%s", + "commands.ride.mount.failure.cant_ride_players": "玩家无法被骑乘", + "commands.ride.mount.failure.generic": "%s无法骑乘%s", + "commands.ride.mount.failure.loop": "无法让实体骑乘自身或其乘客", + "commands.ride.mount.failure.wrong_dimension": "无法骑乘位于不同维度的实体", + "commands.ride.mount.success": "%s已开始骑乘%s", + "commands.ride.not_riding": "%s未骑乘任何载具", + "commands.rotate.success": "已旋转%s", + "commands.save.alreadyOff": "已经关闭世界保存", + "commands.save.alreadyOn": "已经打开世界保存", + "commands.save.disabled": "自动保存已禁用", + "commands.save.enabled": "自动保存已启用", + "commands.save.failed": "无法保存游戏(磁盘空间是否足够?)", + "commands.save.saving": "正在保存游戏(这可能需要一些时间!)", + "commands.save.success": "游戏已保存", + "commands.schedule.cleared.failure": "没有ID为%s的计划", + "commands.schedule.cleared.success": "已移除%s个ID为%s的计划", + "commands.schedule.created.function": "已将函数“%s”计划在%s刻后,即游戏时间%s时执行", + "commands.schedule.created.tag": "已将标签“%s”计划在%s刻后,即游戏时间%s时执行", + "commands.schedule.macro": "无法计划宏", + "commands.schedule.same_tick": "无法将函数计划在当前刻", + "commands.scoreboard.objectives.add.duplicate": "已经存在同名记分项", + "commands.scoreboard.objectives.add.success": "创建了新的记分项%s", + "commands.scoreboard.objectives.display.alreadyEmpty": "无变化,该显示位置本就是空的", + "commands.scoreboard.objectives.display.alreadySet": "无变化,该显示位置已经存在该记分项", + "commands.scoreboard.objectives.display.cleared": "清空了显示位置%s的所有记分项", + "commands.scoreboard.objectives.display.set": "已将显示位置%s设置为展示记分项%s", + "commands.scoreboard.objectives.list.empty": "不存在任何记分项", + "commands.scoreboard.objectives.list.success": "共有%s个记分项:%s", + "commands.scoreboard.objectives.modify.displayAutoUpdate.disable": "已禁用记分项%s的展示自动更新", + "commands.scoreboard.objectives.modify.displayAutoUpdate.enable": "已启用记分项%s的展示自动更新", + "commands.scoreboard.objectives.modify.displayname": "已将%s的显示名称更改为%s", + "commands.scoreboard.objectives.modify.objectiveFormat.clear": "已清除记分项%s的默认数字格式", + "commands.scoreboard.objectives.modify.objectiveFormat.set": "已更改记分项%s的默认数字格式", + "commands.scoreboard.objectives.modify.rendertype": "已更改记分项%s的渲染类型", + "commands.scoreboard.objectives.remove.success": "移除了记分项%s", + "commands.scoreboard.players.add.success.multiple": "将%3$s个实体的%2$s增加了%1$s", + "commands.scoreboard.players.add.success.single": "将%3$s的%2$s增加了%1$s(现在是%4$s)", + "commands.scoreboard.players.display.name.clear.success.multiple": "已清除%s个实体在%s中的显示名称", + "commands.scoreboard.players.display.name.clear.success.single": "已清除%s在%s中的显示名称", + "commands.scoreboard.players.display.name.set.success.multiple": "已将%2$s个实体在%3$s中的显示名称更改为%1$s", + "commands.scoreboard.players.display.name.set.success.single": "已将%2$s在%3$s中的显示名称更改为%1$s", + "commands.scoreboard.players.display.numberFormat.clear.success.multiple": "已清除%s个实体在%s中的数字格式", + "commands.scoreboard.players.display.numberFormat.clear.success.single": "已清除%s在%s中的数字格式", + "commands.scoreboard.players.display.numberFormat.set.success.multiple": "已更改%s个实体在%s中的数字格式", + "commands.scoreboard.players.display.numberFormat.set.success.single": "已更改%s在%s中的数字格式", + "commands.scoreboard.players.enable.failed": "无变化,触发器原已开启", + "commands.scoreboard.players.enable.invalid": "只能启用trigger类记分项", + "commands.scoreboard.players.enable.success.multiple": "已为%2$s个实体启用了触发器%1$s", + "commands.scoreboard.players.enable.success.single": "已为%2$s启用了触发器%1$s", + "commands.scoreboard.players.get.null": "无法获取%2$s的%1$s的值,其尚未被赋值", + "commands.scoreboard.players.get.success": "%1$s在%3$s记分项里拥有%2$s分", + "commands.scoreboard.players.list.empty": "没有正被追踪的实体", + "commands.scoreboard.players.list.entity.empty": "%s无可显示的分数", + "commands.scoreboard.players.list.entity.entry": "%s:%s", + "commands.scoreboard.players.list.entity.success": "%s拥有%s项分数:", + "commands.scoreboard.players.list.success": "共有%s个正被追踪的实体:%s", + "commands.scoreboard.players.operation.success.multiple": "更新了%2$s个实体的%1$s", + "commands.scoreboard.players.operation.success.single": "已将%2$s的%1$s设为%3$s", + "commands.scoreboard.players.remove.success.multiple": "已将%3$s个实体的%2$s分数减少%1$s", + "commands.scoreboard.players.remove.success.single": "已将%3$s的%2$s减少%1$s(现在是%4$s)", + "commands.scoreboard.players.reset.all.multiple": "重置了%s个实体的所有分数", + "commands.scoreboard.players.reset.all.single": "重置了%s的所有分数", + "commands.scoreboard.players.reset.specific.multiple": "重置了%2$s个实体的%1$s", + "commands.scoreboard.players.reset.specific.single": "重置了%2$s的%1$s", + "commands.scoreboard.players.set.success.multiple": "已将%2$s个实体的%1$s设为%3$s", + "commands.scoreboard.players.set.success.single": "已将%2$s的%1$s分数设为%3$s", + "commands.seed.success": "种子:%s", + "commands.setblock.failed": "无法放置方块", + "commands.setblock.success": "更改了位于%s, %s, %s的方块", + "commands.setidletimeout.success": "玩家的闲置限时现在为%s分钟", + "commands.setidletimeout.success.disabled": "玩家的闲置限时已禁用", + "commands.setworldspawn.failure.not_overworld": "只能为主世界设置世界出生点", + "commands.setworldspawn.success": "已将世界的出生点设置为%s, %s, %s [%s]", + "commands.spawnpoint.success.multiple": "已将%6$s名玩家的出生点设为%5$s中的%1$s, %2$s, %3$s [%4$s]", + "commands.spawnpoint.success.single": "已将%6$s的出生点设为%5$s中的%1$s, %2$s, %3$s [%4$s]", + "commands.spectate.not_spectator": "%s尚未处于旁观模式", + "commands.spectate.self": "不能旁观自己", + "commands.spectate.success.started": "正在旁观%s", + "commands.spectate.success.stopped": "不再旁观实体", + "commands.spreadplayers.failed.entities": "%1$s个实体未能围绕%2$s, %3$s分散(空间过小而实体过多,请将分散间距设为%4$s以下)", + "commands.spreadplayers.failed.invalid.height": "无效的maxHeight值:%s,应高于世界最小高度%s", + "commands.spreadplayers.failed.teams": "%1$s支队伍未能围绕%2$s, %3$s分散(空间过小而实体过多,请将分散间距设为%4$s以下)", + "commands.spreadplayers.success.entities": "已将%s个实体围绕%s, %s分散,平均距离为%s个方块", + "commands.spreadplayers.success.teams": "已将%s支队伍围绕%s, %s分散,平均距离为%s个方块", + "commands.stop.stopping": "正在关闭服务器", + "commands.stopsound.success.source.any": "已停止播放所有“%s”的声音", + "commands.stopsound.success.source.sound": "已停止播放来源为“%2$s”的音效“%1$s”", + "commands.stopsound.success.sourceless.any": "已停止播放所有声音", + "commands.stopsound.success.sourceless.sound": "已停止播放声音“%s”", + "commands.summon.failed": "无法召唤实体", + "commands.summon.failed.uuid": "UUID重复,无法召唤实体", + "commands.summon.invalidPosition": "无效的召唤坐标", + "commands.summon.success": "召唤了新的%s", + "commands.tag.add.failed": "对象已拥有此标签或拥有过多标签", + "commands.tag.add.success.multiple": "已为%2$s个实体添加了标签“%1$s”", + "commands.tag.add.success.single": "已为%2$s添加了标签“%1$s”", + "commands.tag.list.multiple.empty": "%s个实体没有任何标签", + "commands.tag.list.multiple.success": "%s个实体拥有共计%s项标签:%s", + "commands.tag.list.single.empty": "%s没有标签", + "commands.tag.list.single.success": "%s拥有%s个标签:%s", + "commands.tag.remove.failed": "对象没有这个标签", + "commands.tag.remove.success.multiple": "已移除%2$s个实体的标签“%1$s”", + "commands.tag.remove.success.single": "已移除%2$s的标签“%1$s”", + "commands.team.add.duplicate": "已经存在同名队伍", + "commands.team.add.success": "已创建队伍%s", + "commands.team.empty.success": "已将%s名成员从队伍%s中移除", + "commands.team.empty.unchanged": "无变化,该队伍本就是空的", + "commands.team.join.success.multiple": "已将%s名成员加入队伍%s", + "commands.team.join.success.single": "已将%s加入队伍%s", + "commands.team.leave.success.multiple": "已将%s名成员从所有队伍中移除", + "commands.team.leave.success.single": "已将%s从所有队伍中移除", + "commands.team.list.members.empty": "队伍%s中没有成员", + "commands.team.list.members.success": "队伍%s含有%s名成员:%s", + "commands.team.list.teams.empty": "没有队伍存在", + "commands.team.list.teams.success": "共有%s支队伍:%s", + "commands.team.option.collisionRule.success": "队伍%s的碰撞规则现在是“%s”", + "commands.team.option.collisionRule.unchanged": "无变化,碰撞规则已经是此值", + "commands.team.option.color.success": "队伍%s的颜色已更新为%s", + "commands.team.option.color.unchanged": "无变化,此队伍本就为此颜色", + "commands.team.option.deathMessageVisibility.success": "队伍%s的死亡消息可见性现在为“%s”", + "commands.team.option.deathMessageVisibility.unchanged": "无变化,死亡消息的可见性已经是此值", + "commands.team.option.friendlyfire.alreadyDisabled": "无变化,友军伤害本就在此队伍上禁用", + "commands.team.option.friendlyfire.alreadyEnabled": "无变化,友军伤害本就在此队伍上启用", + "commands.team.option.friendlyfire.disabled": "已禁用队伍%s的友军伤害", + "commands.team.option.friendlyfire.enabled": "已启用队伍%s的友军伤害", + "commands.team.option.name.success": "已更新队伍%s的名称", + "commands.team.option.name.unchanged": "无变化,此队伍本就为该名称", + "commands.team.option.nametagVisibility.success": "队伍%s的名称标签可见性现在为“%s”", + "commands.team.option.nametagVisibility.unchanged": "无变化,名称标签的可见性已经是此值", + "commands.team.option.prefix.success": "队伍前缀已设为%s", + "commands.team.option.seeFriendlyInvisibles.alreadyDisabled": "无变化,此队伍本就不可看见隐身的队友", + "commands.team.option.seeFriendlyInvisibles.alreadyEnabled": "无变化,此队伍本就可以看见隐身的队友", + "commands.team.option.seeFriendlyInvisibles.disabled": "队伍%s不再可以看见隐身的队友了", + "commands.team.option.seeFriendlyInvisibles.enabled": "队伍%s现在可以看见隐身的队友了", + "commands.team.option.suffix.success": "队伍后缀已设为%s", + "commands.team.remove.success": "移除了队伍%s", + "commands.teammsg.failed.noteam": "你必须在一支队伍内才能发出队伍消息", + "commands.teleport.invalidPosition": "无效的传送坐标", + "commands.teleport.success.entity.multiple": "已传送%s个实体至%s", + "commands.teleport.success.entity.single": "已将%s传送至%s", + "commands.teleport.success.location.multiple": "已传送%s个实体至%s, %s, %s", + "commands.teleport.success.location.single": "已将%s传送至%s, %s, %s", + "commands.test.batch.starting": "正在启动环境%s批次%s", + "commands.test.clear.error.no_tests": "无法找到任何需要清除的测试", + "commands.test.clear.success": "清除了%s个结构", + "commands.test.coordinates": "%s, %s, %s", + "commands.test.coordinates.copy": "单击复制到剪贴板", + "commands.test.create.success": "已为测试%s创建测试设置", + "commands.test.error.no_test_containing_pos": "找不到包含%s, %s, %s的测试实例", + "commands.test.error.no_test_instances": "未找到测试实例", + "commands.test.error.non_existant_test": "测试实例%s不存在", + "commands.test.error.structure_not_found": "无法找到测试结构%s", + "commands.test.error.test_instance_not_found": "无法找到测试实例方块实体", + "commands.test.error.test_instance_not_found.position": "无法在%s, %s, %s处找到测试实例方块实体", + "commands.test.error.too_large": "结构在每条轴上的尺寸必须小于%s个方块", + "commands.test.locate.done": "定位完成,找到%s个结构", + "commands.test.locate.found": "在以下位置找到结构:%s(距离:%s)", + "commands.test.locate.started": "已开始定位测试结构,这可能需要一点时间…", + "commands.test.no_tests": "没有需要运行的测试", + "commands.test.relative_position": "相对于%s的位置:%s", + "commands.test.reset.error.no_tests": "无法找到任何需要重置的测试", + "commands.test.reset.success": "重置了%s个结构", + "commands.test.run.no_tests": "未找到测试", + "commands.test.run.running": "正在运行%s个测试…", + "commands.test.summary": "游戏测试完成!已运行%s个测试", + "commands.test.summary.all_required_passed": "必要测试均已通过 :)", + "commands.test.summary.failed": "%s个必要测试失败 :(", + "commands.test.summary.optional_failed": "%s个可选测试失败", + "commands.tick.query.percentiles": "百分位数:50%%:%s毫秒 95%%:%s毫秒 99%%:%s毫秒,共采样:%s", + "commands.tick.query.rate.running": "目标速率:%s刻每秒。\n每刻平均时间:%s毫秒(目标:%s毫秒)", + "commands.tick.query.rate.sprinting": "目标速率:%s刻每秒(已忽略,仅供参考)。\n每刻平均时间:%s毫秒", + "commands.tick.rate.success": "已设置目标速率为每秒%s刻", + "commands.tick.sprint.report": "游戏加速已完成,速率为每秒%s刻,即每刻%s毫秒", + "commands.tick.sprint.stop.fail": "没有正在执行的游戏刻加速", + "commands.tick.sprint.stop.success": "当前游戏刻加速已中止", + "commands.tick.status.frozen": "游戏冻结中", + "commands.tick.status.lagging": "游戏正在运行,但无法达到目标速率", + "commands.tick.status.running": "游戏正在以正常速度运行", + "commands.tick.status.sprinting": "游戏加速运行中", + "commands.tick.step.fail": "无法步进游戏,请先冻结游戏", + "commands.tick.step.stop.fail": "没有正在执行的游戏刻步进", + "commands.tick.step.stop.success": "当前游戏刻步进已中止", + "commands.tick.step.success": "正在步进%s刻", + "commands.time.query": "当前时间为%s", + "commands.time.set": "已将时间设为%s", + "commands.title.cleared.multiple": "已清除%s名玩家的标题", + "commands.title.cleared.single": "已清除%s的标题", + "commands.title.reset.multiple": "已重置%s名玩家的标题设置", + "commands.title.reset.single": "已重置%s的标题设置", + "commands.title.show.actionbar.multiple": "正在向%s名玩家显示新的动作栏标题", + "commands.title.show.actionbar.single": "正在向%s显示新的动作栏标题", + "commands.title.show.subtitle.multiple": "正在向%s名玩家显示新的副标题", + "commands.title.show.subtitle.single": "正在向%s显示新的副标题", + "commands.title.show.title.multiple": "正在向%s名玩家显示新的标题", + "commands.title.show.title.single": "正在向%s显示新的标题", + "commands.title.times.multiple": "已更改%s名玩家的标题显示时间", + "commands.title.times.single": "已更改%s的标题显示时间", + "commands.transfer.error.no_players": "必须至少指定一个待转移的玩家", + "commands.transfer.success.multiple": "正在转移%s个玩家至%s:%s", + "commands.transfer.success.single": "正在转移%s至%s:%s", + "commands.trigger.add.success": "已触发%s(数值已增加%s)", + "commands.trigger.failed.invalid": "你只能触发“trigger”类型的记分项", + "commands.trigger.failed.unprimed": "你尚无法触发这个记分项", + "commands.trigger.set.success": "已触发%s(数值已设为%s)", + "commands.trigger.simple.success": "已触发%s", + "commands.weather.set.clear": "天气已设为晴天", + "commands.weather.set.rain": "天气已设为雨天", + "commands.weather.set.thunder": "天气已设为雷雨", + "commands.whitelist.add.failed": "玩家已在白名单内", + "commands.whitelist.add.success": "已将%s加入白名单", + "commands.whitelist.alreadyOff": "白名单原已关闭", + "commands.whitelist.alreadyOn": "白名单原已开启", + "commands.whitelist.disabled": "白名单已关闭", + "commands.whitelist.enabled": "白名单已开启", + "commands.whitelist.list": "白名单中共有%s名玩家:%s", + "commands.whitelist.none": "白名单中没有玩家", + "commands.whitelist.reloaded": "已重新读取白名单", + "commands.whitelist.remove.failed": "玩家不在白名单内", + "commands.whitelist.remove.success": "已将%s移出白名单", + "commands.worldborder.center.failed": "无变化,世界边界中心点已经在该处", + "commands.worldborder.center.success": "已将世界边界的中心设为%s, %s", + "commands.worldborder.damage.amount.failed": "无变化,世界边界伤害已经是此值", + "commands.worldborder.damage.amount.success": "已将世界边界的伤害设置为%s每秒每个方块", + "commands.worldborder.damage.buffer.failed": "无变化,世界边界伤害缓冲区已经是此距离", + "commands.worldborder.damage.buffer.success": "已将世界边界的伤害缓冲区设置为%s个方块", + "commands.worldborder.get": "世界边界的当前宽度为%s个方块", + "commands.worldborder.set.failed.big": "世界边界的宽度不能大于%s格", + "commands.worldborder.set.failed.far": "世界边界的位置不能远于%s格", + "commands.worldborder.set.failed.nochange": "无变化,世界边界已经是此大小", + "commands.worldborder.set.failed.small": "世界边界的宽度不能小于1格", + "commands.worldborder.set.grow": "正在将世界边界的宽度扩大为%s个方块,时间%s秒", + "commands.worldborder.set.immediate": "已将世界边界的宽度设为%s", + "commands.worldborder.set.shrink": "正在将世界边界的宽度缩小为%s个方块,用时%s秒", + "commands.worldborder.warning.distance.failed": "无变化,世界边界伤害警告区已经是此距离", + "commands.worldborder.warning.distance.success": "已将世界边界的警告距离设置为%s个方块", + "commands.worldborder.warning.time.failed": "无变化,世界边界警告时间已经是此时长", + "commands.worldborder.warning.time.success": "已将世界边界的警告时间设置为%s秒", + "compliance.playtime.greaterThan24Hours": "你的游戏时长已超过24小时", + "compliance.playtime.hours": "你的游戏时长已达%s小时", + "compliance.playtime.message": "适度游戏益脑,沉迷游戏伤身", + "connect.aborted": "连接中断", + "connect.authorizing": "登录中…", + "connect.connecting": "正在连接到服务器…", + "connect.encrypting": "通讯加密中…", + "connect.failed": "无法连接至服务器", + "connect.failed.transfer": "转移到服务器时连接失败", + "connect.joining": "加入世界中…", + "connect.negotiating": "连接协商中…", + "connect.reconfiging": "重新配置中…", + "connect.reconfiguring": "重新配置中…", + "connect.transferring": "正在转移至新的服务器…", + "container.barrel": "木桶", + "container.beacon": "信标", + "container.beehive.bees": "蜜蜂:%s/%s", + "container.beehive.honey": "蜂蜜:%s/%s", + "container.blast_furnace": "高炉", + "container.brewing": "酿造台", + "container.cartography_table": "制图台", + "container.chest": "箱子", + "container.chestDouble": "大型箱子", + "container.crafter": "合成器", + "container.crafting": "合成", + "container.creative": "物品选栏", + "container.dispenser": "发射器", + "container.dropper": "投掷器", + "container.enchant": "附魔", + "container.enchant.clue": "%s…?", + "container.enchant.lapis.many": "花费:%s颗青金石", + "container.enchant.lapis.one": "花费:1颗青金石", + "container.enchant.level.many": "+ %s级经验", + "container.enchant.level.one": "+ 1级经验", + "container.enchant.level.requirement": "等级要求:%s", + "container.enderchest": "末影箱", + "container.furnace": "熔炉", + "container.grindstone_title": "修复和祛魔", + "container.hopper": "漏斗", + "container.inventory": "物品栏", + "container.isLocked": "%s已被上锁!", + "container.lectern": "讲台", + "container.loom": "织布机", + "container.repair": "修复和命名", + "container.repair.cost": "附魔花费:%1$s", + "container.repair.expensive": "过于昂贵!", + "container.shulkerBox": "潜影盒", + "container.shulkerBox.itemCount": "%s ×%s", + "container.shulkerBox.more": "还有%s项未显示…", + "container.shulkerBox.unknownContents": "???????", + "container.smoker": "烟熏炉", + "container.spectatorCantOpen": "无法打开:战利品尚未生成。", + "container.stonecutter": "切石机", + "container.upgrade": "升级装备", + "container.upgrade.error_tooltip": "该物品无法用此方式升级", + "container.upgrade.missing_template_tooltip": "放入锻造模板", + "controls.keybinds": "按键绑定…", + "controls.keybinds.duplicateKeybinds": "该按键也用于:\n%s", + "controls.keybinds.title": "按键绑定", + "controls.reset": "重置", + "controls.resetAll": "重置按键", + "controls.title": "按键控制", + "createWorld.customize.buffet.biome": "请选择一种生物群系", + "createWorld.customize.buffet.title": "自定义单一生物群系", + "createWorld.customize.flat.height": "高度", + "createWorld.customize.flat.layer": "%s", + "createWorld.customize.flat.layer.bottom": "底层 - %s", + "createWorld.customize.flat.layer.top": "顶层 - %s", + "createWorld.customize.flat.removeLayer": "移除层面", + "createWorld.customize.flat.tile": "此层的材料", + "createWorld.customize.flat.title": "自定义超平坦世界", + "createWorld.customize.presets": "预设", + "createWorld.customize.presets.list": "另外,这里是些我们早期制作好的!", + "createWorld.customize.presets.select": "使用预设", + "createWorld.customize.presets.share": "想要与别人分享你的预设方案吗?使用下面的输入框吧!", + "createWorld.customize.presets.title": "选择一种预设", + "createWorld.preparing": "正在准备生成世界…", + "createWorld.tab.game.title": "游戏", + "createWorld.tab.more.title": "更多", + "createWorld.tab.world.title": "世界", + "credits_and_attribution.button.attribution": "著作权说明", + "credits_and_attribution.button.credits": "鸣谢名单", + "credits_and_attribution.button.licenses": "许可协议", + "credits_and_attribution.screen.title": "鸣谢与著作权说明", + "dataPack.bundle.description": "启用实验性收纳袋物品", + "dataPack.bundle.name": "收纳袋", + "dataPack.minecart_improvements.description": "改进了矿车的运动", + "dataPack.minecart_improvements.name": "矿车改进", + "dataPack.redstone_experiments.description": "实验性红石更改", + "dataPack.redstone_experiments.name": "红石实验性内容", + "dataPack.title": "选择数据包", + "dataPack.trade_rebalance.description": "新版村民交易", + "dataPack.trade_rebalance.name": "村民交易的平衡性调整", + "dataPack.update_1_20.description": "Minecraft 1.20的新功能与内容", + "dataPack.update_1_20.name": "1.20更新", + "dataPack.update_1_21.description": "Minecraft 1.21的新功能与内容", + "dataPack.update_1_21.name": "1.21更新", + "dataPack.validation.back": "返回", + "dataPack.validation.failed": "数据包验证失败!", + "dataPack.validation.reset": "重置为默认", + "dataPack.validation.working": "正在验证已选的数据包…", + "dataPack.vanilla.description": "Minecraft的默认数据包", + "dataPack.vanilla.name": "默认", + "dataPack.winter_drop.description": "冬季小更新的新功能与内容", + "dataPack.winter_drop.name": "冬季小更新", + "datapackFailure.safeMode": "安全模式", + "datapackFailure.safeMode.failed.description": "这个世界包含无效或损坏的存档数据。", + "datapackFailure.safeMode.failed.title": "无法在安全模式下加载世界。", + "datapackFailure.title": "当前选中的数据包中出现了错误,导致世界无法加载。\n你可以尝试仅加载原版数据包(“安全模式”)或回到标题屏幕手动修复该问题。", + "death.attack.anvil": "%1$s被坠落的铁砧压扁了", + "death.attack.anvil.player": "%1$s在与%2$s战斗时被坠落的铁砧压扁了", + "death.attack.arrow": "%1$s被%2$s射杀", + "death.attack.arrow.item": "%1$s被%2$s用%3$s射杀", + "death.attack.badRespawnPoint.link": "刻意的游戏设计", + "death.attack.badRespawnPoint.message": "%1$s被%2$s杀死了", + "death.attack.cactus": "%1$s被戳死了", + "death.attack.cactus.player": "%1$s在试图逃离%2$s时撞上了仙人掌", + "death.attack.cramming": "%1$s因被过度挤压而死", + "death.attack.cramming.player": "%1$s被%2$s挤扁了", + "death.attack.dragonBreath": "%1$s被龙息烤熟了", + "death.attack.dragonBreath.player": "%1$s被%2$s的龙息烤熟了", + "death.attack.drown": "%1$s淹死了", + "death.attack.drown.player": "%1$s在试图逃离%2$s时淹死了", + "death.attack.dryout": "%1$s因脱水而死", + "death.attack.dryout.player": "%1$s在试图逃离%2$s时因脱水而死", + "death.attack.even_more_magic": "%1$s被不为人知的魔法杀死了", + "death.attack.explosion": "%1$s爆炸了", + "death.attack.explosion.player": "%1$s被%2$s炸死了", + "death.attack.explosion.player.item": "%1$s被%2$s用%3$s炸死了", + "death.attack.fall": "%1$s落地过猛", + "death.attack.fall.player": "%1$s在试图逃离%2$s时落地过猛", + "death.attack.fallingBlock": "%1$s被下落的方块压扁了", + "death.attack.fallingBlock.player": "%1$s在与%2$s战斗时被下落的方块压扁了", + "death.attack.fallingStalactite": "%1$s被坠落的钟乳石刺穿了", + "death.attack.fallingStalactite.player": "%1$s在与%2$s战斗时被坠落的钟乳石刺穿了", + "death.attack.fireball": "%1$s被%2$s用火球烧死了", + "death.attack.fireball.item": "%1$s被%2$s用%3$s发射的火球烧死了", + "death.attack.fireworks": "%1$s随着一声巨响消失了", + "death.attack.fireworks.item": "%1$s随着%2$s用%3$s发射的烟花发出的巨响消失了", + "death.attack.fireworks.player": "%1$s在与%2$s战斗时随着一声巨响消失了", + "death.attack.flyIntoWall": "%1$s感受到了动能", + "death.attack.flyIntoWall.player": "%1$s在试图逃离%2$s时感受到了动能", + "death.attack.freeze": "%1$s被冻死了", + "death.attack.freeze.player": "%1$s被%2$s冻死了", + "death.attack.generic": "%1$s死了", + "death.attack.generic.player": "%1$s死于%2$s", + "death.attack.genericKill": "%1$s被杀死了", + "death.attack.genericKill.player": "%1$s在与%2$s战斗时被杀死了", + "death.attack.hotFloor": "%1$s发现了地板是熔岩做的", + "death.attack.hotFloor.player": "%1$s因%2$s而步入危险之地", + "death.attack.inFire": "%1$s浴火焚身", + "death.attack.inFire.player": "%1$s在与%2$s战斗时不慎走入了火中", + "death.attack.inWall": "%1$s在墙里窒息而亡", + "death.attack.inWall.player": "%1$s在与%2$s战斗时在墙里窒息而亡", + "death.attack.indirectMagic": "%1$s被%2$s使用的魔法杀死了", + "death.attack.indirectMagic.item": "%1$s被%2$s用%3$s杀死了", + "death.attack.lava": "%1$s试图在熔岩里游泳", + "death.attack.lava.player": "%1$s在逃离%2$s时试图在熔岩里游泳", + "death.attack.lightningBolt": "%1$s被闪电击中", + "death.attack.lightningBolt.player": "%1$s在与%2$s战斗时被闪电击中", + "death.attack.mace_smash": "%1$s被%2$s一锤毙命", + "death.attack.mace_smash.item": "%1$s被%2$s用%3$s一锤毙命", + "death.attack.magic": "%1$s被魔法杀死了", + "death.attack.magic.player": "%1$s在试图逃离%2$s时被魔法杀死了", + "death.attack.message_too_long": "抱歉!消息太长,无法完整显示。截断后的消息:%s", + "death.attack.mob": "%1$s被%2$s杀死了", + "death.attack.mob.item": "%1$s被%2$s用%3$s杀死了", + "death.attack.onFire": "%1$s被烧死了", + "death.attack.onFire.item": "%1$s在与持有%3$s的%2$s战斗时被烤得酥脆", + "death.attack.onFire.player": "%1$s在与%2$s战斗时被烤得酥脆", + "death.attack.outOfWorld": "%1$s掉出了这个世界", + "death.attack.outOfWorld.player": "%1$s与%2$s不共戴天", + "death.attack.outsideBorder": "%1$s脱离了这个世界", + "death.attack.outsideBorder.player": "%1$s在与%2$s战斗时脱离了这个世界", + "death.attack.player": "%1$s被%2$s杀死了", + "death.attack.player.item": "%1$s被%2$s用%3$s杀死了", + "death.attack.sonic_boom": "%1$s被一道音波尖啸抹除了", + "death.attack.sonic_boom.item": "%1$s在试图逃离持有%3$s的%2$s时被一道音波尖啸抹除了", + "death.attack.sonic_boom.player": "%1$s在试图逃离%2$s时被一道音波尖啸抹除了", + "death.attack.stalagmite": "%1$s被石笋刺穿了", + "death.attack.stalagmite.player": "%1$s在与%2$s战斗时被石笋刺穿了", + "death.attack.starve": "%1$s饿死了", + "death.attack.starve.player": "%1$s在与%2$s战斗时饿死了", + "death.attack.sting": "%1$s被蛰死了", + "death.attack.sting.item": "%1$s被%2$s用%3$s蛰死了", + "death.attack.sting.player": "%1$s被%2$s蛰死了", + "death.attack.sweetBerryBush": "%1$s被甜浆果丛刺死了", + "death.attack.sweetBerryBush.player": "%1$s在试图逃离%2$s时被甜浆果丛刺死了", + "death.attack.thorns": "%1$s在试图伤害%2$s时被杀", + "death.attack.thorns.item": "%1$s在试图伤害%2$s时被%3$s杀死", + "death.attack.thrown": "%1$s被%2$s给砸死了", + "death.attack.thrown.item": "%1$s被%2$s用%3$s给砸死了", + "death.attack.trident": "%1$s被%2$s刺穿了", + "death.attack.trident.item": "%1$s被%2$s用%3$s刺穿了", + "death.attack.wither": "%1$s凋零了", + "death.attack.wither.player": "%1$s在与%2$s战斗时凋零了", + "death.attack.witherSkull": "%1$s被%2$s发射的头颅射杀", + "death.attack.witherSkull.item": "%1$s被%2$s用%3$s发射的头颅射杀", + "death.fell.accident.generic": "%1$s从高处摔了下来", + "death.fell.accident.ladder": "%1$s从梯子上摔了下来", + "death.fell.accident.other_climbable": "%1$s在攀爬时摔了下来", + "death.fell.accident.scaffolding": "%1$s从脚手架上摔了下来", + "death.fell.accident.twisting_vines": "%1$s从缠怨藤上摔了下来", + "death.fell.accident.vines": "%1$s从藤蔓上摔了下来", + "death.fell.accident.weeping_vines": "%1$s从垂泪藤上摔了下来", + "death.fell.assist": "%1$s因为%2$s注定要摔死", + "death.fell.assist.item": "%1$s因为%2$s使用了%3$s注定要摔死", + "death.fell.finish": "%1$s摔伤得太重并被%2$s完结了生命", + "death.fell.finish.item": "%1$s摔伤得太重并被%2$s用%3$s完结了生命", + "death.fell.killer": "%1$s注定要摔死", + "deathScreen.quit.confirm": "你确定要退出吗?", + "deathScreen.respawn": "重生", + "deathScreen.score": "分数", + "deathScreen.score.value": "分数:%s", + "deathScreen.spectate": "旁观世界", + "deathScreen.title": "你死了!", + "deathScreen.title.hardcore": "游戏结束!", + "deathScreen.titleScreen": "标题屏幕", + "debug.advanced_tooltips.help": "F3 + H = 显示高级提示框", + "debug.advanced_tooltips.off": "高级提示框:隐藏", + "debug.advanced_tooltips.on": "高级提示框:显示", + "debug.chunk_boundaries.help": "F3 + G = 显示区块边界", + "debug.chunk_boundaries.off": "区块边界:隐藏", + "debug.chunk_boundaries.on": "区块边界:显示", + "debug.clear_chat.help": "F3 + D = 清空聊天记录", + "debug.copy_location.help": "F3 + C = 用/tp命令的形式复制你的位置,按住F3 + C使游戏崩溃", + "debug.copy_location.message": "坐标已复制到剪贴板", + "debug.crash.message": "F3 + C已被按下。若不放开按键则会使游戏崩溃。", + "debug.crash.warning": "将在%s秒后崩溃…", + "debug.creative_spectator.error": "你没有切换游戏模式的权限", + "debug.creative_spectator.help": "F3 + N = 在上一个模式和旁观模式间切换", + "debug.dump_dynamic_textures": "已将动态纹理保存至%s", + "debug.dump_dynamic_textures.help": "F3 + S = 转储动态纹理", + "debug.gamemodes.error": "你没有权限打开游戏模式切换器", + "debug.gamemodes.help": "F3 + F4 = 打开游戏模式切换器", + "debug.gamemodes.press_f4": "[ F4 ]", + "debug.gamemodes.select_next": "%s 下一个", + "debug.help.help": "F3 + Q = 显示此列表", + "debug.help.message": "按键设置:", + "debug.inspect.client.block": "客户端方块数据已复制到剪贴板", + "debug.inspect.client.entity": "客户端实体数据已复制到剪贴板", + "debug.inspect.help": "F3 + I = 将实体或方块的数据复制到剪贴板", + "debug.inspect.server.block": "服务端方块数据已复制到剪贴板", + "debug.inspect.server.entity": "服务端实体数据已复制到剪贴板", + "debug.pause.help": "F3 + Esc = 暂停但不显示菜单(如果可以暂停的话)", + "debug.pause_focus.help": "F3 + P = 失去焦点时暂停", + "debug.pause_focus.off": "失去焦点时暂停:停用", + "debug.pause_focus.on": "失去焦点时暂停:启用", + "debug.prefix": "[调试]:", + "debug.profiling.help": "F3 + L = 开始/停止分析", + "debug.profiling.start": "分析已启动%s秒。使用F3 + L以提前结束", + "debug.profiling.stop": "分析已结束。结果已保存至%s", + "debug.reload_chunks.help": "F3 + A = 重新加载区块", + "debug.reload_chunks.message": "重新加载所有区块中", + "debug.reload_resourcepacks.help": "F3 + T = 重新加载资源包", + "debug.reload_resourcepacks.message": "已重新加载资源包", + "debug.show_hitboxes.help": "F3 + B = 显示判定箱", + "debug.show_hitboxes.off": "判定箱:隐藏", + "debug.show_hitboxes.on": "判定箱:显示", + "demo.day.1": "此试玩版会在5个游戏日后结束,尽力而为吧!", + "demo.day.2": "第二天", + "demo.day.3": "第三天", + "demo.day.4": "第四天", + "demo.day.5": "这是你游戏内的最后一天!", + "demo.day.6": "你已经度过了5个游戏日的试玩时间,按下%s来为你的成果截图留念。", + "demo.day.warning": "你的试玩时间即将结束!", + "demo.demoExpired": "试玩的时间结束了!", + "demo.help.buy": "即刻购买!", + "demo.help.fullWrapped": "这个试玩将会持续游戏内5天的时间(现实时间大约为1小时40分钟)。查看进度来获得提示!祝你玩得开心!", + "demo.help.inventory": "按%1$s来打开你的物品栏", + "demo.help.jump": "按%1$s来跳跃", + "demo.help.later": "继续游戏!", + "demo.help.movement": "使用%1$s、%2$s、%3$s、%4$s以及鼠标来移动", + "demo.help.movementMouse": "使用鼠标来环顾四周", + "demo.help.movementShort": "通过按下%1$s、%2$s、%3$s、%4$s来移动", + "demo.help.title": "Minecraft试玩模式", + "demo.remainingTime": "剩余时间:%s", + "demo.reminder": "试玩时间已经结束,请购买游戏来继续或开始一个新的世界!", + "difficulty.lock.question": "你确定你要锁定这个世界的难度吗?这会将这个世界的难度锁定为%1$s,并且永远无法再次改变难度。", + "difficulty.lock.title": "锁定世界难度", + "disconnect.endOfStream": "数据流终止", + "disconnect.exceeded_packet_rate": "由于超出数据包速率限制而被踢出游戏", + "disconnect.genericReason": "%s", + "disconnect.ignoring_status_request": "忽略状态请求", + "disconnect.loginFailedInfo": "登录失败:%s", + "disconnect.loginFailedInfo.insufficientPrivileges": "多人游戏已被禁用,请检查你的Microsoft账户设置。", + "disconnect.loginFailedInfo.invalidSession": "无效会话(请尝试重启游戏及启动器)", + "disconnect.loginFailedInfo.serversUnavailable": "暂时无法连接到身份验证服务器,请稍后再试。", + "disconnect.loginFailedInfo.userBanned": "你已被禁止进行多人游戏", + "disconnect.lost": "连接已丢失", + "disconnect.packetError": "网络协议错误", + "disconnect.spam": "由于滥发消息而被踢出游戏", + "disconnect.timeout": "连接超时", + "disconnect.transfer": "已转移至另一个服务器", + "disconnect.unknownHost": "未知的主机", + "download.pack.failed": "有%s个包下载失败,共%s个包", + "download.pack.progress.bytes": "进度:%s(总大小未知)", + "download.pack.progress.percent": "进度:%s%%", + "download.pack.title": "正在下载资源包%s/%s", + "editGamerule.default": "默认:%s", + "editGamerule.title": "编辑游戏规则", + "effect.duration.infinite": "∞", + "effect.minecraft.absorption": "伤害吸收", + "effect.minecraft.bad_omen": "不祥之兆", + "effect.minecraft.blindness": "失明", + "effect.minecraft.conduit_power": "潮涌能量", + "effect.minecraft.darkness": "黑暗", + "effect.minecraft.dolphins_grace": "海豚的恩惠", + "effect.minecraft.fire_resistance": "抗火", + "effect.minecraft.glowing": "发光", + "effect.minecraft.haste": "急迫", + "effect.minecraft.health_boost": "生命提升", + "effect.minecraft.hero_of_the_village": "村庄英雄", + "effect.minecraft.hunger": "饥饿", + "effect.minecraft.infested": "寄生", + "effect.minecraft.instant_damage": "瞬间伤害", + "effect.minecraft.instant_health": "瞬间治疗", + "effect.minecraft.invisibility": "隐身", + "effect.minecraft.jump_boost": "跳跃提升", + "effect.minecraft.levitation": "飘浮", + "effect.minecraft.luck": "幸运", + "effect.minecraft.mining_fatigue": "挖掘疲劳", + "effect.minecraft.nausea": "反胃", + "effect.minecraft.night_vision": "夜视", + "effect.minecraft.oozing": "渗浆", + "effect.minecraft.poison": "中毒", + "effect.minecraft.raid_omen": "袭击之兆", + "effect.minecraft.regeneration": "生命恢复", + "effect.minecraft.resistance": "抗性提升", + "effect.minecraft.saturation": "饱和", + "effect.minecraft.slow_falling": "缓降", + "effect.minecraft.slowness": "缓慢", + "effect.minecraft.speed": "迅捷", + "effect.minecraft.strength": "力量", + "effect.minecraft.trial_omen": "试炼之兆", + "effect.minecraft.unluck": "霉运", + "effect.minecraft.water_breathing": "水下呼吸", + "effect.minecraft.weakness": "虚弱", + "effect.minecraft.weaving": "盘丝", + "effect.minecraft.wind_charged": "蓄风", + "effect.minecraft.wither": "凋零", + "effect.none": "无效果", + "enchantment.level.1": "I", + "enchantment.level.10": "X", + "enchantment.level.2": "II", + "enchantment.level.3": "III", + "enchantment.level.4": "IV", + "enchantment.level.5": "V", + "enchantment.level.6": "VI", + "enchantment.level.7": "VII", + "enchantment.level.8": "VIII", + "enchantment.level.9": "IX", + "enchantment.minecraft.aqua_affinity": "水下速掘", + "enchantment.minecraft.bane_of_arthropods": "节肢杀手", + "enchantment.minecraft.binding_curse": "绑定诅咒", + "enchantment.minecraft.blast_protection": "爆炸保护", + "enchantment.minecraft.breach": "破甲", + "enchantment.minecraft.channeling": "引雷", + "enchantment.minecraft.density": "致密", + "enchantment.minecraft.depth_strider": "深海探索者", + "enchantment.minecraft.efficiency": "效率", + "enchantment.minecraft.feather_falling": "摔落缓冲", + "enchantment.minecraft.fire_aspect": "火焰附加", + "enchantment.minecraft.fire_protection": "火焰保护", + "enchantment.minecraft.flame": "火矢", + "enchantment.minecraft.fortune": "时运", + "enchantment.minecraft.frost_walker": "冰霜行者", + "enchantment.minecraft.impaling": "穿刺", + "enchantment.minecraft.infinity": "无限", + "enchantment.minecraft.knockback": "击退", + "enchantment.minecraft.looting": "抢夺", + "enchantment.minecraft.loyalty": "忠诚", + "enchantment.minecraft.luck_of_the_sea": "海之眷顾", + "enchantment.minecraft.lure": "饵钓", + "enchantment.minecraft.mending": "经验修补", + "enchantment.minecraft.multishot": "多重射击", + "enchantment.minecraft.piercing": "穿透", + "enchantment.minecraft.power": "力量", + "enchantment.minecraft.projectile_protection": "弹射物保护", + "enchantment.minecraft.protection": "保护", + "enchantment.minecraft.punch": "冲击", + "enchantment.minecraft.quick_charge": "快速装填", + "enchantment.minecraft.respiration": "水下呼吸", + "enchantment.minecraft.riptide": "激流", + "enchantment.minecraft.sharpness": "锋利", + "enchantment.minecraft.silk_touch": "精准采集", + "enchantment.minecraft.smite": "亡灵杀手", + "enchantment.minecraft.soul_speed": "灵魂疾行", + "enchantment.minecraft.sweeping": "横扫之刃", + "enchantment.minecraft.sweeping_edge": "横扫之刃", + "enchantment.minecraft.swift_sneak": "迅捷潜行", + "enchantment.minecraft.thorns": "荆棘", + "enchantment.minecraft.unbreaking": "耐久", + "enchantment.minecraft.vanishing_curse": "消失诅咒", + "enchantment.minecraft.wind_burst": "风爆", + "entity.minecraft.acacia_boat": "金合欢木船", + "entity.minecraft.acacia_chest_boat": "金合欢木运输船", + "entity.minecraft.allay": "悦灵", + "entity.minecraft.area_effect_cloud": "区域效果云", + "entity.minecraft.armadillo": "犰狳", + "entity.minecraft.armor_stand": "盔甲架", + "entity.minecraft.arrow": "箭", + "entity.minecraft.axolotl": "美西螈", + "entity.minecraft.bamboo_chest_raft": "运输竹筏", + "entity.minecraft.bamboo_raft": "竹筏", + "entity.minecraft.bat": "蝙蝠", + "entity.minecraft.bee": "蜜蜂", + "entity.minecraft.birch_boat": "白桦木船", + "entity.minecraft.birch_chest_boat": "白桦木运输船", + "entity.minecraft.blaze": "烈焰人", + "entity.minecraft.block_display": "方块展示实体", + "entity.minecraft.boat": "船", + "entity.minecraft.bogged": "沼骸", + "entity.minecraft.breeze": "旋风人", + "entity.minecraft.breeze_wind_charge": "风弹", + "entity.minecraft.camel": "骆驼", + "entity.minecraft.cat": "猫", + "entity.minecraft.cave_spider": "洞穴蜘蛛", + "entity.minecraft.cherry_boat": "樱花木船", + "entity.minecraft.cherry_chest_boat": "樱花木运输船", + "entity.minecraft.chest_boat": "运输船", + "entity.minecraft.chest_minecart": "运输矿车", + "entity.minecraft.chicken": "鸡", + "entity.minecraft.cod": "鳕鱼", + "entity.minecraft.command_block_minecart": "命令方块矿车", + "entity.minecraft.cow": "牛", + "entity.minecraft.creaking": "嘎枝", + "entity.minecraft.creaking_transient": "嘎枝", + "entity.minecraft.creeper": "苦力怕", + "entity.minecraft.dark_oak_boat": "深色橡木船", + "entity.minecraft.dark_oak_chest_boat": "深色橡木运输船", + "entity.minecraft.dolphin": "海豚", + "entity.minecraft.donkey": "驴", + "entity.minecraft.dragon_fireball": "末影龙火球", + "entity.minecraft.drowned": "溺尸", + "entity.minecraft.egg": "掷出的鸡蛋", + "entity.minecraft.elder_guardian": "远古守卫者", + "entity.minecraft.end_crystal": "末地水晶", + "entity.minecraft.ender_dragon": "末影龙", + "entity.minecraft.ender_pearl": "掷出的末影珍珠", + "entity.minecraft.enderman": "末影人", + "entity.minecraft.endermite": "末影螨", + "entity.minecraft.evoker": "唤魔者", + "entity.minecraft.evoker_fangs": "唤魔者尖牙", + "entity.minecraft.experience_bottle": "掷出的附魔之瓶", + "entity.minecraft.experience_orb": "经验球", + "entity.minecraft.eye_of_ender": "末影之眼", + "entity.minecraft.falling_block": "下落的方块", + "entity.minecraft.falling_block_type": "下落的%s", + "entity.minecraft.fireball": "火球", + "entity.minecraft.firework_rocket": "烟花火箭", + "entity.minecraft.fishing_bobber": "浮漂", + "entity.minecraft.fox": "狐狸", + "entity.minecraft.frog": "青蛙", + "entity.minecraft.furnace_minecart": "动力矿车", + "entity.minecraft.ghast": "恶魂", + "entity.minecraft.giant": "巨人", + "entity.minecraft.glow_item_frame": "荧光物品展示框", + "entity.minecraft.glow_squid": "发光鱿鱼", + "entity.minecraft.goat": "山羊", + "entity.minecraft.guardian": "守卫者", + "entity.minecraft.hoglin": "疣猪兽", + "entity.minecraft.hopper_minecart": "漏斗矿车", + "entity.minecraft.horse": "马", + "entity.minecraft.husk": "尸壳", + "entity.minecraft.illusioner": "幻术师", + "entity.minecraft.interaction": "交互实体", + "entity.minecraft.iron_golem": "铁傀儡", + "entity.minecraft.item": "物品", + "entity.minecraft.item_display": "物品展示实体", + "entity.minecraft.item_frame": "物品展示框", + "entity.minecraft.jungle_boat": "丛林木船", + "entity.minecraft.jungle_chest_boat": "丛林木运输船", + "entity.minecraft.killer_bunny": "杀手兔", + "entity.minecraft.leash_knot": "拴绳结", + "entity.minecraft.lightning_bolt": "闪电束", + "entity.minecraft.lingering_potion": "滞留药水", + "entity.minecraft.llama": "羊驼", + "entity.minecraft.llama_spit": "羊驼唾沫", + "entity.minecraft.magma_cube": "岩浆怪", + "entity.minecraft.mangrove_boat": "红树木船", + "entity.minecraft.mangrove_chest_boat": "红树木运输船", + "entity.minecraft.marker": "标记", + "entity.minecraft.minecart": "矿车", + "entity.minecraft.mooshroom": "哞菇", + "entity.minecraft.mule": "骡", + "entity.minecraft.oak_boat": "橡木船", + "entity.minecraft.oak_chest_boat": "橡木运输船", + "entity.minecraft.ocelot": "豹猫", + "entity.minecraft.ominous_item_spawner": "不祥之物生成器", + "entity.minecraft.painting": "画", + "entity.minecraft.pale_oak_boat": "苍白橡木船", + "entity.minecraft.pale_oak_chest_boat": "苍白橡木运输船", + "entity.minecraft.panda": "熊猫", + "entity.minecraft.parrot": "鹦鹉", + "entity.minecraft.phantom": "幻翼", + "entity.minecraft.pig": "猪", + "entity.minecraft.piglin": "猪灵", + "entity.minecraft.piglin_brute": "猪灵蛮兵", + "entity.minecraft.pillager": "掠夺者", + "entity.minecraft.player": "玩家", + "entity.minecraft.polar_bear": "北极熊", + "entity.minecraft.potion": "药水", + "entity.minecraft.pufferfish": "河豚", + "entity.minecraft.rabbit": "兔子", + "entity.minecraft.ravager": "劫掠兽", + "entity.minecraft.salmon": "鲑鱼", + "entity.minecraft.sheep": "绵羊", + "entity.minecraft.shulker": "潜影贝", + "entity.minecraft.shulker_bullet": "潜影弹", + "entity.minecraft.silverfish": "蠹虫", + "entity.minecraft.skeleton": "骷髅", + "entity.minecraft.skeleton_horse": "骷髅马", + "entity.minecraft.slime": "史莱姆", + "entity.minecraft.small_fireball": "小火球", + "entity.minecraft.sniffer": "嗅探兽", + "entity.minecraft.snow_golem": "雪傀儡", + "entity.minecraft.snowball": "雪球", + "entity.minecraft.spawner_minecart": "刷怪笼矿车", + "entity.minecraft.spectral_arrow": "光灵箭", + "entity.minecraft.spider": "蜘蛛", + "entity.minecraft.splash_potion": "喷溅药水", + "entity.minecraft.spruce_boat": "云杉木船", + "entity.minecraft.spruce_chest_boat": "云杉木运输船", + "entity.minecraft.squid": "鱿鱼", + "entity.minecraft.stray": "流浪者", + "entity.minecraft.strider": "炽足兽", + "entity.minecraft.tadpole": "蝌蚪", + "entity.minecraft.text_display": "文本展示实体", + "entity.minecraft.tnt": "被激活的TNT", + "entity.minecraft.tnt_minecart": "TNT矿车", + "entity.minecraft.trader_llama": "行商羊驼", + "entity.minecraft.trident": "三叉戟", + "entity.minecraft.tropical_fish": "热带鱼", + "entity.minecraft.tropical_fish.predefined.0": "海葵鱼", + "entity.minecraft.tropical_fish.predefined.1": "黑刺尾鲷", + "entity.minecraft.tropical_fish.predefined.10": "镰鱼", + "entity.minecraft.tropical_fish.predefined.11": "华丽蝴蝶鱼", + "entity.minecraft.tropical_fish.predefined.12": "鹦嘴鱼", + "entity.minecraft.tropical_fish.predefined.13": "额斑刺蝶鱼", + "entity.minecraft.tropical_fish.predefined.14": "红丽鱼", + "entity.minecraft.tropical_fish.predefined.15": "红唇真蛇鳚", + "entity.minecraft.tropical_fish.predefined.16": "红边笛鲷", + "entity.minecraft.tropical_fish.predefined.17": "马鲅", + "entity.minecraft.tropical_fish.predefined.18": "白条双锯鱼", + "entity.minecraft.tropical_fish.predefined.19": "鳞鲀", + "entity.minecraft.tropical_fish.predefined.2": "蓝刺尾鲷", + "entity.minecraft.tropical_fish.predefined.20": "高鳍鹦嘴鱼", + "entity.minecraft.tropical_fish.predefined.21": "黄刺尾鲷", + "entity.minecraft.tropical_fish.predefined.3": "蝴蝶鱼", + "entity.minecraft.tropical_fish.predefined.4": "丽鱼", + "entity.minecraft.tropical_fish.predefined.5": "小丑鱼", + "entity.minecraft.tropical_fish.predefined.6": "五彩搏鱼", + "entity.minecraft.tropical_fish.predefined.7": "绣雀鲷", + "entity.minecraft.tropical_fish.predefined.8": "川纹笛鲷", + "entity.minecraft.tropical_fish.predefined.9": "拟羊鱼", + "entity.minecraft.tropical_fish.type.betty": "背蒂类", + "entity.minecraft.tropical_fish.type.blockfish": "方身类", + "entity.minecraft.tropical_fish.type.brinely": "咸水类", + "entity.minecraft.tropical_fish.type.clayfish": "陶鱼类", + "entity.minecraft.tropical_fish.type.dasher": "速跃类", + "entity.minecraft.tropical_fish.type.flopper": "飞翼类", + "entity.minecraft.tropical_fish.type.glitter": "闪鳞类", + "entity.minecraft.tropical_fish.type.kob": "石首类", + "entity.minecraft.tropical_fish.type.snooper": "窥伺类", + "entity.minecraft.tropical_fish.type.spotty": "多斑类", + "entity.minecraft.tropical_fish.type.stripey": "条纹类", + "entity.minecraft.tropical_fish.type.sunstreak": "日纹类", + "entity.minecraft.turtle": "海龟", + "entity.minecraft.vex": "恼鬼", + "entity.minecraft.villager": "村民", + "entity.minecraft.villager.armorer": "盔甲匠", + "entity.minecraft.villager.butcher": "屠夫", + "entity.minecraft.villager.cartographer": "制图师", + "entity.minecraft.villager.cleric": "牧师", + "entity.minecraft.villager.farmer": "农民", + "entity.minecraft.villager.fisherman": "渔夫", + "entity.minecraft.villager.fletcher": "制箭师", + "entity.minecraft.villager.leatherworker": "皮匠", + "entity.minecraft.villager.librarian": "图书管理员", + "entity.minecraft.villager.mason": "石匠", + "entity.minecraft.villager.nitwit": "傻子", + "entity.minecraft.villager.none": "村民", + "entity.minecraft.villager.shepherd": "牧羊人", + "entity.minecraft.villager.toolsmith": "工具匠", + "entity.minecraft.villager.weaponsmith": "武器匠", + "entity.minecraft.vindicator": "卫道士", + "entity.minecraft.wandering_trader": "流浪商人", + "entity.minecraft.warden": "监守者", + "entity.minecraft.wind_charge": "风弹", + "entity.minecraft.witch": "女巫", + "entity.minecraft.wither": "凋灵", + "entity.minecraft.wither_skeleton": "凋灵骷髅", + "entity.minecraft.wither_skull": "凋灵之首", + "entity.minecraft.wolf": "狼", + "entity.minecraft.zoglin": "僵尸疣猪兽", + "entity.minecraft.zombie": "僵尸", + "entity.minecraft.zombie_horse": "僵尸马", + "entity.minecraft.zombie_villager": "僵尸村民", + "entity.minecraft.zombified_piglin": "僵尸猪灵", + "entity.not_summonable": "无法召唤类型为%s的实体", + "event.minecraft.raid": "袭击", + "event.minecraft.raid.defeat": "失败", + "event.minecraft.raid.defeat.full": "袭击 - 失败", + "event.minecraft.raid.raiders_remaining": "剩余%s名袭击者", + "event.minecraft.raid.victory": "胜利", + "event.minecraft.raid.victory.full": "袭击 - 胜利", + "filled_map.buried_treasure": "藏宝图", + "filled_map.explorer_jungle": "丛林探险家地图", + "filled_map.explorer_swamp": "沼泽探险家地图", + "filled_map.id": "编号#%s", + "filled_map.level": "(等级 %s/%s)", + "filled_map.locked": "已锁定", + "filled_map.mansion": "林地探险家地图", + "filled_map.monument": "海洋探险家地图", + "filled_map.scale": "比例尺1:%s", + "filled_map.trial_chambers": "试炼探险家地图", + "filled_map.unknown": "未知地图", + "filled_map.village_desert": "沙漠村庄地图", + "filled_map.village_plains": "平原村庄地图", + "filled_map.village_savanna": "热带草原村庄地图", + "filled_map.village_snowy": "雪原村庄地图", + "filled_map.village_taiga": "针叶林村庄地图", + "flat_world_preset.minecraft.bottomless_pit": "无底深渊", + "flat_world_preset.minecraft.classic_flat": "经典平坦", + "flat_world_preset.minecraft.desert": "沙漠", + "flat_world_preset.minecraft.overworld": "主世界", + "flat_world_preset.minecraft.redstone_ready": "红石俱备", + "flat_world_preset.minecraft.snowy_kingdom": "雪之王国", + "flat_world_preset.minecraft.the_void": "虚空", + "flat_world_preset.minecraft.tunnelers_dream": "挖掘工的梦想", + "flat_world_preset.minecraft.water_world": "水世界", + "flat_world_preset.unknown": "???", + "gameMode.adventure": "冒险模式", + "gameMode.changed": "你的游戏模式已被更新为%s", + "gameMode.creative": "创造模式", + "gameMode.hardcore": "极限模式", + "gameMode.spectator": "旁观模式", + "gameMode.survival": "生存模式", + "gamerule.announceAdvancements": "进度通知", + "gamerule.blockExplosionDropDecay": "在方块交互爆炸中,一些方块不会掉落战利品", + "gamerule.blockExplosionDropDecay.description": "在与方块交互引起的爆炸中,部分被破坏方块的掉落物会被炸毁。", + "gamerule.category.chat": "聊天", + "gamerule.category.drops": "掉落", + "gamerule.category.misc": "杂项", + "gamerule.category.mobs": "生物", + "gamerule.category.player": "玩家", + "gamerule.category.spawning": "生成", + "gamerule.category.updates": "世界更新", + "gamerule.commandBlockOutput": "广播命令方块输出", + "gamerule.commandModificationBlockLimit": "命令修改方块数量限制", + "gamerule.commandModificationBlockLimit.description": "单条命令(如fill和clone)最多能更改的方块数量", + "gamerule.disableElytraMovementCheck": "禁用鞘翅移动检测", + "gamerule.disablePlayerMovementCheck": "禁用玩家移动检测", + "gamerule.disableRaids": "禁用袭击", + "gamerule.doDaylightCycle": "游戏内时间流逝", + "gamerule.doEntityDrops": "非生物实体掉落", + "gamerule.doEntityDrops.description": "控制矿车(包括内容物)、物品展示框、船等的物品掉落。", + "gamerule.doFireTick": "火焰蔓延", + "gamerule.doImmediateRespawn": "立即重生", + "gamerule.doInsomnia": "生成幻翼", + "gamerule.doLimitedCrafting": "合成需要配方", + "gamerule.doLimitedCrafting.description": "若启用,玩家只能使用已解锁的配方合成。", + "gamerule.doMobLoot": "生物战利品掉落", + "gamerule.doMobLoot.description": "控制生物死亡后是否掉落资源,包括经验球。", + "gamerule.doMobSpawning": "生成生物", + "gamerule.doMobSpawning.description": "一些实体可能有其特定的规则。", + "gamerule.doPatrolSpawning": "生成灾厄巡逻队", + "gamerule.doTileDrops": "方块掉落", + "gamerule.doTileDrops.description": "控制破坏方块后是否掉落资源,包括经验球。", + "gamerule.doTraderSpawning": "生成流浪商人", + "gamerule.doVinesSpread": "藤蔓蔓延", + "gamerule.doVinesSpread.description": "控制藤蔓方块是否会随机向相邻的方块蔓延。不会影响其他藤蔓类方块(例如垂泪藤和缠怨藤等)。", + "gamerule.doWardenSpawning": "生成监守者", + "gamerule.doWeatherCycle": "天气更替", + "gamerule.drowningDamage": "溺水伤害", + "gamerule.enderPearlsVanishOnDeath": "掷出的末影珍珠在死亡时消失", + "gamerule.enderPearlsVanishOnDeath.description": "玩家投掷的末影珍珠是否在玩家死亡时消失。", + "gamerule.entitiesWithPassengersCanUsePortals": "被骑乘的实体能否使用传送门", + "gamerule.entitiesWithPassengersCanUsePortals.description": "允许被骑乘的实体通过下界传送门、末地传送门和末地折跃门传送。", + "gamerule.fallDamage": "摔落伤害", + "gamerule.fireDamage": "火焰伤害", + "gamerule.forgiveDeadPlayers": "宽恕死亡玩家", + "gamerule.forgiveDeadPlayers.description": "愤怒的中立生物将在其目标玩家于附近死亡后息怒。", + "gamerule.freezeDamage": "冰冻伤害", + "gamerule.globalSoundEvents": "全局声音事件", + "gamerule.globalSoundEvents.description": "特定游戏事件(如Boss生成)发生时,声音可在所有地方听见。", + "gamerule.keepInventory": "死亡后保留物品栏", + "gamerule.lavaSourceConversion": "允许流动熔岩转化为熔岩源", + "gamerule.lavaSourceConversion.description": "流动熔岩在两面与熔岩源相邻时转化为熔岩源。", + "gamerule.logAdminCommands": "通告管理员命令", + "gamerule.maxCommandChainLength": "命令连锁执行数量限制", + "gamerule.maxCommandChainLength.description": "应用于命令方块链和函数。", + "gamerule.maxCommandForkCount": "命令上下文数量限制", + "gamerule.maxCommandForkCount.description": "“execute as”等命令可以使用的上下文数量最大值。", + "gamerule.maxEntityCramming": "实体挤压上限", + "gamerule.minecartMaxSpeed": "矿车最大速度", + "gamerule.minecartMaxSpeed.description": "矿车在地面上移动的默认最大速度。", + "gamerule.mobExplosionDropDecay": "在生物爆炸中,一些方块不会掉落战利品", + "gamerule.mobExplosionDropDecay.description": "在生物引起的爆炸中,部分被破坏方块的掉落物会被炸毁。", + "gamerule.mobGriefing": "允许破坏性生物行为", + "gamerule.naturalRegeneration": "生命值自然恢复", + "gamerule.playersNetherPortalCreativeDelay": "创造模式下玩家在下界传送门中等待的时间", + "gamerule.playersNetherPortalCreativeDelay.description": "创造模式下的玩家通过下界传送门前往其他维度前需要站在其中等待的时间(以刻为单位)。", + "gamerule.playersNetherPortalDefaultDelay": "非创造模式下玩家在下界传送门中等待的时间", + "gamerule.playersNetherPortalDefaultDelay.description": "非创造模式下的玩家通过下界传送门前往其他维度前需要站在其中等待的时间(以刻为单位)。", + "gamerule.playersSleepingPercentage": "入睡占比", + "gamerule.playersSleepingPercentage.description": "跳过夜晚所需的入睡玩家占比。", + "gamerule.projectilesCanBreakBlocks": "弹射物能否破坏方块", + "gamerule.projectilesCanBreakBlocks.description": "控制弹射物是否能破坏可被其破坏的方块。", + "gamerule.randomTickSpeed": "随机刻速率", + "gamerule.reducedDebugInfo": "简化调试信息", + "gamerule.reducedDebugInfo.description": "限制调试屏幕内容。", + "gamerule.sendCommandFeedback": "发送命令反馈", + "gamerule.showDeathMessages": "显示死亡消息", + "gamerule.snowAccumulationHeight": "积雪厚度", + "gamerule.snowAccumulationHeight.description": "降雪时,地面上的雪最多堆积到此处指定的层数。", + "gamerule.spawnChunkRadius": "出生区块半径", + "gamerule.spawnChunkRadius.description": "主世界出生点周围保持加载的区块数量。", + "gamerule.spawnRadius": "重生点半径", + "gamerule.spawnRadius.description": "控制适合玩家生成的出生点周围区域大小。", + "gamerule.spectatorsGenerateChunks": "允许旁观者生成地形", + "gamerule.tntExplosionDropDecay": "在TNT爆炸中,一些方块不会掉落战利品", + "gamerule.tntExplosionDropDecay.description": "在TNT引起的爆炸中,部分被破坏方块的掉落物会被炸毁。", + "gamerule.universalAnger": "无差别愤怒", + "gamerule.universalAnger.description": "愤怒的中立生物将攻击附近的所有玩家,而不再限于激怒它们的玩家。禁用“宽恕死亡玩家”可达到最佳效果。", + "gamerule.waterSourceConversion": "允许流动水转化为水源", + "gamerule.waterSourceConversion.description": "流动水在两面与水源相邻时转化为水源。", + "generator.custom": "自定义", + "generator.customized": "旧版自定义", + "generator.minecraft.amplified": "放大化", + "generator.minecraft.amplified.info": "注意:仅供娱乐!需要强劲的电脑。", + "generator.minecraft.debug_all_block_states": "调试模式", + "generator.minecraft.flat": "超平坦", + "generator.minecraft.large_biomes": "巨型生物群系", + "generator.minecraft.normal": "默认", + "generator.minecraft.single_biome_surface": "单一生物群系", + "generator.single_biome_caves": "洞穴", + "generator.single_biome_floating_islands": "浮岛", + "gui.abuseReport.attestation": "提交此举报,即代表你确认你提供的信息已尽可能准确和完整。", + "gui.abuseReport.comments": "留言", + "gui.abuseReport.describe": "提供详情可以帮助我们更严谨地做出决定。", + "gui.abuseReport.discard.content": "如果离开,该举报及留言将不会被保留。\n确定要离开吗?", + "gui.abuseReport.discard.discard": "离开并放弃举报", + "gui.abuseReport.discard.draft": "保存为草稿", + "gui.abuseReport.discard.return": "继续编辑", + "gui.abuseReport.discard.title": "放弃举报和留言?", + "gui.abuseReport.draft.content": "继续编辑现有举报还是放弃并新建另一份举报?", + "gui.abuseReport.draft.discard": "放弃", + "gui.abuseReport.draft.edit": "继续编辑", + "gui.abuseReport.draft.quittotitle.content": "继续编辑还是放弃?", + "gui.abuseReport.draft.quittotitle.title": "该聊天举报草稿将在退出时丢失", + "gui.abuseReport.draft.title": "编辑聊天举报草稿?", + "gui.abuseReport.error.title": "发送举报时出现问题", + "gui.abuseReport.message": "你在哪里观察到了不良行为?\n这有助于我们调查你的案例。", + "gui.abuseReport.more_comments": "请描述发生的状况:", + "gui.abuseReport.name.comment_box_label": "请描述举报该名称的理由:", + "gui.abuseReport.name.reporting": "正在举报“%s”。", + "gui.abuseReport.name.title": "举报玩家名称", + "gui.abuseReport.observed_what": "你为什么要举报?", + "gui.abuseReport.read_info": "了解举报功能", + "gui.abuseReport.reason.alcohol_tobacco_drugs": "吸毒或饮酒", + "gui.abuseReport.reason.alcohol_tobacco_drugs.description": "有人教唆他人涉毒或教唆未成年人饮酒。", + "gui.abuseReport.reason.child_sexual_exploitation_or_abuse": "对儿童的性剥削或虐待", + "gui.abuseReport.reason.child_sexual_exploitation_or_abuse.description": "有人谈论或以其他方式宣扬涉及儿童的不当行为。", + "gui.abuseReport.reason.defamation_impersonation_false_information": "诽谤", + "gui.abuseReport.reason.defamation_impersonation_false_information.description": "有人以利用或误导他人为目的,正在损害你或他人的名誉,例如散布虚假信息。", + "gui.abuseReport.reason.description": "描述:", + "gui.abuseReport.reason.false_reporting": "不实举报", + "gui.abuseReport.reason.generic": "我想举报该玩家", + "gui.abuseReport.reason.generic.description": "该玩家使我恼怒或做的事情使我反感。", + "gui.abuseReport.reason.harassment_or_bullying": "骚扰或霸凌", + "gui.abuseReport.reason.harassment_or_bullying.description": "有人羞辱、攻击、霸凌你或其他人。这包括在未经允许的情况下不断尝试联系你或其他人,或发布你或其他人的隐私信息。", + "gui.abuseReport.reason.hate_speech": "仇恨言论", + "gui.abuseReport.reason.hate_speech.description": "有人因身份要素(如宗教信仰、种族或性相关)攻击你或其他玩家。", + "gui.abuseReport.reason.imminent_harm": "威胁伤害他人", + "gui.abuseReport.reason.imminent_harm.description": "有人威胁要在现实生活中伤害你或其他人。", + "gui.abuseReport.reason.narration": "%s:%s", + "gui.abuseReport.reason.non_consensual_intimate_imagery": "未经同意发布私密图像", + "gui.abuseReport.reason.non_consensual_intimate_imagery.description": "有人谈论、分享私密或亲密的图像,或以其他方式宣扬有关行为。", + "gui.abuseReport.reason.self_harm_or_suicide": "自残或自杀", + "gui.abuseReport.reason.self_harm_or_suicide.description": "有人威胁要自残或谈论现实生活中的自残行为。", + "gui.abuseReport.reason.sexually_inappropriate": "性相关不当内容", + "gui.abuseReport.reason.sexually_inappropriate.description": "皮肤图案与性行为、性器官和性暴力相关。", + "gui.abuseReport.reason.terrorism_or_violent_extremism": "恐怖主义或暴力极端主义", + "gui.abuseReport.reason.terrorism_or_violent_extremism.description": "有人因政治、宗教、意识形态或其他原因,谈论、宣扬、威胁实施恐怖主义或极端暴力行为。", + "gui.abuseReport.reason.title": "选择举报类型", + "gui.abuseReport.report_sent_msg": "我们已经成功收到你的举报。非常感谢!\n\n我们的团队将尽快进行审核。", + "gui.abuseReport.select_reason": "选择举报类型", + "gui.abuseReport.send": "发送举报", + "gui.abuseReport.send.comment_too_long": "请缩短留言", + "gui.abuseReport.send.error_message": "发送举报时返回了错误:\n“%s”", + "gui.abuseReport.send.generic_error": "发送举报时遇到意外错误。", + "gui.abuseReport.send.http_error": "发送举报时发生了意外的HTTP错误。", + "gui.abuseReport.send.json_error": "发送举报时遇到了格式错误的负载。", + "gui.abuseReport.send.no_reason": "请选择举报类型", + "gui.abuseReport.send.not_attested": "请先阅读上方文本并勾选复选框后,再发送举报", + "gui.abuseReport.send.service_unavailable": "无法使用举报服务。请检查是否已联网,然后重试。", + "gui.abuseReport.sending.title": "发送举报中…", + "gui.abuseReport.sent.title": "举报已发送", + "gui.abuseReport.skin.title": "举报玩家皮肤", + "gui.abuseReport.title": "举报玩家", + "gui.abuseReport.type.chat": "聊天消息", + "gui.abuseReport.type.name": "玩家名称", + "gui.abuseReport.type.skin": "玩家皮肤", + "gui.acknowledge": "了解", + "gui.advancements": "进度", + "gui.all": "全部", + "gui.back": "返回", + "gui.banned.description": "%s\n\n%s\n\n点击以下链接了解更多:%s", + "gui.banned.description.permanent": "你的账户已被永久封禁,无法进行多人游戏或加入Realms。", + "gui.banned.description.reason": "我们最近收到举报称你的账户存在不良行为。我们的监督员已审核你的案件并将其认定为%s,这违反了Minecraft社区准则。", + "gui.banned.description.reason_id": "代码:%s", + "gui.banned.description.reason_id_message": "代码:%s - %s", + "gui.banned.description.temporary": "%s期限结束前,你无法进行多人游戏或加入Realms。", + "gui.banned.description.temporary.duration": "你的账户已被暂时封禁并将于%s后解封。", + "gui.banned.description.unknownreason": "我们最近收到举报称你的账户存在不良行为。我们的监督员已审核你的案件并认定你违反了Minecraft社区准则。", + "gui.banned.name.description": "你当前的名称(“%s”)违反了我们的社区准则。你可以进行单人游戏,但需要更改名称才能进行在线游戏。\n\n通过以下链接了解更多或提交案件审查:%s", + "gui.banned.name.title": "多人游戏中不允许使用当前名称", + "gui.banned.reason.defamation_impersonation_false_information": "假冒或分享信息以利用或误导他人", + "gui.banned.reason.drugs": "推销非法药品", + "gui.banned.reason.extreme_violence_or_gore": "描述现实生活中过度暴力或血腥的场面", + "gui.banned.reason.false_reporting": "过多虚假或不实举报", + "gui.banned.reason.fraud": "欺骗性获取或使用内容", + "gui.banned.reason.generic_violation": "违反社区准则", + "gui.banned.reason.harassment_or_bullying": "有针对性地、有害地使用辱骂性语言", + "gui.banned.reason.hate_speech": "仇恨或歧视言论", + "gui.banned.reason.hate_terrorism_notorious_figure": "推广仇恨群体、恐怖组织或不法分子", + "gui.banned.reason.imminent_harm_to_person_or_property": "意图在现实生活中造成人身或财产伤害", + "gui.banned.reason.nudity_or_pornography": "展示淫秽或色情材料", + "gui.banned.reason.sexually_inappropriate": "性相关话题或内容", + "gui.banned.reason.spam_or_advertising": "滥发消息或广告宣传", + "gui.banned.skin.description": "你当前的皮肤违反了我们的社区准则。你仍然可以使用默认皮肤,或者选择新皮肤进行游戏。\n\n通过以下链接了解更多或提交案件审查:%s", + "gui.banned.skin.title": "当前皮肤不允许使用", + "gui.banned.title.permanent": "账户已被永久封禁", + "gui.banned.title.temporary": "账户已被暂时封禁", + "gui.cancel": "取消", + "gui.chatReport.comments": "留言", + "gui.chatReport.describe": "提供详情可以帮助我们更严谨地做出决定。", + "gui.chatReport.discard.content": "如果离开,该举报及留言将不会被保留。\n确定要离开吗?", + "gui.chatReport.discard.discard": "离开并放弃举报", + "gui.chatReport.discard.draft": "保存为草稿", + "gui.chatReport.discard.return": "继续编辑", + "gui.chatReport.discard.title": "放弃举报和留言?", + "gui.chatReport.draft.content": "继续编辑现有举报还是放弃并新建另一份举报?", + "gui.chatReport.draft.discard": "放弃", + "gui.chatReport.draft.edit": "继续编辑", + "gui.chatReport.draft.quittotitle.content": "继续编辑还是放弃?", + "gui.chatReport.draft.quittotitle.title": "你有一份聊天举报草稿将在退出时丢失", + "gui.chatReport.draft.title": "编辑聊天举报草稿?", + "gui.chatReport.more_comments": "请描述发生的状况:", + "gui.chatReport.observed_what": "你为什么要举报?", + "gui.chatReport.read_info": "了解举报功能", + "gui.chatReport.report_sent_msg": "我们已经成功收到你的举报。非常感谢!\n\n我们的团队会尽快进行审核。", + "gui.chatReport.select_chat": "选择要举报的聊天消息", + "gui.chatReport.select_reason": "选择举报类型", + "gui.chatReport.selected_chat": "已选择%s条要举报的聊天消息", + "gui.chatReport.send": "发送举报", + "gui.chatReport.send.comments_too_long": "请缩短留言", + "gui.chatReport.send.no_reason": "请选择举报类型", + "gui.chatReport.send.no_reported_messages": "请选择至少一条要举报的聊天消息", + "gui.chatReport.send.too_many_messages": "举报中包含的消息过多", + "gui.chatReport.title": "举报玩家聊天", + "gui.chatSelection.context": "所选消息前后的部分消息将会用于提供额外的辅助信息", + "gui.chatSelection.fold": "已隐藏%s条消息", + "gui.chatSelection.heading": "%s %s", + "gui.chatSelection.join": "%s加入了聊天", + "gui.chatSelection.message.narrate": "%1$s在%3$s说:%2$s", + "gui.chatSelection.selected": "已选中%s条消息,共%s条", + "gui.chatSelection.title": "选择要举报的聊天消息", + "gui.continue": "继续", + "gui.copy_link_to_clipboard": "复制链接到剪贴板", + "gui.days": "%s天", + "gui.done": "完成", + "gui.down": "向下", + "gui.entity_tooltip.type": "类型:%s", + "gui.fileDropFailure.detail": "拒绝了%s个文件", + "gui.fileDropFailure.title": "添加文件失败", + "gui.hours": "%s小时", + "gui.loadingMinecraft": "正在加载Minecraft", + "gui.minutes": "%s分钟", + "gui.multiLineEditBox.character_limit": "%s/%s", + "gui.narrate.button": "%s按钮", + "gui.narrate.editBox": "%s编辑框:%s", + "gui.narrate.slider": "%s滑块", + "gui.narrate.tab": "%s标签页", + "gui.no": "否", + "gui.none": "无", + "gui.ok": "确定", + "gui.open_report_dir": "打开报告目录", + "gui.proceed": "继续", + "gui.recipebook.moreRecipes": "单击鼠标右键获取更多信息", + "gui.recipebook.page": "%s/%s", + "gui.recipebook.search_hint": "搜索…", + "gui.recipebook.toggleRecipes.all": "显示全部", + "gui.recipebook.toggleRecipes.blastable": "仅显示可冶炼", + "gui.recipebook.toggleRecipes.craftable": "仅显示可合成", + "gui.recipebook.toggleRecipes.smeltable": "仅显示可烧炼", + "gui.recipebook.toggleRecipes.smokable": "仅显示可熏制", + "gui.report_to_server": "向服务器报告", + "gui.socialInteractions.blocking_hint": "使用Microsoft账户管理", + "gui.socialInteractions.empty_blocked": "未屏蔽任何玩家的聊天消息", + "gui.socialInteractions.empty_hidden": "未隐藏任何玩家的聊天消息", + "gui.socialInteractions.hidden_in_chat": "%s的聊天消息将会被隐藏", + "gui.socialInteractions.hide": "在聊天中隐藏", + "gui.socialInteractions.narration.hide": "隐藏%s发送的消息", + "gui.socialInteractions.narration.report": "举报玩家%s", + "gui.socialInteractions.narration.show": "显示%s发送的消息", + "gui.socialInteractions.report": "举报", + "gui.socialInteractions.search_empty": "未找到使用此名称的玩家", + "gui.socialInteractions.search_hint": "搜索…", + "gui.socialInteractions.server_label.multiple": "%s - %s名玩家", + "gui.socialInteractions.server_label.single": "%s - %s名玩家", + "gui.socialInteractions.show": "在聊天中显示", + "gui.socialInteractions.shown_in_chat": "%s的聊天消息将会被显示", + "gui.socialInteractions.status_blocked": "已屏蔽", + "gui.socialInteractions.status_blocked_offline": "已屏蔽 - 离线", + "gui.socialInteractions.status_hidden": "隐藏", + "gui.socialInteractions.status_hidden_offline": "隐藏 - 离线", + "gui.socialInteractions.status_offline": "离线", + "gui.socialInteractions.tab_all": "全部", + "gui.socialInteractions.tab_blocked": "已屏蔽", + "gui.socialInteractions.tab_hidden": "已隐藏", + "gui.socialInteractions.title": "社交", + "gui.socialInteractions.tooltip.hide": "隐藏消息", + "gui.socialInteractions.tooltip.report": "举报玩家", + "gui.socialInteractions.tooltip.report.disabled": "举报服务不可用", + "gui.socialInteractions.tooltip.report.no_messages": "玩家%s没有可举报的消息", + "gui.socialInteractions.tooltip.report.not_reportable": "无法举报该玩家,服务器无法验证其聊天消息。", + "gui.socialInteractions.tooltip.show": "显示消息", + "gui.stats": "统计信息", + "gui.toMenu": "返回到服务器列表", + "gui.toRealms": "返回到Realm列表", + "gui.toTitle": "返回到标题屏幕", + "gui.toWorld": "返回到世界列表", + "gui.togglable_slot": "点击以禁用槽位", + "gui.up": "向上", + "gui.yes": "是", + "hanging_sign.edit": "编辑悬挂式告示牌消息", + "instrument.minecraft.admire_goat_horn": "仰慕", + "instrument.minecraft.call_goat_horn": "呼唤", + "instrument.minecraft.dream_goat_horn": "想象", + "instrument.minecraft.feel_goat_horn": "感受", + "instrument.minecraft.ponder_goat_horn": "沉思", + "instrument.minecraft.seek_goat_horn": "寻觅", + "instrument.minecraft.sing_goat_horn": "歌颂", + "instrument.minecraft.yearn_goat_horn": "憧憬", + "inventory.binSlot": "摧毁物品", + "inventory.hotbarInfo": "用%1$s+%2$s来保存快捷栏", + "inventory.hotbarSaved": "已保存物品快捷栏(用%1$s+%2$s来加载)", + "item.canBreak": "能破坏:", + "item.canPlace": "可以放在:", + "item.canUse.unknown": "未知", + "item.color": "颜色:%s", + "item.components": "%s个组件", + "item.disabled": "已禁用物品", + "item.durability": "耐久度:%s / %s", + "item.dyed": "已染色", + "item.minecraft.acacia_boat": "金合欢木船", + "item.minecraft.acacia_chest_boat": "金合欢木运输船", + "item.minecraft.allay_spawn_egg": "悦灵刷怪蛋", + "item.minecraft.amethyst_shard": "紫水晶碎片", + "item.minecraft.angler_pottery_shard": "垂钓纹样陶片", + "item.minecraft.angler_pottery_sherd": "垂钓纹样陶片", + "item.minecraft.apple": "苹果", + "item.minecraft.archer_pottery_shard": "弓箭纹样陶片", + "item.minecraft.archer_pottery_sherd": "弓箭纹样陶片", + "item.minecraft.armadillo_scute": "犰狳鳞甲", + "item.minecraft.armadillo_spawn_egg": "犰狳刷怪蛋", + "item.minecraft.armor_stand": "盔甲架", + "item.minecraft.arms_up_pottery_shard": "举臂纹样陶片", + "item.minecraft.arms_up_pottery_sherd": "举臂纹样陶片", + "item.minecraft.arrow": "箭", + "item.minecraft.axolotl_bucket": "美西螈桶", + "item.minecraft.axolotl_spawn_egg": "美西螈刷怪蛋", + "item.minecraft.baked_potato": "烤马铃薯", + "item.minecraft.bamboo_chest_raft": "运输竹筏", + "item.minecraft.bamboo_raft": "竹筏", + "item.minecraft.bat_spawn_egg": "蝙蝠刷怪蛋", + "item.minecraft.bee_spawn_egg": "蜜蜂刷怪蛋", + "item.minecraft.beef": "生牛肉", + "item.minecraft.beetroot": "甜菜根", + "item.minecraft.beetroot_seeds": "甜菜种子", + "item.minecraft.beetroot_soup": "甜菜汤", + "item.minecraft.birch_boat": "白桦木船", + "item.minecraft.birch_chest_boat": "白桦木运输船", + "item.minecraft.black_bundle": "黑色收纳袋", + "item.minecraft.black_dye": "黑色染料", + "item.minecraft.blade_pottery_shard": "利刃纹样陶片", + "item.minecraft.blade_pottery_sherd": "利刃纹样陶片", + "item.minecraft.blaze_powder": "烈焰粉", + "item.minecraft.blaze_rod": "烈焰棒", + "item.minecraft.blaze_spawn_egg": "烈焰人刷怪蛋", + "item.minecraft.blue_bundle": "蓝色收纳袋", + "item.minecraft.blue_dye": "蓝色染料", + "item.minecraft.bogged_spawn_egg": "沼骸刷怪蛋", + "item.minecraft.bolt_armor_trim_smithing_template": "锻造模板", + "item.minecraft.bolt_armor_trim_smithing_template.new": "镶铆盔甲纹饰", + "item.minecraft.bone": "骨头", + "item.minecraft.bone_meal": "骨粉", + "item.minecraft.book": "书", + "item.minecraft.bordure_indented_banner_pattern": "波纹边旗帜图案", + "item.minecraft.bow": "弓", + "item.minecraft.bowl": "碗", + "item.minecraft.bread": "面包", + "item.minecraft.breeze_rod": "旋风棒", + "item.minecraft.breeze_spawn_egg": "旋风人刷怪蛋", + "item.minecraft.brewer_pottery_shard": "佳酿纹样陶片", + "item.minecraft.brewer_pottery_sherd": "佳酿纹样陶片", + "item.minecraft.brewing_stand": "酿造台", + "item.minecraft.brick": "红砖", + "item.minecraft.brown_bundle": "棕色收纳袋", + "item.minecraft.brown_dye": "棕色染料", + "item.minecraft.brush": "刷子", + "item.minecraft.bucket": "铁桶", + "item.minecraft.bundle": "收纳袋", + "item.minecraft.bundle.empty": "空", + "item.minecraft.bundle.empty.description": "可容纳一组混合的物品", + "item.minecraft.bundle.full": "满", + "item.minecraft.bundle.fullness": "%s/%s", + "item.minecraft.burn_pottery_shard": "烈焰纹样陶片", + "item.minecraft.burn_pottery_sherd": "烈焰纹样陶片", + "item.minecraft.camel_spawn_egg": "骆驼刷怪蛋", + "item.minecraft.carrot": "胡萝卜", + "item.minecraft.carrot_on_a_stick": "胡萝卜钓竿", + "item.minecraft.cat_spawn_egg": "猫刷怪蛋", + "item.minecraft.cauldron": "炼药锅", + "item.minecraft.cave_spider_spawn_egg": "洞穴蜘蛛刷怪蛋", + "item.minecraft.chainmail_boots": "锁链靴子", + "item.minecraft.chainmail_chestplate": "锁链胸甲", + "item.minecraft.chainmail_helmet": "锁链头盔", + "item.minecraft.chainmail_leggings": "锁链护腿", + "item.minecraft.charcoal": "木炭", + "item.minecraft.cherry_boat": "樱花木船", + "item.minecraft.cherry_chest_boat": "樱花木运输船", + "item.minecraft.chest_minecart": "运输矿车", + "item.minecraft.chicken": "生鸡肉", + "item.minecraft.chicken_spawn_egg": "鸡刷怪蛋", + "item.minecraft.chorus_fruit": "紫颂果", + "item.minecraft.clay_ball": "黏土球", + "item.minecraft.clock": "时钟", + "item.minecraft.coal": "煤炭", + "item.minecraft.coast_armor_trim_smithing_template": "锻造模板", + "item.minecraft.coast_armor_trim_smithing_template.new": "海岸盔甲纹饰", + "item.minecraft.cocoa_beans": "可可豆", + "item.minecraft.cod": "生鳕鱼", + "item.minecraft.cod_bucket": "鳕鱼桶", + "item.minecraft.cod_spawn_egg": "鳕鱼刷怪蛋", + "item.minecraft.command_block_minecart": "命令方块矿车", + "item.minecraft.compass": "指南针", + "item.minecraft.cooked_beef": "牛排", + "item.minecraft.cooked_chicken": "熟鸡肉", + "item.minecraft.cooked_cod": "熟鳕鱼", + "item.minecraft.cooked_mutton": "熟羊肉", + "item.minecraft.cooked_porkchop": "熟猪排", + "item.minecraft.cooked_rabbit": "熟兔肉", + "item.minecraft.cooked_salmon": "熟鲑鱼", + "item.minecraft.cookie": "曲奇", + "item.minecraft.copper_ingot": "铜锭", + "item.minecraft.cow_spawn_egg": "牛刷怪蛋", + "item.minecraft.creaking_spawn_egg": "嘎枝刷怪蛋", + "item.minecraft.creeper_banner_pattern": "旗帜图案", + "item.minecraft.creeper_banner_pattern.desc": "苦力怕盾徽", + "item.minecraft.creeper_banner_pattern.new": "苦力怕盾徽旗帜图案", + "item.minecraft.creeper_spawn_egg": "苦力怕刷怪蛋", + "item.minecraft.crossbow": "弩", + "item.minecraft.crossbow.projectile": "弹射物:", + "item.minecraft.cyan_bundle": "青色收纳袋", + "item.minecraft.cyan_dye": "青色染料", + "item.minecraft.danger_pottery_shard": "危机纹样陶片", + "item.minecraft.danger_pottery_sherd": "危机纹样陶片", + "item.minecraft.dark_oak_boat": "深色橡木船", + "item.minecraft.dark_oak_chest_boat": "深色橡木运输船", + "item.minecraft.debug_stick": "调试棒", + "item.minecraft.debug_stick.empty": "%s不具备属性", + "item.minecraft.debug_stick.select": "已选择“%s”(%s)", + "item.minecraft.debug_stick.update": "“%s”设为%s", + "item.minecraft.diamond": "钻石", + "item.minecraft.diamond_axe": "钻石斧", + "item.minecraft.diamond_boots": "钻石靴子", + "item.minecraft.diamond_chestplate": "钻石胸甲", + "item.minecraft.diamond_helmet": "钻石头盔", + "item.minecraft.diamond_hoe": "钻石锄", + "item.minecraft.diamond_horse_armor": "钻石马铠", + "item.minecraft.diamond_leggings": "钻石护腿", + "item.minecraft.diamond_pickaxe": "钻石镐", + "item.minecraft.diamond_shovel": "钻石锹", + "item.minecraft.diamond_sword": "钻石剑", + "item.minecraft.disc_fragment_5": "唱片残片", + "item.minecraft.disc_fragment_5.desc": "音乐唱片 - 5", + "item.minecraft.dolphin_spawn_egg": "海豚刷怪蛋", + "item.minecraft.donkey_spawn_egg": "驴刷怪蛋", + "item.minecraft.dragon_breath": "龙息", + "item.minecraft.dried_kelp": "干海带", + "item.minecraft.drowned_spawn_egg": "溺尸刷怪蛋", + "item.minecraft.dune_armor_trim_smithing_template": "锻造模板", + "item.minecraft.dune_armor_trim_smithing_template.new": "沙丘盔甲纹饰", + "item.minecraft.echo_shard": "回响碎片", + "item.minecraft.egg": "鸡蛋", + "item.minecraft.elder_guardian_spawn_egg": "远古守卫者刷怪蛋", + "item.minecraft.elytra": "鞘翅", + "item.minecraft.emerald": "绿宝石", + "item.minecraft.enchanted_book": "附魔书", + "item.minecraft.enchanted_golden_apple": "附魔金苹果", + "item.minecraft.end_crystal": "末地水晶", + "item.minecraft.ender_dragon_spawn_egg": "末影龙刷怪蛋", + "item.minecraft.ender_eye": "末影之眼", + "item.minecraft.ender_pearl": "末影珍珠", + "item.minecraft.enderman_spawn_egg": "末影人刷怪蛋", + "item.minecraft.endermite_spawn_egg": "末影螨刷怪蛋", + "item.minecraft.evoker_spawn_egg": "唤魔者刷怪蛋", + "item.minecraft.experience_bottle": "附魔之瓶", + "item.minecraft.explorer_pottery_shard": "探险纹样陶片", + "item.minecraft.explorer_pottery_sherd": "探险纹样陶片", + "item.minecraft.eye_armor_trim_smithing_template": "锻造模板", + "item.minecraft.eye_armor_trim_smithing_template.new": "眼眸盔甲纹饰", + "item.minecraft.feather": "羽毛", + "item.minecraft.fermented_spider_eye": "发酵蛛眼", + "item.minecraft.field_masoned_banner_pattern": "砖纹旗帜图案", + "item.minecraft.filled_map": "地图", + "item.minecraft.fire_charge": "火焰弹", + "item.minecraft.firework_rocket": "烟花火箭", + "item.minecraft.firework_rocket.flight": "飞行时间:", + "item.minecraft.firework_star": "烟火之星", + "item.minecraft.firework_star.black": "黑色", + "item.minecraft.firework_star.blue": "蓝色", + "item.minecraft.firework_star.brown": "棕色", + "item.minecraft.firework_star.custom_color": "自定义", + "item.minecraft.firework_star.cyan": "青色", + "item.minecraft.firework_star.fade_to": "淡化至", + "item.minecraft.firework_star.flicker": "闪烁", + "item.minecraft.firework_star.gray": "灰色", + "item.minecraft.firework_star.green": "绿色", + "item.minecraft.firework_star.light_blue": "淡蓝色", + "item.minecraft.firework_star.light_gray": "淡灰色", + "item.minecraft.firework_star.lime": "黄绿色", + "item.minecraft.firework_star.magenta": "品红色", + "item.minecraft.firework_star.orange": "橙色", + "item.minecraft.firework_star.pink": "粉红色", + "item.minecraft.firework_star.purple": "紫色", + "item.minecraft.firework_star.red": "红色", + "item.minecraft.firework_star.shape": "未知形状", + "item.minecraft.firework_star.shape.burst": "喷发状", + "item.minecraft.firework_star.shape.creeper": "苦力怕状", + "item.minecraft.firework_star.shape.large_ball": "大型球状", + "item.minecraft.firework_star.shape.small_ball": "小型球状", + "item.minecraft.firework_star.shape.star": "星形", + "item.minecraft.firework_star.trail": "踪迹", + "item.minecraft.firework_star.white": "白色", + "item.minecraft.firework_star.yellow": "黄色", + "item.minecraft.fishing_rod": "钓鱼竿", + "item.minecraft.flint": "燧石", + "item.minecraft.flint_and_steel": "打火石", + "item.minecraft.flow_armor_trim_smithing_template": "锻造模板", + "item.minecraft.flow_armor_trim_smithing_template.new": "涡流盔甲纹饰", + "item.minecraft.flow_banner_pattern": "旗帜图案", + "item.minecraft.flow_banner_pattern.desc": "涡流", + "item.minecraft.flow_banner_pattern.new": "涡流旗帜图案", + "item.minecraft.flow_pottery_sherd": "涡流纹样陶片", + "item.minecraft.flower_banner_pattern": "旗帜图案", + "item.minecraft.flower_banner_pattern.desc": "花朵盾徽", + "item.minecraft.flower_banner_pattern.new": "花朵盾徽旗帜图案", + "item.minecraft.flower_pot": "花盆", + "item.minecraft.fox_spawn_egg": "狐狸刷怪蛋", + "item.minecraft.friend_pottery_shard": "挚友纹样陶片", + "item.minecraft.friend_pottery_sherd": "挚友纹样陶片", + "item.minecraft.frog_spawn_egg": "青蛙刷怪蛋", + "item.minecraft.furnace_minecart": "动力矿车", + "item.minecraft.ghast_spawn_egg": "恶魂刷怪蛋", + "item.minecraft.ghast_tear": "恶魂之泪", + "item.minecraft.glass_bottle": "玻璃瓶", + "item.minecraft.glistering_melon_slice": "闪烁的西瓜片", + "item.minecraft.globe_banner_pattern": "旗帜图案", + "item.minecraft.globe_banner_pattern.desc": "地球", + "item.minecraft.globe_banner_pattern.new": "地球旗帜图案", + "item.minecraft.glow_berries": "发光浆果", + "item.minecraft.glow_ink_sac": "荧光墨囊", + "item.minecraft.glow_item_frame": "荧光物品展示框", + "item.minecraft.glow_squid_spawn_egg": "发光鱿鱼刷怪蛋", + "item.minecraft.glowstone_dust": "荧石粉", + "item.minecraft.goat_horn": "山羊角", + "item.minecraft.goat_spawn_egg": "山羊刷怪蛋", + "item.minecraft.gold_ingot": "金锭", + "item.minecraft.gold_nugget": "金粒", + "item.minecraft.golden_apple": "金苹果", + "item.minecraft.golden_axe": "金斧", + "item.minecraft.golden_boots": "金靴子", + "item.minecraft.golden_carrot": "金胡萝卜", + "item.minecraft.golden_chestplate": "金胸甲", + "item.minecraft.golden_helmet": "金头盔", + "item.minecraft.golden_hoe": "金锄", + "item.minecraft.golden_horse_armor": "金马铠", + "item.minecraft.golden_leggings": "金护腿", + "item.minecraft.golden_pickaxe": "金镐", + "item.minecraft.golden_shovel": "金锹", + "item.minecraft.golden_sword": "金剑", + "item.minecraft.gray_bundle": "灰色收纳袋", + "item.minecraft.gray_dye": "灰色染料", + "item.minecraft.green_bundle": "绿色收纳袋", + "item.minecraft.green_dye": "绿色染料", + "item.minecraft.guardian_spawn_egg": "守卫者刷怪蛋", + "item.minecraft.gunpowder": "火药", + "item.minecraft.guster_banner_pattern": "旗帜图案", + "item.minecraft.guster_banner_pattern.desc": "旋风", + "item.minecraft.guster_banner_pattern.new": "旋风旗帜图案", + "item.minecraft.guster_pottery_sherd": "旋风纹样陶片", + "item.minecraft.heart_of_the_sea": "海洋之心", + "item.minecraft.heart_pottery_shard": "爱心纹样陶片", + "item.minecraft.heart_pottery_sherd": "爱心纹样陶片", + "item.minecraft.heartbreak_pottery_shard": "心碎纹样陶片", + "item.minecraft.heartbreak_pottery_sherd": "心碎纹样陶片", + "item.minecraft.hoglin_spawn_egg": "疣猪兽刷怪蛋", + "item.minecraft.honey_bottle": "蜂蜜瓶", + "item.minecraft.honeycomb": "蜜脾", + "item.minecraft.hopper_minecart": "漏斗矿车", + "item.minecraft.horse_spawn_egg": "马刷怪蛋", + "item.minecraft.host_armor_trim_smithing_template": "锻造模板", + "item.minecraft.host_armor_trim_smithing_template.new": "雇主盔甲纹饰", + "item.minecraft.howl_pottery_shard": "狼嚎纹样陶片", + "item.minecraft.howl_pottery_sherd": "狼嚎纹样陶片", + "item.minecraft.husk_spawn_egg": "尸壳刷怪蛋", + "item.minecraft.ink_sac": "墨囊", + "item.minecraft.iron_axe": "铁斧", + "item.minecraft.iron_boots": "铁靴子", + "item.minecraft.iron_chestplate": "铁胸甲", + "item.minecraft.iron_golem_spawn_egg": "铁傀儡刷怪蛋", + "item.minecraft.iron_helmet": "铁头盔", + "item.minecraft.iron_hoe": "铁锄", + "item.minecraft.iron_horse_armor": "铁马铠", + "item.minecraft.iron_ingot": "铁锭", + "item.minecraft.iron_leggings": "铁护腿", + "item.minecraft.iron_nugget": "铁粒", + "item.minecraft.iron_pickaxe": "铁镐", + "item.minecraft.iron_shovel": "铁锹", + "item.minecraft.iron_sword": "铁剑", + "item.minecraft.item_frame": "物品展示框", + "item.minecraft.jungle_boat": "丛林木船", + "item.minecraft.jungle_chest_boat": "丛林木运输船", + "item.minecraft.knowledge_book": "知识之书", + "item.minecraft.lapis_lazuli": "青金石", + "item.minecraft.lava_bucket": "熔岩桶", + "item.minecraft.lead": "拴绳", + "item.minecraft.leather": "皮革", + "item.minecraft.leather_boots": "皮革靴子", + "item.minecraft.leather_chestplate": "皮革外套", + "item.minecraft.leather_helmet": "皮革帽子", + "item.minecraft.leather_horse_armor": "皮革马铠", + "item.minecraft.leather_leggings": "皮革裤子", + "item.minecraft.light_blue_bundle": "淡蓝色收纳袋", + "item.minecraft.light_blue_dye": "淡蓝色染料", + "item.minecraft.light_gray_bundle": "淡灰色收纳袋", + "item.minecraft.light_gray_dye": "淡灰色染料", + "item.minecraft.lime_bundle": "黄绿色收纳袋", + "item.minecraft.lime_dye": "黄绿色染料", + "item.minecraft.lingering_potion": "滞留药水", + "item.minecraft.lingering_potion.effect.awkward": "滞留型粗制的药水", + "item.minecraft.lingering_potion.effect.empty": "不可合成的滞留型药水", + "item.minecraft.lingering_potion.effect.fire_resistance": "滞留型抗火药水", + "item.minecraft.lingering_potion.effect.harming": "滞留型伤害药水", + "item.minecraft.lingering_potion.effect.healing": "滞留型治疗药水", + "item.minecraft.lingering_potion.effect.infested": "滞留型虫蚀药水", + "item.minecraft.lingering_potion.effect.invisibility": "滞留型隐身药水", + "item.minecraft.lingering_potion.effect.leaping": "滞留型跳跃药水", + "item.minecraft.lingering_potion.effect.levitation": "滞留型飘浮药水", + "item.minecraft.lingering_potion.effect.luck": "滞留型幸运药水", + "item.minecraft.lingering_potion.effect.mundane": "滞留型平凡的药水", + "item.minecraft.lingering_potion.effect.night_vision": "滞留型夜视药水", + "item.minecraft.lingering_potion.effect.oozing": "滞留型渗浆药水", + "item.minecraft.lingering_potion.effect.poison": "滞留型剧毒药水", + "item.minecraft.lingering_potion.effect.regeneration": "滞留型再生药水", + "item.minecraft.lingering_potion.effect.slow_falling": "滞留型缓降药水", + "item.minecraft.lingering_potion.effect.slowness": "滞留型迟缓药水", + "item.minecraft.lingering_potion.effect.strength": "滞留型力量药水", + "item.minecraft.lingering_potion.effect.swiftness": "滞留型迅捷药水", + "item.minecraft.lingering_potion.effect.thick": "滞留型浓稠的药水", + "item.minecraft.lingering_potion.effect.turtle_master": "滞留型神龟药水", + "item.minecraft.lingering_potion.effect.water": "滞留型水瓶", + "item.minecraft.lingering_potion.effect.water_breathing": "滞留型水肺药水", + "item.minecraft.lingering_potion.effect.weakness": "滞留型虚弱药水", + "item.minecraft.lingering_potion.effect.weaving": "滞留型盘丝药水", + "item.minecraft.lingering_potion.effect.wind_charged": "滞留型蓄风药水", + "item.minecraft.llama_spawn_egg": "羊驼刷怪蛋", + "item.minecraft.lodestone_compass": "磁石指针", + "item.minecraft.mace": "重锤", + "item.minecraft.magenta_bundle": "品红色收纳袋", + "item.minecraft.magenta_dye": "品红色染料", + "item.minecraft.magma_cream": "岩浆膏", + "item.minecraft.magma_cube_spawn_egg": "岩浆怪刷怪蛋", + "item.minecraft.mangrove_boat": "红树木船", + "item.minecraft.mangrove_chest_boat": "红树木运输船", + "item.minecraft.map": "空地图", + "item.minecraft.melon_seeds": "西瓜种子", + "item.minecraft.melon_slice": "西瓜片", + "item.minecraft.milk_bucket": "奶桶", + "item.minecraft.minecart": "矿车", + "item.minecraft.miner_pottery_shard": "采矿纹样陶片", + "item.minecraft.miner_pottery_sherd": "采矿纹样陶片", + "item.minecraft.mojang_banner_pattern": "旗帜图案", + "item.minecraft.mojang_banner_pattern.desc": "Mojang徽标", + "item.minecraft.mojang_banner_pattern.new": "Mojang徽标旗帜图案", + "item.minecraft.mooshroom_spawn_egg": "哞菇刷怪蛋", + "item.minecraft.mourner_pottery_shard": "悲恸纹样陶片", + "item.minecraft.mourner_pottery_sherd": "悲恸纹样陶片", + "item.minecraft.mule_spawn_egg": "骡刷怪蛋", + "item.minecraft.mushroom_stew": "蘑菇煲", + "item.minecraft.music_disc_11": "音乐唱片", + "item.minecraft.music_disc_11.desc": "C418 - 11", + "item.minecraft.music_disc_13": "音乐唱片", + "item.minecraft.music_disc_13.desc": "C418 - 13", + "item.minecraft.music_disc_5": "音乐唱片", + "item.minecraft.music_disc_5.desc": "Samuel Åberg - 5", + "item.minecraft.music_disc_blocks": "音乐唱片", + "item.minecraft.music_disc_blocks.desc": "C418 - blocks", + "item.minecraft.music_disc_cat": "音乐唱片", + "item.minecraft.music_disc_cat.desc": "C418 - cat", + "item.minecraft.music_disc_chirp": "音乐唱片", + "item.minecraft.music_disc_chirp.desc": "C418 - chirp", + "item.minecraft.music_disc_creator": "音乐唱片", + "item.minecraft.music_disc_creator.desc": "Lena Raine - Creator", + "item.minecraft.music_disc_creator_music_box": "音乐唱片", + "item.minecraft.music_disc_creator_music_box.desc": "Lena Raine - Creator(八音盒)", + "item.minecraft.music_disc_far": "音乐唱片", + "item.minecraft.music_disc_far.desc": "C418 - far", + "item.minecraft.music_disc_mall": "音乐唱片", + "item.minecraft.music_disc_mall.desc": "C418 - mall", + "item.minecraft.music_disc_mellohi": "音乐唱片", + "item.minecraft.music_disc_mellohi.desc": "C418 - mellohi", + "item.minecraft.music_disc_otherside": "音乐唱片", + "item.minecraft.music_disc_otherside.desc": "Lena Raine - otherside", + "item.minecraft.music_disc_pigstep": "音乐唱片", + "item.minecraft.music_disc_pigstep.desc": "Lena Raine - Pigstep", + "item.minecraft.music_disc_precipice": "音乐唱片", + "item.minecraft.music_disc_precipice.desc": "Aaron Cherof - Precipice", + "item.minecraft.music_disc_relic": "音乐唱片", + "item.minecraft.music_disc_relic.desc": "Aaron Cherof - Relic", + "item.minecraft.music_disc_stal": "音乐唱片", + "item.minecraft.music_disc_stal.desc": "C418 - stal", + "item.minecraft.music_disc_strad": "音乐唱片", + "item.minecraft.music_disc_strad.desc": "C418 - strad", + "item.minecraft.music_disc_wait": "音乐唱片", + "item.minecraft.music_disc_wait.desc": "C418 - wait", + "item.minecraft.music_disc_ward": "音乐唱片", + "item.minecraft.music_disc_ward.desc": "C418 - ward", + "item.minecraft.mutton": "生羊肉", + "item.minecraft.name_tag": "命名牌", + "item.minecraft.nautilus_shell": "鹦鹉螺壳", + "item.minecraft.nether_brick": "下界砖", + "item.minecraft.nether_star": "下界之星", + "item.minecraft.nether_wart": "下界疣", + "item.minecraft.netherite_axe": "下界合金斧", + "item.minecraft.netherite_boots": "下界合金靴子", + "item.minecraft.netherite_chestplate": "下界合金胸甲", + "item.minecraft.netherite_helmet": "下界合金头盔", + "item.minecraft.netherite_hoe": "下界合金锄", + "item.minecraft.netherite_ingot": "下界合金锭", + "item.minecraft.netherite_leggings": "下界合金护腿", + "item.minecraft.netherite_pickaxe": "下界合金镐", + "item.minecraft.netherite_scrap": "下界合金碎片", + "item.minecraft.netherite_shovel": "下界合金锹", + "item.minecraft.netherite_sword": "下界合金剑", + "item.minecraft.netherite_upgrade_smithing_template": "锻造模板", + "item.minecraft.netherite_upgrade_smithing_template.new": "下界合金升级", + "item.minecraft.oak_boat": "橡木船", + "item.minecraft.oak_chest_boat": "橡木运输船", + "item.minecraft.ocelot_spawn_egg": "豹猫刷怪蛋", + "item.minecraft.ominous_bottle": "不祥之瓶", + "item.minecraft.ominous_trial_key": "不祥试炼钥匙", + "item.minecraft.orange_bundle": "橙色收纳袋", + "item.minecraft.orange_dye": "橙色染料", + "item.minecraft.painting": "画", + "item.minecraft.pale_oak_boat": "苍白橡木船", + "item.minecraft.pale_oak_chest_boat": "苍白橡木运输船", + "item.minecraft.panda_spawn_egg": "熊猫刷怪蛋", + "item.minecraft.paper": "纸", + "item.minecraft.parrot_spawn_egg": "鹦鹉刷怪蛋", + "item.minecraft.phantom_membrane": "幻翼膜", + "item.minecraft.phantom_spawn_egg": "幻翼刷怪蛋", + "item.minecraft.pig_spawn_egg": "猪刷怪蛋", + "item.minecraft.piglin_banner_pattern": "旗帜图案", + "item.minecraft.piglin_banner_pattern.desc": "猪鼻", + "item.minecraft.piglin_banner_pattern.new": "猪鼻旗帜图案", + "item.minecraft.piglin_brute_spawn_egg": "猪灵蛮兵刷怪蛋", + "item.minecraft.piglin_spawn_egg": "猪灵刷怪蛋", + "item.minecraft.pillager_spawn_egg": "掠夺者刷怪蛋", + "item.minecraft.pink_bundle": "粉红色收纳袋", + "item.minecraft.pink_dye": "粉红色染料", + "item.minecraft.pitcher_plant": "瓶子草", + "item.minecraft.pitcher_pod": "瓶子草荚果", + "item.minecraft.plenty_pottery_shard": "富饶纹样陶片", + "item.minecraft.plenty_pottery_sherd": "富饶纹样陶片", + "item.minecraft.poisonous_potato": "毒马铃薯", + "item.minecraft.polar_bear_spawn_egg": "北极熊刷怪蛋", + "item.minecraft.popped_chorus_fruit": "爆裂紫颂果", + "item.minecraft.porkchop": "生猪排", + "item.minecraft.potato": "马铃薯", + "item.minecraft.potion": "药水", + "item.minecraft.potion.effect.awkward": "粗制的药水", + "item.minecraft.potion.effect.empty": "不可合成的药水", + "item.minecraft.potion.effect.fire_resistance": "抗火药水", + "item.minecraft.potion.effect.harming": "伤害药水", + "item.minecraft.potion.effect.healing": "治疗药水", + "item.minecraft.potion.effect.infested": "虫蚀药水", + "item.minecraft.potion.effect.invisibility": "隐身药水", + "item.minecraft.potion.effect.leaping": "跳跃药水", + "item.minecraft.potion.effect.levitation": "飘浮药水", + "item.minecraft.potion.effect.luck": "幸运药水", + "item.minecraft.potion.effect.mundane": "平凡的药水", + "item.minecraft.potion.effect.night_vision": "夜视药水", + "item.minecraft.potion.effect.oozing": "渗浆药水", + "item.minecraft.potion.effect.poison": "剧毒药水", + "item.minecraft.potion.effect.regeneration": "再生药水", + "item.minecraft.potion.effect.slow_falling": "缓降药水", + "item.minecraft.potion.effect.slowness": "迟缓药水", + "item.minecraft.potion.effect.strength": "力量药水", + "item.minecraft.potion.effect.swiftness": "迅捷药水", + "item.minecraft.potion.effect.thick": "浓稠的药水", + "item.minecraft.potion.effect.turtle_master": "神龟药水", + "item.minecraft.potion.effect.water": "水瓶", + "item.minecraft.potion.effect.water_breathing": "水肺药水", + "item.minecraft.potion.effect.weakness": "虚弱药水", + "item.minecraft.potion.effect.weaving": "盘丝药水", + "item.minecraft.potion.effect.wind_charged": "蓄风药水", + "item.minecraft.pottery_shard_archer": "弓箭纹样陶片", + "item.minecraft.pottery_shard_arms_up": "举臂纹样陶片", + "item.minecraft.pottery_shard_prize": "珍宝纹样陶片", + "item.minecraft.pottery_shard_skull": "头颅纹样陶片", + "item.minecraft.powder_snow_bucket": "细雪桶", + "item.minecraft.prismarine_crystals": "海晶砂粒", + "item.minecraft.prismarine_shard": "海晶碎片", + "item.minecraft.prize_pottery_shard": "珍宝纹样陶片", + "item.minecraft.prize_pottery_sherd": "珍宝纹样陶片", + "item.minecraft.pufferfish": "河豚", + "item.minecraft.pufferfish_bucket": "河豚桶", + "item.minecraft.pufferfish_spawn_egg": "河豚刷怪蛋", + "item.minecraft.pumpkin_pie": "南瓜派", + "item.minecraft.pumpkin_seeds": "南瓜种子", + "item.minecraft.purple_bundle": "紫色收纳袋", + "item.minecraft.purple_dye": "紫色染料", + "item.minecraft.quartz": "下界石英", + "item.minecraft.rabbit": "生兔肉", + "item.minecraft.rabbit_foot": "兔子脚", + "item.minecraft.rabbit_hide": "兔子皮", + "item.minecraft.rabbit_spawn_egg": "兔子刷怪蛋", + "item.minecraft.rabbit_stew": "兔肉煲", + "item.minecraft.raiser_armor_trim_smithing_template": "锻造模板", + "item.minecraft.raiser_armor_trim_smithing_template.new": "牧民盔甲纹饰", + "item.minecraft.ravager_spawn_egg": "劫掠兽刷怪蛋", + "item.minecraft.raw_copper": "粗铜", + "item.minecraft.raw_gold": "粗金", + "item.minecraft.raw_iron": "粗铁", + "item.minecraft.recovery_compass": "追溯指针", + "item.minecraft.red_bundle": "红色收纳袋", + "item.minecraft.red_dye": "红色染料", + "item.minecraft.redstone": "红石粉", + "item.minecraft.resin_brick": "树脂砖", + "item.minecraft.resin_clump": "树脂团", + "item.minecraft.rib_armor_trim_smithing_template": "锻造模板", + "item.minecraft.rib_armor_trim_smithing_template.new": "肋骨盔甲纹饰", + "item.minecraft.rotten_flesh": "腐肉", + "item.minecraft.saddle": "鞍", + "item.minecraft.salmon": "生鲑鱼", + "item.minecraft.salmon_bucket": "鲑鱼桶", + "item.minecraft.salmon_spawn_egg": "鲑鱼刷怪蛋", + "item.minecraft.scrape_pottery_sherd": "刮削纹样陶片", + "item.minecraft.scute": "鳞甲", + "item.minecraft.sentry_armor_trim_smithing_template": "锻造模板", + "item.minecraft.sentry_armor_trim_smithing_template.new": "哨兵盔甲纹饰", + "item.minecraft.shaper_armor_trim_smithing_template": "锻造模板", + "item.minecraft.shaper_armor_trim_smithing_template.new": "塑造盔甲纹饰", + "item.minecraft.sheaf_pottery_shard": "麦捆纹样陶片", + "item.minecraft.sheaf_pottery_sherd": "麦捆纹样陶片", + "item.minecraft.shears": "剪刀", + "item.minecraft.sheep_spawn_egg": "绵羊刷怪蛋", + "item.minecraft.shelter_pottery_shard": "树荫纹样陶片", + "item.minecraft.shelter_pottery_sherd": "树荫纹样陶片", + "item.minecraft.shield": "盾牌", + "item.minecraft.shield.black": "黑色盾牌", + "item.minecraft.shield.blue": "蓝色盾牌", + "item.minecraft.shield.brown": "棕色盾牌", + "item.minecraft.shield.cyan": "青色盾牌", + "item.minecraft.shield.gray": "灰色盾牌", + "item.minecraft.shield.green": "绿色盾牌", + "item.minecraft.shield.light_blue": "淡蓝色盾牌", + "item.minecraft.shield.light_gray": "淡灰色盾牌", + "item.minecraft.shield.lime": "黄绿色盾牌", + "item.minecraft.shield.magenta": "品红色盾牌", + "item.minecraft.shield.orange": "橙色盾牌", + "item.minecraft.shield.pink": "粉红色盾牌", + "item.minecraft.shield.purple": "紫色盾牌", + "item.minecraft.shield.red": "红色盾牌", + "item.minecraft.shield.white": "白色盾牌", + "item.minecraft.shield.yellow": "黄色盾牌", + "item.minecraft.shulker_shell": "潜影壳", + "item.minecraft.shulker_spawn_egg": "潜影贝刷怪蛋", + "item.minecraft.sign": "告示牌", + "item.minecraft.silence_armor_trim_smithing_template": "锻造模板", + "item.minecraft.silence_armor_trim_smithing_template.new": "幽静盔甲纹饰", + "item.minecraft.silverfish_spawn_egg": "蠹虫刷怪蛋", + "item.minecraft.skeleton_horse_spawn_egg": "骷髅马刷怪蛋", + "item.minecraft.skeleton_spawn_egg": "骷髅刷怪蛋", + "item.minecraft.skull_banner_pattern": "旗帜图案", + "item.minecraft.skull_banner_pattern.desc": "头颅盾徽", + "item.minecraft.skull_banner_pattern.new": "头颅盾徽旗帜图案", + "item.minecraft.skull_pottery_shard": "头颅纹样陶片", + "item.minecraft.skull_pottery_sherd": "头颅纹样陶片", + "item.minecraft.slime_ball": "黏液球", + "item.minecraft.slime_spawn_egg": "史莱姆刷怪蛋", + "item.minecraft.smithing_template": "锻造模板", + "item.minecraft.smithing_template.applies_to": "可应用于:", + "item.minecraft.smithing_template.armor_trim.additions_slot_description": "放入铸锭或晶体", + "item.minecraft.smithing_template.armor_trim.applies_to": "盔甲", + "item.minecraft.smithing_template.armor_trim.base_slot_description": "放入盔甲", + "item.minecraft.smithing_template.armor_trim.ingredients": "铸锭或晶体", + "item.minecraft.smithing_template.ingredients": "所需原材料:", + "item.minecraft.smithing_template.netherite_upgrade.additions_slot_description": "放入下界合金锭", + "item.minecraft.smithing_template.netherite_upgrade.applies_to": "钻石装备", + "item.minecraft.smithing_template.netherite_upgrade.base_slot_description": "放入钻石盔甲、武器或工具", + "item.minecraft.smithing_template.netherite_upgrade.ingredients": "下界合金锭", + "item.minecraft.smithing_template.upgrade": "已有升级:", + "item.minecraft.sniffer_spawn_egg": "嗅探兽刷怪蛋", + "item.minecraft.snort_pottery_shard": "嗅探纹样陶片", + "item.minecraft.snort_pottery_sherd": "嗅探纹样陶片", + "item.minecraft.snout_armor_trim_smithing_template": "锻造模板", + "item.minecraft.snout_armor_trim_smithing_template.new": "猪鼻盔甲纹饰", + "item.minecraft.snow_golem_spawn_egg": "雪傀儡刷怪蛋", + "item.minecraft.snowball": "雪球", + "item.minecraft.spectral_arrow": "光灵箭", + "item.minecraft.spider_eye": "蜘蛛眼", + "item.minecraft.spider_spawn_egg": "蜘蛛刷怪蛋", + "item.minecraft.spire_armor_trim_smithing_template": "锻造模板", + "item.minecraft.spire_armor_trim_smithing_template.new": "尖塔盔甲纹饰", + "item.minecraft.splash_potion": "喷溅药水", + "item.minecraft.splash_potion.effect.awkward": "喷溅型粗制的药水", + "item.minecraft.splash_potion.effect.empty": "不可合成的喷溅型药水", + "item.minecraft.splash_potion.effect.fire_resistance": "喷溅型抗火药水", + "item.minecraft.splash_potion.effect.harming": "喷溅型伤害药水", + "item.minecraft.splash_potion.effect.healing": "喷溅型治疗药水", + "item.minecraft.splash_potion.effect.infested": "喷溅型虫蚀药水", + "item.minecraft.splash_potion.effect.invisibility": "喷溅型隐身药水", + "item.minecraft.splash_potion.effect.leaping": "喷溅型跳跃药水", + "item.minecraft.splash_potion.effect.levitation": "喷溅型飘浮药水", + "item.minecraft.splash_potion.effect.luck": "喷溅型幸运药水", + "item.minecraft.splash_potion.effect.mundane": "喷溅型平凡的药水", + "item.minecraft.splash_potion.effect.night_vision": "喷溅型夜视药水", + "item.minecraft.splash_potion.effect.oozing": "喷溅型渗浆药水", + "item.minecraft.splash_potion.effect.poison": "喷溅型剧毒药水", + "item.minecraft.splash_potion.effect.regeneration": "喷溅型再生药水", + "item.minecraft.splash_potion.effect.slow_falling": "喷溅型缓降药水", + "item.minecraft.splash_potion.effect.slowness": "喷溅型迟缓药水", + "item.minecraft.splash_potion.effect.strength": "喷溅型力量药水", + "item.minecraft.splash_potion.effect.swiftness": "喷溅型迅捷药水", + "item.minecraft.splash_potion.effect.thick": "喷溅型浓稠的药水", + "item.minecraft.splash_potion.effect.turtle_master": "喷溅型神龟药水", + "item.minecraft.splash_potion.effect.water": "喷溅型水瓶", + "item.minecraft.splash_potion.effect.water_breathing": "喷溅型水肺药水", + "item.minecraft.splash_potion.effect.weakness": "喷溅型虚弱药水", + "item.minecraft.splash_potion.effect.weaving": "喷溅型盘丝药水", + "item.minecraft.splash_potion.effect.wind_charged": "喷溅型蓄风药水", + "item.minecraft.spruce_boat": "云杉木船", + "item.minecraft.spruce_chest_boat": "云杉木运输船", + "item.minecraft.spyglass": "望远镜", + "item.minecraft.squid_spawn_egg": "鱿鱼刷怪蛋", + "item.minecraft.stick": "木棍", + "item.minecraft.stone_axe": "石斧", + "item.minecraft.stone_hoe": "石锄", + "item.minecraft.stone_pickaxe": "石镐", + "item.minecraft.stone_shovel": "石锹", + "item.minecraft.stone_sword": "石剑", + "item.minecraft.stray_spawn_egg": "流浪者刷怪蛋", + "item.minecraft.strider_spawn_egg": "炽足兽刷怪蛋", + "item.minecraft.string": "线", + "item.minecraft.sugar": "糖", + "item.minecraft.suspicious_stew": "谜之炖菜", + "item.minecraft.sweet_berries": "甜浆果", + "item.minecraft.tadpole_bucket": "蝌蚪桶", + "item.minecraft.tadpole_spawn_egg": "蝌蚪刷怪蛋", + "item.minecraft.tide_armor_trim_smithing_template": "锻造模板", + "item.minecraft.tide_armor_trim_smithing_template.new": "潮汐盔甲纹饰", + "item.minecraft.tipped_arrow": "药箭", + "item.minecraft.tipped_arrow.effect.awkward": "药箭", + "item.minecraft.tipped_arrow.effect.empty": "不可合成的药箭", + "item.minecraft.tipped_arrow.effect.fire_resistance": "抗火之箭", + "item.minecraft.tipped_arrow.effect.harming": "伤害之箭", + "item.minecraft.tipped_arrow.effect.healing": "治疗之箭", + "item.minecraft.tipped_arrow.effect.infested": "虫蚀之箭", + "item.minecraft.tipped_arrow.effect.invisibility": "隐身之箭", + "item.minecraft.tipped_arrow.effect.leaping": "跳跃之箭", + "item.minecraft.tipped_arrow.effect.levitation": "飘浮之箭", + "item.minecraft.tipped_arrow.effect.luck": "幸运之箭", + "item.minecraft.tipped_arrow.effect.mundane": "药箭", + "item.minecraft.tipped_arrow.effect.night_vision": "夜视之箭", + "item.minecraft.tipped_arrow.effect.oozing": "渗浆之箭", + "item.minecraft.tipped_arrow.effect.poison": "剧毒之箭", + "item.minecraft.tipped_arrow.effect.regeneration": "再生之箭", + "item.minecraft.tipped_arrow.effect.slow_falling": "缓降之箭", + "item.minecraft.tipped_arrow.effect.slowness": "迟缓之箭", + "item.minecraft.tipped_arrow.effect.strength": "力量之箭", + "item.minecraft.tipped_arrow.effect.swiftness": "迅捷之箭", + "item.minecraft.tipped_arrow.effect.thick": "药箭", + "item.minecraft.tipped_arrow.effect.turtle_master": "神龟之箭", + "item.minecraft.tipped_arrow.effect.water": "喷溅之箭", + "item.minecraft.tipped_arrow.effect.water_breathing": "水肺之箭", + "item.minecraft.tipped_arrow.effect.weakness": "虚弱之箭", + "item.minecraft.tipped_arrow.effect.weaving": "盘丝之箭", + "item.minecraft.tipped_arrow.effect.wind_charged": "蓄风之箭", + "item.minecraft.tnt_minecart": "TNT矿车", + "item.minecraft.torchflower_seeds": "火把花种子", + "item.minecraft.totem_of_undying": "不死图腾", + "item.minecraft.trader_llama_spawn_egg": "行商羊驼刷怪蛋", + "item.minecraft.trial_key": "试炼钥匙", + "item.minecraft.trident": "三叉戟", + "item.minecraft.tropical_fish": "热带鱼", + "item.minecraft.tropical_fish_bucket": "热带鱼桶", + "item.minecraft.tropical_fish_spawn_egg": "热带鱼刷怪蛋", + "item.minecraft.turtle_helmet": "海龟壳", + "item.minecraft.turtle_scute": "海龟鳞甲", + "item.minecraft.turtle_spawn_egg": "海龟刷怪蛋", + "item.minecraft.vex_armor_trim_smithing_template": "锻造模板", + "item.minecraft.vex_armor_trim_smithing_template.new": "恼鬼盔甲纹饰", + "item.minecraft.vex_spawn_egg": "恼鬼刷怪蛋", + "item.minecraft.villager_spawn_egg": "村民刷怪蛋", + "item.minecraft.vindicator_spawn_egg": "卫道士刷怪蛋", + "item.minecraft.wandering_trader_spawn_egg": "流浪商人刷怪蛋", + "item.minecraft.ward_armor_trim_smithing_template": "锻造模板", + "item.minecraft.ward_armor_trim_smithing_template.new": "监守盔甲纹饰", + "item.minecraft.warden_spawn_egg": "监守者刷怪蛋", + "item.minecraft.warped_fungus_on_a_stick": "诡异菌钓竿", + "item.minecraft.water_bucket": "水桶", + "item.minecraft.wayfinder_armor_trim_smithing_template": "锻造模板", + "item.minecraft.wayfinder_armor_trim_smithing_template.new": "向导盔甲纹饰", + "item.minecraft.wheat": "小麦", + "item.minecraft.wheat_seeds": "小麦种子", + "item.minecraft.white_bundle": "白色收纳袋", + "item.minecraft.white_dye": "白色染料", + "item.minecraft.wild_armor_trim_smithing_template": "锻造模板", + "item.minecraft.wild_armor_trim_smithing_template.new": "荒野盔甲纹饰", + "item.minecraft.wind_charge": "风弹", + "item.minecraft.witch_spawn_egg": "女巫刷怪蛋", + "item.minecraft.wither_skeleton_spawn_egg": "凋灵骷髅刷怪蛋", + "item.minecraft.wither_spawn_egg": "凋灵刷怪蛋", + "item.minecraft.wolf_armor": "狼铠", + "item.minecraft.wolf_spawn_egg": "狼刷怪蛋", + "item.minecraft.wooden_axe": "木斧", + "item.minecraft.wooden_hoe": "木锄", + "item.minecraft.wooden_pickaxe": "木镐", + "item.minecraft.wooden_shovel": "木锹", + "item.minecraft.wooden_sword": "木剑", + "item.minecraft.writable_book": "书与笔", + "item.minecraft.written_book": "成书", + "item.minecraft.yellow_bundle": "黄色收纳袋", + "item.minecraft.yellow_dye": "黄色染料", + "item.minecraft.zoglin_spawn_egg": "僵尸疣猪兽刷怪蛋", + "item.minecraft.zombie_horse_spawn_egg": "僵尸马刷怪蛋", + "item.minecraft.zombie_spawn_egg": "僵尸刷怪蛋", + "item.minecraft.zombie_villager_spawn_egg": "僵尸村民刷怪蛋", + "item.minecraft.zombified_piglin_spawn_egg": "僵尸猪灵刷怪蛋", + "item.modifiers.any": "装备时:", + "item.modifiers.armor": "穿戴时:", + "item.modifiers.body": "装备时:", + "item.modifiers.chest": "穿在身上时:", + "item.modifiers.feet": "穿在脚上时:", + "item.modifiers.hand": "手持时:", + "item.modifiers.head": "戴在头上时:", + "item.modifiers.legs": "穿在腿上时:", + "item.modifiers.mainhand": "在主手时:", + "item.modifiers.offhand": "在副手时:", + "item.nbt_tags": "NBT:%s个标签", + "item.op_block_warning.line1": "警告:", + "item.op_block_warning.line2": "使用该物品可能会导致命令执行", + "item.op_block_warning.line3": "除非知道确切内容,否则请勿使用!", + "item.unbreakable": "无法破坏", + "itemGroup.buildingBlocks": "建筑方块", + "itemGroup.coloredBlocks": "染色方块", + "itemGroup.combat": "战斗用品", + "itemGroup.consumables": "消耗品", + "itemGroup.crafting": "合成用品", + "itemGroup.foodAndDrink": "食物与饮品", + "itemGroup.functional": "功能方块", + "itemGroup.hotbar": "已保存的快捷栏", + "itemGroup.ingredients": "原材料", + "itemGroup.inventory": "生存模式物品栏", + "itemGroup.natural": "自然方块", + "itemGroup.op": "管理员用品", + "itemGroup.redstone": "红石方块", + "itemGroup.search": "搜索物品", + "itemGroup.spawnEggs": "刷怪蛋", + "itemGroup.tools": "工具与实用物品", + "item_modifier.unknown": "未知的物品修饰器:%s", + "jigsaw_block.final_state": "转变为:", + "jigsaw_block.generate": "生成", + "jigsaw_block.joint.aligned": "固定", + "jigsaw_block.joint.rollable": "可旋转", + "jigsaw_block.joint_label": "拼接类型:", + "jigsaw_block.keep_jigsaws": "保留拼图", + "jigsaw_block.levels": "层数:%s", + "jigsaw_block.name": "名称:", + "jigsaw_block.placement_priority": "放置优先级:", + "jigsaw_block.placement_priority.tooltip": "当此拼图方块连接到部件时,此处即为在更大的结构中各部件处理连接的顺序。\n\n部件按照优先级由高到低处理,若优先级相同,则按照插入顺序处理。", + "jigsaw_block.pool": "目标池:", + "jigsaw_block.selection_priority": "选择优先级:", + "jigsaw_block.selection_priority.tooltip": "当父级片段处理连接时,此处即为各拼图方块尝试连接目标部件的顺序。\n\n拼图方块将会按照优先级由高到低处理,同级则随机处理。", + "jigsaw_block.target": "目标名称:", + "jukebox_song.minecraft.11": "C418 - 11", + "jukebox_song.minecraft.13": "C418 - 13", + "jukebox_song.minecraft.5": "Samuel Åberg - 5", + "jukebox_song.minecraft.blocks": "C418 - blocks", + "jukebox_song.minecraft.cat": "C418 - cat", + "jukebox_song.minecraft.chirp": "C418 - chirp", + "jukebox_song.minecraft.creator": "Lena Raine - Creator", + "jukebox_song.minecraft.creator_music_box": "Lena Raine - Creator(八音盒)", + "jukebox_song.minecraft.far": "C418 - far", + "jukebox_song.minecraft.mall": "C418 - mall", + "jukebox_song.minecraft.mellohi": "C418 - mellohi", + "jukebox_song.minecraft.otherside": "Lena Raine - otherside", + "jukebox_song.minecraft.pigstep": "Lena Raine - Pigstep", + "jukebox_song.minecraft.precipice": "Aaron Cherof - Precipice", + "jukebox_song.minecraft.relic": "Aaron Cherof - Relic", + "jukebox_song.minecraft.stal": "C418 - stal", + "jukebox_song.minecraft.strad": "C418 - strad", + "jukebox_song.minecraft.wait": "C418 - wait", + "jukebox_song.minecraft.ward": "C418 - ward", + "key.advancements": "进度", + "key.attack": "攻击/摧毁", + "key.back": "向后移动", + "key.categories.creative": "创造模式", + "key.categories.gameplay": "游戏内容", + "key.categories.inventory": "物品栏", + "key.categories.misc": "杂项", + "key.categories.movement": "移动", + "key.categories.multiplayer": "多人游戏", + "key.categories.ui": "游戏界面", + "key.chat": "打开聊天栏", + "key.command": "输入命令", + "key.drop": "丢弃所选物品", + "key.forward": "向前移动", + "key.fullscreen": "切换全屏显示", + "key.hotbar.1": "快捷栏1", + "key.hotbar.2": "快捷栏2", + "key.hotbar.3": "快捷栏3", + "key.hotbar.4": "快捷栏4", + "key.hotbar.5": "快捷栏5", + "key.hotbar.6": "快捷栏6", + "key.hotbar.7": "快捷栏7", + "key.hotbar.8": "快捷栏8", + "key.hotbar.9": "快捷栏9", + "key.inventory": "开启/关闭物品栏", + "key.jump": "跳跃", + "key.keyboard.apostrophe": "'", + "key.keyboard.backslash": "\\", + "key.keyboard.backspace": "Backspace", + "key.keyboard.caps.lock": "Caps Lock", + "key.keyboard.comma": ",", + "key.keyboard.delete": "Delete", + "key.keyboard.down": "下方向键", + "key.keyboard.end": "End", + "key.keyboard.enter": "Enter", + "key.keyboard.equal": "=", + "key.keyboard.escape": "Esc", + "key.keyboard.f1": "F1", + "key.keyboard.f10": "F10", + "key.keyboard.f11": "F11", + "key.keyboard.f12": "F12", + "key.keyboard.f13": "F13", + "key.keyboard.f14": "F14", + "key.keyboard.f15": "F15", + "key.keyboard.f16": "F16", + "key.keyboard.f17": "F17", + "key.keyboard.f18": "F18", + "key.keyboard.f19": "F19", + "key.keyboard.f2": "F2", + "key.keyboard.f20": "F20", + "key.keyboard.f21": "F21", + "key.keyboard.f22": "F22", + "key.keyboard.f23": "F23", + "key.keyboard.f24": "F24", + "key.keyboard.f25": "F25", + "key.keyboard.f3": "F3", + "key.keyboard.f4": "F4", + "key.keyboard.f5": "F5", + "key.keyboard.f6": "F6", + "key.keyboard.f7": "F7", + "key.keyboard.f8": "F8", + "key.keyboard.f9": "F9", + "key.keyboard.grave.accent": "`", + "key.keyboard.home": "Home", + "key.keyboard.insert": "Insert", + "key.keyboard.keypad.0": "小键盘 0", + "key.keyboard.keypad.1": "小键盘 1", + "key.keyboard.keypad.2": "小键盘 2", + "key.keyboard.keypad.3": "小键盘 3", + "key.keyboard.keypad.4": "小键盘 4", + "key.keyboard.keypad.5": "小键盘 5", + "key.keyboard.keypad.6": "小键盘 6", + "key.keyboard.keypad.7": "小键盘 7", + "key.keyboard.keypad.8": "小键盘 8", + "key.keyboard.keypad.9": "小键盘 9", + "key.keyboard.keypad.add": "小键盘 +", + "key.keyboard.keypad.decimal": "小键盘 .", + "key.keyboard.keypad.divide": "小键盘 /", + "key.keyboard.keypad.enter": "小键盘 Enter", + "key.keyboard.keypad.equal": "小键盘 =", + "key.keyboard.keypad.multiply": "小键盘 *", + "key.keyboard.keypad.subtract": "小键盘 -", + "key.keyboard.left": "左方向键", + "key.keyboard.left.alt": "左Alt", + "key.keyboard.left.bracket": "[", + "key.keyboard.left.control": "左Ctrl", + "key.keyboard.left.shift": "左Shift", + "key.keyboard.left.win": "左Win", + "key.keyboard.menu": "菜单", + "key.keyboard.minus": "-", + "key.keyboard.num.lock": "Num Lock", + "key.keyboard.page.down": "Page Down", + "key.keyboard.page.up": "Page Up", + "key.keyboard.pause": "Pause", + "key.keyboard.period": ".", + "key.keyboard.print.screen": "Print Screen", + "key.keyboard.right": "右方向键", + "key.keyboard.right.alt": "右Alt", + "key.keyboard.right.bracket": "]", + "key.keyboard.right.control": "右Ctrl", + "key.keyboard.right.shift": "右Shift", + "key.keyboard.right.win": "右Win", + "key.keyboard.scroll.lock": "Scroll Lock", + "key.keyboard.semicolon": ";", + "key.keyboard.slash": "/", + "key.keyboard.space": "空格", + "key.keyboard.tab": "Tab", + "key.keyboard.unknown": "未指定", + "key.keyboard.up": "上方向键", + "key.keyboard.world.1": "World 1", + "key.keyboard.world.2": "World 2", + "key.left": "向左移动", + "key.loadToolbarActivator": "加载快捷栏", + "key.mouse": "鼠标按键%1$s", + "key.mouse.left": "左键", + "key.mouse.middle": "中键", + "key.mouse.right": "右键", + "key.pickItem": "选取方块", + "key.playerlist": "显示玩家列表", + "key.right": "向右移动", + "key.saveToolbarActivator": "保存快捷栏", + "key.screenshot": "截图", + "key.smoothCamera": "切换电影视角", + "key.sneak": "潜行", + "key.socialInteractions": "社交屏幕", + "key.spectatorOutlines": "高亮玩家(旁观者)", + "key.sprint": "疾跑", + "key.swapOffhand": "与副手交换物品", + "key.togglePerspective": "切换视角", + "key.use": "使用物品/放置方块", + "known_server_link.announcements": "公告", + "known_server_link.community": "社区", + "known_server_link.community_guidelines": "社区方针", + "known_server_link.feedback": "反馈", + "known_server_link.forums": "论坛", + "known_server_link.news": "新闻", + "known_server_link.report_bug": "报告服务器漏洞", + "known_server_link.status": "状态", + "known_server_link.support": "支持", + "known_server_link.website": "网站", + "lanServer.otherPlayers": "对其他玩家的设置", + "lanServer.port": "端口号", + "lanServer.port.invalid": "端口无效。\n请将编辑框留空,或输入一个介于1024和65535之间的数字。", + "lanServer.port.invalid.new": "端口无效。\n请将编辑框留空,或输入一个介于%s和%s之间的数字。", + "lanServer.port.unavailable": "端口不可用。\n请将编辑框留空,或输入一个介于1024和65535之间的新数字。", + "lanServer.port.unavailable.new": "端口不可用。\n请将编辑框留空,或输入一个介于%s和%s之间的新数字。", + "lanServer.scanning": "正在你的本地网络中寻找游戏", + "lanServer.start": "创建局域网世界", + "lanServer.title": "局域网世界", + "language.code": "zho-Hans_CN", + "language.name": "简体中文", + "language.region": "中国大陆", + "lectern.take_book": "取书", + "loading.progress": "%s%%", + "mco.account.privacy.info": "阅读更多关于Mojang和其他隐私政策的信息", + "mco.account.privacy.info.button": "阅读关于《通用数据保护条例》的内容", + "mco.account.privacy.information": "Mojang实施许多方法来帮助保护儿童及其隐私权,包括履行《儿童在线隐私权保护法案》(COPPA)与《通用数据保护条例》(GDPR)。\n\n在取得Realms账户访问权之前,你可能需要获得父母的同意。", + "mco.account.privacyinfo": "Mojang实施许多方法来促进儿童及其隐私权的保护,包括履行《儿童在线隐私权保护法案》(COPPA)与《通用数据保护条例》(GDPR)。\n\n在访问你的Realms账户之前,你可能需要征得父母的同意。\n\n如果你拥有的是旧版的Minecraft账户(登录时使用用户名),你需要将其合并到Mojang账户以访问Realms。", + "mco.account.update": "更新账户", + "mco.activity.noactivity": "过去%s天无动态", + "mco.activity.title": "玩家动态", + "mco.backup.button.download": "下载最新版本", + "mco.backup.button.reset": "重置世界", + "mco.backup.button.restore": "还原", + "mco.backup.button.upload": "上传世界", + "mco.backup.changes.tooltip": "更改", + "mco.backup.entry": "备份(%s)", + "mco.backup.entry.description": "描述", + "mco.backup.entry.enabledPack": "已启用的资源包", + "mco.backup.entry.gameDifficulty": "游戏难度", + "mco.backup.entry.gameMode": "游戏模式", + "mco.backup.entry.gameServerVersion": "游戏服务器版本", + "mco.backup.entry.name": "名称", + "mco.backup.entry.seed": "种子", + "mco.backup.entry.templateName": "模板名称", + "mco.backup.entry.undefined": "未定义的更改", + "mco.backup.entry.uploaded": "已上传", + "mco.backup.entry.worldType": "世界类型", + "mco.backup.generate.world": "生成世界", + "mco.backup.info.title": "自上次备份以来发生的更改", + "mco.backup.narration": "备份自%s", + "mco.backup.nobackups": "这个Realm目前没有任何备份。", + "mco.backup.restoring": "正在还原你的Realm", + "mco.backup.unknown": "未知", + "mco.brokenworld.download": "下载", + "mco.brokenworld.downloaded": "已下载", + "mco.brokenworld.message.line1": "请重置或选择另一个世界。", + "mco.brokenworld.message.line2": "你可以选择将世界下载至单人模式中。", + "mco.brokenworld.minigame.title": "此小游戏已不受支持", + "mco.brokenworld.nonowner.error": "请等待Realms所有者重置世界", + "mco.brokenworld.nonowner.title": "世界已过期", + "mco.brokenworld.play": "开始游戏", + "mco.brokenworld.reset": "重置", + "mco.brokenworld.title": "当前的世界已不受支持", + "mco.client.incompatible.msg.line1": "你的客户端与Realms不兼容。", + "mco.client.incompatible.msg.line2": "请使用最新版本的Minecraft。", + "mco.client.incompatible.msg.line3": "Realms不与快照版本兼容。", + "mco.client.incompatible.title": "客户端不兼容!", + "mco.client.outdated.stable.version": "你的客户端版本(%s)与Realms不兼容。\n\n请使用最新版本的Minecraft。", + "mco.client.unsupported.snapshot.version": "你的客户端版本(%s)与Realms不兼容。\n\nRealms在此快照版本不可用。", + "mco.compatibility.downgrade": "降级", + "mco.compatibility.downgrade.description": "这个世界上次是在%s版本中打开的,你正在使用%s版本。降低世界的游戏版本可能会导致存档损坏,我们无法保证它可以正确加载和运行。\n\n此世界的备份将会保存在“世界备份”下。如有需要,请恢复原来的世界。", + "mco.compatibility.incompatible.popup.title": "版本不兼容", + "mco.compatibility.incompatible.releaseType.popup.message": "尝试加入的世界与所在版本不兼容。", + "mco.compatibility.incompatible.series.popup.message": "这个世界上次是在%s版本中打开的,你正在使用%s版本。\n\n这些系列互不兼容。此版本需要创建新的世界。", + "mco.compatibility.unverifiable.message": "无法验证上次打开这个世界的版本。如果升级或降级这个世界,备份将会被自动创建并保存在“世界备份”下。", + "mco.compatibility.unverifiable.title": "无法验证兼容性", + "mco.compatibility.upgrade": "升级", + "mco.compatibility.upgrade.description": "这个世界上次是在%s版本中打开的,你正在使用%s版本。\n\n此世界的备份将会保存在“世界备份”下。如有需要,请恢复原来的世界。", + "mco.compatibility.upgrade.friend.description": "这个世界上次是在%s版本中打开的,你正在使用%s版本。\n\n此世界的备份将会保存在“世界备份”下。\n\n如有需要,Realm的所有者可以恢复原来的世界。", + "mco.compatibility.upgrade.title": "你真的想升级此世界吗?", + "mco.configure.current.minigame": "当前", + "mco.configure.world.activityfeed.disabled": "玩家推送暂不可用", + "mco.configure.world.backup": "世界备份", + "mco.configure.world.buttons.activity": "玩家动态", + "mco.configure.world.buttons.close": "关闭Realm", + "mco.configure.world.buttons.delete": "删除", + "mco.configure.world.buttons.done": "完成", + "mco.configure.world.buttons.edit": "设置", + "mco.configure.world.buttons.invite": "邀请玩家", + "mco.configure.world.buttons.moreoptions": "更多选项", + "mco.configure.world.buttons.open": "开启Realm", + "mco.configure.world.buttons.options": "世界选项", + "mco.configure.world.buttons.players": "玩家", + "mco.configure.world.buttons.resetworld": "重置世界", + "mco.configure.world.buttons.settings": "设置", + "mco.configure.world.buttons.subscription": "订阅", + "mco.configure.world.buttons.switchminigame": "切换小游戏", + "mco.configure.world.close.question.line1": "你的Realm将变为不可用。", + "mco.configure.world.close.question.line2": "你确定要继续吗?", + "mco.configure.world.closing": "关闭Realm中…", + "mco.configure.world.commandBlocks": "命令方块", + "mco.configure.world.delete.button": "删除Realm", + "mco.configure.world.delete.question.line1": "你的Realm将被永久删除", + "mco.configure.world.delete.question.line2": "你确定要继续吗?", + "mco.configure.world.description": "Realm描述", + "mco.configure.world.edit.slot.name": "世界名称", + "mco.configure.world.edit.subscreen.adventuremap": "由于你当前的世界是冒险世界,有些选项将会被禁用", + "mco.configure.world.edit.subscreen.experience": "由于你当前的世界是体验世界,有些选项将会被禁用", + "mco.configure.world.edit.subscreen.inspiration": "由于你当前的世界是示例世界,有些选项将会被禁用", + "mco.configure.world.forceGameMode": "强制游戏模式", + "mco.configure.world.invite.narration": "你有%s个新邀请", + "mco.configure.world.invite.profile.name": "名称", + "mco.configure.world.invited": "已邀请", + "mco.configure.world.invited.number": "已邀请(%s)", + "mco.configure.world.invites.normal.tooltip": "普通玩家", + "mco.configure.world.invites.ops.tooltip": "管理员", + "mco.configure.world.invites.remove.tooltip": "移除", + "mco.configure.world.leave.question.line1": "如果你离开这个Realm,你只有被再次邀请才能看见它。", + "mco.configure.world.leave.question.line2": "你确定要继续吗?", + "mco.configure.world.location": "位置", + "mco.configure.world.minigame": "当前:%s", + "mco.configure.world.name": "Realm名称", + "mco.configure.world.opening": "开启Realm中…", + "mco.configure.world.players.error": "不存在该指定名称的玩家", + "mco.configure.world.players.inviting": "邀请玩家中…", + "mco.configure.world.players.title": "玩家", + "mco.configure.world.pvp": "玩家对战", + "mco.configure.world.reset.question.line1": "你的Realm将被重新生成,而当前的Realm将会消失", + "mco.configure.world.reset.question.line2": "你确定要继续吗?", + "mco.configure.world.resourcepack.question": "这个Realm需要一个自定义的资源包\n\n你想要自动下载并安装它吗?", + "mco.configure.world.resourcepack.question.line1": "这个Realm需要一个自定义的资源包", + "mco.configure.world.resourcepack.question.line2": "你想要自动下载并安装它吗?", + "mco.configure.world.restore.download.question.line1": "世界将会被下载并添加至你的单人游戏世界中", + "mco.configure.world.restore.download.question.line2": "你确定要继续吗?", + "mco.configure.world.restore.question.line1": "你的Realm世界将被还原至“%s”(%s)", + "mco.configure.world.restore.question.line2": "你确定要继续吗?", + "mco.configure.world.settings.title": "设置", + "mco.configure.world.slot": "世界%s", + "mco.configure.world.slot.empty": "空", + "mco.configure.world.slot.switch.question.line1": "你的Realm将会被切换至另一个世界", + "mco.configure.world.slot.switch.question.line2": "你确定要继续吗?", + "mco.configure.world.slot.tooltip": "切换到世界", + "mco.configure.world.slot.tooltip.active": "加入", + "mco.configure.world.slot.tooltip.minigame": "切换至小游戏", + "mco.configure.world.spawnAnimals": "生成动物", + "mco.configure.world.spawnMonsters": "生成怪物", + "mco.configure.world.spawnNPCs": "生成NPC", + "mco.configure.world.spawnProtection": "重生点保护", + "mco.configure.world.spawn_toggle.message": "关闭此选项后,所有现有的该类型实体将会被移除", + "mco.configure.world.spawn_toggle.message.npc": "关闭此选项后,所有现有的该类型实体将会被移除,例如村民", + "mco.configure.world.spawn_toggle.title": "警告!", + "mco.configure.world.status": "状态", + "mco.configure.world.subscription.day": "天", + "mco.configure.world.subscription.days": "天", + "mco.configure.world.subscription.expired": "已过期", + "mco.configure.world.subscription.extend": "延长订阅时间", + "mco.configure.world.subscription.less_than_a_day": "不足一天", + "mco.configure.world.subscription.month": "月", + "mco.configure.world.subscription.months": "月", + "mco.configure.world.subscription.recurring.daysleft": "自动续期剩余时间:", + "mco.configure.world.subscription.recurring.info": "对你的Realms订阅所做的更改(如续期和取消自动续订)将在下个结算日生效。", + "mco.configure.world.subscription.remaining.days": "%1$s天", + "mco.configure.world.subscription.remaining.months": "%1$s个月", + "mco.configure.world.subscription.remaining.months.days": "%1$s个月%2$s天", + "mco.configure.world.subscription.start": "开始日期", + "mco.configure.world.subscription.timeleft": "剩余时间", + "mco.configure.world.subscription.title": "订阅信息", + "mco.configure.world.subscription.unknown": "未知", + "mco.configure.world.switch.slot": "创建世界", + "mco.configure.world.switch.slot.subtitle": "这个世界是空的,请选择创建世界的方式", + "mco.configure.world.title": "配置Realm:", + "mco.configure.world.uninvite.player": "你确定要取消邀请“%s”吗?", + "mco.configure.world.uninvite.question": "你确定要取消邀请吗", + "mco.configure.worlds.title": "世界", + "mco.connect.authorizing": "登录中…", + "mco.connect.connecting": "正在连接至Realm…", + "mco.connect.failed": "无法连接至Realm", + "mco.connect.success": "完成", + "mco.create.world": "创建", + "mco.create.world.error": "你必须输入一个名字!", + "mco.create.world.failed": "创建世界失败!", + "mco.create.world.reset.title": "正在创建世界…", + "mco.create.world.skip": "跳过", + "mco.create.world.subtitle": "可选,选择你想放进新Realm的世界", + "mco.create.world.wait": "正在创建Realm…", + "mco.download.cancelled": "下载已取消", + "mco.download.confirmation.line1": "你将下载的世界大小大于%s", + "mco.download.confirmation.line2": "你无法再次将这个世界上传至Realm", + "mco.download.confirmation.oversized": "你将下载的世界大小大于%s\n\n你无法再次将这个世界上传至Realm", + "mco.download.done": "下载完成", + "mco.download.downloading": "正在下载", + "mco.download.extracting": "提取中", + "mco.download.failed": "下载失败", + "mco.download.percent": "%s %%", + "mco.download.preparing": "正在准备下载", + "mco.download.resourcePack.fail": "资源包下载失败!", + "mco.download.speed": "(%s/s)", + "mco.download.speed.narration": "%s/s", + "mco.download.title": "下载最新的世界", + "mco.error.invalid.session.message": "请尝试重启Minecraft", + "mco.error.invalid.session.title": "无效的会话ID", + "mco.errorMessage.6001": "客户端已过期!", + "mco.errorMessage.6002": "尚未同意服务条款", + "mco.errorMessage.6003": "超过下载限制", + "mco.errorMessage.6004": "超过上传限制", + "mco.errorMessage.6005": "世界已锁定", + "mco.errorMessage.6006": "世界已过期", + "mco.errorMessage.6007": "玩家所在的Realm数量过多", + "mco.errorMessage.6008": "无效的Realm名称", + "mco.errorMessage.6009": "无效的Realm描述", + "mco.errorMessage.connectionFailure": "出错了,请稍后再试。", + "mco.errorMessage.generic": "出现错误:", + "mco.errorMessage.initialize.failed": "初始化Realm失败", + "mco.errorMessage.noDetails": "未提供错误详情", + "mco.errorMessage.realmsService": "出现错误(%s):", + "mco.errorMessage.realmsService.connectivity": "无法连接至Realm:%s", + "mco.errorMessage.realmsService.realmsError": "Realms(%s):", + "mco.errorMessage.realmsService.unknownCompatibility": "无法检查兼容版本,响应为:%s", + "mco.errorMessage.retry": "重试操作", + "mco.errorMessage.serviceBusy": "Realms目前处于繁忙状态。\n请在几分钟后再次尝试连接至你的Realm。", + "mco.gui.button": "按钮", + "mco.gui.ok": "确定", + "mco.info": "信息!", + "mco.invited.player.narration": "已邀请玩家%s", + "mco.invites.button.accept": "接受", + "mco.invites.button.reject": "拒绝", + "mco.invites.nopending": "没有未决邀请!", + "mco.invites.pending": "新邀请!", + "mco.invites.title": "未决邀请", + "mco.minigame.world.changeButton": "选择另一个小游戏", + "mco.minigame.world.info.line1": "这会暂时将你的世界替换成一个小游戏!", + "mco.minigame.world.info.line2": "之后你可以毫无损失地返回到原来的世界。", + "mco.minigame.world.noSelection": "请选择", + "mco.minigame.world.restore": "结束小游戏中…", + "mco.minigame.world.restore.question.line1": "小游戏将结束而你的Realm将被恢复。", + "mco.minigame.world.restore.question.line2": "你确定要继续吗?", + "mco.minigame.world.selected": "所选的小游戏:", + "mco.minigame.world.slot.screen.title": "切换世界中…", + "mco.minigame.world.startButton": "切换", + "mco.minigame.world.starting.screen.title": "开始小游戏中…", + "mco.minigame.world.stopButton": "结束小游戏", + "mco.minigame.world.switch.new": "选择另一个小游戏?", + "mco.minigame.world.switch.title": "切换小游戏", + "mco.minigame.world.title": "将Realm切换成小游戏", + "mco.news": "Realms新闻", + "mco.notification.dismiss": "关闭", + "mco.notification.transferSubscription.buttonText": "立即转移", + "mco.notification.transferSubscription.message": "Java版Realms订阅即将转移到Microsoft Store。别让你的订阅过期了!\n现在转移即可免费获得30天的Realms服务。\n请在minecraft.net的“档案”页面转移你的订阅。", + "mco.notification.visitUrl.buttonText.default": "打开链接", + "mco.notification.visitUrl.message.default": "请访问下方的链接", + "mco.onlinePlayers": "在线玩家", + "mco.question": "问题", + "mco.reset.world.adventure": "冒险", + "mco.reset.world.experience": "体验", + "mco.reset.world.generate": "新的世界", + "mco.reset.world.inspiration": "示例", + "mco.reset.world.resetting.screen.title": "重置世界中…", + "mco.reset.world.seed": "世界种子(可选)", + "mco.reset.world.template": "世界模板", + "mco.reset.world.title": "重置世界", + "mco.reset.world.upload": "上传世界", + "mco.reset.world.warning": "这将替换掉你目前的Realm世界!", + "mco.selectServer.buy": "购买Realm服务器!", + "mco.selectServer.close": "关闭", + "mco.selectServer.closed": "已关闭的Realm", + "mco.selectServer.closeserver": "关闭Realm", + "mco.selectServer.configure": "配置Realm", + "mco.selectServer.configureRealm": "配置Realm", + "mco.selectServer.create": "创建Realm", + "mco.selectServer.create.subtitle": "选择想放进你新Realm的世界", + "mco.selectServer.expired": "已过期的Realm", + "mco.selectServer.expiredList": "你的订阅已过期", + "mco.selectServer.expiredRenew": "续期", + "mco.selectServer.expiredSubscribe": "订阅", + "mco.selectServer.expiredTrial": "你的试用期已结束", + "mco.selectServer.expires.day": "将在一天内过期", + "mco.selectServer.expires.days": "将在%s天后过期", + "mco.selectServer.expires.soon": "即将过期", + "mco.selectServer.leave": "离开Realm", + "mco.selectServer.loading": "加载Realm列表中", + "mco.selectServer.mapOnlySupportedForVersion": "此地图不受%s支持", + "mco.selectServer.minigame": "小游戏:", + "mco.selectServer.minigameName": "小游戏:%s", + "mco.selectServer.minigameNotSupportedInVersion": "无法在%s中进行这个小游戏", + "mco.selectServer.noRealms": "你似乎还没有Realm。添加Realm与朋友们一同畅玩。", + "mco.selectServer.note": "注意:", + "mco.selectServer.open": "已开启的Realm", + "mco.selectServer.openserver": "开启Realm", + "mco.selectServer.play": "开始游戏", + "mco.selectServer.popup": "Realms最多可以同时支持十位好友安全且简单地在线享受Minecraft世界。它也支持多种小游戏与海量自定义世界!只有Realm的所有者需要付款。", + "mco.selectServer.purchase": "添加Realm", + "mco.selectServer.trial": "开始试用!", + "mco.selectServer.uninitialized": "点击以创建新的Realm!", + "mco.snapshot.createSnapshotPopup.text": "你即将创建免费的快照版Realm,它会与你的付费Realm订阅绑定。新的快照版Realm在付费订阅生效期间可以随时访问。原本的付费Realm不会受到影响。", + "mco.snapshot.createSnapshotPopup.title": "是否创建快照版Realm?", + "mco.snapshot.creating": "正在创建快照版Realm…", + "mco.snapshot.description": "与%s绑定", + "mco.snapshot.friendsRealm.downgrade": "你需要使用版本%s加入此Realm", + "mco.snapshot.friendsRealm.upgrade": "在你使用此快照进行游戏之前,%s需要升级其Realm", + "mco.snapshot.paired": "此快照版Realm与%s绑定", + "mco.snapshot.parent.tooltip": "使用最新的Minecraft正式版在此Realm上进行游戏", + "mco.snapshot.start": "开启免费的快照版Realm", + "mco.snapshot.subscription.info": "这是与你的Realm订阅“%s”绑定的快照版Realm。它会在与其绑定的Realm可用时保持可用。", + "mco.snapshot.tooltip": "使用快照版Realm抢先了解即将推出的Minecraft版本,其中可能包括新功能和其他更改。\n\n你可以在游戏的正式版中找到普通的Realm。", + "mco.snapshotRealmsPopup.message": "Realms从23w41a开始可在快照版本中使用。每份Realms订阅中包含一份和你普通Java版Realm相互独立的免费快照版Realm!", + "mco.snapshotRealmsPopup.title": "Realms现可在快照版本中使用", + "mco.snapshotRealmsPopup.urlText": "了解更多", + "mco.template.button.publisher": "发布者", + "mco.template.button.select": "选择", + "mco.template.button.trailer": "预告片", + "mco.template.default.name": "世界模板", + "mco.template.info.tooltip": "发布者的网站", + "mco.template.name": "模板", + "mco.template.select.failure": "我们无法接收这个分类的内容。\n请检查你的网络连接,或稍后再试。", + "mco.template.select.narrate.authors": "作者:%s", + "mco.template.select.narrate.version": "版本:%s", + "mco.template.select.none": "抱歉,看来这个分类目前是空的。\n请之后再回来检查是否有新内容,如果你是一名创作者的话,\n%s。", + "mco.template.select.none.linkTitle": "尝试自己提交一些自己的内容", + "mco.template.title": "世界模板", + "mco.template.title.minigame": "Realm小游戏", + "mco.template.trailer.tooltip": "地图预告", + "mco.terms.buttons.agree": "同意", + "mco.terms.buttons.disagree": "不同意", + "mco.terms.sentence.1": "我已阅读并接受Minecraft Realms的", + "mco.terms.sentence.2": "服务条款", + "mco.terms.title": "Realms服务条款", + "mco.time.daysAgo": "%1$s天前", + "mco.time.hoursAgo": "%1$s小时前", + "mco.time.minutesAgo": "%1$s分钟前", + "mco.time.now": "刚刚", + "mco.time.secondsAgo": "%1$s秒前", + "mco.trial.message.line1": "你想获得属于自己的Realm吗?", + "mco.trial.message.line2": "点击此处来获得更多消息!", + "mco.upload.button.name": "上传", + "mco.upload.cancelled": "上传已取消", + "mco.upload.close.failure": "无法关闭服务器,请稍后再试", + "mco.upload.done": "上传已完成", + "mco.upload.entry.cheats": "%1$s, %2$s", + "mco.upload.entry.commands": "%1$s, %2$s", + "mco.upload.entry.id": "%1$s(%2$s)", + "mco.upload.failed": "上传失败!(%s)", + "mco.upload.failed.too_big.description": "选中的世界过大。最大允许大小为%s。", + "mco.upload.failed.too_big.title": "世界过大", + "mco.upload.hardcore": "不能上传极限模式世界!", + "mco.upload.percent": "%s %%", + "mco.upload.preparing": "正在准备你的世界数据", + "mco.upload.select.world.none": "找不到单人游戏世界!", + "mco.upload.select.world.subtitle": "请选择一个单人游戏世界来上传", + "mco.upload.select.world.title": "上传世界", + "mco.upload.size.failure.line1": "“%s”太大了!", + "mco.upload.size.failure.line2": "你想上传%s的地图,超过了%s的限制。", + "mco.upload.uploading": "正在上传“%s”", + "mco.upload.verifying": "正在验证你的世界", + "mco.version": "版本:%s", + "mco.warning": "警告!", + "mco.worldSlot.minigame": "小游戏", + "menu.disconnect": "断开连接", + "menu.feedback": "反馈…", + "menu.feedback.title": "反馈", + "menu.game": "游戏菜单", + "menu.modded": "(已修改)", + "menu.multiplayer": "多人游戏", + "menu.online": "Minecraft Realms", + "menu.options": "选项…", + "menu.paused": "游戏暂停", + "menu.playdemo": "开始试玩世界", + "menu.playerReporting": "举报玩家", + "menu.preparingSpawn": "准备生成区域中:%s%%", + "menu.quit": "退出游戏", + "menu.reportBugs": "报告漏洞", + "menu.resetdemo": "重置试玩世界", + "menu.returnToGame": "回到游戏", + "menu.returnToMenu": "保存并退回到标题屏幕", + "menu.savingChunks": "保存区块中", + "menu.savingLevel": "保存世界中", + "menu.sendFeedback": "提供反馈", + "menu.server_links": "服务器链接…", + "menu.server_links.title": "服务器链接", + "menu.shareToLan": "对局域网开放", + "menu.singleplayer": "单人游戏", + "menu.working": "处理中…", + "merchant.deprecated": "村民每天最多补货两次。", + "merchant.level.1": "新手", + "merchant.level.2": "学徒", + "merchant.level.3": "老手", + "merchant.level.4": "专家", + "merchant.level.5": "大师", + "merchant.title": "%s - %s", + "merchant.trades": "交易", + "mirror.front_back": "↑ ↓", + "mirror.left_right": "← →", + "mirror.none": "|", + "mount.onboard": "按下%1$s来脱离", + "multiplayer.applyingPack": "正在应用资源包", + "multiplayer.disconnect.authservers_down": "身份验证服务器目前处于宕机状态。请稍后再试,抱歉!", + "multiplayer.disconnect.banned": "你已被此服务器封禁", + "multiplayer.disconnect.banned.expiration": "\n你的封禁将于%s解除", + "multiplayer.disconnect.banned.reason": "你已被此服务器封禁。\n原因:%s", + "multiplayer.disconnect.banned_ip.expiration": "\n你的封禁将于%s解除", + "multiplayer.disconnect.banned_ip.reason": "你的IP已被此服务器封禁。\n原因:%s", + "multiplayer.disconnect.chat_validation_failed": "聊天消息验证失败", + "multiplayer.disconnect.duplicate_login": "你已在异地登录", + "multiplayer.disconnect.expired_public_key": "个人信息公钥过期。请确保系统时间同步,然后尝试重启游戏。", + "multiplayer.disconnect.flying": "此服务器未启用飞行", + "multiplayer.disconnect.generic": "连接中断", + "multiplayer.disconnect.idling": "你的未操作时间过长!", + "multiplayer.disconnect.illegal_characters": "非法聊天字符", + "multiplayer.disconnect.incompatible": "客户端不兼容!请使用%s", + "multiplayer.disconnect.invalid_entity_attacked": "试图攻击无效实体", + "multiplayer.disconnect.invalid_packet": "服务器发送了一个无效的数据包", + "multiplayer.disconnect.invalid_player_data": "无效的玩家数据", + "multiplayer.disconnect.invalid_player_movement": "收到了包含非法玩家移动的数据包", + "multiplayer.disconnect.invalid_public_key_signature": "无效的个人信息公钥签名。\n请尝试重启游戏。", + "multiplayer.disconnect.invalid_public_key_signature.new": "无效的个人信息公钥签名。\n请尝试重启游戏。", + "multiplayer.disconnect.invalid_vehicle_movement": "收到了包含非法载具移动的数据包", + "multiplayer.disconnect.ip_banned": "你的IP地址已被此服务器封禁", + "multiplayer.disconnect.kicked": "被管理员踢出游戏", + "multiplayer.disconnect.missing_tags": "从服务器接收到不完整标签集。\n请联系服务器管理员。", + "multiplayer.disconnect.name_taken": "此名称已被占用", + "multiplayer.disconnect.not_whitelisted": "你不在此服务器的白名单中!", + "multiplayer.disconnect.out_of_order_chat": "接收到了乱序的聊天数据包。你的系统时间是否被更改过?", + "multiplayer.disconnect.outdated_client": "客户端不兼容!请使用%s", + "multiplayer.disconnect.outdated_server": "客户端不兼容!请使用%s", + "multiplayer.disconnect.server_full": "服务器已满!", + "multiplayer.disconnect.server_shutdown": "服务器已关闭", + "multiplayer.disconnect.slow_login": "登录超时", + "multiplayer.disconnect.too_many_pending_chats": "没有确认的聊天消息过多", + "multiplayer.disconnect.transfers_disabled": "服务器不接受转移", + "multiplayer.disconnect.unexpected_query_response": "从客户端收到未知的自定义数据", + "multiplayer.disconnect.unsigned_chat": "接收到了缺少签名或含有无效签名的聊天数据包。", + "multiplayer.disconnect.unverified_username": "验证用户名失败!", + "multiplayer.downloadingStats": "正在获取统计信息…", + "multiplayer.downloadingTerrain": "加载地形中…", + "multiplayer.lan.server_found": "发现新的服务器:%s", + "multiplayer.message_not_delivered": "无法发送聊天消息,请检查服务器日志:%s", + "multiplayer.player.joined": "%s加入了游戏", + "multiplayer.player.joined.renamed": "%s(之前被称为%s)加入了游戏", + "multiplayer.player.left": "%s退出了游戏", + "multiplayer.player.list.hp": "%s生命值", + "multiplayer.player.list.narration": "在线玩家:%s", + "multiplayer.requiredTexturePrompt.disconnect": "服务器需要自定义资源包", + "multiplayer.requiredTexturePrompt.line1": "这个服务器需要使用自定义的资源包。", + "multiplayer.requiredTexturePrompt.line2": "拒绝使用该自定义资源包将会断开你与该服务器间的连接。", + "multiplayer.socialInteractions.not_available": "社交屏幕仅在多人游戏世界中可用", + "multiplayer.status.and_more": "…及其他%s名玩家…", + "multiplayer.status.cancelled": "已取消", + "multiplayer.status.cannot_connect": "无法连接到服务器", + "multiplayer.status.cannot_resolve": "无法解析主机名", + "multiplayer.status.finished": "已完成", + "multiplayer.status.incompatible": "版本不兼容!", + "multiplayer.status.motd.narration": "服务器信息:%s", + "multiplayer.status.no_connection": "(无连接)", + "multiplayer.status.old": "旧版", + "multiplayer.status.online": "在线", + "multiplayer.status.ping": "%s毫秒", + "multiplayer.status.ping.narration": "延迟为%s毫秒", + "multiplayer.status.pinging": "检测中…", + "multiplayer.status.player_count": "%s/%s", + "multiplayer.status.player_count.narration": "%s名玩家在线,共%s名玩家", + "multiplayer.status.quitting": "正在退出", + "multiplayer.status.request_handled": "状态请求已处理", + "multiplayer.status.unknown": "???", + "multiplayer.status.unrequested": "收到了未请求的状态", + "multiplayer.status.version.narration": "服务器版本:%s", + "multiplayer.stopSleeping": "起床", + "multiplayer.texturePrompt.failure.line1": "无法应用服务器资源包", + "multiplayer.texturePrompt.failure.line2": "所有依赖自定义资源包的功能都有可能不按预期工作", + "multiplayer.texturePrompt.line1": "这个服务器推荐使用自定义的资源包。", + "multiplayer.texturePrompt.line2": "你想要自动下载和安装它吗?", + "multiplayer.texturePrompt.serverPrompt": "%s\n\n来自服务器的消息:\n%s", + "multiplayer.title": "多人游戏", + "multiplayer.unsecureserver.toast": "该服务器上发送的消息可能会被修改,可能无法反映原始消息", + "multiplayer.unsecureserver.toast.title": "无法验证聊天消息", + "multiplayerWarning.check": "不再显示此屏幕", + "multiplayerWarning.header": "警告:在线游戏由第三方提供", + "multiplayerWarning.message": "警告:在线游戏由第三方服务器提供,不由Mojang Studios或Microsoft拥有、运营和监督。 在线游戏的过程中,你可能会收到不受规制的聊天消息或者其他用户生成内容,而这些内容可能不适合所有人。", + "narration.button": "按钮:%s", + "narration.button.usage.focused": "按下Enter键激活", + "narration.button.usage.hovered": "单击鼠标左键激活", + "narration.checkbox": "复选框:%s", + "narration.checkbox.usage.focused": "按下Enter键切换", + "narration.checkbox.usage.hovered": "单击鼠标左键切换", + "narration.component_list.usage": "按下Tab键移动到下一个屏幕控件", + "narration.cycle_button.usage.focused": "按下Enter键切换到%s", + "narration.cycle_button.usage.hovered": "单击鼠标左键切换到%s", + "narration.edit_box": "编辑框:%s", + "narration.recipe": "%s的配方", + "narration.recipe.usage": "单击鼠标左键选中", + "narration.recipe.usage.more": "单击鼠标右键显示更多配方", + "narration.selection.usage": "使用上下方向键移动到另一项", + "narration.slider.usage.focused": "使用左右方向键更改数值", + "narration.slider.usage.hovered": "拖动滑块更改数值", + "narration.suggestion": "已选中%2$s项建议中的第%1$s项:%3$s", + "narration.suggestion.tooltip": "已选中%2$s项建议中的第%1$s项:%3$s(%4$s)", + "narration.suggestion.usage.cycle.fixed": "按下Tab键切换到下一个建议", + "narration.suggestion.usage.cycle.hidable": "按下Tab键切换到下一个建议,或按下Escape键忽略建议", + "narration.suggestion.usage.fill.fixed": "按下Tab键使用建议", + "narration.suggestion.usage.fill.hidable": "按下Tab键使用建议,或按下Escape键忽略建议", + "narration.tab_navigation.usage": "按下Ctrl键和Tab键切换标签页", + "narrator.button.accessibility": "辅助功能", + "narrator.button.difficulty_lock": "难度锁定", + "narrator.button.difficulty_lock.locked": "已锁定", + "narrator.button.difficulty_lock.unlocked": "未锁定", + "narrator.button.language": "语言", + "narrator.controls.bound": "%s已绑定至%s", + "narrator.controls.reset": "重置%s按钮", + "narrator.controls.unbound": "%s未绑定", + "narrator.joining": "加入中", + "narrator.loading": "加载中:%s", + "narrator.loading.done": "完成", + "narrator.position.list": "已选中列表的第%s行,共%s行", + "narrator.position.object_list": "已选中一列控件中的第%s项,共%s项", + "narrator.position.screen": "屏幕控件,第%s个,共%s个", + "narrator.position.tab": "已选中第%s个标签页,共%s个", + "narrator.ready_to_play": "准备就绪", + "narrator.screen.title": "标题屏幕", + "narrator.screen.usage": "使用鼠标指针或Tab键选择屏幕控件", + "narrator.select": "已选择:%s", + "narrator.select.world": "已选择%s,上次进入游戏:%s,%s,%s,版本:%s", + "narrator.select.world_info": "已选择%s,上次进入游戏:%s,%s", + "narrator.toast.disabled": "复述功能已关闭", + "narrator.toast.enabled": "复述功能已开启", + "optimizeWorld.confirm.description": "这将尝试用最新的游戏格式储存所有数据来达到优化世界的效果。取决于世界的状况,这可能会花费不少时间。一旦完成,进行游戏时可能会更流畅,但你的世界将不再与旧版游戏兼容。你确定要继续吗?", + "optimizeWorld.confirm.proceed": "创建备份并优化", + "optimizeWorld.confirm.title": "优化世界", + "optimizeWorld.info.converted": "已更新的区块数:%s", + "optimizeWorld.info.skipped": "已忽略的区块数:%s", + "optimizeWorld.info.total": "总区块数:%s", + "optimizeWorld.progress.counter": "%s / %s", + "optimizeWorld.progress.percentage": "%s%%", + "optimizeWorld.stage.counting": "统计区块中…", + "optimizeWorld.stage.failed": "失败了!:(", + "optimizeWorld.stage.finished": "即将完成…", + "optimizeWorld.stage.finished.chunks": "即将完成区块的升级…", + "optimizeWorld.stage.finished.entities": "即将完成实体的升级…", + "optimizeWorld.stage.finished.poi": "即将完成兴趣点的升级…", + "optimizeWorld.stage.upgrading": "升级所有区块中…", + "optimizeWorld.stage.upgrading.chunks": "升级所有区块中…", + "optimizeWorld.stage.upgrading.entities": "升级所有实体中…", + "optimizeWorld.stage.upgrading.poi": "升级所有兴趣点中…", + "optimizeWorld.title": "正在优化世界“%s”", + "options.accessibility": "辅助功能设置…", + "options.accessibility.high_contrast": "高对比度", + "options.accessibility.high_contrast.error.tooltip": "高对比度资源包不可用。", + "options.accessibility.high_contrast.tooltip": "提高UI控件的对比度。", + "options.accessibility.high_contrast_block_outline": "高对比度方块轮廓", + "options.accessibility.high_contrast_block_outline.tooltip": "提高选中方块的轮廓对比度。", + "options.accessibility.link": "辅助功能指南", + "options.accessibility.menu_background_blurriness": "菜单背景模糊程度", + "options.accessibility.menu_background_blurriness.tooltip": "更改菜单背景的模糊程度。", + "options.accessibility.narrator_hotkey": "复述功能快捷键", + "options.accessibility.narrator_hotkey.mac.tooltip": "允许使用“Cmd+B”开关复述功能。", + "options.accessibility.narrator_hotkey.tooltip": "允许使用“Ctrl+B”开关复述功能。", + "options.accessibility.panorama_speed": "全景图滚动速度", + "options.accessibility.text_background": "文本背景", + "options.accessibility.text_background.chat": "聊天", + "options.accessibility.text_background.everywhere": "全局", + "options.accessibility.text_background_opacity": "文本背景不透明度", + "options.accessibility.title": "辅助功能设置", + "options.allowServerListing": "允许列入服务器玩家列表", + "options.allowServerListing.tooltip": "服务器可能会公开列出目前在线的玩家。\n若关闭此选项,你的名字将不会显示在此列表中。", + "options.ao": "平滑光照", + "options.ao.max": "最大", + "options.ao.min": "最小", + "options.ao.off": "关", + "options.attack.crosshair": "十字准星", + "options.attack.hotbar": "快捷栏", + "options.attackIndicator": "攻击指示器", + "options.audioDevice": "设备", + "options.audioDevice.default": "系统默认", + "options.autoJump": "自动跳跃", + "options.autoSuggestCommands": "命令提示", + "options.autosaveIndicator": "自动保存指示器", + "options.biomeBlendRadius": "生物群系过渡距离", + "options.biomeBlendRadius.1": "关(最快)", + "options.biomeBlendRadius.11": "11×11(极高)", + "options.biomeBlendRadius.13": "13×13(显著)", + "options.biomeBlendRadius.15": "15×15(最高)", + "options.biomeBlendRadius.3": "3×3(快)", + "options.biomeBlendRadius.5": "5×5(普通)", + "options.biomeBlendRadius.7": "7×7(高)", + "options.biomeBlendRadius.9": "9×9(很高)", + "options.chat": "聊天设置…", + "options.chat.color": "颜色", + "options.chat.delay": "聊天延迟:%s秒", + "options.chat.delay_none": "聊天延迟:无", + "options.chat.height.focused": "聚焦高度", + "options.chat.height.unfocused": "淡化高度", + "options.chat.line_spacing": "行距", + "options.chat.links": "网页链接", + "options.chat.links.prompt": "链接提示", + "options.chat.opacity": "聊天文本不透明度", + "options.chat.scale": "聊天文本大小", + "options.chat.title": "聊天设置", + "options.chat.visibility": "聊天", + "options.chat.visibility.full": "显示", + "options.chat.visibility.hidden": "隐藏", + "options.chat.visibility.system": "仅限命令", + "options.chat.width": "宽度", + "options.chunks": "%s个区块", + "options.clouds.fancy": "高品质", + "options.clouds.fast": "流畅", + "options.controls": "按键控制…", + "options.credits_and_attribution": "鸣谢与著作权说明…", + "options.damageTiltStrength": "受伤抖动效果", + "options.damageTiltStrength.tooltip": "受到伤害导致的游戏视角晃动程度。", + "options.darkMojangStudiosBackgroundColor": "黑白徽标", + "options.darkMojangStudiosBackgroundColor.tooltip": "将Mojang Studios加载屏幕的背景颜色更改为黑色。", + "options.darknessEffectScale": "黑暗脉动效果", + "options.darknessEffectScale.tooltip": "控制监守者或幽匿尖啸体给予的黑暗脉动效果程度。", + "options.difficulty": "难度", + "options.difficulty.easy": "简单", + "options.difficulty.easy.info": "敌对生物会生成,但伤害较低。生命值在饥饿值耗尽后最终会降至5颗心。", + "options.difficulty.hard": "困难", + "options.difficulty.hard.info": "敌对生物会生成,伤害较高。饥饿值耗尽后最终会饿死。", + "options.difficulty.hardcore": "极限", + "options.difficulty.normal": "普通", + "options.difficulty.normal.info": "敌对生物会生成,伤害适中。生命值在饥饿值耗尽后最终会降至半颗心。", + "options.difficulty.online": "服务器难度", + "options.difficulty.peaceful": "和平", + "options.difficulty.peaceful.info": "不会生成绝大多数敌对生物。饥饿值不会消耗,且生命值会自然恢复。", + "options.directionalAudio": "定向音频", + "options.directionalAudio.off.tooltip": "经典立体声。", + "options.directionalAudio.on.tooltip": "通过基于HRTF算法的定向环绕音频来改善立体音效的模拟。需使用与HRTF兼容的音频硬件,推荐在使用时佩戴耳机。", + "options.discrete_mouse_scroll": "离散滚动", + "options.entityDistanceScaling": "实体渲染距离", + "options.entityShadows": "实体阴影", + "options.font": "字体设置…", + "options.font.title": "字体设置", + "options.forceUnicodeFont": "强制使用Unicode字体", + "options.fov": "视场角", + "options.fov.max": "广角", + "options.fov.min": "中", + "options.fovEffectScale": "视场角效果", + "options.fovEffectScale.tooltip": "控制游戏机制效果改变视场角的程度。", + "options.framerate": "%s fps", + "options.framerateLimit": "最大帧率", + "options.framerateLimit.max": "无限制", + "options.fullscreen": "全屏", + "options.fullscreen.current": "当前分辨率", + "options.fullscreen.entry": "%s×%s@%sHz(%s位色深)", + "options.fullscreen.resolution": "全屏分辨率", + "options.fullscreen.unavailable": "设置不可用", + "options.gamma": "亮度", + "options.gamma.default": "默认", + "options.gamma.max": "明亮", + "options.gamma.min": "昏暗", + "options.generic_value": "%s:%s", + "options.glintSpeed": "附魔光效闪烁速度", + "options.glintSpeed.tooltip": "控制附魔物品的光效闪动速度。", + "options.glintStrength": "附魔光效闪烁强度", + "options.glintStrength.tooltip": "控制附魔物品光效的透明程度。", + "options.graphics": "图像品质", + "options.graphics.fabulous": "极佳!", + "options.graphics.fabulous.tooltip": "%s画质使用屏幕着色器绘制天气、云以及半透明方块和水后面的粒子。\n这也许会在便携设备和4K显示屏上造成严重的性能负担。", + "options.graphics.fancy": "高品质", + "options.graphics.fancy.tooltip": "高品质画质会为大多数设备平衡性能和质量。\n天气、云和粒子可能不会在半透明方块或水的后面显示。", + "options.graphics.fast": "流畅", + "options.graphics.fast.tooltip": "流畅画质将减少雨雪的可见数量。\n树叶等方块的透明效果将被禁用。", + "options.graphics.warning.accept": "在不受支持的情况下继续", + "options.graphics.warning.cancel": "算了", + "options.graphics.warning.message": "检测到你的图形设备不支持%s画质选项。\n\n你可以忽略此提示并继续,但如果你依然选择使用%s画质,你的设备将不会受到支持。", + "options.graphics.warning.renderer": "检测到渲染器:[%s]", + "options.graphics.warning.title": "图形设备不受支持", + "options.graphics.warning.vendor": "检测到厂商:[%s]", + "options.graphics.warning.version": "检测到OpenGL版本:[%s]", + "options.guiScale": "界面尺寸", + "options.guiScale.auto": "自动", + "options.hidden": "隐藏", + "options.hideLightningFlashes": "隐藏闪电的闪烁效果", + "options.hideLightningFlashes.tooltip": "关闭闪电造成的天空闪烁效果。闪电本身仍将可见。", + "options.hideMatchedNames": "隐藏匹配的名称", + "options.hideMatchedNames.tooltip": "第三方服务器也许会发送非标准格式的聊天消息。\n此设置打开后,被隐藏的玩家将会被根据发言者的名字匹配。", + "options.hideSplashTexts": "隐藏闪烁标语", + "options.hideSplashTexts.tooltip": "隐藏主菜单上的黄色闪烁标语。", + "options.inactivityFpsLimit": "降低帧率", + "options.inactivityFpsLimit.afk": "无操作时", + "options.inactivityFpsLimit.afk.tooltip": "若超过1分钟无操作,帧率将被限制至30。9分钟后进一步降至10。", + "options.inactivityFpsLimit.minimized": "最小化时", + "options.inactivityFpsLimit.minimized.tooltip": "仅在游戏窗口最小化时限制帧率。", + "options.invertMouse": "鼠标反转", + "options.japaneseGlyphVariants": "日本字形变体", + "options.japaneseGlyphVariants.tooltip": "在默认字体中为CJK字符使用日本字形。", + "options.key.hold": "按住", + "options.key.toggle": "切换", + "options.language": "语言…", + "options.language.title": "语言", + "options.languageAccuracyWarning": "(语言翻译不一定100%%准确)", + "options.languageWarning": "语言翻译不一定100%%准确", + "options.mainHand": "主手", + "options.mainHand.left": "左手", + "options.mainHand.right": "右手", + "options.mipmapLevels": "Mipmap级别", + "options.modelPart.cape": "披风", + "options.modelPart.hat": "帽子", + "options.modelPart.jacket": "外套", + "options.modelPart.left_pants_leg": "左裤腿", + "options.modelPart.left_sleeve": "左袖", + "options.modelPart.right_pants_leg": "右裤腿", + "options.modelPart.right_sleeve": "右袖", + "options.mouseWheelSensitivity": "滚轮灵敏度", + "options.mouse_settings": "鼠标设置…", + "options.mouse_settings.title": "鼠标设置", + "options.multiplayer.title": "多人游戏设置…", + "options.multiplier": "%s×", + "options.narrator": "复述功能", + "options.narrator.all": "复述所有", + "options.narrator.chat": "复述聊天消息", + "options.narrator.notavailable": "不可用", + "options.narrator.off": "关", + "options.narrator.system": "复述系统消息", + "options.notifications.display_time": "通知显示时长", + "options.notifications.display_time.tooltip": "影响所有通知在屏幕上停留的时长。", + "options.off": "关", + "options.off.composed": "%s:关", + "options.on": "开", + "options.on.composed": "%s:开", + "options.online": "在线选项…", + "options.online.title": "在线选项", + "options.onlyShowSecureChat": "仅显示安全的聊天", + "options.onlyShowSecureChat.tooltip": "仅显示来自其他玩家的可验证为该玩家发送且未被修改的消息。", + "options.operatorItemsTab": "管理员用品标签页", + "options.particles": "粒子效果", + "options.particles.all": "全部", + "options.particles.decreased": "少量", + "options.particles.minimal": "最少", + "options.percent_add_value": "%s:+%s%%", + "options.percent_value": "%s:%s%%", + "options.pixel_value": "%s:%spx", + "options.prioritizeChunkUpdates": "区块构建器", + "options.prioritizeChunkUpdates.byPlayer": "半阻塞", + "options.prioritizeChunkUpdates.byPlayer.tooltip": "区块内部的某些行为会导致区块立刻重新编译。这包括放置或破坏方块。", + "options.prioritizeChunkUpdates.nearby": "全阻塞", + "options.prioritizeChunkUpdates.nearby.tooltip": "附近的区块总会被立刻编译。这可能影响放置或破坏方块时的游戏性能。", + "options.prioritizeChunkUpdates.none": "线程化", + "options.prioritizeChunkUpdates.none.tooltip": "附近的区块会在并行的线程中编译。这可能导致破坏方块时短暂出现图像空洞。", + "options.rawMouseInput": "原始输入", + "options.realmsNotifications": "Realms新闻与邀请", + "options.realmsNotifications.tooltip": "在标题屏幕上获取Realms新闻和邀请,并在Realms按钮上显示相应图标。", + "options.reducedDebugInfo": "简化调试信息", + "options.renderClouds": "云", + "options.renderDistance": "渲染距离", + "options.resourcepack": "资源包…", + "options.rotateWithMinecart": "同步矿车转动", + "options.rotateWithMinecart.tooltip": "是否在矿车转动时同步转动玩家视角。仅在开启“矿车改进”实验性设置的世界中可用。", + "options.screenEffectScale": "屏幕扭曲效果", + "options.screenEffectScale.tooltip": "反胃状态效果和下界传送门所使用的屏幕扭曲效果的强度。\n值较低时,反胃的扭曲效果会被一层绿色的视觉效果替代。", + "options.sensitivity": "鼠标灵敏度", + "options.sensitivity.max": "超高速!!!", + "options.sensitivity.min": "*哈欠*", + "options.showSubtitles": "显示字幕", + "options.simulationDistance": "模拟距离", + "options.skinCustomisation": "自定义皮肤…", + "options.skinCustomisation.title": "自定义皮肤", + "options.sounds": "音乐和声音…", + "options.sounds.title": "音乐和声音选项", + "options.telemetry": "遥测数据…", + "options.telemetry.button": "数据收集", + "options.telemetry.button.tooltip": "“%s”仅包含必要数据。\n“%s”包含可选数据和必要数据。", + "options.telemetry.disabled": "遥测已禁用。", + "options.telemetry.state.all": "全部", + "options.telemetry.state.minimal": "最少", + "options.telemetry.state.none": "无", + "options.title": "选项", + "options.touchscreen": "触屏模式", + "options.video": "视频设置…", + "options.videoTitle": "视频设置", + "options.viewBobbing": "视角摇晃", + "options.visible": "显示", + "options.vsync": "垂直同步", + "outOfMemory.message": "Minecraft已耗尽内存。\n\n原因可能是游戏存在漏洞,或分配给Java虚拟机的内存过少。\n\n为避免世界损坏,当前游戏已退出。我们已尝试腾出内存,让你可以回到主菜单并继续游戏,但这不一定奏效。\n\n如果你再次看见此消息,请重启游戏。", + "outOfMemory.title": "内存溢出!", + "pack.available.title": "可用", + "pack.copyFailure": "复制包失败", + "pack.dropConfirm": "你确定要将这些包添加进Minecraft中吗?", + "pack.dropInfo": "将文件拖放到这个窗口内来添加包", + "pack.dropRejected.message": "以下项目不是有效的包,未能执行复制操作:\n %s", + "pack.dropRejected.title": "所添加的内容不是包", + "pack.folderInfo": "(请将包放在这里)", + "pack.incompatible": "不兼容", + "pack.incompatible.confirm.new": "这个包是为更新的Minecraft版本所打造的,在此版本可能不会正常工作。", + "pack.incompatible.confirm.old": "这个包是为更旧的Minecraft版本所打造的,在此版本可能不会正常工作。", + "pack.incompatible.confirm.title": "你确定要加载此包吗?", + "pack.incompatible.new": "(适用于新版本的Minecraft)", + "pack.incompatible.old": "(适用于旧版本的Minecraft)", + "pack.nameAndSource": "%s(%s)", + "pack.openFolder": "打开包文件夹", + "pack.selected.title": "已选", + "pack.source.builtin": "内置", + "pack.source.feature": "功能", + "pack.source.local": "本地", + "pack.source.server": "服务器", + "pack.source.world": "世界", + "painting.dimensions": "%s×%s", + "painting.minecraft.alban.author": "Kristoffer Zetterstrand", + "painting.minecraft.alban.title": "Albanian", + "painting.minecraft.aztec.author": "Kristoffer Zetterstrand", + "painting.minecraft.aztec.title": "de_aztec", + "painting.minecraft.aztec2.author": "Kristoffer Zetterstrand", + "painting.minecraft.aztec2.title": "de_aztec", + "painting.minecraft.backyard.author": "Kristoffer Zetterstrand", + "painting.minecraft.backyard.title": "Backyard", + "painting.minecraft.baroque.author": "Sarah Boeving", + "painting.minecraft.baroque.title": "Baroque", + "painting.minecraft.bomb.author": "Kristoffer Zetterstrand", + "painting.minecraft.bomb.title": "Target Successfully Bombed", + "painting.minecraft.bouquet.author": "Kristoffer Zetterstrand", + "painting.minecraft.bouquet.title": "Bouquet", + "painting.minecraft.burning_skull.author": "Kristoffer Zetterstrand", + "painting.minecraft.burning_skull.title": "Skull On Fire", + "painting.minecraft.bust.author": "Kristoffer Zetterstrand", + "painting.minecraft.bust.title": "Bust", + "painting.minecraft.cavebird.author": "Kristoffer Zetterstrand", + "painting.minecraft.cavebird.title": "Cavebird", + "painting.minecraft.changing.author": "Kristoffer Zetterstrand", + "painting.minecraft.changing.title": "Changing", + "painting.minecraft.cotan.author": "Kristoffer Zetterstrand", + "painting.minecraft.cotan.title": "Cotán", + "painting.minecraft.courbet.author": "Kristoffer Zetterstrand", + "painting.minecraft.courbet.title": "Bonjour Monsieur Courbet", + "painting.minecraft.creebet.author": "Kristoffer Zetterstrand", + "painting.minecraft.creebet.title": "Creebet", + "painting.minecraft.donkey_kong.author": "Kristoffer Zetterstrand", + "painting.minecraft.donkey_kong.title": "Kong", + "painting.minecraft.earth.author": "Mojang", + "painting.minecraft.earth.title": "Earth", + "painting.minecraft.endboss.author": "Kristoffer Zetterstrand", + "painting.minecraft.endboss.title": "Endboss", + "painting.minecraft.fern.author": "Kristoffer Zetterstrand", + "painting.minecraft.fern.title": "Fern", + "painting.minecraft.fighters.author": "Kristoffer Zetterstrand", + "painting.minecraft.fighters.title": "Fighters", + "painting.minecraft.finding.author": "Kristoffer Zetterstrand", + "painting.minecraft.finding.title": "Finding", + "painting.minecraft.fire.author": "Mojang", + "painting.minecraft.fire.title": "Fire", + "painting.minecraft.graham.author": "Kristoffer Zetterstrand", + "painting.minecraft.graham.title": "Graham", + "painting.minecraft.humble.author": "Sarah Boeving", + "painting.minecraft.humble.title": "Humble", + "painting.minecraft.kebab.author": "Kristoffer Zetterstrand", + "painting.minecraft.kebab.title": "Kebab med tre pepperoni", + "painting.minecraft.lowmist.author": "Kristoffer Zetterstrand", + "painting.minecraft.lowmist.title": "Lowmist", + "painting.minecraft.match.author": "Kristoffer Zetterstrand", + "painting.minecraft.match.title": "Match", + "painting.minecraft.meditative.author": "Sarah Boeving", + "painting.minecraft.meditative.title": "Meditative", + "painting.minecraft.orb.author": "Kristoffer Zetterstrand", + "painting.minecraft.orb.title": "Orb", + "painting.minecraft.owlemons.author": "Kristoffer Zetterstrand", + "painting.minecraft.owlemons.title": "Owlemons", + "painting.minecraft.passage.author": "Kristoffer Zetterstrand", + "painting.minecraft.passage.title": "Passage", + "painting.minecraft.pigscene.author": "Kristoffer Zetterstrand", + "painting.minecraft.pigscene.title": "Pigscene", + "painting.minecraft.plant.author": "Kristoffer Zetterstrand", + "painting.minecraft.plant.title": "Paradisträd", + "painting.minecraft.pointer.author": "Kristoffer Zetterstrand", + "painting.minecraft.pointer.title": "Pointer", + "painting.minecraft.pond.author": "Kristoffer Zetterstrand", + "painting.minecraft.pond.title": "Pond", + "painting.minecraft.pool.author": "Kristoffer Zetterstrand", + "painting.minecraft.pool.title": "The Pool", + "painting.minecraft.prairie_ride.author": "Sarah Boeving", + "painting.minecraft.prairie_ride.title": "Prairie Ride", + "painting.minecraft.sea.author": "Kristoffer Zetterstrand", + "painting.minecraft.sea.title": "Seaside", + "painting.minecraft.skeleton.author": "Kristoffer Zetterstrand", + "painting.minecraft.skeleton.title": "Mortal Coil", + "painting.minecraft.skull_and_roses.author": "Kristoffer Zetterstrand", + "painting.minecraft.skull_and_roses.title": "Skull and Roses", + "painting.minecraft.stage.author": "Kristoffer Zetterstrand", + "painting.minecraft.stage.title": "The Stage Is Set", + "painting.minecraft.sunflowers.author": "Kristoffer Zetterstrand", + "painting.minecraft.sunflowers.title": "Sunflowers", + "painting.minecraft.sunset.author": "Kristoffer Zetterstrand", + "painting.minecraft.sunset.title": "sunset_dense", + "painting.minecraft.tides.author": "Kristoffer Zetterstrand", + "painting.minecraft.tides.title": "Tides", + "painting.minecraft.unpacked.author": "Sarah Boeving", + "painting.minecraft.unpacked.title": "Unpacked", + "painting.minecraft.void.author": "Kristoffer Zetterstrand", + "painting.minecraft.void.title": "The void", + "painting.minecraft.wanderer.author": "Kristoffer Zetterstrand", + "painting.minecraft.wanderer.title": "Wanderer", + "painting.minecraft.wasteland.author": "Kristoffer Zetterstrand", + "painting.minecraft.wasteland.title": "Wasteland", + "painting.minecraft.water.author": "Mojang", + "painting.minecraft.water.title": "Water", + "painting.minecraft.wind.author": "Mojang", + "painting.minecraft.wind.title": "Wind", + "painting.minecraft.wither.author": "Mojang", + "painting.minecraft.wither.title": "Wither", + "painting.random": "随机样式", + "parsing.bool.expected": "应为布尔型", + "parsing.bool.invalid": "无效的布尔型数据,应为“true”或“false”却出现了“%s”", + "parsing.double.expected": "应为双精度浮点型", + "parsing.double.invalid": "无效的双精度浮点型数据“%s”", + "parsing.expected": "缺少“%s”", + "parsing.float.expected": "应为浮点型", + "parsing.float.invalid": "无效的浮点型数据“%s”", + "parsing.int.expected": "应为整型", + "parsing.int.invalid": "无效的整型数据“%s”", + "parsing.long.expected": "应为长整型", + "parsing.long.invalid": "无效的长整型数据“%s”", + "parsing.quote.escape": "引号内的字符串包含无效的转义序列“\\%s”", + "parsing.quote.expected.end": "字符串引号不成对", + "parsing.quote.expected.start": "字符串开端应有引号", + "particle.invalidOptions": "无法解析粒子选项:%s", + "particle.notFound": "未知的粒子:%s", + "permissions.requires.entity": "需要一个实体来执行此命令", + "permissions.requires.player": "需要一名玩家来执行此命令", + "potion.potency.1": "II", + "potion.potency.2": "III", + "potion.potency.3": "IV", + "potion.potency.4": "V", + "potion.potency.5": "VI", + "potion.whenDrank": "当生效后:", + "potion.withAmplifier": "%s %s", + "potion.withDuration": "%s(%s)", + "predicate.unknown": "未知的谓词:%s", + "quickplay.error.invalid_identifier": "无法找到具有指定标识符的世界", + "quickplay.error.realm_connect": "无法连接至Realm", + "quickplay.error.realm_permission": "权限不足,无法连接至此Realm", + "quickplay.error.title": "无法快速进入游戏", + "realms.missing.snapshot.error.text": "快照当前不支持Realms", + "recipe.notFound": "未知的配方:%s", + "recipe.toast.description": "请检查你的配方书", + "recipe.toast.title": "新配方已解锁!", + "record.nowPlaying": "正在播放:%s", + "recover_world.bug_tracker": "报告漏洞", + "recover_world.button": "尝试恢复", + "recover_world.done.failed": "无法从之前的状态恢复。", + "recover_world.done.success": "恢复成功!", + "recover_world.done.title": "恢复已完成", + "recover_world.issue.missing_file": "缺失文件", + "recover_world.issue.none": "没有问题", + "recover_world.message": "尝试读取世界文件夹“%s”时出现以下问题。\n可以尝试从先前的状态恢复此世界,也可以在漏洞追踪器上报告此问题。", + "recover_world.no_fallback": "没有可以恢复的状态", + "recover_world.restore": "尝试恢复", + "recover_world.restoring": "尝试恢复世界中…", + "recover_world.state_entry": "%s的状态:", + "recover_world.state_entry.unknown": "未知", + "recover_world.title": "加载世界失败", + "recover_world.warning": "加载世界摘要失败", + "resourcePack.broken_assets": "检测到损坏的资产", + "resourcePack.high_contrast.name": "高对比度", + "resourcePack.load_fail": "重载资源失败", + "resourcePack.programmer_art.name": "Programmer Art", + "resourcePack.runtime_failure": "检测到资源包错误", + "resourcePack.server.name": "世界指定资源包", + "resourcePack.title": "选择资源包", + "resourcePack.vanilla.description": "Minecraft的默认观感", + "resourcePack.vanilla.name": "默认", + "resourcepack.downloading": "正在下载资源包", + "resourcepack.progress": "下载文件中(%s MB)…", + "resourcepack.requesting": "正在发送请求…", + "screenshot.failure": "无法保存截图:%s", + "screenshot.success": "已将截图保存为%s", + "selectServer.add": "添加服务器", + "selectServer.defaultName": "Minecraft服务器", + "selectServer.delete": "删除", + "selectServer.deleteButton": "删除", + "selectServer.deleteQuestion": "你确定要删除此服务器吗?", + "selectServer.deleteWarning": "“%s”将会永久消失!(真的很久!)", + "selectServer.direct": "直接连接", + "selectServer.edit": "编辑", + "selectServer.hiddenAddress": "(隐藏)", + "selectServer.refresh": "刷新", + "selectServer.select": "加入服务器", + "selectWorld.access_failure": "加载世界失败", + "selectWorld.allowCommands": "允许作弊", + "selectWorld.allowCommands.info": "例如/gamemode、/experience等命令", + "selectWorld.allowCommands.new": "允许命令", + "selectWorld.backupEraseCache": "清除缓存数据", + "selectWorld.backupJoinConfirmButton": "创建备份并加载", + "selectWorld.backupJoinSkipButton": "我知道我在做什么!", + "selectWorld.backupQuestion.customized": "自定义世界已不受支持", + "selectWorld.backupQuestion.downgrade": "不支持对存档版本进行降级", + "selectWorld.backupQuestion.experimental": "使用“实验性设置”的世界不受支持", + "selectWorld.backupQuestion.snapshot": "你真的想加载此世界吗?", + "selectWorld.backupWarning.customized": "不巧,我们在这个版本的Minecraft中不支持自定义世界。我们可以继续加载这个世界并保持原状,但任何新生成的地形将不再被自定义。抱歉给你带来不便。", + "selectWorld.backupWarning.downgrade": "这个世界上次是在%s版本中打开的,你正在使用%s版本。降低世界的游戏版本可能会导致存档损坏,我们无法保证它可以被加载和运行。如果你仍要继续,请备份该存档。", + "selectWorld.backupWarning.experimental": "这个世界使用的实验性设置可能会随时停止运作。我们无法保证这些设置将来能够加载或运作。务必谨慎!", + "selectWorld.backupWarning.snapshot": "这个世界上次是在%s版本中打开的,你正在使用%s版本。请备份你的世界,以防世界崩溃。", + "selectWorld.bonusItems": "奖励箱", + "selectWorld.cheats": "作弊", + "selectWorld.commands": "命令", + "selectWorld.conversion": "必须进行转换!", + "selectWorld.conversion.tooltip": "此世界必须在较旧版本(比如1.6.4)中打开,以便安全转换", + "selectWorld.create": "创建新的世界", + "selectWorld.customizeType": "自定义", + "selectWorld.dataPacks": "数据包", + "selectWorld.data_read": "读取世界数据中…", + "selectWorld.delete": "删除", + "selectWorld.deleteButton": "删除", + "selectWorld.deleteQuestion": "你确定要删除这个世界吗?", + "selectWorld.deleteWarning": "“%s”将会永久消失!(真的很久!)", + "selectWorld.delete_failure": "删除世界失败", + "selectWorld.edit": "编辑", + "selectWorld.edit.backup": "进行备份", + "selectWorld.edit.backupCreated": "已备份:%s", + "selectWorld.edit.backupFailed": "备份失败", + "selectWorld.edit.backupFolder": "打开备份文件夹", + "selectWorld.edit.backupSize": "大小:%s MB", + "selectWorld.edit.export_worldgen_settings": "导出世界生成设置", + "selectWorld.edit.export_worldgen_settings.failure": "导出失败", + "selectWorld.edit.export_worldgen_settings.success": "导出成功", + "selectWorld.edit.openFolder": "打开世界文件夹", + "selectWorld.edit.optimize": "优化世界", + "selectWorld.edit.resetIcon": "重置图标", + "selectWorld.edit.save": "保存", + "selectWorld.edit.title": "编辑世界", + "selectWorld.enterName": "世界名称", + "selectWorld.enterSeed": "世界生成器的种子", + "selectWorld.experimental": "实验性", + "selectWorld.experimental.details": "详细信息", + "selectWorld.experimental.details.entry": "需要的实验性功能:%s", + "selectWorld.experimental.details.title": "实验性功能需求", + "selectWorld.experimental.message": "请注意!\n此配置需要使用仍在开发中的功能。你的世界可能会崩溃、损坏或不兼容未来的更新。", + "selectWorld.experimental.title": "实验性功能警告", + "selectWorld.experiments": "实验性内容", + "selectWorld.experiments.info": "实验性内容,即仍在开发的新功能。请小心,启用这些内容有损坏风险。已开启的实验性内容在创建世界后将无法再关闭。", + "selectWorld.futureworld.error.text": "试图加载来自未来版本的世界时发生错误。这本就并非万无一失,很抱歉没能成功。", + "selectWorld.futureworld.error.title": "出错了!", + "selectWorld.gameMode": "游戏模式", + "selectWorld.gameMode.adventure": "冒险", + "selectWorld.gameMode.adventure.info": "与生存模式相同,但无法放置或破坏方块。", + "selectWorld.gameMode.adventure.line1": "与生存模式相同,但无法", + "selectWorld.gameMode.adventure.line2": "放置或者移除方块", + "selectWorld.gameMode.creative": "创造", + "selectWorld.gameMode.creative.info": "无拘无束地探索创造。可以飞行,材料取之不尽,且不受怪物伤害。", + "selectWorld.gameMode.creative.line1": "无限的资源、自由地飞翔", + "selectWorld.gameMode.creative.line2": "并且能够瞬间破坏方块", + "selectWorld.gameMode.hardcore": "极限", + "selectWorld.gameMode.hardcore.info": "难度锁定为“困难”的生存模式,且死亡后无法重生。", + "selectWorld.gameMode.hardcore.line1": "难度锁定在困难的生存模式", + "selectWorld.gameMode.hardcore.line2": "且只有一条生命", + "selectWorld.gameMode.spectator": "旁观", + "selectWorld.gameMode.spectator.info": "你可以旁观,但不能互动。", + "selectWorld.gameMode.spectator.line1": "你可以旁观,但不能互动", + "selectWorld.gameMode.survival": "生存", + "selectWorld.gameMode.survival.info": "探索未知的世界,尽情建造、收集、合成并与怪物战斗。", + "selectWorld.gameMode.survival.line1": "探索世界、收集资源、合成道具、", + "selectWorld.gameMode.survival.line2": "提高等级、补充体力和生命值", + "selectWorld.gameRules": "游戏规则", + "selectWorld.import_worldgen_settings": "导入设置", + "selectWorld.import_worldgen_settings.failure": "导入设置时出错", + "selectWorld.import_worldgen_settings.select_file": "选择设置文件(.json)", + "selectWorld.incompatible.description": "此世界无法在此版本中打开。\n最后一次打开此世界的版本是%s。", + "selectWorld.incompatible.info": "不兼容的版本:%s", + "selectWorld.incompatible.title": "版本不兼容", + "selectWorld.incompatible.tooltip": "此世界在不兼容的版本上创建,故无法打开。", + "selectWorld.incompatible_series": "创建于不兼容的版本", + "selectWorld.load_folder_access": "无法读取或访问游戏世界存档所在的文件夹!", + "selectWorld.loading_list": "加载世界列表中", + "selectWorld.locked": "被另一个正在运行的Minecraft实例锁定", + "selectWorld.mapFeatures": "生成结构", + "selectWorld.mapFeatures.info": "村庄、沉船等", + "selectWorld.mapType": "世界类型", + "selectWorld.mapType.normal": "普通", + "selectWorld.moreWorldOptions": "更多世界选项…", + "selectWorld.newWorld": "新的世界", + "selectWorld.recreate": "重建", + "selectWorld.recreate.customized.text": "自定义世界在这个版本的Minecraft中已不受支持。我们可以尝试用同样的种子与选项重建它,但任何自定义的地形都会丢失。抱歉给你带来不便。", + "selectWorld.recreate.customized.title": "自定义世界已不受支持", + "selectWorld.recreate.error.text": "尝试重建世界时出错。", + "selectWorld.recreate.error.title": "出错了!", + "selectWorld.resource_load": "准备资源中…", + "selectWorld.resultFolder": "将会保存于:", + "selectWorld.search": "搜索世界", + "selectWorld.seedInfo": "留空以生成随机种子", + "selectWorld.select": "进入选中的世界", + "selectWorld.targetFolder": "存档文件夹:%s", + "selectWorld.title": "选择世界", + "selectWorld.tooltip.fromNewerVersion1": "世界是在更新的版本中被保存的,", + "selectWorld.tooltip.fromNewerVersion2": "加载这个世界可能会产生问题!", + "selectWorld.tooltip.snapshot1": "在这个快照中加载它之前,", + "selectWorld.tooltip.snapshot2": "不要忘了备份这个世界!", + "selectWorld.unable_to_load": "无法加载世界", + "selectWorld.version": "版本:", + "selectWorld.versionJoinButton": "仍然加载", + "selectWorld.versionQuestion": "你真的想加载此世界吗?", + "selectWorld.versionUnknown": "未知", + "selectWorld.versionWarning": "此世界上次是在%s版本中打开的,在此版本中加载可能会导致数据损坏!", + "selectWorld.warning.deprecated.question": "一些使用中的功能已被淘汰并会在将来失效。你确定要继续吗?", + "selectWorld.warning.deprecated.title": "警告!这些设置使用了已淘汰的功能", + "selectWorld.warning.experimental.question": "这些设置是实验性的,将来可能停止运作。你确定要继续吗?", + "selectWorld.warning.experimental.title": "警告!这些设置使用了实验性功能", + "selectWorld.warning.lowDiskSpace.description": "设备上的存储空间不足。\n运行游戏时磁盘空间不足可能会导致世界损坏。", + "selectWorld.warning.lowDiskSpace.title": "警告!磁盘空间不足!", + "selectWorld.world": "世界", + "sign.edit": "编辑告示牌消息", + "sleep.not_possible": "已入睡玩家的数量不足以跳过夜晚", + "sleep.players_sleeping": "%s/%s名玩家已入睡", + "sleep.skipping_night": "今夜将在睡梦中度过", + "slot.only_single_allowed": "只允许有单个槽位,实际为“%s”", + "slot.unknown": "未知的槽位“%s”", + "soundCategory.ambient": "环境", + "soundCategory.block": "方块", + "soundCategory.hostile": "敌对生物", + "soundCategory.master": "主音量", + "soundCategory.music": "音乐", + "soundCategory.neutral": "友好生物", + "soundCategory.player": "玩家", + "soundCategory.record": "唱片机/音符盒", + "soundCategory.voice": "声音/语音", + "soundCategory.weather": "天气", + "spectatorMenu.close": "关闭菜单", + "spectatorMenu.next_page": "下一页", + "spectatorMenu.previous_page": "上一页", + "spectatorMenu.root.prompt": "按下一个键来选择命令,再按一次来使用它。", + "spectatorMenu.team_teleport": "传送到队伍成员", + "spectatorMenu.team_teleport.prompt": "选择一支队伍作为传送目标", + "spectatorMenu.teleport": "传送到玩家", + "spectatorMenu.teleport.prompt": "选择一名玩家作为传送目标", + "stat.generalButton": "通用", + "stat.itemsButton": "物品", + "stat.minecraft.animals_bred": "繁殖动物次数", + "stat.minecraft.aviate_one_cm": "鞘翅滑行距离", + "stat.minecraft.bell_ring": "鸣钟次数", + "stat.minecraft.boat_one_cm": "坐船移动距离", + "stat.minecraft.clean_armor": "清洗盔甲次数", + "stat.minecraft.clean_banner": "清洗旗帜次数", + "stat.minecraft.clean_shulker_box": "潜影盒清洗次数", + "stat.minecraft.climb_one_cm": "已攀爬距离", + "stat.minecraft.crouch_one_cm": "潜行距离", + "stat.minecraft.damage_absorbed": "吸收的伤害", + "stat.minecraft.damage_blocked_by_shield": "盾牌抵挡的伤害", + "stat.minecraft.damage_dealt": "造成伤害", + "stat.minecraft.damage_dealt_absorbed": "造成伤害(被吸收)", + "stat.minecraft.damage_dealt_resisted": "造成伤害(被抵挡)", + "stat.minecraft.damage_resisted": "抵挡的伤害", + "stat.minecraft.damage_taken": "受到伤害", + "stat.minecraft.deaths": "死亡次数", + "stat.minecraft.drop": "物品掉落", + "stat.minecraft.eat_cake_slice": "吃掉的蛋糕片数", + "stat.minecraft.enchant_item": "物品附魔次数", + "stat.minecraft.fall_one_cm": "摔落高度", + "stat.minecraft.fill_cauldron": "炼药锅装水次数", + "stat.minecraft.fish_caught": "捕鱼数", + "stat.minecraft.fly_one_cm": "飞行距离", + "stat.minecraft.horse_one_cm": "骑马移动距离", + "stat.minecraft.inspect_dispenser": "搜查发射器次数", + "stat.minecraft.inspect_dropper": "搜查投掷器次数", + "stat.minecraft.inspect_hopper": "搜查漏斗次数", + "stat.minecraft.interact_with_anvil": "与铁砧交互次数", + "stat.minecraft.interact_with_beacon": "与信标交互次数", + "stat.minecraft.interact_with_blast_furnace": "与高炉交互次数", + "stat.minecraft.interact_with_brewingstand": "与酿造台交互次数", + "stat.minecraft.interact_with_campfire": "与营火交互次数", + "stat.minecraft.interact_with_cartography_table": "与制图台交互次数", + "stat.minecraft.interact_with_crafting_table": "与工作台交互次数", + "stat.minecraft.interact_with_furnace": "与熔炉交互次数", + "stat.minecraft.interact_with_grindstone": "与砂轮交互次数", + "stat.minecraft.interact_with_lectern": "与讲台交互次数", + "stat.minecraft.interact_with_loom": "与织布机交互次数", + "stat.minecraft.interact_with_smithing_table": "与锻造台交互次数", + "stat.minecraft.interact_with_smoker": "与烟熏炉交互次数", + "stat.minecraft.interact_with_stonecutter": "与切石机交互次数", + "stat.minecraft.jump": "跳跃次数", + "stat.minecraft.leave_game": "游戏退出次数", + "stat.minecraft.minecart_one_cm": "坐矿车移动距离", + "stat.minecraft.mob_kills": "生物击杀数", + "stat.minecraft.open_barrel": "木桶打开次数", + "stat.minecraft.open_chest": "箱子打开次数", + "stat.minecraft.open_enderchest": "末影箱打开次数", + "stat.minecraft.open_shulker_box": "潜影盒打开次数", + "stat.minecraft.pig_one_cm": "骑猪移动距离", + "stat.minecraft.play_noteblock": "音符盒播放次数", + "stat.minecraft.play_record": "播放唱片数", + "stat.minecraft.play_time": "游戏时长", + "stat.minecraft.player_kills": "玩家击杀数", + "stat.minecraft.pot_flower": "盆栽种植数", + "stat.minecraft.raid_trigger": "触发袭击次数", + "stat.minecraft.raid_win": "袭击胜利次数", + "stat.minecraft.sleep_in_bed": "躺在床上的次数", + "stat.minecraft.sneak_time": "潜行时间", + "stat.minecraft.sprint_one_cm": "疾跑距离", + "stat.minecraft.strider_one_cm": "骑炽足兽移动距离", + "stat.minecraft.swim_one_cm": "游泳距离", + "stat.minecraft.talked_to_villager": "村民交互次数", + "stat.minecraft.target_hit": "击中标靶次数", + "stat.minecraft.time_since_death": "自上次死亡", + "stat.minecraft.time_since_rest": "自上次入眠", + "stat.minecraft.total_world_time": "世界打开时间", + "stat.minecraft.traded_with_villager": "村民交易次数", + "stat.minecraft.trigger_trapped_chest": "陷阱箱触发次数", + "stat.minecraft.tune_noteblock": "音符盒调音次数", + "stat.minecraft.use_cauldron": "从炼药锅取水次数", + "stat.minecraft.walk_on_water_one_cm": "水面行走距离", + "stat.minecraft.walk_one_cm": "行走距离", + "stat.minecraft.walk_under_water_one_cm": "水下行走距离", + "stat.mobsButton": "生物", + "stat_type.minecraft.broken": "损坏次数", + "stat_type.minecraft.crafted": "合成次数", + "stat_type.minecraft.dropped": "丢弃个数", + "stat_type.minecraft.killed": "你杀死了%s个%s", + "stat_type.minecraft.killed.none": "你从来没有杀死过%s", + "stat_type.minecraft.killed_by": "%s杀死了你%s次", + "stat_type.minecraft.killed_by.none": "你从来没有被%s杀死过", + "stat_type.minecraft.mined": "开采次数", + "stat_type.minecraft.picked_up": "拾起个数", + "stat_type.minecraft.used": "使用次数", + "stats.none": "-", + "structure_block.button.detect_size": "探测", + "structure_block.button.load": "加载", + "structure_block.button.save": "保存", + "structure_block.custom_data": "自定义数据标签名", + "structure_block.detect_size": "探测结构大小和位置:", + "structure_block.hover.corner": "角落:%s", + "structure_block.hover.data": "数据:%s", + "structure_block.hover.load": "加载:%s", + "structure_block.hover.save": "保存:%s", + "structure_block.include_entities": "包括实体:", + "structure_block.integrity": "结构完整性及种子", + "structure_block.integrity.integrity": "结构完整性", + "structure_block.integrity.seed": "结构种子", + "structure_block.invalid_structure_name": "无效的结构名“%s”", + "structure_block.load_not_found": "不存在名为“%s”的结构 ", + "structure_block.load_prepare": "结构“%s”的加载位置已就绪", + "structure_block.load_success": "成功从“%s”中加载结构", + "structure_block.mode.corner": "角落模式", + "structure_block.mode.data": "数据模式", + "structure_block.mode.load": "加载模式", + "structure_block.mode.save": "保存模式", + "structure_block.mode_info.corner": "角落模式 — 位置和大小标记", + "structure_block.mode_info.data": "数据模式 — 游戏逻辑标记", + "structure_block.mode_info.load": "加载模式 — 从文件中加载", + "structure_block.mode_info.save": "保存模式 — 写入文件", + "structure_block.position": "相对位置", + "structure_block.position.x": "相对X坐标", + "structure_block.position.y": "相对Y坐标", + "structure_block.position.z": "相对Z坐标", + "structure_block.save_failure": "无法保存结构“%s”", + "structure_block.save_success": "成功将结构保存为“%s”", + "structure_block.show_air": "显示隐形方块:", + "structure_block.show_boundingbox": "显示边框:", + "structure_block.size": "结构大小", + "structure_block.size.x": "结构X轴大小", + "structure_block.size.y": "结构Y轴大小", + "structure_block.size.z": "结构Z轴大小", + "structure_block.size_failure": "无法检测结构大小。请放置与结构名称对应的角落结构方块", + "structure_block.size_success": "“%s”的大小已成功检测", + "structure_block.strict": "严格放置:", + "structure_block.structure_name": "结构名称", + "subtitles.ambient.cave": "怪异的噪声", + "subtitles.ambient.sound": "怪异的噪声", + "subtitles.block.amethyst_block.chime": "紫水晶:叮铃", + "subtitles.block.amethyst_block.resonate": "紫水晶:共振", + "subtitles.block.anvil.destroy": "铁砧:被毁", + "subtitles.block.anvil.land": "铁砧:着陆", + "subtitles.block.anvil.use": "铁砧:使用", + "subtitles.block.barrel.close": "木桶:关闭", + "subtitles.block.barrel.open": "木桶:打开", + "subtitles.block.beacon.activate": "信标:激活", + "subtitles.block.beacon.ambient": "信标:嗡嗡作响", + "subtitles.block.beacon.deactivate": "信标:失效", + "subtitles.block.beacon.power_select": "信标:选择效果", + "subtitles.block.beehive.drip": "蜂蜜:滴落", + "subtitles.block.beehive.enter": "蜜蜂:入巢", + "subtitles.block.beehive.exit": "蜜蜂:离巢", + "subtitles.block.beehive.shear": "剪刀:刮削", + "subtitles.block.beehive.work": "蜜蜂:工作", + "subtitles.block.bell.resonate": "钟:回响", + "subtitles.block.bell.use": "钟:响起", + "subtitles.block.big_dripleaf.tilt_down": "垂滴叶:折下", + "subtitles.block.big_dripleaf.tilt_up": "垂滴叶:升起", + "subtitles.block.blastfurnace.fire_crackle": "高炉:噼啪作响", + "subtitles.block.brewing_stand.brew": "酿造台:冒泡", + "subtitles.block.bubble_column.bubble_pop": "气泡:破裂", + "subtitles.block.bubble_column.upwards_ambient": "气泡:上浮", + "subtitles.block.bubble_column.upwards_inside": "气泡:飞升", + "subtitles.block.bubble_column.whirlpool_ambient": "气泡:旋转", + "subtitles.block.bubble_column.whirlpool_inside": "气泡:骤降", + "subtitles.block.button.click": "按钮:咔哒", + "subtitles.block.cake.add_candle": "蛋糕:吧唧", + "subtitles.block.campfire.crackle": "营火:噼啪作响", + "subtitles.block.candle.crackle": "蜡烛:噼啪作响", + "subtitles.block.candle.extinguish": "蜡烛:熄灭", + "subtitles.block.chest.close": "箱子:关闭", + "subtitles.block.chest.locked": "箱子:锁上", + "subtitles.block.chest.open": "箱子:开启", + "subtitles.block.chorus_flower.death": "紫颂花:凋零", + "subtitles.block.chorus_flower.grow": "紫颂花:生长", + "subtitles.block.comparator.click": "比较器:模式变更", + "subtitles.block.composter.empty": "堆肥桶:清空", + "subtitles.block.composter.fill": "堆肥桶:填充", + "subtitles.block.composter.ready": "堆肥桶:堆肥", + "subtitles.block.conduit.activate": "潮涌核心:激活", + "subtitles.block.conduit.ambient": "潮涌核心:涌动", + "subtitles.block.conduit.attack.target": "潮涌核心:攻击", + "subtitles.block.conduit.deactivate": "潮涌核心:失效", + "subtitles.block.copper_bulb.turn_off": "铜灯:熄灭", + "subtitles.block.copper_bulb.turn_on": "铜灯:亮起", + "subtitles.block.copper_trapdoor.close": "活板门:关闭", + "subtitles.block.copper_trapdoor.open": "活板门:打开", + "subtitles.block.crafter.craft": "合成器:合成", + "subtitles.block.crafter.fail": "合成器:合成失败", + "subtitles.block.creaking_heart.hurt": "嘎枝之心:咕哝", + "subtitles.block.creaking_heart.idle": "怪异的噪声", + "subtitles.block.creaking_heart.spawn": "嘎枝之心:苏醒", + "subtitles.block.decorated_pot.insert": "饰纹陶罐:装入", + "subtitles.block.decorated_pot.insert_fail": "饰纹陶罐:晃动", + "subtitles.block.decorated_pot.shatter": "饰纹陶罐:碎裂", + "subtitles.block.dispenser.dispense": "物品:被射出", + "subtitles.block.dispenser.fail": "发射器:发射失败", + "subtitles.block.door.toggle": "门:嘎吱作响", + "subtitles.block.enchantment_table.use": "附魔台:使用", + "subtitles.block.end_portal.spawn": "末地传送门:开启", + "subtitles.block.end_portal_frame.fill": "末影之眼:嵌入", + "subtitles.block.eyeblossom.close": "眼眸花:闭合", + "subtitles.block.eyeblossom.idle": "眼眸花:沙沙作响", + "subtitles.block.eyeblossom.open": "眼眸花:张开", + "subtitles.block.fence_gate.toggle": "栅栏门:嘎吱作响", + "subtitles.block.fire.ambient": "火:噼啪作响", + "subtitles.block.fire.extinguish": "火:熄灭", + "subtitles.block.frogspawn.hatch": "蝌蚪:孵化", + "subtitles.block.furnace.fire_crackle": "熔炉:噼啪作响", + "subtitles.block.generic.break": "方块:被破坏", + "subtitles.block.generic.fall": "某物:摔落到方块上", + "subtitles.block.generic.footsteps": "脚步声", + "subtitles.block.generic.hit": "方块:损坏中", + "subtitles.block.generic.place": "方块:被放置", + "subtitles.block.grindstone.use": "砂轮:使用", + "subtitles.block.growing_plant.crop": "植物:被修剪", + "subtitles.block.hanging_sign.waxed_interact_fail": "告示牌:晃动", + "subtitles.block.honey_block.slide": "从蜂蜜块滑下", + "subtitles.block.iron_trapdoor.close": "活板门:关闭", + "subtitles.block.iron_trapdoor.open": "活板门:打开", + "subtitles.block.lava.ambient": "熔岩:迸裂", + "subtitles.block.lava.extinguish": "熔岩:嘶嘶声", + "subtitles.block.lever.click": "拉杆:拉动", + "subtitles.block.note_block.note": "音符盒:播放", + "subtitles.block.pale_hanging_moss.idle": "怪异的噪声", + "subtitles.block.piston.move": "活塞:移动", + "subtitles.block.pointed_dripstone.drip_lava": "熔岩:滴落", + "subtitles.block.pointed_dripstone.drip_lava_into_cauldron": "熔岩:滴入炼药锅", + "subtitles.block.pointed_dripstone.drip_water": "水:滴落", + "subtitles.block.pointed_dripstone.drip_water_into_cauldron": "水:滴入炼药锅", + "subtitles.block.pointed_dripstone.land": "钟乳石:塌落", + "subtitles.block.portal.ambient": "传送门:呼啸", + "subtitles.block.portal.travel": "传送门:噪声消散", + "subtitles.block.portal.trigger": "传送门:噪声渐响", + "subtitles.block.pressure_plate.click": "压力板:咔哒", + "subtitles.block.pumpkin.carve": "剪刀:雕刻", + "subtitles.block.redstone_torch.burnout": "红石火把:熄灭", + "subtitles.block.respawn_anchor.ambient": "重生锚:呼啸", + "subtitles.block.respawn_anchor.charge": "重生锚:充能", + "subtitles.block.respawn_anchor.deplete": "重生锚:耗能", + "subtitles.block.respawn_anchor.set_spawn": "重生锚:设置出生点", + "subtitles.block.sculk.charge": "幽匿块:冒泡", + "subtitles.block.sculk.spread": "幽匿块:蔓延", + "subtitles.block.sculk_catalyst.bloom": "幽匿催发体:催发", + "subtitles.block.sculk_sensor.clicking": "幽匿感测体:颤动", + "subtitles.block.sculk_sensor.clicking_stop": "幽匿感测体:停息", + "subtitles.block.sculk_shrieker.shriek": "幽匿尖啸体:尖啸", + "subtitles.block.shulker_box.close": "潜影盒:关闭", + "subtitles.block.shulker_box.open": "潜影盒:开启", + "subtitles.block.sign.waxed_interact_fail": "告示牌:晃动", + "subtitles.block.smithing_table.use": "锻造台:使用", + "subtitles.block.smoker.smoke": "烟熏炉:烟熏", + "subtitles.block.sniffer_egg.crack": "嗅探兽蛋:裂开", + "subtitles.block.sniffer_egg.hatch": "嗅探兽蛋:孵化", + "subtitles.block.sniffer_egg.plop": "嗅探兽:下蛋", + "subtitles.block.sponge.absorb": "海绵:吸水", + "subtitles.block.sweet_berry_bush.pick_berries": "浆果:弹出", + "subtitles.block.trapdoor.toggle": "活板门:嘎吱作响", + "subtitles.block.trial_spawner.about_to_spawn_item": "不祥之物:准备", + "subtitles.block.trial_spawner.ambient": "试炼刷怪笼:噼啪作响", + "subtitles.block.trial_spawner.ambient_charged": "不祥试炼刷怪笼:噼啪作响", + "subtitles.block.trial_spawner.ambient_ominous": "不祥试炼刷怪笼:噼啪作响", + "subtitles.block.trial_spawner.charge_activate": "试炼刷怪笼:不祥涌动", + "subtitles.block.trial_spawner.close_shutter": "试炼刷怪笼:关闭", + "subtitles.block.trial_spawner.detect_player": "试炼刷怪笼:充能", + "subtitles.block.trial_spawner.eject_item": "试炼刷怪笼:喷出物品", + "subtitles.block.trial_spawner.ominous_activate": "试炼刷怪笼:不祥涌动", + "subtitles.block.trial_spawner.open_shutter": "试炼刷怪笼:开启", + "subtitles.block.trial_spawner.spawn_item": "不祥之物:掉落", + "subtitles.block.trial_spawner.spawn_item_begin": "不祥之物:出现", + "subtitles.block.trial_spawner.spawn_mob": "试炼刷怪笼:生成生物", + "subtitles.block.tripwire.attach": "绊线:连接", + "subtitles.block.tripwire.click": "绊线:咔哒", + "subtitles.block.tripwire.detach": "绊线:断开", + "subtitles.block.vault.activate": "宝库:燃起", + "subtitles.block.vault.ambient": "宝库:噼啪作响", + "subtitles.block.vault.close_shutter": "宝库:关闭", + "subtitles.block.vault.deactivate": "宝库:熄灭", + "subtitles.block.vault.eject_item": "宝库:喷出物品", + "subtitles.block.vault.insert_item": "宝库:解锁", + "subtitles.block.vault.insert_item_fail": "宝库:解锁失败", + "subtitles.block.vault.open_shutter": "宝库:打开", + "subtitles.block.vault.reject_rewarded_player": "宝库:拒绝已奖励玩家", + "subtitles.block.water.ambient": "水:流动", + "subtitles.block.wet_sponge.dries": "海绵:烘干", + "subtitles.chiseled_bookshelf.insert": "书:被放入", + "subtitles.chiseled_bookshelf.insert_enchanted": "附魔书:被放入", + "subtitles.chiseled_bookshelf.take": "书:被取出", + "subtitles.chiseled_bookshelf.take_enchanted": "附魔书:被取出", + "subtitles.enchant.thorns.hit": "荆棘:反刺", + "subtitles.entity.allay.ambient_with_item": "悦灵:搜寻", + "subtitles.entity.allay.ambient_without_item": "悦灵:渴求", + "subtitles.entity.allay.death": "悦灵:死亡", + "subtitles.entity.allay.hurt": "悦灵:受伤", + "subtitles.entity.allay.item_given": "悦灵:嬉笑", + "subtitles.entity.allay.item_taken": "悦灵:愉悦", + "subtitles.entity.allay.item_thrown": "悦灵:投掷", + "subtitles.entity.armadillo.ambient": "犰狳:呼噜", + "subtitles.entity.armadillo.brush": "鳞甲:被刷落", + "subtitles.entity.armadillo.death": "犰狳:死亡", + "subtitles.entity.armadillo.eat": "犰狳:进食", + "subtitles.entity.armadillo.hurt": "犰狳:受伤", + "subtitles.entity.armadillo.hurt_reduced": "犰狳:自卫", + "subtitles.entity.armadillo.land": "犰狳:着陆", + "subtitles.entity.armadillo.peek": "犰狳:窥视", + "subtitles.entity.armadillo.roll": "犰狳:蜷缩", + "subtitles.entity.armadillo.scute_drop": "犰狳:脱落鳞甲", + "subtitles.entity.armadillo.unroll_finish": "犰狳:伸展", + "subtitles.entity.armadillo.unroll_start": "犰狳:窥视", + "subtitles.entity.armor_stand.fall": "某物:着地", + "subtitles.entity.arrow.hit": "箭:击中", + "subtitles.entity.arrow.hit_player": "箭:击中玩家", + "subtitles.entity.arrow.shoot": "箭:被射出", + "subtitles.entity.axolotl.attack": "美西螈:攻击", + "subtitles.entity.axolotl.death": "美西螈:死亡", + "subtitles.entity.axolotl.hurt": "美西螈:受伤", + "subtitles.entity.axolotl.idle_air": "美西螈:啾啾", + "subtitles.entity.axolotl.idle_water": "美西螈:啾啾", + "subtitles.entity.axolotl.splash": "美西螈:溅起水花", + "subtitles.entity.axolotl.swim": "美西螈:游泳", + "subtitles.entity.bat.ambient": "蝙蝠:尖声叫", + "subtitles.entity.bat.death": "蝙蝠:死亡", + "subtitles.entity.bat.hurt": "蝙蝠:受伤", + "subtitles.entity.bat.takeoff": "蝙蝠:起飞", + "subtitles.entity.bee.ambient": "蜜蜂:嗡嗡", + "subtitles.entity.bee.death": "蜜蜂:死亡", + "subtitles.entity.bee.hurt": "蜜蜂:受伤", + "subtitles.entity.bee.loop": "蜜蜂:嗡嗡", + "subtitles.entity.bee.loop_aggressive": "蜜蜂:愤怒地嗡嗡叫", + "subtitles.entity.bee.pollinate": "蜜蜂:高兴地嗡嗡叫", + "subtitles.entity.bee.sting": "蜜蜂:刺蛰", + "subtitles.entity.blaze.ambient": "烈焰人:呼吸", + "subtitles.entity.blaze.burn": "烈焰人:噼啪作响", + "subtitles.entity.blaze.death": "烈焰人:死亡", + "subtitles.entity.blaze.hurt": "烈焰人:受伤", + "subtitles.entity.blaze.shoot": "烈焰人:射击", + "subtitles.entity.boat.paddle_land": "划船", + "subtitles.entity.boat.paddle_water": "划船", + "subtitles.entity.bogged.ambient": "沼骸:咯咯声", + "subtitles.entity.bogged.death": "沼骸:死亡", + "subtitles.entity.bogged.hurt": "沼骸:受伤", + "subtitles.entity.breeze.charge": "旋风人:蓄力", + "subtitles.entity.breeze.death": "旋风人:死亡", + "subtitles.entity.breeze.deflect": "旋风人:反弹", + "subtitles.entity.breeze.hurt": "旋风人:受伤", + "subtitles.entity.breeze.idle_air": "旋风人:飞行", + "subtitles.entity.breeze.idle_ground": "旋风人:呼啸", + "subtitles.entity.breeze.inhale": "旋风人:吸气", + "subtitles.entity.breeze.jump": "旋风人:跳跃", + "subtitles.entity.breeze.land": "旋风人:着陆", + "subtitles.entity.breeze.shoot": "旋风人:射击", + "subtitles.entity.breeze.slide": "旋风人:滑行", + "subtitles.entity.breeze.whirl": "旋风人:旋转", + "subtitles.entity.breeze.wind_burst": "风弹:爆裂", + "subtitles.entity.camel.ambient": "骆驼:呼噜", + "subtitles.entity.camel.dash": "骆驼:冲刺", + "subtitles.entity.camel.dash_ready": "骆驼:恢复体力", + "subtitles.entity.camel.death": "骆驼:死亡", + "subtitles.entity.camel.eat": "骆驼:进食", + "subtitles.entity.camel.hurt": "骆驼:受伤", + "subtitles.entity.camel.saddle": "鞍:装备", + "subtitles.entity.camel.sit": "骆驼:坐下", + "subtitles.entity.camel.stand": "骆驼:站起", + "subtitles.entity.camel.step": "骆驼:脚步声", + "subtitles.entity.camel.step_sand": "骆驼:踏沙", + "subtitles.entity.cat.ambient": "猫:喵~", + "subtitles.entity.cat.beg_for_food": "猫:求食", + "subtitles.entity.cat.death": "猫:死亡", + "subtitles.entity.cat.eat": "猫:进食", + "subtitles.entity.cat.hiss": "猫:嘶嘶声", + "subtitles.entity.cat.hurt": "猫:受伤", + "subtitles.entity.cat.purr": "猫:呼噜声", + "subtitles.entity.chicken.ambient": "鸡:咯咯叫", + "subtitles.entity.chicken.death": "鸡:死亡", + "subtitles.entity.chicken.egg": "鸡:下蛋", + "subtitles.entity.chicken.hurt": "鸡:受伤", + "subtitles.entity.cod.death": "鳕鱼:死亡", + "subtitles.entity.cod.flop": "鳕鱼:扑腾", + "subtitles.entity.cod.hurt": "鳕鱼:受伤", + "subtitles.entity.cow.ambient": "牛:哞~", + "subtitles.entity.cow.death": "牛:死亡", + "subtitles.entity.cow.hurt": "牛:受伤", + "subtitles.entity.cow.milk": "牛:被挤奶", + "subtitles.entity.creaking.activate": "嘎枝:注视", + "subtitles.entity.creaking.ambient": "嘎枝:嘎吱作响", + "subtitles.entity.creaking.attack": "嘎枝:攻击", + "subtitles.entity.creaking.deactivate": "嘎枝:静息", + "subtitles.entity.creaking.death": "嘎枝:碎裂", + "subtitles.entity.creaking.freeze": "嘎枝:驻足", + "subtitles.entity.creaking.spawn": "嘎枝:显现", + "subtitles.entity.creaking.sway": "嘎枝:被击打", + "subtitles.entity.creaking.twitch": "嘎枝:抽搐", + "subtitles.entity.creaking.unfreeze": "嘎枝:移动", + "subtitles.entity.creeper.death": "苦力怕:死亡", + "subtitles.entity.creeper.hurt": "苦力怕:受伤", + "subtitles.entity.creeper.primed": "苦力怕:嘶~", + "subtitles.entity.dolphin.ambient": "海豚:啾啾", + "subtitles.entity.dolphin.ambient_water": "海豚:吹口哨", + "subtitles.entity.dolphin.attack": "海豚:攻击", + "subtitles.entity.dolphin.death": "海豚:死亡", + "subtitles.entity.dolphin.eat": "海豚:进食", + "subtitles.entity.dolphin.hurt": "海豚:受伤", + "subtitles.entity.dolphin.jump": "海豚:跃起", + "subtitles.entity.dolphin.play": "海豚:嬉戏", + "subtitles.entity.dolphin.splash": "海豚:溅起水花", + "subtitles.entity.dolphin.swim": "海豚:游泳", + "subtitles.entity.donkey.ambient": "驴:嘶叫", + "subtitles.entity.donkey.angry": "驴:嘶鸣", + "subtitles.entity.donkey.chest": "驴:装备箱子", + "subtitles.entity.donkey.death": "驴:死亡", + "subtitles.entity.donkey.eat": "驴:进食", + "subtitles.entity.donkey.hurt": "驴:受伤", + "subtitles.entity.donkey.jump": "驴:跳跃", + "subtitles.entity.drowned.ambient": "溺尸:呻吟", + "subtitles.entity.drowned.ambient_water": "溺尸:呻吟", + "subtitles.entity.drowned.death": "溺尸:死亡", + "subtitles.entity.drowned.hurt": "溺尸:受伤", + "subtitles.entity.drowned.shoot": "溺尸:投掷三叉戟", + "subtitles.entity.drowned.step": "溺尸:脚步声", + "subtitles.entity.drowned.swim": "溺尸:游泳", + "subtitles.entity.egg.throw": "鸡蛋:飞出", + "subtitles.entity.elder_guardian.ambient": "远古守卫者:低鸣", + "subtitles.entity.elder_guardian.ambient_land": "远古守卫者:弹跳", + "subtitles.entity.elder_guardian.curse": "远古守卫者:诅咒", + "subtitles.entity.elder_guardian.death": "远古守卫者:死亡", + "subtitles.entity.elder_guardian.flop": "远古守卫者:扑腾", + "subtitles.entity.elder_guardian.hurt": "远古守卫者:受伤", + "subtitles.entity.ender_dragon.ambient": "末影龙:咆哮", + "subtitles.entity.ender_dragon.death": "末影龙:死亡", + "subtitles.entity.ender_dragon.flap": "末影龙:拍打翅膀", + "subtitles.entity.ender_dragon.growl": "末影龙:吼叫", + "subtitles.entity.ender_dragon.hurt": "末影龙:受伤", + "subtitles.entity.ender_dragon.shoot": "末影龙:射击", + "subtitles.entity.ender_eye.death": "末影之眼:掉落", + "subtitles.entity.ender_eye.launch": "末影之眼:射出", + "subtitles.entity.ender_pearl.throw": "末影珍珠:飞出", + "subtitles.entity.enderman.ambient": "末影人:低鸣", + "subtitles.entity.enderman.death": "末影人:死亡", + "subtitles.entity.enderman.hurt": "末影人:受伤", + "subtitles.entity.enderman.scream": "末影人:尖叫", + "subtitles.entity.enderman.stare": "末影人:喊叫", + "subtitles.entity.enderman.teleport": "末影人:传送", + "subtitles.entity.endermite.ambient": "末影螨:窜动", + "subtitles.entity.endermite.death": "末影螨:死亡", + "subtitles.entity.endermite.hurt": "末影螨:受伤", + "subtitles.entity.evoker.ambient": "唤魔者:咕哝", + "subtitles.entity.evoker.cast_spell": "唤魔者:施法", + "subtitles.entity.evoker.celebrate": "唤魔者:欢呼", + "subtitles.entity.evoker.death": "唤魔者:死亡", + "subtitles.entity.evoker.hurt": "唤魔者:受伤", + "subtitles.entity.evoker.prepare_attack": "唤魔者:准备攻击", + "subtitles.entity.evoker.prepare_summon": "唤魔者:准备召唤", + "subtitles.entity.evoker.prepare_wololo": "唤魔者:准备施咒", + "subtitles.entity.evoker_fangs.attack": "尖牙:咬合", + "subtitles.entity.experience_orb.pickup": "获得经验", + "subtitles.entity.firework_rocket.blast": "烟花:爆裂", + "subtitles.entity.firework_rocket.launch": "烟花:发射", + "subtitles.entity.firework_rocket.twinkle": "烟火:闪烁", + "subtitles.entity.fish.swim": "溅起水花", + "subtitles.entity.fishing_bobber.retrieve": "浮漂:收回", + "subtitles.entity.fishing_bobber.splash": "浮漂:溅起水花", + "subtitles.entity.fishing_bobber.throw": "浮漂:甩出", + "subtitles.entity.fox.aggro": "狐狸:愤怒", + "subtitles.entity.fox.ambient": "狐狸:吱吱叫", + "subtitles.entity.fox.bite": "狐狸:撕咬", + "subtitles.entity.fox.death": "狐狸:死亡", + "subtitles.entity.fox.eat": "狐狸:进食", + "subtitles.entity.fox.hurt": "狐狸:受伤", + "subtitles.entity.fox.screech": "狐狸:尖声叫", + "subtitles.entity.fox.sleep": "狐狸:打鼾", + "subtitles.entity.fox.sniff": "狐狸:嗅探", + "subtitles.entity.fox.spit": "狐狸:吐出", + "subtitles.entity.fox.teleport": "狐狸:传送", + "subtitles.entity.frog.ambient": "青蛙:咕呱", + "subtitles.entity.frog.death": "青蛙:死亡", + "subtitles.entity.frog.eat": "青蛙:进食", + "subtitles.entity.frog.hurt": "青蛙:受伤", + "subtitles.entity.frog.lay_spawn": "青蛙:产卵", + "subtitles.entity.frog.long_jump": "青蛙:跳跃", + "subtitles.entity.generic.big_fall": "某物:着地", + "subtitles.entity.generic.burn": "燃烧", + "subtitles.entity.generic.death": "死亡", + "subtitles.entity.generic.drink": "啜饮", + "subtitles.entity.generic.eat": "进食", + "subtitles.entity.generic.explode": "爆炸", + "subtitles.entity.generic.extinguish_fire": "火:熄灭", + "subtitles.entity.generic.hurt": "某物:受伤", + "subtitles.entity.generic.small_fall": "某物:摔倒", + "subtitles.entity.generic.splash": "溅起水花", + "subtitles.entity.generic.swim": "游泳", + "subtitles.entity.generic.wind_burst": "风弹:爆裂", + "subtitles.entity.ghast.ambient": "恶魂:哭泣", + "subtitles.entity.ghast.death": "恶魂:死亡", + "subtitles.entity.ghast.hurt": "恶魂:受伤", + "subtitles.entity.ghast.shoot": "恶魂:射击", + "subtitles.entity.glow_item_frame.add_item": "荧光物品展示框:填充", + "subtitles.entity.glow_item_frame.break": "荧光物品展示框:被破坏", + "subtitles.entity.glow_item_frame.place": "荧光物品展示框:被放置", + "subtitles.entity.glow_item_frame.remove_item": "荧光物品展示框:清空", + "subtitles.entity.glow_item_frame.rotate_item": "荧光物品展示框:转动", + "subtitles.entity.glow_squid.ambient": "发光鱿鱼:游泳", + "subtitles.entity.glow_squid.death": "发光鱿鱼:死亡", + "subtitles.entity.glow_squid.hurt": "发光鱿鱼:受伤", + "subtitles.entity.glow_squid.squirt": "发光鱿鱼:喷墨", + "subtitles.entity.goat.ambient": "山羊:咩~", + "subtitles.entity.goat.death": "山羊:死亡", + "subtitles.entity.goat.eat": "山羊:进食", + "subtitles.entity.goat.horn_break": "山羊:断角", + "subtitles.entity.goat.hurt": "山羊:受伤", + "subtitles.entity.goat.long_jump": "山羊:跳跃", + "subtitles.entity.goat.milk": "山羊:被挤奶", + "subtitles.entity.goat.prepare_ram": "山羊:跺脚", + "subtitles.entity.goat.ram_impact": "山羊:冲撞", + "subtitles.entity.goat.screaming.ambient": "山羊:喊叫", + "subtitles.entity.goat.step": "山羊:脚步声", + "subtitles.entity.guardian.ambient": "守卫者:低鸣", + "subtitles.entity.guardian.ambient_land": "守卫者:弹跳", + "subtitles.entity.guardian.attack": "守卫者:射击", + "subtitles.entity.guardian.death": "守卫者:死亡", + "subtitles.entity.guardian.flop": "守卫者:扑腾", + "subtitles.entity.guardian.hurt": "守卫者:受伤", + "subtitles.entity.hoglin.ambient": "疣猪兽:咆哮", + "subtitles.entity.hoglin.angry": "疣猪兽:怒吼", + "subtitles.entity.hoglin.attack": "疣猪兽:攻击", + "subtitles.entity.hoglin.converted_to_zombified": "疣猪兽:转化为僵尸疣猪兽", + "subtitles.entity.hoglin.death": "疣猪兽:死亡", + "subtitles.entity.hoglin.hurt": "疣猪兽:受伤", + "subtitles.entity.hoglin.retreat": "疣猪兽:退缩", + "subtitles.entity.hoglin.step": "疣猪兽:脚步声", + "subtitles.entity.horse.ambient": "马:嘶鸣", + "subtitles.entity.horse.angry": "马:嘶鸣", + "subtitles.entity.horse.armor": "马铠:装备", + "subtitles.entity.horse.breathe": "马:呼吸", + "subtitles.entity.horse.death": "马:死亡", + "subtitles.entity.horse.eat": "马:进食", + "subtitles.entity.horse.gallop": "马:奔腾", + "subtitles.entity.horse.hurt": "马:受伤", + "subtitles.entity.horse.jump": "马:跳跃", + "subtitles.entity.horse.saddle": "鞍:装备", + "subtitles.entity.husk.ambient": "尸壳:低吼", + "subtitles.entity.husk.converted_to_zombie": "尸壳:转化为僵尸", + "subtitles.entity.husk.death": "尸壳:死亡", + "subtitles.entity.husk.hurt": "尸壳:受伤", + "subtitles.entity.illusioner.ambient": "幻术师:咕哝", + "subtitles.entity.illusioner.cast_spell": "幻术师:施法", + "subtitles.entity.illusioner.death": "幻术师:死亡", + "subtitles.entity.illusioner.hurt": "幻术师:受伤", + "subtitles.entity.illusioner.mirror_move": "幻术师:分影", + "subtitles.entity.illusioner.prepare_blindness": "幻术师:准备失明法术", + "subtitles.entity.illusioner.prepare_mirror": "幻术师:准备分影法术", + "subtitles.entity.iron_golem.attack": "铁傀儡:攻击", + "subtitles.entity.iron_golem.damage": "铁傀儡:受损", + "subtitles.entity.iron_golem.death": "铁傀儡:死亡", + "subtitles.entity.iron_golem.hurt": "铁傀儡:受伤", + "subtitles.entity.iron_golem.repair": "铁傀儡:修复", + "subtitles.entity.item.break": "物品:损坏", + "subtitles.entity.item.pickup": "物品:被拾起", + "subtitles.entity.item_frame.add_item": "物品展示框:填充", + "subtitles.entity.item_frame.break": "物品展示框:被破坏", + "subtitles.entity.item_frame.place": "物品展示框:被放置", + "subtitles.entity.item_frame.remove_item": "物品展示框:清空", + "subtitles.entity.item_frame.rotate_item": "物品展示框:转动", + "subtitles.entity.leash_knot.break": "拴绳结:被破坏", + "subtitles.entity.leash_knot.place": "拴绳结:被系上", + "subtitles.entity.lightning_bolt.impact": "电闪", + "subtitles.entity.lightning_bolt.thunder": "雷鸣", + "subtitles.entity.llama.ambient": "羊驼:吼叫", + "subtitles.entity.llama.angry": "羊驼:怒吼", + "subtitles.entity.llama.chest": "羊驼:装备箱子", + "subtitles.entity.llama.death": "羊驼:死亡", + "subtitles.entity.llama.eat": "羊驼:进食", + "subtitles.entity.llama.hurt": "羊驼:受伤", + "subtitles.entity.llama.spit": "羊驼:喷射唾沫", + "subtitles.entity.llama.step": "羊驼:脚步声", + "subtitles.entity.llama.swag": "羊驼:被装饰", + "subtitles.entity.magma_cube.death": "岩浆怪:死亡", + "subtitles.entity.magma_cube.hurt": "岩浆怪:受伤", + "subtitles.entity.magma_cube.squish": "岩浆怪:挤压", + "subtitles.entity.minecart.inside": "矿车:哐啷作响", + "subtitles.entity.minecart.inside_underwater": "矿车:在水中哐啷作响", + "subtitles.entity.minecart.riding": "矿车:行进", + "subtitles.entity.mooshroom.convert": "哞菇:转化", + "subtitles.entity.mooshroom.eat": "哞菇:进食", + "subtitles.entity.mooshroom.milk": "哞菇:被挤奶", + "subtitles.entity.mooshroom.suspicious_milk": "哞菇:被挤出谜之炖菜", + "subtitles.entity.mule.ambient": "骡:鸣叫", + "subtitles.entity.mule.angry": "骡:嘶鸣", + "subtitles.entity.mule.chest": "骡:装备箱子", + "subtitles.entity.mule.death": "骡:死亡", + "subtitles.entity.mule.eat": "骡:进食", + "subtitles.entity.mule.hurt": "骡:受伤", + "subtitles.entity.mule.jump": "骡:跳跃", + "subtitles.entity.painting.break": "画:被破坏", + "subtitles.entity.painting.place": "画:被放置", + "subtitles.entity.panda.aggressive_ambient": "熊猫:发怒", + "subtitles.entity.panda.ambient": "熊猫:喘息", + "subtitles.entity.panda.bite": "熊猫:撕咬", + "subtitles.entity.panda.cant_breed": "熊猫:哀鸣", + "subtitles.entity.panda.death": "熊猫:死亡", + "subtitles.entity.panda.eat": "熊猫:进食", + "subtitles.entity.panda.hurt": "熊猫:受伤", + "subtitles.entity.panda.pre_sneeze": "熊猫:鼻痒", + "subtitles.entity.panda.sneeze": "熊猫:打喷嚏", + "subtitles.entity.panda.step": "熊猫:脚步声", + "subtitles.entity.panda.worried_ambient": "熊猫:呜咽", + "subtitles.entity.parrot.ambient": "鹦鹉:说话", + "subtitles.entity.parrot.death": "鹦鹉:死亡", + "subtitles.entity.parrot.eats": "鹦鹉:进食", + "subtitles.entity.parrot.fly": "鹦鹉:扑翼", + "subtitles.entity.parrot.hurts": "鹦鹉:受伤", + "subtitles.entity.parrot.imitate.blaze": "鹦鹉:呼吸", + "subtitles.entity.parrot.imitate.bogged": "鹦鹉:咯咯声", + "subtitles.entity.parrot.imitate.breeze": "鹦鹉:呼啸", + "subtitles.entity.parrot.imitate.creaking": "鹦鹉:嘎吱作响", + "subtitles.entity.parrot.imitate.creeper": "鹦鹉:嘶~", + "subtitles.entity.parrot.imitate.drowned": "鹦鹉:呻吟", + "subtitles.entity.parrot.imitate.elder_guardian": "鹦鹉:低鸣", + "subtitles.entity.parrot.imitate.ender_dragon": "鹦鹉:咆哮", + "subtitles.entity.parrot.imitate.endermite": "鹦鹉:窜动", + "subtitles.entity.parrot.imitate.evoker": "鹦鹉:咕哝", + "subtitles.entity.parrot.imitate.ghast": "鹦鹉:哭泣", + "subtitles.entity.parrot.imitate.guardian": "鹦鹉:低鸣", + "subtitles.entity.parrot.imitate.hoglin": "鹦鹉:咆哮", + "subtitles.entity.parrot.imitate.husk": "鹦鹉:低吼", + "subtitles.entity.parrot.imitate.illusioner": "鹦鹉:咕哝", + "subtitles.entity.parrot.imitate.magma_cube": "鹦鹉:挤压", + "subtitles.entity.parrot.imitate.phantom": "鹦鹉:尖声叫", + "subtitles.entity.parrot.imitate.piglin": "鹦鹉:哼叫", + "subtitles.entity.parrot.imitate.piglin_brute": "鹦鹉:哼叫", + "subtitles.entity.parrot.imitate.pillager": "鹦鹉:咕哝", + "subtitles.entity.parrot.imitate.ravager": "鹦鹉:呼噜", + "subtitles.entity.parrot.imitate.shulker": "鹦鹉:窥视", + "subtitles.entity.parrot.imitate.silverfish": "鹦鹉:嘶嘶", + "subtitles.entity.parrot.imitate.skeleton": "鹦鹉:咯咯声", + "subtitles.entity.parrot.imitate.slime": "鹦鹉:挤压", + "subtitles.entity.parrot.imitate.spider": "鹦鹉:嘶嘶", + "subtitles.entity.parrot.imitate.stray": "鹦鹉:咯咯声", + "subtitles.entity.parrot.imitate.vex": "鹦鹉:恼人", + "subtitles.entity.parrot.imitate.vindicator": "鹦鹉:低语", + "subtitles.entity.parrot.imitate.warden": "鹦鹉:呻吟", + "subtitles.entity.parrot.imitate.witch": "鹦鹉:暗笑", + "subtitles.entity.parrot.imitate.wither": "鹦鹉:愤怒", + "subtitles.entity.parrot.imitate.wither_skeleton": "鹦鹉:咯咯声", + "subtitles.entity.parrot.imitate.zoglin": "鹦鹉:咆哮", + "subtitles.entity.parrot.imitate.zombie": "鹦鹉:低吼", + "subtitles.entity.parrot.imitate.zombie_villager": "鹦鹉:低吼", + "subtitles.entity.phantom.ambient": "幻翼:尖声叫", + "subtitles.entity.phantom.bite": "幻翼:撕咬", + "subtitles.entity.phantom.death": "幻翼:死亡", + "subtitles.entity.phantom.flap": "幻翼:振翅", + "subtitles.entity.phantom.hurt": "幻翼:受伤", + "subtitles.entity.phantom.swoop": "幻翼:俯冲", + "subtitles.entity.pig.ambient": "猪:哼叫", + "subtitles.entity.pig.death": "猪:死亡", + "subtitles.entity.pig.hurt": "猪:受伤", + "subtitles.entity.pig.saddle": "鞍:装备", + "subtitles.entity.piglin.admiring_item": "猪灵:端详物品", + "subtitles.entity.piglin.ambient": "猪灵:哼叫", + "subtitles.entity.piglin.angry": "猪灵:愤怒地哼叫", + "subtitles.entity.piglin.celebrate": "猪灵:庆祝", + "subtitles.entity.piglin.converted_to_zombified": "猪灵:转化为僵尸猪灵", + "subtitles.entity.piglin.death": "猪灵:死亡", + "subtitles.entity.piglin.hurt": "猪灵:受伤", + "subtitles.entity.piglin.jealous": "猪灵:羡慕地哼叫", + "subtitles.entity.piglin.retreat": "猪灵:退缩", + "subtitles.entity.piglin.step": "猪灵:脚步声", + "subtitles.entity.piglin_brute.ambient": "猪灵蛮兵:哼叫", + "subtitles.entity.piglin_brute.angry": "猪灵蛮兵:愤怒地哼叫", + "subtitles.entity.piglin_brute.converted_to_zombified": "猪灵蛮兵:转化为僵尸猪灵", + "subtitles.entity.piglin_brute.death": "猪灵蛮兵:死亡", + "subtitles.entity.piglin_brute.hurt": "猪灵蛮兵:受伤", + "subtitles.entity.piglin_brute.step": "猪灵蛮兵:脚步声", + "subtitles.entity.pillager.ambient": "掠夺者:咕哝", + "subtitles.entity.pillager.celebrate": "掠夺者:欢呼", + "subtitles.entity.pillager.death": "掠夺者:死亡", + "subtitles.entity.pillager.hurt": "掠夺者:受伤", + "subtitles.entity.player.attack.crit": "暴击", + "subtitles.entity.player.attack.knockback": "击退攻击", + "subtitles.entity.player.attack.strong": "重击", + "subtitles.entity.player.attack.sweep": "横扫攻击", + "subtitles.entity.player.attack.weak": "轻击", + "subtitles.entity.player.burp": "打嗝", + "subtitles.entity.player.death": "玩家:死亡", + "subtitles.entity.player.freeze_hurt": "玩家:冻伤", + "subtitles.entity.player.hurt": "玩家:受伤", + "subtitles.entity.player.hurt_drown": "玩家:溺水", + "subtitles.entity.player.hurt_on_fire": "玩家:燃烧", + "subtitles.entity.player.levelup": "玩家:升级", + "subtitles.entity.player.teleport": "玩家:传送", + "subtitles.entity.polar_bear.ambient": "北极熊:低吼", + "subtitles.entity.polar_bear.ambient_baby": "幼年北极熊:哼哼", + "subtitles.entity.polar_bear.death": "北极熊:死亡", + "subtitles.entity.polar_bear.hurt": "北极熊:受伤", + "subtitles.entity.polar_bear.warning": "北极熊:咆哮", + "subtitles.entity.potion.splash": "玻璃瓶:碎裂", + "subtitles.entity.potion.throw": "玻璃瓶:扔出", + "subtitles.entity.puffer_fish.blow_out": "河豚:收缩", + "subtitles.entity.puffer_fish.blow_up": "河豚:膨胀", + "subtitles.entity.puffer_fish.death": "河豚:死亡", + "subtitles.entity.puffer_fish.flop": "河豚:扑腾", + "subtitles.entity.puffer_fish.hurt": "河豚:受伤", + "subtitles.entity.puffer_fish.sting": "河豚:刺蛰", + "subtitles.entity.rabbit.ambient": "兔子:吱吱叫", + "subtitles.entity.rabbit.attack": "兔子:攻击", + "subtitles.entity.rabbit.death": "兔子:死亡", + "subtitles.entity.rabbit.hurt": "兔子:受伤", + "subtitles.entity.rabbit.jump": "兔子:跳动", + "subtitles.entity.ravager.ambient": "劫掠兽:呼噜", + "subtitles.entity.ravager.attack": "劫掠兽:撕咬", + "subtitles.entity.ravager.celebrate": "劫掠兽:欢呼", + "subtitles.entity.ravager.death": "劫掠兽:死亡", + "subtitles.entity.ravager.hurt": "劫掠兽:受伤", + "subtitles.entity.ravager.roar": "劫掠兽:咆哮", + "subtitles.entity.ravager.step": "劫掠兽:脚步声", + "subtitles.entity.ravager.stunned": "劫掠兽:眩晕", + "subtitles.entity.salmon.death": "鲑鱼:死亡", + "subtitles.entity.salmon.flop": "鲑鱼:扑腾", + "subtitles.entity.salmon.hurt": "鲑鱼:受伤", + "subtitles.entity.sheep.ambient": "绵羊:咩~", + "subtitles.entity.sheep.death": "绵羊:死亡", + "subtitles.entity.sheep.hurt": "绵羊:受伤", + "subtitles.entity.shulker.ambient": "潜影贝:窥视", + "subtitles.entity.shulker.close": "潜影贝:闭合", + "subtitles.entity.shulker.death": "潜影贝:死亡", + "subtitles.entity.shulker.hurt": "潜影贝:受伤", + "subtitles.entity.shulker.open": "潜影贝:打开", + "subtitles.entity.shulker.shoot": "潜影贝:射击", + "subtitles.entity.shulker.teleport": "潜影贝:传送", + "subtitles.entity.shulker_bullet.hit": "潜影弹:爆炸", + "subtitles.entity.shulker_bullet.hurt": "潜影弹:碎裂", + "subtitles.entity.silverfish.ambient": "蠹虫:嘶嘶", + "subtitles.entity.silverfish.death": "蠹虫:死亡", + "subtitles.entity.silverfish.hurt": "蠹虫:受伤", + "subtitles.entity.skeleton.ambient": "骷髅:咯咯声", + "subtitles.entity.skeleton.converted_to_stray": "骷髅:转化为流浪者", + "subtitles.entity.skeleton.death": "骷髅:死亡", + "subtitles.entity.skeleton.hurt": "骷髅:受伤", + "subtitles.entity.skeleton.shoot": "骷髅:射击", + "subtitles.entity.skeleton_horse.ambient": "骷髅马:嘶叫", + "subtitles.entity.skeleton_horse.death": "骷髅马:死亡", + "subtitles.entity.skeleton_horse.hurt": "骷髅马:受伤", + "subtitles.entity.skeleton_horse.jump_water": "骷髅马:跳跃", + "subtitles.entity.skeleton_horse.swim": "骷髅马:游泳", + "subtitles.entity.slime.attack": "史莱姆:攻击", + "subtitles.entity.slime.death": "史莱姆:死亡", + "subtitles.entity.slime.hurt": "史莱姆:受伤", + "subtitles.entity.slime.squish": "史莱姆:挤压", + "subtitles.entity.sniffer.death": "嗅探兽:死亡", + "subtitles.entity.sniffer.digging": "嗅探兽:挖掘", + "subtitles.entity.sniffer.digging_stop": "嗅探兽:站起", + "subtitles.entity.sniffer.drop_seed": "嗅探兽:丢下种子", + "subtitles.entity.sniffer.eat": "嗅探兽:进食", + "subtitles.entity.sniffer.egg_crack": "嗅探兽蛋:裂开", + "subtitles.entity.sniffer.egg_hatch": "嗅探兽蛋:孵化", + "subtitles.entity.sniffer.happy": "嗅探兽:愉悦", + "subtitles.entity.sniffer.hurt": "嗅探兽:受伤", + "subtitles.entity.sniffer.idle": "嗅探兽:呼噜", + "subtitles.entity.sniffer.scenting": "嗅探兽:嗅闻", + "subtitles.entity.sniffer.searching": "嗅探兽:搜寻", + "subtitles.entity.sniffer.sniffing": "嗅探兽:嗅探", + "subtitles.entity.sniffer.step": "嗅探兽:脚步声", + "subtitles.entity.snow_golem.death": "雪傀儡:死亡", + "subtitles.entity.snow_golem.hurt": "雪傀儡:受伤", + "subtitles.entity.snowball.throw": "雪球:飞出", + "subtitles.entity.spider.ambient": "蜘蛛:嘶嘶", + "subtitles.entity.spider.death": "蜘蛛:死亡", + "subtitles.entity.spider.hurt": "蜘蛛:受伤", + "subtitles.entity.squid.ambient": "鱿鱼:游泳", + "subtitles.entity.squid.death": "鱿鱼:死亡", + "subtitles.entity.squid.hurt": "鱿鱼:受伤", + "subtitles.entity.squid.squirt": "鱿鱼:喷墨", + "subtitles.entity.stray.ambient": "流浪者:咯咯声", + "subtitles.entity.stray.death": "流浪者:死亡", + "subtitles.entity.stray.hurt": "流浪者:受伤", + "subtitles.entity.strider.death": "炽足兽:死亡", + "subtitles.entity.strider.eat": "炽足兽:进食", + "subtitles.entity.strider.happy": "炽足兽:颤鸣", + "subtitles.entity.strider.hurt": "炽足兽:受伤", + "subtitles.entity.strider.idle": "炽足兽:啾啾", + "subtitles.entity.strider.retreat": "炽足兽:退缩", + "subtitles.entity.tadpole.death": "蝌蚪:死亡", + "subtitles.entity.tadpole.flop": "蝌蚪:扑腾", + "subtitles.entity.tadpole.grow_up": "蝌蚪:成长", + "subtitles.entity.tadpole.hurt": "蝌蚪:受伤", + "subtitles.entity.tnt.primed": "TNT:嘶嘶作响", + "subtitles.entity.tropical_fish.death": "热带鱼:死亡", + "subtitles.entity.tropical_fish.flop": "热带鱼:扑腾", + "subtitles.entity.tropical_fish.hurt": "热带鱼:受伤", + "subtitles.entity.turtle.ambient_land": "海龟:啾啾", + "subtitles.entity.turtle.death": "海龟:死亡", + "subtitles.entity.turtle.death_baby": "幼年海龟:死亡", + "subtitles.entity.turtle.egg_break": "海龟蛋:破裂", + "subtitles.entity.turtle.egg_crack": "海龟蛋:裂开", + "subtitles.entity.turtle.egg_hatch": "海龟蛋:孵化", + "subtitles.entity.turtle.hurt": "海龟:受伤", + "subtitles.entity.turtle.hurt_baby": "幼年海龟:受伤", + "subtitles.entity.turtle.lay_egg": "海龟:产卵", + "subtitles.entity.turtle.shamble": "海龟:爬行", + "subtitles.entity.turtle.shamble_baby": "幼年海龟:爬行", + "subtitles.entity.turtle.swim": "海龟:游泳", + "subtitles.entity.vex.ambient": "恼鬼:恼人", + "subtitles.entity.vex.charge": "恼鬼:尖叫", + "subtitles.entity.vex.death": "恼鬼:死亡", + "subtitles.entity.vex.hurt": "恼鬼:受伤", + "subtitles.entity.villager.ambient": "村民:喃喃自语", + "subtitles.entity.villager.celebrate": "村民:欢呼", + "subtitles.entity.villager.death": "村民:死亡", + "subtitles.entity.villager.hurt": "村民:受伤", + "subtitles.entity.villager.no": "村民:拒绝", + "subtitles.entity.villager.trade": "村民:交易", + "subtitles.entity.villager.work_armorer": "盔甲匠:工作", + "subtitles.entity.villager.work_butcher": "屠夫:工作", + "subtitles.entity.villager.work_cartographer": "制图师:工作", + "subtitles.entity.villager.work_cleric": "牧师:工作", + "subtitles.entity.villager.work_farmer": "农民:工作", + "subtitles.entity.villager.work_fisherman": "渔夫:工作", + "subtitles.entity.villager.work_fletcher": "制箭师:工作", + "subtitles.entity.villager.work_leatherworker": "皮匠:工作", + "subtitles.entity.villager.work_librarian": "图书管理员:工作", + "subtitles.entity.villager.work_mason": "石匠:工作", + "subtitles.entity.villager.work_shepherd": "牧羊人:工作", + "subtitles.entity.villager.work_toolsmith": "工具匠:工作", + "subtitles.entity.villager.work_weaponsmith": "武器匠:工作", + "subtitles.entity.villager.yes": "村民:同意", + "subtitles.entity.vindicator.ambient": "卫道士:低语", + "subtitles.entity.vindicator.celebrate": "卫道士:欢呼", + "subtitles.entity.vindicator.death": "卫道士:死亡", + "subtitles.entity.vindicator.hurt": "卫道士:受伤", + "subtitles.entity.wandering_trader.ambient": "流浪商人:喃喃自语", + "subtitles.entity.wandering_trader.death": "流浪商人:死亡", + "subtitles.entity.wandering_trader.disappeared": "流浪商人:隐身", + "subtitles.entity.wandering_trader.drink_milk": "流浪商人:喝奶", + "subtitles.entity.wandering_trader.drink_potion": "流浪商人:饮用药水", + "subtitles.entity.wandering_trader.hurt": "流浪商人:受伤", + "subtitles.entity.wandering_trader.no": "流浪商人:拒绝", + "subtitles.entity.wandering_trader.reappeared": "流浪商人:现身", + "subtitles.entity.wandering_trader.trade": "流浪商人:交易", + "subtitles.entity.wandering_trader.yes": "流浪商人:同意", + "subtitles.entity.warden.agitated": "监守者:愤怒地呻吟", + "subtitles.entity.warden.ambient": "监守者:呻吟", + "subtitles.entity.warden.angry": "监守者:狂怒", + "subtitles.entity.warden.attack_impact": "监守者:击中", + "subtitles.entity.warden.death": "监守者:死亡", + "subtitles.entity.warden.dig": "监守者:掘地", + "subtitles.entity.warden.emerge": "监守者:现身", + "subtitles.entity.warden.heartbeat": "监守者:心跳声", + "subtitles.entity.warden.hurt": "监守者:受伤", + "subtitles.entity.warden.listening": "监守者:察觉到某物", + "subtitles.entity.warden.listening_angry": "监守者:愤怒地察觉到某物", + "subtitles.entity.warden.nearby_close": "监守者:接近", + "subtitles.entity.warden.nearby_closer": "监守者:逼近", + "subtitles.entity.warden.nearby_closest": "监守者:迫近", + "subtitles.entity.warden.roar": "监守者:咆哮", + "subtitles.entity.warden.sniff": "监守者:嗅闻", + "subtitles.entity.warden.sonic_boom": "监守者:发射音波", + "subtitles.entity.warden.sonic_charge": "监守者:蓄力", + "subtitles.entity.warden.step": "监守者:脚步声", + "subtitles.entity.warden.tendril_clicks": "监守者:卷须颤动", + "subtitles.entity.wind_charge.throw": "风弹:飞行", + "subtitles.entity.wind_charge.wind_burst": "风弹:爆裂", + "subtitles.entity.witch.ambient": "女巫:暗笑", + "subtitles.entity.witch.celebrate": "女巫:欢呼", + "subtitles.entity.witch.death": "女巫:死亡", + "subtitles.entity.witch.drink": "女巫:饮用药水", + "subtitles.entity.witch.hurt": "女巫:受伤", + "subtitles.entity.witch.throw": "女巫:投掷", + "subtitles.entity.wither.ambient": "凋灵:愤怒", + "subtitles.entity.wither.death": "凋灵:死亡", + "subtitles.entity.wither.hurt": "凋灵:受伤", + "subtitles.entity.wither.shoot": "凋灵:攻击", + "subtitles.entity.wither.spawn": "凋灵:解放", + "subtitles.entity.wither_skeleton.ambient": "凋灵骷髅:咯咯声", + "subtitles.entity.wither_skeleton.death": "凋灵骷髅:死亡", + "subtitles.entity.wither_skeleton.hurt": "凋灵骷髅:受伤", + "subtitles.entity.wolf.ambient": "狼:喘息", + "subtitles.entity.wolf.death": "狼:死亡", + "subtitles.entity.wolf.growl": "狼:嚎叫", + "subtitles.entity.wolf.hurt": "狼:受伤", + "subtitles.entity.wolf.shake": "狼:摇动", + "subtitles.entity.zoglin.ambient": "僵尸疣猪兽:咆哮", + "subtitles.entity.zoglin.angry": "僵尸疣猪兽:怒吼", + "subtitles.entity.zoglin.attack": "僵尸疣猪兽:攻击", + "subtitles.entity.zoglin.death": "僵尸疣猪兽:死亡", + "subtitles.entity.zoglin.hurt": "僵尸疣猪兽:受伤", + "subtitles.entity.zoglin.step": "僵尸疣猪兽:脚步声", + "subtitles.entity.zombie.ambient": "僵尸:低吼", + "subtitles.entity.zombie.attack_wooden_door": "门:晃动", + "subtitles.entity.zombie.break_wooden_door": "门:毁坏", + "subtitles.entity.zombie.converted_to_drowned": "僵尸:转化为溺尸", + "subtitles.entity.zombie.death": "僵尸:死亡", + "subtitles.entity.zombie.destroy_egg": "海龟蛋:被踩踏", + "subtitles.entity.zombie.hurt": "僵尸:受伤", + "subtitles.entity.zombie.infect": "僵尸:感染", + "subtitles.entity.zombie_horse.ambient": "僵尸马:嘶叫", + "subtitles.entity.zombie_horse.death": "僵尸马:死亡", + "subtitles.entity.zombie_horse.hurt": "僵尸马:受伤", + "subtitles.entity.zombie_villager.ambient": "僵尸村民:低吼", + "subtitles.entity.zombie_villager.converted": "僵尸村民:哀嚎", + "subtitles.entity.zombie_villager.cure": "僵尸村民:撕心裂肺", + "subtitles.entity.zombie_villager.death": "僵尸村民:死亡", + "subtitles.entity.zombie_villager.hurt": "僵尸村民:受伤", + "subtitles.entity.zombified_piglin.ambient": "僵尸猪灵:呼噜", + "subtitles.entity.zombified_piglin.angry": "僵尸猪灵:愤怒地呼噜", + "subtitles.entity.zombified_piglin.death": "僵尸猪灵:死亡", + "subtitles.entity.zombified_piglin.hurt": "僵尸猪灵:受伤", + "subtitles.event.mob_effect.bad_omen": "不祥之兆:显现", + "subtitles.event.mob_effect.raid_omen": "袭击:迫在眉睫", + "subtitles.event.mob_effect.trial_omen": "不祥试炼:迫在眉睫", + "subtitles.event.raid.horn": "不祥号角:鸣响", + "subtitles.item.armor.equip": "盔甲:装备", + "subtitles.item.armor.equip_chain": "锁链盔甲:碰擦", + "subtitles.item.armor.equip_diamond": "钻石盔甲:碰擦", + "subtitles.item.armor.equip_elytra": "鞘翅:沙沙作响", + "subtitles.item.armor.equip_gold": "金盔甲:叮当", + "subtitles.item.armor.equip_iron": "铁盔甲:铿锵", + "subtitles.item.armor.equip_leather": "皮革盔甲:摩擦", + "subtitles.item.armor.equip_netherite": "下界合金盔甲:铿锵", + "subtitles.item.armor.equip_turtle": "海龟壳:咕咚", + "subtitles.item.armor.equip_wolf": "狼铠:装备", + "subtitles.item.armor.unequip_wolf": "狼铠:滑落", + "subtitles.item.axe.scrape": "斧:刮削", + "subtitles.item.axe.strip": "斧:削皮", + "subtitles.item.axe.wax_off": "脱蜡", + "subtitles.item.bone_meal.use": "骨粉:沙沙作响", + "subtitles.item.book.page_turn": "书页:沙沙作响", + "subtitles.item.book.put": "书:放置", + "subtitles.item.bottle.empty": "玻璃瓶:倒空", + "subtitles.item.bottle.fill": "玻璃瓶:装满", + "subtitles.item.brush.brushing.generic": "刷子:刷扫中", + "subtitles.item.brush.brushing.gravel": "刷子:刷扫沙砾中", + "subtitles.item.brush.brushing.gravel.complete": "刷子:刷扫沙砾完毕", + "subtitles.item.brush.brushing.sand": "刷子:刷扫沙子中", + "subtitles.item.brush.brushing.sand.complete": "刷子:刷扫沙子完毕", + "subtitles.item.bucket.empty": "铁桶:倒空", + "subtitles.item.bucket.fill": "铁桶:装满", + "subtitles.item.bucket.fill_axolotl": "美西螈:被装起", + "subtitles.item.bucket.fill_fish": "鱼:被捕获", + "subtitles.item.bucket.fill_tadpole": "蝌蚪:被捕获", + "subtitles.item.bundle.drop_contents": "收纳袋:倒空", + "subtitles.item.bundle.insert": "物品:装入袋中", + "subtitles.item.bundle.insert_fail": "收纳袋:已满", + "subtitles.item.bundle.remove_one": "物品:从袋中取出", + "subtitles.item.chorus_fruit.teleport": "玩家:传送", + "subtitles.item.crop.plant": "作物:种植", + "subtitles.item.crossbow.charge": "弩:蓄力", + "subtitles.item.crossbow.hit": "箭:击中", + "subtitles.item.crossbow.load": "弩:装填", + "subtitles.item.crossbow.shoot": "弩:发射", + "subtitles.item.dye.use": "染料:染色", + "subtitles.item.elytra.flying": "呼啸声", + "subtitles.item.firecharge.use": "火焰弹:呼啸", + "subtitles.item.flintandsteel.use": "打火石:点火", + "subtitles.item.glow_ink_sac.use": "荧光墨囊:涂抹", + "subtitles.item.goat_horn.play": "山羊角:吹奏", + "subtitles.item.hoe.till": "锄:犁地", + "subtitles.item.honey_bottle.drink": "吞咽", + "subtitles.item.honeycomb.wax_on": "涂蜡", + "subtitles.item.ink_sac.use": "墨囊:涂抹", + "subtitles.item.lodestone_compass.lock": "磁石指针:绑定磁石", + "subtitles.item.mace.smash_air": "重锤:猛击", + "subtitles.item.mace.smash_ground": "重锤:猛击", + "subtitles.item.nether_wart.plant": "作物:种植", + "subtitles.item.ominous_bottle.dispose": "玻璃瓶:碎裂", + "subtitles.item.shears.shear": "剪刀:剪断", + "subtitles.item.shield.block": "盾牌:格挡", + "subtitles.item.shovel.flatten": "锹:压地", + "subtitles.item.spyglass.stop_using": "望远镜:缩小", + "subtitles.item.spyglass.use": "望远镜:放大", + "subtitles.item.totem.use": "图腾:发动", + "subtitles.item.trident.hit": "三叉戟:突刺", + "subtitles.item.trident.hit_ground": "三叉戟:振动", + "subtitles.item.trident.return": "三叉戟:收回", + "subtitles.item.trident.riptide": "三叉戟:突进", + "subtitles.item.trident.throw": "三叉戟:铿锵", + "subtitles.item.trident.thunder": "三叉戟:电闪雷鸣", + "subtitles.item.wolf_armor.break": "狼铠:损坏", + "subtitles.item.wolf_armor.crack": "狼铠:裂开", + "subtitles.item.wolf_armor.damage": "狼铠:受损", + "subtitles.item.wolf_armor.repair": "狼铠:被修复", + "subtitles.particle.soul_escape": "灵魂:逸散", + "subtitles.ui.cartography_table.take_result": "地图:绘制", + "subtitles.ui.hud.bubble_pop": "氧气值:减少", + "subtitles.ui.loom.take_result": "织布机:使用", + "subtitles.ui.stonecutter.take_result": "切石机:使用", + "subtitles.weather.rain": "雨:落下", + "symlink_warning.message": "从含有符号链接的文件夹中加载世界可能导致风险,尤其是在你不清楚链接目标内容时。请访问%s了解更多信息。", + "symlink_warning.message.pack": "继续操作前请知晓,加载含有符号链接的包可能会有风险。请访问%s了解更多信息。", + "symlink_warning.message.world": "继续操作前请知晓,从含有符号链接的文件夹中加载世界可能会有风险。请访问%s了解更多信息。", + "symlink_warning.more_info": "更多信息", + "symlink_warning.title": "世界文件夹包含符号链接", + "symlink_warning.title.pack": "添加的包中含有符号链接", + "symlink_warning.title.world": "世界文件夹中含有符号链接", + "team.collision.always": "总是碰撞", + "team.collision.never": "禁用碰撞", + "team.collision.pushOtherTeams": "队伍间碰撞", + "team.collision.pushOwnTeam": "队伍内碰撞", + "team.notFound": "未知的队伍“%s”", + "team.visibility.always": "始终显示", + "team.visibility.hideForOtherTeams": "对别队隐藏", + "team.visibility.hideForOwnTeam": "对本队隐藏", + "team.visibility.never": "始终隐藏", + "telemetry.event.advancement_made.description": "了解达成进度前后发生的事情有助于我们更好了解及改进游戏流程。", + "telemetry.event.advancement_made.title": "达成进度", + "telemetry.event.game_load_times.description": "此事件会测量游戏启动各个阶段的时长以帮助我们找到游戏启动的性能优化点。", + "telemetry.event.game_load_times.title": "游戏加载时间", + "telemetry.event.optional": "%s(可选)", + "telemetry.event.optional.disabled": "%s(可选,已禁用)", + "telemetry.event.performance_metrics.description": "了解Minecraft的整体性能概况,可以帮助我们针对各种机型和操作系统调整与优化游戏。 \n游戏版本信息也包含在其中,以帮助我们比较Minecraft新版本的性能概况。", + "telemetry.event.performance_metrics.title": "性能指标", + "telemetry.event.required": "%s(必要)", + "telemetry.event.world_load_times.description": "对我们而言,了解加入世界所需的时长,以及这一时长随着时间推移会如何变化是很重要的。比如,当我们加入新功能或进行较大的技术更改时,我们需要知道这对加载时间有何影响。", + "telemetry.event.world_load_times.title": "世界加载时间", + "telemetry.event.world_loaded.description": "了解玩家玩Minecraft时的细节(例如游戏模式、客户端或服务端是否被修改,以及游戏版本)有助于我们在游戏更新时注重改进玩家最关心的部分。\n世界加载事件将同世界卸载事件一起用于计算游戏会话的持续时长。", + "telemetry.event.world_loaded.title": "世界加载", + "telemetry.event.world_unloaded.description": "此事件与世界加载事件用于计算世界会话的持续时间。\n持续时间(以秒和刻为单位)在世界会话结束时(退回到标题屏幕,或从服务器断开连接)测量。", + "telemetry.event.world_unloaded.title": "世界卸载", + "telemetry.property.advancement_game_time.title": "游戏时间(刻)", + "telemetry.property.advancement_id.title": "进度ID", + "telemetry.property.client_id.title": "客户端ID", + "telemetry.property.client_modded.title": "客户端是否被修改", + "telemetry.property.dedicated_memory_kb.title": "专用内存(kB)", + "telemetry.property.event_timestamp_utc.title": "事件时间戳(UTC)", + "telemetry.property.frame_rate_samples.title": "帧率样本(FPS)", + "telemetry.property.game_mode.title": "游戏模式", + "telemetry.property.game_version.title": "游戏版本", + "telemetry.property.launcher_name.title": "启动器名称", + "telemetry.property.load_time_bootstrap_ms.title": "启动引导时间(毫秒)", + "telemetry.property.load_time_loading_overlay_ms.title": "加载屏幕显示时间(毫秒)", + "telemetry.property.load_time_pre_window_ms.title": "窗口开启前时间(毫秒)", + "telemetry.property.load_time_total_time_ms.title": "总计加载时间(毫秒)", + "telemetry.property.minecraft_session_id.title": "Minecraft会话ID", + "telemetry.property.new_world.title": "是否为新的世界", + "telemetry.property.number_of_samples.title": "样本数", + "telemetry.property.operating_system.title": "操作系统", + "telemetry.property.opt_in.title": "可选择加入", + "telemetry.property.platform.title": "平台", + "telemetry.property.realms_map_content.title": "Realm地图内容(小游戏名称)", + "telemetry.property.render_distance.title": "渲染距离", + "telemetry.property.render_time_samples.title": "渲染用时样本", + "telemetry.property.seconds_since_load.title": "自加载后的时间(秒)", + "telemetry.property.server_modded.title": "服务端是否被修改", + "telemetry.property.server_type.title": "服务器类型", + "telemetry.property.ticks_since_load.title": "自加载后的时间(刻)", + "telemetry.property.used_memory_samples.title": "已使用的内存", + "telemetry.property.user_id.title": "用户ID", + "telemetry.property.world_load_time_ms.title": "世界加载时间(毫秒)", + "telemetry.property.world_session_id.title": "世界会话ID", + "telemetry_info.button.give_feedback": "提供反馈", + "telemetry_info.button.privacy_statement": "隐私声明", + "telemetry_info.button.show_data": "打开我的数据", + "telemetry_info.opt_in.description": "我同意发送可选的遥测数据", + "telemetry_info.property_title": "包含的数据", + "telemetry_info.screen.description": "收集这些数据可以让我们了解与玩家有关的实际情况,以便帮助我们改进Minecraft。\n你也可以发送其他反馈来帮助我们持续改进Minecraft。", + "telemetry_info.screen.title": "遥测数据收集", + "test.error.block_property_mismatch": "属性%s应为%s,实际为%s", + "test.error.block_property_missing": "缺少方块属性,属性%s应为%s", + "test.error.entity_property": "实体%s测试失败:%s", + "test.error.entity_property_details": "实体%s测试失败:%s,应为:%s,实际为:%s", + "test.error.expected_block": "方块应为%s,实际为%s", + "test.error.expected_block_tag": "方块应为#%s中之一,实际为%s", + "test.error.expected_container_contents": "容器应包含:%s", + "test.error.expected_container_contents_single": "容器应含有单个:%s", + "test.error.expected_empty_container": "容器应为空", + "test.error.expected_entity": "缺少%s", + "test.error.expected_entity_around": "%2$s, %3$s, %4$s周围应存在%1$s", + "test.error.expected_entity_count": "应有%s个类型为%s的实体,实际有%s个", + "test.error.expected_entity_data": "实体数据应为:%s,实际为:%s", + "test.error.expected_entity_data_predicate": "%s的实体数据不匹配", + "test.error.expected_entity_effect": "%s应有%s %s效果", + "test.error.expected_entity_having": "实体物品栏中应包含%s", + "test.error.expected_entity_holding": "实体应持有%s", + "test.error.expected_entity_in_test": "%s应存在于测试中", + "test.error.expected_entity_not_touching": "%s不应接触%s, %s, %s(相对位置:%s, %s, %s)", + "test.error.expected_entity_touching": "%s应接触%s, %s, %s(相对位置:%s, %s, %s)", + "test.error.expected_item": "应为%s类型的物品", + "test.error.expected_items_count": "应为%s个%s类型的物品,实际为%s个", + "test.error.fail": "满足失败条件", + "test.error.invalid_block_type": "发现非预期的方块类型:%s", + "test.error.missing_block_entity": "缺少方块实体", + "test.error.position": "%1$s,位于%2$s, %3$s, %4$s(相对位置:%5$s, %6$s, %7$s),%8$s刻", + "test.error.sequence.condition_already_triggered": "条件已在%s刻时触发", + "test.error.sequence.condition_not_triggered": "条件未触发", + "test.error.sequence.invalid_tick": "成功于无效的刻:应为%s", + "test.error.sequence.not_completed": "测试在序列完成前超时", + "test.error.set_biome": "设置测试生物群系失败", + "test.error.spawn_failure": "创建实体%s失败", + "test.error.state_not_equal": "状态错误。应为%s,实际为%s", + "test.error.tick": "%s,时间为%s刻", + "test.error.too_many_entities": "%2$s, %3$s, %4$s周围应仅存在一个%1$s,实际为%5$s个", + "test.error.unexpected_block": "方块不应为%s", + "test.error.unexpected_entity": "%s不应存在", + "test.error.unexpected_item": "物品不应为类型%s", + "test.error.value_not_equal": "属性%s应为%s,实际为%s", + "test.error.wrong_block_entity": "错误的方块实体类型:%s", + "test_block.error.missing": "测试结构缺少%s方块", + "test_block.error.too_many": "%s方块过多", + "test_block.invalid_timeout": "无效的时限(%s),刻数必须为正数", + "test_block.message": "消息:", + "test_block.mode.accept": "接受", + "test_block.mode.fail": "失败", + "test_block.mode.log": "日志输出", + "test_block.mode.start": "启动", + "test_block.mode_info.accept": "接受模式 - 接受(部分)测试的成功", + "test_block.mode_info.fail": "失败模式 - 使测试失败", + "test_block.mode_info.log": "日志输出模式 - 将消息输出到日志", + "test_block.mode_info.start": "启动模式 - 测试的起点", + "test_instance.action.reset": "重置并加载", + "test_instance.action.run": "加载并运行", + "test_instance.action.save": "保存结构", + "test_instance.description.batch": "批次:%s", + "test_instance.description.failed": "失败:%s", + "test_instance.description.function": "函数:%s", + "test_instance.description.invalid_id": "无效的测试ID", + "test_instance.description.no_test": "此测试不存在", + "test_instance.description.structure": "结构:%s", + "test_instance.description.type": "类型:%s", + "test_instance.type.block_based": "基于方块的测试", + "test_instance.type.function": "内置函数测试", + "test_instance_block.entities": "实体:", + "test_instance_block.error.no_test": "存在未定义的测试,无法在%s, %s, %s处运行测试实例", + "test_instance_block.error.no_test_structure": "没有测试结构,无法在%s, %s, %s处运行测试实例", + "test_instance_block.error.unable_to_save": "无法保存%s, %s, %s处测试实例的测试结构模板", + "test_instance_block.invalid": "[无效]", + "test_instance_block.reset_success": "测试重置成功:%s", + "test_instance_block.rotation": "旋转角度:", + "test_instance_block.size": "测试结构大小", + "test_instance_block.starting": "正在启动测试%s", + "test_instance_block.test_id": "测试实例ID", + "title.32bit.deprecation": "检测到32位系统:未来将需要64位系统,使用32位系统可能将无法进行游戏!", + "title.32bit.deprecation.realms": "Minecraft不久后需要64位系统才能运行,届时你将无法使用该设备进行游戏或使用Realms服务。你需要自行取消所有Realms订阅。", + "title.32bit.deprecation.realms.check": "不再显示此屏幕", + "title.32bit.deprecation.realms.header": "检测到32位系统", + "title.credits": "© Mojang AB 请勿二次分发!", + "title.multiplayer.disabled": "多人游戏已被禁用,请检查你的Microsoft账户设置。", + "title.multiplayer.disabled.banned.name": "你必须在进行在线游戏前更改名称", + "title.multiplayer.disabled.banned.permanent": "你的账户已被永久封禁,无法进行多人游戏", + "title.multiplayer.disabled.banned.temporary": "你的账户已被暂时封禁,无法进行多人游戏", + "title.multiplayer.lan": "多人游戏(局域网)", + "title.multiplayer.other": "多人游戏(第三方服务器)", + "title.multiplayer.realms": "多人游戏(Realms)", + "title.singleplayer": "单人游戏", + "translation.test.args": "%s %s", + "translation.test.complex": "前缀,%s%2$s 然后是 %s 和 %1$s 最后是 %s 还有 %1$s!", + "translation.test.escape": "%%s %%%s %%%%s %%%%%s", + "translation.test.invalid": "% 你好", + "translation.test.invalid2": "%s 你好", + "translation.test.none": "你好,世界!", + "translation.test.world": "世界", + "trim_material.minecraft.amethyst": "紫水晶质", + "trim_material.minecraft.copper": "铜质", + "trim_material.minecraft.diamond": "钻石质", + "trim_material.minecraft.emerald": "绿宝石质", + "trim_material.minecraft.gold": "金质", + "trim_material.minecraft.iron": "铁质", + "trim_material.minecraft.lapis": "青金石质", + "trim_material.minecraft.netherite": "下界合金质", + "trim_material.minecraft.quartz": "石英质", + "trim_material.minecraft.redstone": "红石质", + "trim_material.minecraft.resin": "树脂质", + "trim_pattern.minecraft.bolt": "镶铆盔甲纹饰", + "trim_pattern.minecraft.coast": "海岸盔甲纹饰", + "trim_pattern.minecraft.dune": "沙丘盔甲纹饰", + "trim_pattern.minecraft.eye": "眼眸盔甲纹饰", + "trim_pattern.minecraft.flow": "涡流盔甲纹饰", + "trim_pattern.minecraft.host": "雇主盔甲纹饰", + "trim_pattern.minecraft.raiser": "牧民盔甲纹饰", + "trim_pattern.minecraft.rib": "肋骨盔甲纹饰", + "trim_pattern.minecraft.sentry": "哨兵盔甲纹饰", + "trim_pattern.minecraft.shaper": "塑造盔甲纹饰", + "trim_pattern.minecraft.silence": "幽静盔甲纹饰", + "trim_pattern.minecraft.snout": "猪鼻盔甲纹饰", + "trim_pattern.minecraft.spire": "尖塔盔甲纹饰", + "trim_pattern.minecraft.tide": "潮汐盔甲纹饰", + "trim_pattern.minecraft.vex": "恼鬼盔甲纹饰", + "trim_pattern.minecraft.ward": "监守盔甲纹饰", + "trim_pattern.minecraft.wayfinder": "向导盔甲纹饰", + "trim_pattern.minecraft.wild": "荒野盔甲纹饰", + "tutorial.bundleInsert.description": "单击鼠标右键收纳物品", + "tutorial.bundleInsert.title": "使用收纳袋", + "tutorial.craft_planks.description": "配方书能提供帮助", + "tutorial.craft_planks.title": "合成木板", + "tutorial.find_tree.description": "敲击木头来收集它", + "tutorial.find_tree.title": "找到一棵树", + "tutorial.look.description": "使用你的鼠标来转动", + "tutorial.look.title": "环顾四周", + "tutorial.move.description": "用%s来跳跃", + "tutorial.move.title": "用%s、%s、%s和%s来移动", + "tutorial.open_inventory.description": "按下%s", + "tutorial.open_inventory.title": "打开你的物品栏", + "tutorial.punch_tree.description": "按住%s", + "tutorial.punch_tree.title": "摧毁树木", + "tutorial.socialInteractions.description": "按%s打开", + "tutorial.socialInteractions.title": "社交", + "upgrade.minecraft.netherite_upgrade": "下界合金升级" +} diff --git a/patches/api/0001-Leaves-Server-Config.patch b/patches/api/0001-Leaves-Server-Config.patch deleted file mode 100644 index c69a7992..00000000 --- a/patches/api/0001-Leaves-Server-Config.patch +++ /dev/null @@ -1,25 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc -Date: Fri, 29 Oct 2021 16:59:07 +0800 -Subject: [PATCH] Leaves Server Config - - -diff --git a/src/main/java/org/bukkit/Server.java b/src/main/java/org/bukkit/Server.java -index 0b78564256ebc647ebac402e549d86ab6e307c8d..cbe69a0267981173fe8f7d715764d982bd9dcc8d 100644 ---- a/src/main/java/org/bukkit/Server.java -+++ b/src/main/java/org/bukkit/Server.java -@@ -2282,6 +2282,14 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi - throw new UnsupportedOperationException("Not supported yet."); - } - // Paper end -+ -+ // Leaves start - Not supported yet too -+ @NotNull -+ public org.bukkit.configuration.file.YamlConfiguration getLeavesConfig() -+ { -+ throw new UnsupportedOperationException("Not supported yet."); -+ } -+ // Leaves end - Not supported yet too - - /** - * Sends the component to the player diff --git a/patches/api/0005-Hide-irrelevant-compilation-warnings.patch b/patches/api/0005-Hide-irrelevant-compilation-warnings.patch deleted file mode 100644 index 038c8e02..00000000 --- a/patches/api/0005-Hide-irrelevant-compilation-warnings.patch +++ /dev/null @@ -1,35 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 19 Jul 2023 20:08:16 +0800 -Subject: [PATCH] Hide irrelevant compilation warnings - - -diff --git a/build.gradle.kts b/build.gradle.kts -index e29e5024fa693baae469d47fe77b57118f14627c..7a60cd45449950cfb116b1cb29bc3da4ba074e7d 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -148,6 +148,15 @@ val generateApiVersioningFile by tasks.registering { - } - } - -+// Leaves start - hide irrelevant compilation warnings -+tasks.withType { -+ val compilerArgs = options.compilerArgs -+ compilerArgs.add("-Xlint:-module") -+ compilerArgs.add("-Xlint:-removal") -+ compilerArgs.add("-Xlint:-dep-ann") -+} -+// Leaves end - hide irrelevant compilation warnings -+ - tasks.jar { - from(generateApiVersioningFile.map { it.outputs.files.singleFile }) { - into("META-INF/maven/${project.group}/${project.name}") -@@ -207,6 +216,8 @@ tasks.withType { - into("build/docs/javadoc") - } - } -+ -+ options.addStringOption("Xdoclint:none", "-quiet") // Leaves - hide irrelevant compilation warnings - } - - tasks.test { diff --git a/patches/api/0006-SIMD-support.patch b/patches/api/0006-SIMD-support.patch deleted file mode 100644 index ffb5cf41..00000000 --- a/patches/api/0006-SIMD-support.patch +++ /dev/null @@ -1,26 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 19 Jul 2023 20:09:38 +0800 -Subject: [PATCH] SIMD support - - -diff --git a/build.gradle.kts b/build.gradle.kts -index 7a60cd45449950cfb116b1cb29bc3da4ba074e7d..caaffd1d0a323a2b292fb9c8ceeac271ba9dd635 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -154,6 +154,7 @@ tasks.withType { - compilerArgs.add("-Xlint:-module") - compilerArgs.add("-Xlint:-removal") - compilerArgs.add("-Xlint:-dep-ann") -+ compilerArgs.add("--add-modules=jdk.incubator.vector") // Leaves - SIMD support - } - // Leaves end - hide irrelevant compilation warnings - -@@ -218,6 +219,7 @@ tasks.withType { - } - - options.addStringOption("Xdoclint:none", "-quiet") // Leaves - hide irrelevant compilation warnings -+ options.addStringOption("-add-modules", "jdk.incubator.vector") // Leaves - SIMD support - } - - tasks.test { diff --git a/patches/server/0026-Remove-iterators-from-inventory-contains.patch b/patches/removed/server/0026-Remove-iterators-from-inventory-contains.patch similarity index 100% rename from patches/server/0026-Remove-iterators-from-inventory-contains.patch rename to patches/removed/server/0026-Remove-iterators-from-inventory-contains.patch diff --git a/patches/server/0002-Dev-Fix.patch b/patches/server/0002-Dev-Fix.patch deleted file mode 100644 index 38ab6228..00000000 --- a/patches/server/0002-Dev-Fix.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 20 May 2024 17:15:18 +0800 -Subject: [PATCH] Dev Fix - - -diff --git a/src/main/java/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java b/src/main/java/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java -index bc885d2cc95572ec68f1df62555fdba7998b1d93..0211311b3b63bcdea7ebf7bcb24629674c771402 100644 ---- a/src/main/java/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java -+++ b/src/main/java/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java -@@ -34,7 +34,7 @@ public interface CustomPacketPayload { - - private void writeCap(B value, CustomPacketPayload.Type id, CustomPacketPayload payload) { - value.writeResourceLocation(id.id()); -- StreamCodec streamCodec = this.findCodec(id.id); -+ StreamCodec streamCodec = (StreamCodec) this.findCodec(id.id); // Leaves - dev fix - streamCodec.encode(value, (T)payload); - } - diff --git a/patches/server/0004-Leaves-Server-Utils.patch b/patches/server/0004-Leaves-Server-Utils.patch deleted file mode 100644 index e9bf2e11..00000000 --- a/patches/server/0004-Leaves-Server-Utils.patch +++ /dev/null @@ -1,426 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 13 Sep 2022 16:59:31 +0800 -Subject: [PATCH] Leaves Server Utils - - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index a15546e433ebba6c0de01bdaaef201a3d99a87b5..922996adcf2b85e55a934191e90a12c42f169b0f 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -389,6 +389,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - public boolean freezeLocked = false; // Paper - Freeze Tick Lock API - public boolean fixedPose = false; // Paper - Expand Pose API - private final int despawnTime; // Paper - entity despawn time limit -+ private CompoundTag leavesData = new CompoundTag(); // Leaves - Leaves ex data - - public void setOrigin(@javax.annotation.Nonnull Location location) { - this.origin = location.toVector(); -@@ -2690,6 +2691,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - nbttagcompound.putBoolean("Paper.FreezeLock", true); - } - // Paper end -+ nbttagcompound.put("Leaves.Data", leavesData); // Leaves - leaves ex data - return nbttagcompound; - } catch (Throwable throwable) { - CrashReport crashreport = CrashReport.forThrowable(throwable, "Saving entity NBT"); -@@ -2838,6 +2840,11 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - freezeLocked = nbt.getBoolean("Paper.FreezeLock"); - } - // Paper end -+ // Leaves start - leaves ex data -+ if (nbt.contains("Leaves.Data")) { -+ leavesData = nbt.getCompound("Leaves.Data"); -+ } -+ // Leaves end - leaves ex data - - } catch (Throwable throwable) { - CrashReport crashreport = CrashReport.forThrowable(throwable, "Loading entity NBT"); -@@ -5325,4 +5332,10 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return ((net.minecraft.server.level.ServerLevel) this.level).isPositionEntityTicking(this.blockPosition()); - } - // Paper end - Expose entity id counter -+ -+ // Leaves start - leaves ex data -+ public CompoundTag getLeavesData() { -+ return leavesData; -+ } -+ // Leaves end - leaves ex data - } -diff --git a/src/main/java/org/leavesmc/leaves/LeavesLogger.java b/src/main/java/org/leavesmc/leaves/LeavesLogger.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bc45935c96553c9bd9d9b6ab3a64e28f52862198 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/LeavesLogger.java -@@ -0,0 +1,24 @@ -+package org.leavesmc.leaves; -+ -+import org.bukkit.Bukkit; -+ -+import java.util.logging.Level; -+import java.util.logging.Logger; -+ -+public class LeavesLogger extends Logger { -+ public static final LeavesLogger LOGGER = new LeavesLogger(); -+ -+ private LeavesLogger() { -+ super("Leaves", null); -+ setParent(Bukkit.getLogger()); -+ setLevel(Level.ALL); -+ } -+ -+ public void severe(String msg, Exception exception) { -+ this.log(Level.SEVERE, msg, exception); -+ } -+ -+ public void warning(String msg, Exception exception) { -+ this.log(Level.WARNING, msg, exception); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f50c3871e3ab435abc6de5bfb67b85b09d235733 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java -@@ -0,0 +1,82 @@ -+package org.leavesmc.leaves.command; -+ -+import com.google.common.base.Functions; -+import com.google.common.collect.Iterables; -+import com.google.common.collect.Lists; -+import io.papermc.paper.command.PaperCommand; -+import net.minecraft.resources.ResourceLocation; -+import org.bukkit.command.CommandSender; -+import org.checkerframework.checker.nullness.qual.NonNull; -+import org.checkerframework.framework.qual.DefaultQualifier; -+ -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Collection; -+import java.util.Iterator; -+import java.util.List; -+ -+@DefaultQualifier(NonNull.class) -+public class LeavesCommandUtil { -+ -+ private LeavesCommandUtil() { -+ } -+ -+ // Code from Mojang - copyright them -+ public static List getListMatchingLast( -+ final CommandSender sender, -+ final String[] args, -+ final String... matches -+ ) { -+ return getListMatchingLast(sender, args, Arrays.asList(matches)); -+ } -+ -+ public static boolean matches(final String s, final String s1) { -+ return s1.regionMatches(true, 0, s, 0, s.length()); -+ } -+ -+ public static List getListMatchingLast( -+ final CommandSender sender, -+ final String[] strings, -+ final Collection collection -+ ) { -+ return getListMatchingLast(sender, strings, collection, LeavesCommand.BASE_PERM, "bukkit.command.leaves"); -+ } -+ -+ public static List getListMatchingLast( -+ final CommandSender sender, -+ final String[] strings, -+ final Collection collection, -+ final String basePermission, -+ final String overridePermission -+ ) { -+ String last = strings[strings.length - 1]; -+ ArrayList results = Lists.newArrayList(); -+ -+ if (!collection.isEmpty()) { -+ Iterator iterator = Iterables.transform(collection, Functions.toStringFunction()).iterator(); -+ -+ while (iterator.hasNext()) { -+ String s1 = (String) iterator.next(); -+ -+ if (matches(last, s1) && (sender.hasPermission(basePermission + s1) || sender.hasPermission(overridePermission))) { -+ results.add(s1); -+ } -+ } -+ -+ if (results.isEmpty()) { -+ iterator = collection.iterator(); -+ -+ while (iterator.hasNext()) { -+ Object object = iterator.next(); -+ -+ if (object instanceof ResourceLocation && matches(last, ((ResourceLocation) object).getPath())) { -+ results.add(String.valueOf(object)); -+ } -+ } -+ } -+ } -+ -+ return results; -+ } -+ // end copy stuff -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/util/AsyncExecutor.java b/src/main/java/org/leavesmc/leaves/util/AsyncExecutor.java -new file mode 100644 -index 0000000000000000000000000000000000000000..067e351f6e96f2ac1a46d78dc952b2e845c1efef ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/AsyncExecutor.java -@@ -0,0 +1,76 @@ -+package org.leavesmc.leaves.util; -+ -+import com.google.common.collect.Queues; -+import org.leavesmc.leaves.LeavesLogger; -+ -+import java.util.Queue; -+import java.util.concurrent.locks.Condition; -+import java.util.concurrent.locks.Lock; -+import java.util.concurrent.locks.ReentrantLock; -+import java.util.logging.Level; -+ -+// Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) -+public class AsyncExecutor implements Runnable { -+ -+ private final Queue jobs = Queues.newArrayDeque(); -+ private final Lock mutex = new ReentrantLock(); -+ private final Condition cond = mutex.newCondition(); -+ private final Thread thread; -+ private volatile boolean killswitch = false; -+ -+ public AsyncExecutor(String threadName) { -+ this.thread = new Thread(this, threadName); -+ } -+ -+ public void start() { -+ thread.start(); -+ } -+ -+ public void kill() { -+ killswitch = true; -+ cond.signalAll(); -+ } -+ -+ public void submit(Runnable runnable) { -+ mutex.lock(); -+ try { -+ jobs.offer(runnable); -+ cond.signalAll(); -+ } finally { -+ mutex.unlock(); -+ } -+ } -+ -+ @Override -+ public void run() { -+ while (!killswitch) { -+ try { -+ Runnable runnable = takeRunnable(); -+ if (runnable != null) { -+ runnable.run(); -+ } -+ } catch (InterruptedException e) { -+ Thread.currentThread().interrupt(); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.log(Level.SEVERE, e, () -> "Failed to execute async job for thread " + thread.getName()); -+ } -+ } -+ } -+ -+ private Runnable takeRunnable() throws InterruptedException { -+ mutex.lock(); -+ try { -+ while (jobs.isEmpty() && !killswitch) { -+ cond.await(); -+ } -+ -+ if (jobs.isEmpty()) { -+ return null; -+ } -+ -+ return jobs.remove(); -+ } finally { -+ mutex.unlock(); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/util/IterableWrapper.java b/src/main/java/org/leavesmc/leaves/util/IterableWrapper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a07feaf53905df87e45e8414b1dca02d4e8411d5 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/IterableWrapper.java -@@ -0,0 +1,21 @@ -+package org.leavesmc.leaves.util; -+ -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.Iterator; -+ -+// Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) -+public class IterableWrapper implements Iterable { -+ -+ private final Iterator iterator; -+ -+ public IterableWrapper(Iterator iterator) { -+ this.iterator = iterator; -+ } -+ -+ @NotNull -+ @Override -+ public Iterator iterator() { -+ return iterator; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/util/Long2ObjectOpenHashMapWrapper.java b/src/main/java/org/leavesmc/leaves/util/Long2ObjectOpenHashMapWrapper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..440c4d903e145229bc54eb5b6f3578fdd8bcfb84 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/Long2ObjectOpenHashMapWrapper.java -@@ -0,0 +1,41 @@ -+package org.leavesmc.leaves.util; -+ -+import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -+import org.jetbrains.annotations.Nullable; -+ -+import java.util.Map; -+ -+public class Long2ObjectOpenHashMapWrapper extends Long2ObjectOpenHashMap { -+ -+ private final Map backingMap; -+ -+ public Long2ObjectOpenHashMapWrapper(Map map) { -+ backingMap = map; -+ } -+ -+ @Override -+ public V put(Long key, V value) { -+ return backingMap.put(key, value); -+ } -+ -+ @Override -+ public V get(Object key) { -+ return backingMap.get(key); -+ } -+ -+ @Override -+ public V remove(Object key) { -+ return backingMap.remove(key); -+ } -+ -+ @Nullable -+ @Override -+ public V putIfAbsent(Long key, V value) { -+ return backingMap.putIfAbsent(key, value); -+ } -+ -+ @Override -+ public int size() { -+ return backingMap.size(); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/util/MathUtils.java b/src/main/java/org/leavesmc/leaves/util/MathUtils.java -new file mode 100644 -index 0000000000000000000000000000000000000000..56050158a7e2217f4fdad11067cc377dda9ca770 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/MathUtils.java -@@ -0,0 +1,96 @@ -+package org.leavesmc.leaves.util; -+ -+import org.bukkit.util.NumberConversions; -+import org.bukkit.util.Vector; -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.regex.Pattern; -+ -+public class MathUtils { -+ -+ private static final Pattern numericPattern = Pattern.compile("^-?[1-9]\\d*$|^0$"); -+ -+ public static boolean isNumeric(String str) { -+ return numericPattern.matcher(str).matches(); -+ } -+ -+ public static float @NotNull [] fetchYawPitch(@NotNull Vector dir) { -+ double x = dir.getX(); -+ double z = dir.getZ(); -+ -+ float[] out = new float[2]; -+ -+ if (x == 0.0D && z == 0.0D) { -+ out[1] = (float) (dir.getY() > 0.0D ? -90 : 90); -+ } else { -+ double theta = Math.atan2(-x, z); -+ out[0] = (float) Math.toDegrees((theta + 6.283185307179586D) % 6.283185307179586D); -+ -+ double x2 = NumberConversions.square(x); -+ double z2 = NumberConversions.square(z); -+ double xz = Math.sqrt(x2 + z2); -+ out[1] = (float) Math.toDegrees(Math.atan(-dir.getY() / xz)); -+ } -+ -+ return out; -+ } -+ -+ public static float fetchPitch(@NotNull Vector dir) { -+ double x = dir.getX(); -+ double z = dir.getZ(); -+ -+ float result; -+ -+ if (x == 0.0D && z == 0.0D) { -+ result = (float) (dir.getY() > 0.0D ? -90 : 90); -+ } else { -+ double x2 = NumberConversions.square(x); -+ double z2 = NumberConversions.square(z); -+ double xz = Math.sqrt(x2 + z2); -+ result = (float) Math.toDegrees(Math.atan(-dir.getY() / xz)); -+ } -+ -+ return result; -+ } -+ -+ @NotNull -+ public static Vector getDirection(double rotX, double rotY) { -+ Vector vector = new Vector(); -+ -+ rotX = Math.toRadians(rotX); -+ rotY = Math.toRadians(rotY); -+ -+ double xz = Math.abs(Math.cos(rotY)); -+ -+ vector.setX(-Math.sin(rotX) * xz); -+ vector.setZ(Math.cos(rotX) * xz); -+ vector.setY(-Math.sin(rotY)); -+ -+ return vector; -+ } -+ -+ private static final int[] MULTIPLY_DE_BRUIJN_BIT_POSITION = new int[]{0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8, 31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9}; -+ -+ public static int floorLog2(int value) { -+ return ceilLog2(value) - (isPowerOfTwo(value) ? 0 : 1); -+ } -+ -+ public static int ceilLog2(int value) { -+ value = isPowerOfTwo(value) ? value : smallestEncompassingPowerOfTwo(value); -+ return MULTIPLY_DE_BRUIJN_BIT_POSITION[(int) ((long) value * 125613361L >> 27) & 31]; -+ } -+ -+ public static boolean isPowerOfTwo(int value) { -+ return value != 0 && (value & value - 1) == 0; -+ } -+ -+ public static int smallestEncompassingPowerOfTwo(int value) { -+ int i = value - 1; -+ i |= i >> 1; -+ i |= i >> 2; -+ i |= i >> 4; -+ i |= i >> 8; -+ i |= i >> 16; -+ return i + 1; -+ } -+} diff --git a/patches/server/0005-Update-version-fetcher-repo.patch b/patches/server/0005-Update-version-fetcher-repo.patch deleted file mode 100644 index 84e36586..00000000 --- a/patches/server/0005-Update-version-fetcher-repo.patch +++ /dev/null @@ -1,175 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 26 Oct 2021 14:13:50 +0800 -Subject: [PATCH] Update version fetcher repo - - -diff --git a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java -index 532306cacd52579cdf37e4aca25887b1ed3ba6a1..917ffaae401f3374d07d7fb7c024234a43ee54e4 100644 ---- a/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java -+++ b/src/main/java/com/destroystokyo/paper/PaperVersionFetcher.java -@@ -107,7 +107,7 @@ public class PaperVersionFetcher implements VersionFetcher { - } - - // Contributed by Techcable in GH-65 -- private static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) { -+ protected static int fetchDistanceFromGitHub(final String repo, final String branch, final String hash) { // Leaves - private -> protected - try { - final HttpURLConnection connection = (HttpURLConnection) URI.create("https://api.github.com/repos/%s/compare/%s...%s".formatted(repo, branch, hash)).toURL().openConnection(); - connection.connect(); -@@ -130,7 +130,7 @@ public class PaperVersionFetcher implements VersionFetcher { - } - } - -- private @Nullable Component getHistory() { -+ protected @Nullable Component getHistory() { // Leaves - private -> protected - final VersionHistoryManager.@Nullable VersionData data = VersionHistoryManager.INSTANCE.getVersionData(); - if (data == null) { - return null; -diff --git a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -index 83020837e29ee627b1081daddb4bdee147b95af3..b0f9010e28d865f059675954cd1db575c61d675e 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/CraftMagicNumbers.java -@@ -505,7 +505,7 @@ public final class CraftMagicNumbers implements UnsafeValues { - // Paper start - @Override - public com.destroystokyo.paper.util.VersionFetcher getVersionFetcher() { -- return new com.destroystokyo.paper.PaperVersionFetcher(); -+ return new org.leavesmc.leaves.util.LeavesVersionFetcher(); // Leaves - Leaves version fetcher - } - - @Override -diff --git a/src/main/java/org/leavesmc/leaves/util/LeavesVersionFetcher.java b/src/main/java/org/leavesmc/leaves/util/LeavesVersionFetcher.java -new file mode 100644 -index 0000000000000000000000000000000000000000..83f9bf9464ded48858b816107b4f14a065d80399 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/LeavesVersionFetcher.java -@@ -0,0 +1,128 @@ -+package org.leavesmc.leaves.util; -+ -+import com.destroystokyo.paper.PaperVersionFetcher; -+import com.google.common.base.Charsets; -+import com.google.common.io.Resources; -+import com.google.gson.Gson; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonSyntaxException; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.ServerBuildInfo; -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.event.ClickEvent; -+import net.kyori.adventure.text.format.NamedTextColor; -+import org.checkerframework.checker.nullness.qual.Nullable; -+import org.jetbrains.annotations.NotNull; -+import org.slf4j.Logger; -+ -+import java.io.BufferedReader; -+import java.io.IOException; -+import java.net.URI; -+import java.util.Optional; -+import java.util.OptionalInt; -+import java.util.stream.StreamSupport; -+ -+import static net.kyori.adventure.text.Component.text; -+import static net.kyori.adventure.text.format.TextColor.color; -+ -+public class LeavesVersionFetcher extends PaperVersionFetcher { -+ -+ private static final Logger LOGGER = LogUtils.getClassLogger(); -+ -+ private static final int DISTANCE_ERROR = -1; -+ private static final int DISTANCE_UNKNOWN = -2; -+ private static final String DOWNLOAD_PAGE = "https://leavesmc.org/downloads/leaves"; -+ -+ @NotNull -+ @Override -+ public Component getVersionMessage(@NotNull String serverVersion) { -+ final Component updateMessage; -+ final ServerBuildInfo build = ServerBuildInfo.buildInfo(); -+ if (build.buildNumber().isEmpty() && build.gitCommit().isEmpty()) { -+ updateMessage = text("You are running a development version without access to version information", color(0xFF5300)); -+ } else if (build.buildNumber().isEmpty()) { -+ updateMessage = text("You are running a development version form CI", color(0xFF5300)); -+ } else { -+ updateMessage = getUpdateStatusMessage("LeavesMC/Leaves", build); -+ } -+ final @Nullable Component history = this.getHistory(); -+ -+ return history != null ? Component.textOfChildren(updateMessage, Component.newline(), history) : updateMessage; -+ } -+ -+ private static Component getUpdateStatusMessage(@NotNull final String repo, @NotNull final ServerBuildInfo build) { -+ int distance = fetchDistanceFromLeavesApiV2Build(build); -+ -+ if (distance == DISTANCE_ERROR) { -+ distance = fetchDistanceFromLeavesApiV2Hash(build); -+ } -+ -+ if (distance == DISTANCE_ERROR) { -+ final Optional gitBranch = build.gitBranch(); -+ final Optional gitCommit = build.gitCommit(); -+ if (gitBranch.isPresent() && gitCommit.isPresent()) { -+ distance = fetchDistanceFromGitHub(repo, gitBranch.get(), gitCommit.get()); -+ } -+ } -+ -+ return switch (distance) { -+ case DISTANCE_ERROR -> Component.text("Error obtaining version information", NamedTextColor.YELLOW); -+ case 0 -> Component.text("You are running the latest version", NamedTextColor.GREEN); -+ case DISTANCE_UNKNOWN -> Component.text("Unknown version", NamedTextColor.YELLOW); -+ default -> Component.text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW) -+ .append(Component.newline()) -+ .append(Component.text("Download the new version at: ") -+ .append(Component.text(DOWNLOAD_PAGE, NamedTextColor.GOLD) -+ .hoverEvent(Component.text("Click to open", NamedTextColor.WHITE)) -+ .clickEvent(ClickEvent.openUrl(DOWNLOAD_PAGE)))); -+ }; -+ } -+ -+ private static int fetchDistanceFromLeavesApiV2Build(final ServerBuildInfo build) { -+ OptionalInt buildNumber = build.buildNumber(); -+ if (buildNumber.isEmpty()) { -+ return DISTANCE_ERROR; -+ } -+ -+ try { -+ try (final BufferedReader reader = Resources.asCharSource( -+ URI.create("https://api.leavesmc.org/v2/projects/leaves/versions/" + build.minecraftVersionId()).toURL(), -+ Charsets.UTF_8 -+ ).openBufferedStream()) { -+ final JsonObject json = new Gson().fromJson(reader, JsonObject.class); -+ final JsonArray builds = json.getAsJsonArray("builds"); -+ final int latest = StreamSupport.stream(builds.spliterator(), false) -+ .mapToInt(JsonElement::getAsInt) -+ .max() -+ .orElseThrow(); -+ return latest - buildNumber.getAsInt(); -+ } catch (final JsonSyntaxException ex) { -+ LOGGER.error("Error parsing json from Leaves's downloads API", ex); -+ return DISTANCE_ERROR; -+ } -+ } catch (final IOException e) { -+ LOGGER.error("Error while parsing version", e); -+ return DISTANCE_ERROR; -+ } -+ } -+ -+ private static int fetchDistanceFromLeavesApiV2Hash(final ServerBuildInfo build) { -+ if (build.gitCommit().isEmpty()) { -+ return DISTANCE_ERROR; -+ } -+ -+ try { -+ try (BufferedReader reader = Resources.asCharSource( -+ URI.create("https://api.leavesmc.org/v2/projects/leaves/versions/" + build.minecraftVersionId() + "/differ/" + build.gitCommit().get()).toURL(), -+ Charsets.UTF_8 -+ ).openBufferedStream()) { -+ return Integer.parseInt(reader.readLine()); -+ } -+ } catch (IOException e) { -+ LOGGER.error("Error while parsing version", e); -+ return DISTANCE_ERROR; -+ } -+ } -+} diff --git a/patches/server/0006-Leaves-Server-Config-And-Command.patch b/patches/server/0006-Leaves-Server-Config-And-Command.patch deleted file mode 100644 index b0a292ee..00000000 --- a/patches/server/0006-Leaves-Server-Config-And-Command.patch +++ /dev/null @@ -1,2051 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Fri, 29 Oct 2021 16:52:57 +0800 -Subject: [PATCH] Leaves Server Config And Command - - -diff --git a/build.gradle.kts b/build.gradle.kts -index d88a9d1908373ba44143013cda1ae51477a835cf..b8132401ddaedf13bc9ddc74524166d0e0dcf419 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -240,6 +240,14 @@ tasks.registerRunTask("runDevServer") { - jvmArgs("-DPaper.pushPaperAssetsRoot=true") - } - -+// Leaves start - create config file -+tasks.registerRunTask("createLeavesConfig") { -+ description = "Create a new leaves.yml" -+ mainClass = "org.leavesmc.leaves.config.GlobalConfigCreator" -+ classpath(sourceSets.main.map { it.runtimeClasspath }) -+} -+// Leaves end - create config file -+ - tasks.registerRunTask("runBundler") { - description = "Spin up a test server from the Mojang mapped bundler jar" - classpath(rootProject.tasks.named("createMojmapBundlerJar").flatMap { it.outputZip }) -diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -index faeb700647522379046f3cb3abcf478ff5aae95d..a8c5fa172b01b85df51fa3b4d20b6c4f734dfdda 100644 ---- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -@@ -236,6 +236,9 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark - com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now - -+ org.leavesmc.leaves.LeavesConfig.init((java.io.File) options.valueOf("leaves-settings")); // Leaves - Server Config -+ System.setProperty("spark.serverconfigs.extra", "leaves.yml"); // Leaves - spark config -+ - com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics // Leaves - down - - this.setPvpAllowed(dedicatedserverproperties.pvp); -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index ac8af406180bc680d46e8edc3da0fc2e5211345a..3d18ffbf3604705d8b99f69df156392dfed1863b 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -1114,6 +1114,7 @@ public final class CraftServer implements Server { - playerMetadata.removeAll(plugin); - } - // Paper end -+ org.leavesmc.leaves.LeavesConfig.init((File) console.options.valueOf("leaves-settings")); // Leaves - Server Config - this.reloadData(); - org.spigotmc.SpigotConfig.registerCommands(); // Spigot - io.papermc.paper.command.PaperCommands.registerCommands(this.console); // Paper -@@ -3030,6 +3031,14 @@ public final class CraftServer implements Server { - { - return CraftServer.this.console.paperConfigurations.createLegacyObject(CraftServer.this.console); - } -+ -+ // Leaves start - add config to timings report -+ @Override -+ public YamlConfiguration getLeavesConfig() -+ { -+ return org.leavesmc.leaves.LeavesConfig.config; -+ } -+ /// Leaves end - add config to timings report - - @Override - public void restart() { -diff --git a/src/main/java/org/bukkit/craftbukkit/Main.java b/src/main/java/org/bukkit/craftbukkit/Main.java -index a41e6f5dc2d5516f081d7340e2dacffaf5663485..8f1230fcfa4fc27b513a4eb1023f107c8c7dd818 100644 ---- a/src/main/java/org/bukkit/craftbukkit/Main.java -+++ b/src/main/java/org/bukkit/craftbukkit/Main.java -@@ -168,6 +168,14 @@ public class Main { - .ofType(File.class) - .defaultsTo(new File("paper.yml")) - .describedAs("Yml file"); -+ -+ // Leaves start - Server Config -+ acceptsAll(asList("leaves", "leaves-settings"), "File for leaves settings") -+ .withRequiredArg() -+ .ofType(File.class) -+ .defaultsTo(new File("leaves.yml")) -+ .describedAs("Yml file"); -+ // Leaves end - Server Config - - acceptsAll(asList("add-plugin", "add-extra-plugin-jar"), "Specify paths to extra plugin jars to be loaded in addition to those in the plugins folder. This argument can be specified multiple times, once for each extra plugin jar path.") - .withRequiredArg() -diff --git a/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/src/main/java/org/leavesmc/leaves/LeavesConfig.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c306d4827e645d47692746c12100f793c5b83973 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/LeavesConfig.java -@@ -0,0 +1,962 @@ -+package org.leavesmc.leaves; -+ -+import com.destroystokyo.paper.util.SneakyThrow; -+import io.papermc.paper.configuration.GlobalConfiguration; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import org.bukkit.command.Command; -+import org.bukkit.configuration.file.YamlConfiguration; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.command.LeavesCommand; -+import org.leavesmc.leaves.config.GlobalConfig; -+import org.leavesmc.leaves.config.GlobalConfigCategory; -+import org.leavesmc.leaves.config.RemovedConfig; -+import org.leavesmc.leaves.config.GlobalConfigManager; -+import org.leavesmc.leaves.region.RegionFileFormat; -+import org.leavesmc.leaves.util.MathUtils; -+ -+import org.leavesmc.leaves.config.ConfigValidatorImpl.BooleanConfigValidator; -+import org.leavesmc.leaves.config.ConfigValidatorImpl.IntConfigValidator; -+import org.leavesmc.leaves.config.ConfigValidatorImpl.StringConfigValidator; -+import org.leavesmc.leaves.config.ConfigValidatorImpl.DoubleConfigValidator; -+import org.leavesmc.leaves.config.ConfigValidatorImpl.ListConfigValidator; -+import org.leavesmc.leaves.config.ConfigValidatorImpl.EnumConfigValidator; -+ -+import java.io.File; -+import java.io.IOException; -+import java.util.Collections; -+import java.util.List; -+import java.util.Locale; -+import java.util.Random; -+ -+import org.leavesmc.leaves.protocol.CarpetServerProtocol.CarpetRule; -+import org.leavesmc.leaves.protocol.CarpetServerProtocol.CarpetRules; -+ -+import org.leavesmc.leaves.protocol.bladeren.BladerenProtocol.LeavesFeatureSet; -+import org.leavesmc.leaves.protocol.bladeren.BladerenProtocol.LeavesFeature; -+ -+public final class LeavesConfig { -+ -+ public static final String CONFIG_HEADER = "Configuration file for Leaves."; -+ public static final int CURRENT_CONFIG_VERSION = 6; -+ -+ private static File configFile; -+ public static YamlConfiguration config; -+ -+ public static void init(final @NotNull File file) { -+ LeavesConfig.configFile = file; -+ config = new YamlConfiguration(); -+ config.options().setHeader(Collections.singletonList(CONFIG_HEADER)); -+ config.options().copyDefaults(true); -+ -+ if (!file.exists()) { -+ try { -+ boolean is = file.createNewFile(); -+ if (!is) { -+ throw new IOException("Can't create file"); -+ } -+ } catch (final Exception ex) { -+ LeavesLogger.LOGGER.severe("Failure to create leaves config", ex); -+ } -+ } else { -+ try { -+ config.load(file); -+ } catch (final Exception ex) { -+ LeavesLogger.LOGGER.severe("Failure to load leaves config", ex); -+ SneakyThrow.sneaky(ex); -+ throw new RuntimeException(ex); -+ } -+ } -+ -+ LeavesConfig.config.set("config-version", CURRENT_CONFIG_VERSION); -+ -+ GlobalConfigManager.init(); -+ -+ registerCommand("leaves", new LeavesCommand("leaves")); -+ } -+ -+ public static void save() { -+ try { -+ config.save(LeavesConfig.configFile); -+ } catch (final Exception ex) { -+ LeavesLogger.LOGGER.severe("Unable to save leaves config", ex); -+ } -+ } -+ -+ public static void registerCommand(String name, Command command) { -+ MinecraftServer.getServer().server.getCommandMap().register(name, "leaves", command); -+ MinecraftServer.getServer().server.syncCommands(); -+ } -+ -+ public static void unregisterCommand(String name) { -+ name = name.toLowerCase(Locale.ENGLISH).trim(); -+ MinecraftServer.getServer().server.getCommandMap().getKnownCommands().remove(name); -+ MinecraftServer.getServer().server.getCommandMap().getKnownCommands().remove("leaves:" + name); -+ MinecraftServer.getServer().server.syncCommands(); -+ } -+ -+ public static ModifyConfig modify = new ModifyConfig(); -+ -+ @GlobalConfigCategory("modify") -+ public static class ModifyConfig { -+ -+ public FakeplayerConfig fakeplayer = new FakeplayerConfig(); -+ -+ @GlobalConfigCategory("fakeplayer") -+ public static class FakeplayerConfig { -+ -+ @RemovedConfig(name = "enable", category = "fakeplayer", transform = true) -+ @GlobalConfig(value = "enable", validator = FakeplayerValidator.class) -+ public boolean enable = true; -+ -+ private static class FakeplayerValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ if (value) { -+ registerCommand("bot", new org.leavesmc.leaves.bot.BotCommand("bot")); -+ org.leavesmc.leaves.bot.agent.Actions.registerAll(); -+ } else { -+ unregisterCommand("bot"); -+ } -+ } -+ } -+ -+ @RemovedConfig(name = "unable-fakeplayer-names", category = "fakeplayer", convert = ListConfigValidator.STRING.class, transform = true) -+ @GlobalConfig(value = "unable-fakeplayer-names", validator = ListConfigValidator.STRING.class) -+ public List unableNames = List.of("player-name"); -+ -+ @GlobalConfig(value = "limit", validator = IntConfigValidator.class) -+ public int limit = 10; -+ -+ @GlobalConfig(value = "prefix", validator = StringConfigValidator.class) -+ public String prefix = ""; -+ -+ @GlobalConfig(value = "suffix", validator = StringConfigValidator.class) -+ public String suffix = ""; -+ -+ @GlobalConfig(value = "regen-amount", validator = RegenAmountValidator.class) -+ public double regenAmount = 0.0; -+ -+ private static class RegenAmountValidator extends DoubleConfigValidator { -+ @Override -+ public void verify(Double old, Double value) throws IllegalArgumentException { -+ if (value < 0.0) { -+ throw new IllegalArgumentException("regen-amount need >= 0.0"); -+ } -+ } -+ } -+ -+ @GlobalConfig("always-send-data") -+ public boolean canSendDataAlways = true; -+ -+ @GlobalConfig("resident-fakeplayer") -+ public boolean canResident = false; -+ -+ @GlobalConfig("open-fakeplayer-inventory") -+ public boolean canOpenInventory = false; -+ -+ @GlobalConfig("skip-sleep-check") -+ public boolean canSkipSleep = false; -+ -+ @GlobalConfig("spawn-phantom") -+ public boolean canSpawnPhantom = false; -+ -+ @GlobalConfig("use-action") -+ public boolean canUseAction = true; -+ -+ @GlobalConfig("modify-config") -+ public boolean canModifyConfig = false; -+ -+ @GlobalConfig("manual-save-and-load") -+ public boolean canManualSaveAndLoad = false; -+ -+ @GlobalConfig(value = "cache-skin", lock = true) -+ public boolean useSkinCache = false; -+ } -+ -+ public MinecraftOLDConfig oldMC = new MinecraftOLDConfig(); -+ -+ @GlobalConfigCategory("minecraft-old") -+ public static class MinecraftOLDConfig { -+ -+ public BlockUpdaterConfig updater = new BlockUpdaterConfig(); -+ -+ @GlobalConfigCategory("block-updater") -+ public static class BlockUpdaterConfig { -+ @RemovedConfig(name = "instant-block-updater-reintroduced", category = "modify", transform = true) -+ @RemovedConfig(name = "instant-block-updater-reintroduced", category = {"modify", "minecraft-old"}, transform = true) -+ @GlobalConfig(value = "instant-block-updater-reintroduced", lock = true) -+ public boolean instantBlockUpdaterReintroduced = false; -+ -+ @RemovedConfig(name = "cce-update-suppression", category = {"modify", "minecraft-old"}, transform = true) -+ @GlobalConfig("cce-update-suppression") -+ public boolean cceUpdateSuppression = false; -+ -+ @RemovedConfig(name = "redstone-wire-dont-connect-if-on-trapdoor", category = "modify", transform = true) -+ @RemovedConfig(name = "redstone-wire-dont-connect-if-on-trapdoor", category = {"modify", "minecraft-old"}, transform = true) -+ @GlobalConfig("redstone-wire-dont-connect-if-on-trapdoor") -+ public boolean redstoneDontCantOnTrapDoor = false; -+ } -+ -+ @RemovedConfig(name = "shears-in-dispenser-can-zero-amount", category = {}, transform = true) -+ @RemovedConfig(name = "shears-in-dispenser-can-zero-amount", category = "modify", transform = true) -+ @GlobalConfig("shears-in-dispenser-can-zero-amount") -+ public boolean shearsInDispenserCanZeroAmount = false; -+ -+ @GlobalConfig("armor-stand-cant-kill-by-mob-projectile") -+ public boolean armorStandCantKillByMobProjectile = false; -+ -+ @GlobalConfig(value = "villager-infinite-discounts", validator = VillagerInfiniteDiscountsValidator.class) -+ public boolean villagerInfiniteDiscounts = false; -+ -+ private static class VillagerInfiniteDiscountsValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ org.leavesmc.leaves.util.VillagerInfiniteDiscountHelper.doVillagerInfiniteDiscount(value); -+ } -+ } -+ -+ @GlobalConfig("copper-bulb-1gt-delay") -+ public boolean copperBulb1gt = false; -+ -+ @GlobalConfig("crafter-1gt-delay") -+ public boolean crafter1gt = false; -+ -+ @RemovedConfig(name = "zero-tick-plants", category = "modify", transform = true) -+ @GlobalConfig("zero-tick-plants") -+ public boolean zeroTickPlants = false; -+ -+ @RemovedConfig(name = "loot-world-random", category = {"modify", "minecraft-old"}, transform = true) -+ @GlobalConfig(value = "rng-fishing", lock = true, validator = RNGFishingValidator.class) -+ public boolean rngFishing = false; -+ -+ private static class RNGFishingValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ LeavesFeatureSet.register(LeavesFeature.of("rng_fishing", value)); -+ } -+ } -+ -+ @GlobalConfig("allow-grindstone-overstacking") -+ public boolean allowGrindstoneOverstacking = false; -+ -+ @GlobalConfig("allow-entity-portal-with-passenger") -+ public boolean allowEntityPortalWithPassenger = true; -+ -+ @GlobalConfig("disable-gateway-portal-entity-ticking") -+ public boolean disableGatewayPortalEntityTicking = false; -+ -+ @GlobalConfig("disable-LivingEntity-ai-step-alive-check") -+ public boolean disableLivingEntityAiStepAliveCheck = false; -+ -+ @GlobalConfig("fix-fortress-mob-spawn") -+ public boolean fixFortressMobSpawn = false; -+ -+ @GlobalConfig("old-block-entity-behaviour") -+ public boolean oldBlockEntityBehaviour = false; -+ -+ @GlobalConfig("old-hopper-suck-in-behavior") -+ public boolean oldHopperSuckInBehavior = false; -+ -+ public RaidConfig raid = new RaidConfig(); -+ -+ @GlobalConfigCategory("revert-raid-changes") -+ public static class RaidConfig { -+ @GlobalConfig("allow-bad-omen-trigger-raid") -+ public boolean allowBadOmenTriggerRaid = false; -+ -+ @GlobalConfig("give-bad-omen-when-kill-patrol-leader") -+ public boolean giveBadOmenWhenKillPatrolLeader = false; -+ } -+ -+ @GlobalConfig("allow-anvil-destroy-item-entities") -+ public boolean allowAnvilDestroyItemEntities = false; -+ -+ @GlobalConfig("string-tripwire-hook-duplicate") -+ public boolean stringTripwireHookDuplicate = false; -+ } -+ -+ public ElytraAeronauticsConfig elytraAeronautics = new ElytraAeronauticsConfig(); -+ -+ @GlobalConfigCategory("elytra-aeronautics") -+ public static class ElytraAeronauticsConfig { -+ @GlobalConfig("no-chunk-load") -+ public boolean noChunk = false; -+ -+ @GlobalConfig(value = "no-chunk-height", validator = DoubleConfigValidator.class) -+ public double noChunkHeight = 500.0D; -+ -+ @GlobalConfig(value = "no-chunk-speed", validator = DoubleConfigValidator.class) -+ public double noChunkSpeed = -1.0D; -+ -+ @GlobalConfig("message") -+ public boolean noChunkMes = true; -+ -+ @GlobalConfig(value = "message-start", validator = StringConfigValidator.class) -+ public String noChunkStartMes = "Flight enter cruise mode"; -+ -+ @GlobalConfig(value = "message-end", validator = StringConfigValidator.class) -+ public String noChunkEndMes = "Flight exit cruise mode"; -+ } -+ -+ @RemovedConfig(name = "redstone-shears-wrench", category = {}, transform = true) -+ @GlobalConfig("redstone-shears-wrench") -+ public boolean redstoneShearsWrench = true; -+ -+ @RemovedConfig(name = "budding-amethyst-can-push-by-piston", category = {}, transform = true) -+ @GlobalConfig("budding-amethyst-can-push-by-piston") -+ public boolean buddingAmethystCanPushByPiston = false; -+ -+ @RemovedConfig(name = "spectator-dont-get-advancement", category = {}, transform = true) -+ @GlobalConfig("spectator-dont-get-advancement") -+ public boolean spectatorDontGetAdvancement = false; -+ -+ @RemovedConfig(name = "stick-change-armorstand-arm-status", category = {}, transform = true) -+ @GlobalConfig("stick-change-armorstand-arm-status") -+ public boolean stickChangeArmorStandArmStatus = true; -+ -+ @RemovedConfig(name = "snowball-and-egg-can-knockback-player", category = {}, transform = true) -+ @GlobalConfig("snowball-and-egg-can-knockback-player") -+ public boolean snowballAndEggCanKnockback = true; -+ -+ @GlobalConfig("flatten-triangular-distribution") -+ public boolean flattenTriangularDistribution = false; -+ -+ @GlobalConfig("player-operation-limiter") -+ public boolean playerOperationLimiter = false; -+ -+ @GlobalConfig(value = "renewable-elytra", validator = RenewableElytraValidator.class) -+ public double renewableElytra = -1.0F; -+ -+ private static class RenewableElytraValidator extends DoubleConfigValidator { -+ @Override -+ public void verify(Double old, Double value) throws IllegalArgumentException { -+ if (value > 1.0) { -+ throw new IllegalArgumentException("renewable-elytra need <= 1.0f"); -+ } -+ } -+ } -+ -+ public int shulkerBoxStackSize = 1; -+ @GlobalConfig(value = "stackable-shulker-boxes", validator = StackableShulkerValidator.class) -+ private String stackableShulkerBoxes = "false"; -+ -+ private static class StackableShulkerValidator extends StringConfigValidator { -+ @Override -+ public void verify(String old, String value) throws IllegalArgumentException { -+ String realValue = MathUtils.isNumeric(value) ? value : value.equals("true") ? "2" : "1"; -+ LeavesConfig.modify.shulkerBoxStackSize = Integer.parseInt(realValue); -+ } -+ } -+ -+ @GlobalConfig("force-void-trade") -+ public boolean forceVoidTrade = false; -+ -+ @GlobalConfig(value = "mc-technical-survival-mode", validator = McTechnicalModeValidator.class, lock = true) -+ public boolean mcTechnicalMode = true; -+ -+ private static class McTechnicalModeValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ if (value) { -+ org.leavesmc.leaves.util.McTechnicalModeHelper.doMcTechnicalMode(); -+ } -+ } -+ } -+ -+ @GlobalConfig("return-nether-portal-fix") -+ public boolean netherPortalFix = false; -+ -+ @GlobalConfig(value = "use-vanilla-random", lock = true, validator = UseVanillaRandomValidator.class) -+ public boolean useVanillaRandom = false; -+ -+ private static class UseVanillaRandomValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ LeavesFeatureSet.register(LeavesFeature.of("use_vanilla_random", value)); -+ } -+ } -+ -+ @GlobalConfig("fix-update-suppression-crash") -+ public boolean updateSuppressionCrashFix = true; -+ -+ @GlobalConfig(value = "bedrock-break-list", lock = true) -+ public boolean bedrockBreakList = false; -+ -+ @GlobalConfig(value = "disable-distance-check-for-use-item", validator = DisableDistanceCheckForUseItemValidator.class) -+ public boolean disableDistanceCheckForUseItem = false; -+ -+ private static class DisableDistanceCheckForUseItemValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ if (!value && old != null && LeavesConfig.protocol.alternativeBlockPlacement != ProtocolConfig.AlternativePlaceType.NONE) { -+ throw new IllegalArgumentException("alternative-block-placement is enable, disable-distance-check-for-use-item always need true"); -+ } -+ } -+ } -+ -+ @GlobalConfig("no-feather-falling-trample") -+ public boolean noFeatherFallingTrample = false; -+ -+ @GlobalConfig("shared-villager-discounts") -+ public boolean sharedVillagerDiscounts = false; -+ -+ @GlobalConfig("disable-check-out-of-order-command") -+ public boolean disableCheckOutOfOrderCommand = false; -+ -+ @GlobalConfig("despawn-enderman-with-block") -+ public boolean despawnEndermanWithBlock = false; -+ -+ @GlobalConfig(value = "creative-no-clip", validator = CreativeNoClipValidator.class) -+ public boolean creativeNoClip = false; -+ -+ private static class CreativeNoClipValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ CarpetRules.register(CarpetRule.of("carpet", "creativeNoClip", value)); -+ } -+ } -+ -+ @GlobalConfig("shave-snow-layers") -+ public boolean shaveSnowLayers = true; -+ -+ @GlobalConfig("ignore-lc") -+ public boolean ignoreLC = false; -+ -+ @GlobalConfig("disable-packet-limit") -+ public boolean disablePacketLimit = false; -+ -+ @GlobalConfig(value = "lava-riptide", validator = LavaRiptideValidator.class) -+ public boolean lavaRiptide = false; -+ -+ private static class LavaRiptideValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ LeavesFeatureSet.register(LeavesFeature.of("lava_riptide", value)); -+ } -+ } -+ -+ @GlobalConfig(value = "no-block-update-command", validator = NoBlockUpdateValidator.class) -+ public boolean noBlockUpdateCommand = false; -+ -+ private static class NoBlockUpdateValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ if (value) { -+ registerCommand("blockupdate", new org.leavesmc.leaves.command.NoBlockUpdateCommand("blockupdate")); -+ } else { -+ unregisterCommand("blockupdate"); -+ } -+ } -+ } -+ -+ @GlobalConfig("no-tnt-place-update") -+ public boolean noTNTPlaceUpdate = false; -+ -+ @GlobalConfig("raider-die-skip-self-raid-check") -+ public boolean skipSelfRaidCheck = false; -+ -+ @GlobalConfig("container-passthrough") -+ public boolean containerPassthrough = false; -+ -+ @GlobalConfig(value = "avoid-anvil-too-expensive", validator = AnvilNotExpensiveValidator.class) -+ public boolean avoidAnvilTooExpensive = false; -+ -+ private static class AnvilNotExpensiveValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ CarpetRules.register(CarpetRule.of("pca", "avoidAnvilTooExpensive", value)); -+ } -+ } -+ -+ @GlobalConfig("bow-infinity-fix") -+ public boolean bowInfinityFix = false; -+ -+ @GlobalConfig("hopper-counter") -+ public boolean hopperCounter = false; -+ -+ @GlobalConfig(value = "spider-jockeys-drop-gapples", validator = JockeysDropGAppleValidator.class) -+ public double spiderJockeysDropGapples = -1.0; -+ -+ private static class JockeysDropGAppleValidator extends DoubleConfigValidator { -+ @Override -+ public void verify(Double old, Double value) throws IllegalArgumentException { -+ if (value > 1.0) { -+ throw new IllegalArgumentException("spider-jockeys-drop-gapples need <= 1.0f"); -+ } -+ } -+ } -+ -+ @GlobalConfig("renewable-deepslate") -+ public boolean renewableDeepslate = false; -+ -+ @GlobalConfig("renewable-sponges") -+ public boolean renewableSponges = false; -+ -+ @GlobalConfig(value = "renewable-coral", validator = RenewableCoralValidator.class) -+ public RenewableCoralType renewableCoral = RenewableCoralType.FALSE; -+ -+ public enum RenewableCoralType { -+ FALSE, TRUE, EXPANDED -+ } -+ -+ private static class RenewableCoralValidator extends EnumConfigValidator { -+ @Override -+ public void verify(RenewableCoralType old, RenewableCoralType value) throws IllegalArgumentException { -+ CarpetRules.register(CarpetRule.of("carpet", "renewableCoral", value)); -+ } -+ } -+ -+ @GlobalConfig("fast-resume") -+ public boolean fastResume = false; -+ -+ @GlobalConfig(value = "force-peaceful-mode", validator = ForcePeacefulModeValidator.class) -+ public int forcePeacefulMode = -1; -+ -+ private static class ForcePeacefulModeValidator extends IntConfigValidator { -+ @Override -+ public void verify(Integer old, Integer value) throws IllegalArgumentException { -+ for (ServerLevel level : MinecraftServer.getServer().getAllLevels()) { -+ level.chunkSource.peacefulModeSwitchTick = value; -+ } -+ } -+ } -+ -+ @GlobalConfig("disable-vault-blacklist") -+ public boolean disableVaultBlacklist = false; -+ -+ @RemovedConfig(name = "tick-command", category = "modify") -+ @RemovedConfig(name = "player-can-edit-sign", category = "modify") -+ @RemovedConfig(name = "mending-compatibility-infinity", category = {"modify", "minecraft-old"}) -+ @RemovedConfig(name = "protection-stacking", category = {"modify", "minecraft-old"}) -+ @RemovedConfig(name = "disable-moved-wrongly-threshold", category = {"modify"}) -+ private final boolean removed = false; -+ } -+ -+ public static PerformanceConfig performance = new PerformanceConfig(); -+ -+ @GlobalConfigCategory("performance") -+ public static class PerformanceConfig { -+ -+ public PerformanceRemoveConfig remove = new PerformanceRemoveConfig(); -+ -+ @GlobalConfigCategory("remove") -+ public static class PerformanceRemoveConfig { -+ @GlobalConfig("tick-guard-lambda") -+ public boolean tickGuardLambda = true; -+ -+ @GlobalConfig("inventory-contains-iterators") -+ public boolean inventoryContainsIterators = true; -+ -+ @GlobalConfig("damage-lambda") -+ public boolean damageLambda = true; -+ } -+ -+ @GlobalConfig("optimized-dragon-respawn") -+ public boolean optimizedDragonRespawn = false; -+ -+ @GlobalConfig("dont-send-useless-entity-packets") -+ public boolean dontSendUselessEntityPackets = true; -+ -+ @GlobalConfig("enable-suffocation-optimization") -+ public boolean enableSuffocationOptimization = true; -+ -+ @GlobalConfig("check-spooky-season-once-an-hour") -+ public boolean checkSpookySeasonOnceAnHour = true; -+ -+ @GlobalConfig("inactive-goal-selector-disable") -+ public boolean throttleInactiveGoalSelectorTick = false; -+ -+ @GlobalConfig("reduce-entity-allocations") -+ public boolean reduceEntityAllocations = true; -+ -+ @GlobalConfig("cache-climb-check") -+ public boolean cacheClimbCheck = true; -+ -+ @GlobalConfig(value = "biome-temperatures-use-aging-cache", lock = true) -+ public boolean biomeTemperaturesUseAgingCache = true; -+ -+ @GlobalConfig("reduce-chuck-load-and-lookup") -+ public boolean reduceChuckLoadAndLookup = true; -+ -+ @GlobalConfig("cache-ignite-odds") -+ public boolean cacheIgniteOdds = true; -+ -+ @GlobalConfig("faster-chunk-serialization") -+ public boolean fasterChunkSerialization = true; -+ -+ @GlobalConfig("skip-secondary-POI-sensor-if-absent") -+ public boolean skipSecondaryPOISensorIfAbsent = true; -+ -+ @GlobalConfig("store-mob-counts-in-array") -+ public boolean storeMobCountsInArray = true; -+ -+ @GlobalConfig("optimize-noise-generation") -+ public boolean optimizeNoiseGeneration = false; -+ -+ @GlobalConfig("optimize-sun-burn-tick") -+ public boolean optimizeSunBurnTick = true; -+ -+ @GlobalConfig("optimized-CubePointRange") -+ public boolean optimizedCubePointRange = true; -+ -+ @GlobalConfig("check-frozen-ticks-before-landing-block") -+ public boolean checkFrozenTicksBeforeLandingBlock = true; -+ -+ @GlobalConfig("skip-entity-move-if-movement-is-zero") -+ public boolean skipEntityMoveIfMovementIsZero = true; -+ -+ @GlobalConfig("skip-cloning-advancement-criteria") -+ public boolean skipCloningAdvancementCriteria = false; -+ -+ @GlobalConfig("skip-negligible-planar-movement-multiplication") -+ public boolean skipNegligiblePlanarMovementMultiplication = true; -+ -+ @GlobalConfig("fix-villagers-dont-release-memory") -+ public boolean villagersDontReleaseMemoryFix = false; -+ -+ @RemovedConfig(name = "cache-world-generator-sea-level", category = "performance") -+ @RemovedConfig(name = "cache-ominous-banner-item", category = "performance") -+ @RemovedConfig(name = "use-optimized-collection", category = "performance") -+ @RemovedConfig(name = "async-pathfinding", category = "performance") -+ @RemovedConfig(name = "async-mob-spawning", category = "performance") -+ @RemovedConfig(name = "async-entity-tracker", category = "performance") -+ @RemovedConfig(name = "fix-paper-6045", category = {"performance", "fix"}) -+ @RemovedConfig(name = "fix-paper-9372", category = {"performance", "fix"}) -+ @RemovedConfig(name = "skip-clone-loot-parameters", category = "performance") -+ @RemovedConfig(name = "skip-poi-find-in-vehicle", category = "performance") -+ @RemovedConfig(name = "strip-raytracing-for-entity", category = "performance") -+ @RemovedConfig(name = "get-nearby-players-streams", category = {"performance", "remove"}) -+ @RemovedConfig(name = "optimize-world-generation-and-block-access", category = "performance") -+ @RemovedConfig(name = "cache-CubeVoxelShape-shape-array", category = "performance") -+ @RemovedConfig(name = "reduce-entity-fluid-lookup", category = "performance") -+ @RemovedConfig(name = "optimize-entity-coordinate-key", category = "performance") -+ @RemovedConfig(name = "entity-target-find-optimization", category = "performance") -+ @RemovedConfig(name = "use-more-thread-unsafe-random", category = "performance") -+ @RemovedConfig(name = "range-check-streams-and-iterators", category = {"performance", "remove"}) -+ @RemovedConfig(name = "improve-fluid-direction-caching", category = "performance") -+ @RemovedConfig(name = "cache-BlockStatePairKey-hash", category = "performance") -+ @RemovedConfig(name = "optimize-chunk-ticking", category = "performance") -+ private final boolean removedPerformance = true; -+ } -+ -+ public static ProtocolConfig protocol = new ProtocolConfig(); -+ -+ @GlobalConfigCategory("protocol") -+ public static class ProtocolConfig { -+ -+ public BladerenConfig bladeren = new BladerenConfig(); -+ -+ @GlobalConfigCategory("bladeren") -+ public static class BladerenConfig { -+ @GlobalConfig("protocol") -+ public boolean enable = true; -+ -+ @GlobalConfig(value = "mspt-sync-protocol", validator = MSPTSyncValidator.class) -+ public boolean msptSyncProtocol = false; -+ -+ private static class MSPTSyncValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ LeavesFeatureSet.register(LeavesFeature.of("mspt_sync", value)); -+ } -+ } -+ -+ @GlobalConfig(value = "mspt-sync-tick-interval", validator = MSPTSyncIntervalValidator.class) -+ public int msptSyncTickInterval = 20; -+ -+ private static class MSPTSyncIntervalValidator extends IntConfigValidator { -+ @Override -+ public void verify(Integer old, Integer value) throws IllegalArgumentException { -+ if (value <= 0) { -+ throw new IllegalArgumentException("mspt-sync-tick-interval need > 0"); -+ } -+ } -+ } -+ } -+ -+ public SyncmaticaConfig syncmatica = new SyncmaticaConfig(); -+ -+ @GlobalConfigCategory("syncmatica") -+ public static class SyncmaticaConfig { -+ @GlobalConfig(value = "enable", validator = SyncmaticaValidator.class) -+ public boolean enable = false; -+ -+ public static class SyncmaticaValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ if (value) { -+ org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol.init(); -+ } -+ } -+ } -+ -+ @GlobalConfig("quota") -+ public boolean useQuota = false; -+ -+ @GlobalConfig(value = "quota-limit", validator = IntConfigValidator.class) -+ public int quotaLimit = 40000000; -+ } -+ -+ public PCAConfig pca = new PCAConfig(); -+ -+ @GlobalConfigCategory("pca") -+ public static class PCAConfig { -+ @RemovedConfig(name = "pca-sync-protocol", category = "protocol", transform = true) -+ @GlobalConfig(value = "pca-sync-protocol", validator = PcaValidator.class) -+ public boolean enable = false; -+ -+ public static class PcaValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ if (old != null && old != value) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.onConfigModify(value); -+ } -+ } -+ } -+ -+ @RemovedConfig(name = "pca-sync-player-entity", category = "protocol", convert = PcaPlayerEntityValidator.class, transform = true) -+ @GlobalConfig(value = "pca-sync-player-entity", validator = PcaPlayerEntityValidator.class) -+ public PcaPlayerEntityType syncPlayerEntity = PcaPlayerEntityType.OPS; -+ -+ public enum PcaPlayerEntityType { -+ NOBODY, BOT, OPS, OPS_AND_SELF, EVERYONE -+ } -+ -+ private static class PcaPlayerEntityValidator extends EnumConfigValidator { -+ } -+ } -+ -+ public AppleSkinConfig appleskin = new AppleSkinConfig(); -+ -+ @GlobalConfigCategory("appleskin") -+ public static class AppleSkinConfig { -+ @RemovedConfig(name = "appleskin-protocol", category = "protocol") -+ @GlobalConfig("protocol") -+ public boolean enable = false; -+ -+ @GlobalConfig("sync-tick-interval") -+ public int syncTickInterval = 20; -+ } -+ -+ public ServuxConfig servux = new ServuxConfig(); -+ -+ @GlobalConfigCategory("servux") -+ public static class ServuxConfig { -+ @RemovedConfig(name = "servux-protocol", category = "protocol", transform = true) -+ @GlobalConfig("structure-protocol") -+ public boolean structureProtocol = false; -+ -+ @GlobalConfig("entity-protocol") -+ public boolean entityProtocol = false; -+ } -+ -+ @GlobalConfig("bbor-protocol") -+ public boolean bborProtocol = false; -+ -+ @GlobalConfig("jade-protocol") -+ public boolean jadeProtocol = false; -+ -+ @GlobalConfig(value = "alternative-block-placement", validator = AlternativePlaceValidator.class) -+ public AlternativePlaceType alternativeBlockPlacement = AlternativePlaceType.NONE; -+ -+ public enum AlternativePlaceType { -+ NONE, CARPET, CARPET_FIX, LITEMATICA -+ } -+ -+ private static class AlternativePlaceValidator extends EnumConfigValidator { -+ @Override -+ public void runAfterLoader(AlternativePlaceType value, boolean firstLoad) { -+ if (value != AlternativePlaceType.NONE) { -+ LeavesConfig.modify.disableDistanceCheckForUseItem = true; -+ } -+ } -+ } -+ -+ @GlobalConfig("xaero-map-protocol") -+ public boolean xaeroMapProtocol = false; -+ -+ @GlobalConfig(value = "xaero-map-server-id", validator = IntConfigValidator.class) -+ public int xaeroMapServerID = new Random().nextInt(); -+ -+ @GlobalConfig("leaves-carpet-support") -+ public boolean leavesCarpetSupport = false; -+ } -+ -+ public static MiscConfig mics = new MiscConfig(); -+ -+ @GlobalConfigCategory("misc") -+ public static class MiscConfig { -+ -+ public AutoUpdateConfig autoUpdate = new AutoUpdateConfig(); -+ -+ @GlobalConfigCategory("auto-update") -+ public static class AutoUpdateConfig { -+ @GlobalConfig(value = "enable", lock = true, validator = AutoUpdateValidator.class) -+ public boolean enable = false; -+ -+ private static class AutoUpdateValidator extends BooleanConfigValidator { -+ @Override -+ public void runAfterLoader(Boolean value, boolean firstLoad) { -+ if (firstLoad) { -+ org.leavesmc.leaves.util.LeavesUpdateHelper.init(); -+ if (value) { -+ LeavesLogger.LOGGER.warning("Auto-Update is not completely safe. Enabling it may cause data security problems!"); -+ } -+ } -+ } -+ } -+ -+ @GlobalConfig(value = "download-source", lock = true, validator = DownloadSourceValidator.class) -+ public String source = "application"; -+ -+ public static class DownloadSourceValidator extends StringConfigValidator { -+ private static final List suggestSourceList = List.of("application", "ghproxy", "cloud"); -+ -+ @Override -+ public List valueSuggest() { -+ return suggestSourceList; -+ } -+ } -+ -+ @GlobalConfig("allow-experimental") -+ public Boolean allowExperimental = false; -+ -+ @GlobalConfig(value = "time", lock = true, validator = ListConfigValidator.STRING.class) -+ public List updateTime = List.of("14:00", "2:00"); -+ } -+ -+ public ExtraYggdrasilConfig yggdrasil = new ExtraYggdrasilConfig(); -+ -+ @GlobalConfigCategory("extra-yggdrasil-service") -+ public static class ExtraYggdrasilConfig { -+ @GlobalConfig(value = "enable", validator = ExtraYggdrasilValidator.class) -+ public boolean enable = false; -+ -+ public static class ExtraYggdrasilValidator extends BooleanConfigValidator { -+ @Override -+ public void verify(Boolean old, Boolean value) throws IllegalArgumentException { -+ if (value) { -+ LeavesLogger.LOGGER.warning("extra-yggdrasil-service is an unofficial support. Enabling it may cause data security problems!"); -+ GlobalConfiguration.get().unsupportedSettings.performUsernameValidation = true; // always check username -+ } -+ } -+ } -+ -+ @GlobalConfig("login-protect") -+ public boolean loginProtect = false; -+ -+ @GlobalConfig(value = "urls", lock = true, validator = ExtraYggdrasilUrlsValidator.class) -+ public List serviceList = List.of("https://url.with.authlib-injector-yggdrasil"); -+ -+ public static class ExtraYggdrasilUrlsValidator extends ListConfigValidator.STRING { -+ @Override -+ public void verify(List old, List value) throws IllegalArgumentException { -+ org.leavesmc.leaves.profile.LeavesMinecraftSessionService.initExtraYggdrasilList(value); -+ } -+ } -+ } -+ -+ @GlobalConfig("disable-method-profiler") -+ public boolean disableMethodProfiler = true; -+ -+ @RemovedConfig(name = "no-chat-sign", category = {}, transform = true) -+ @GlobalConfig("no-chat-sign") -+ public boolean noChatSign = true; -+ -+ @GlobalConfig("dont-respond-ping-before-start-fully") -+ public boolean dontRespondPingBeforeStart = true; -+ -+ @GlobalConfig(value = "server-lang", lock = true, validator = ServerLangValidator.class) -+ public String serverLang = "en_us"; -+ -+ private static class ServerLangValidator extends StringConfigValidator { -+ private static final List supportLang = List.of("en_us", "zh_cn"); -+ -+ @Override -+ public void verify(String old, String value) throws IllegalArgumentException { -+ if (!supportLang.contains(value)) { -+ throw new IllegalArgumentException("lang " + value + " not supported"); -+ } -+ } -+ -+ @Override -+ public List valueSuggest() { -+ return supportLang; -+ } -+ } -+ -+ @GlobalConfig(value = "server-mod-name", validator = StringConfigValidator.class) -+ public String serverModName = "Leaves"; -+ -+ @GlobalConfig("bstats-privacy-mode") -+ public boolean bstatsPrivacyMode = false; -+ -+ @GlobalConfig("force-minecraft-command") -+ public boolean forceMinecraftCommand = false; -+ -+ @GlobalConfig("leaves-packet-event") -+ public boolean leavesPacketEvent = true; -+ } -+ -+ public static RegionConfig region = new RegionConfig(); -+ -+ @GlobalConfigCategory("region") -+ public static class RegionConfig { -+ -+ @GlobalConfig(value = "format", lock = true, validator = RegionFormatValidator.class) -+ public org.leavesmc.leaves.region.RegionFileFormat format = org.leavesmc.leaves.region.RegionFileFormat.ANVIL; -+ -+ private static class RegionFormatValidator extends EnumConfigValidator { -+ } -+ -+ public LinearConfig linear = new LinearConfig(); -+ -+ @GlobalConfigCategory("linear") -+ public static class LinearConfig { -+ -+ @GlobalConfig(value = "flush-frequency", lock = true, validator = IntConfigValidator.class) -+ public int flushFrequency = 10; -+ -+ @GlobalConfig(value = "auto-convert-anvil-to-linear", lock = true) -+ public boolean autoConvertAnvilToLinear = false; -+ -+ @GlobalConfig(value = "flush-max-threads", lock = true, validator = IntConfigValidator.class) -+ public int flushThreads = 1; -+ -+ public int getLinearFlushThreads() { -+ if (flushThreads < 0) { -+ return Math.max(Runtime.getRuntime().availableProcessors() + flushThreads, 1); -+ } else { -+ return Math.max(flushThreads, 1); -+ } -+ } -+ -+ @GlobalConfig(value = "compression-level", lock = true, validator = LinearCompressValidator.class) -+ public int compressionLevel = 1; -+ -+ private static class LinearCompressValidator extends IntConfigValidator { -+ @Override -+ public void verify(Integer old, Integer value) throws IllegalArgumentException { -+ if (value < 1 || value > 22) { -+ throw new IllegalArgumentException("linear.compression-level need between 1 and 22"); -+ } -+ } -+ } -+ -+ @RemovedConfig(name = "crash-on-broken-symlink", category = {"region", "linear"}) -+ private final boolean linearCrashOnBrokenSymlink = true; -+ } -+ } -+ -+ public static FixConfig fix = new FixConfig(); -+ -+ @GlobalConfigCategory("fix") -+ public static class FixConfig { -+ @GlobalConfig("vanilla-hopper") -+ public boolean vanillaHopper = false; -+ -+ @RemovedConfig(name = "spigot-EndPlatform-destroy", category = "fix") -+ private final boolean spigotEndPlatformDestroy = false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/command/CommandArgument.java b/src/main/java/org/leavesmc/leaves/command/CommandArgument.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0bccbf7816ef621316f0da4911ec112f4753f88e ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgument.java -@@ -0,0 +1,55 @@ -+package org.leavesmc.leaves.command; -+ -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.List; -+ -+public class CommandArgument { -+ -+ public static final CommandArgument EMPTY = new CommandArgument(); -+ -+ private final List> argumentTypes; -+ private final List> tabComplete; -+ -+ private CommandArgument(CommandArgumentType... argumentTypes) { -+ this.argumentTypes = List.of(argumentTypes); -+ this.tabComplete = new ArrayList<>(); -+ for (int i = 0; i < argumentTypes.length; i++) { -+ tabComplete.add(new ArrayList<>()); -+ } -+ } -+ -+ public static CommandArgument of(CommandArgumentType... argumentTypes) { -+ return new CommandArgument(argumentTypes); -+ } -+ -+ public List tabComplete(int n) { -+ if (tabComplete.size() > n) { -+ return tabComplete.get(n); -+ } else { -+ return List.of(); -+ } -+ } -+ -+ public CommandArgument setTabComplete(int index, List list) { -+ tabComplete.set(index, list); -+ return this; -+ } -+ -+ public CommandArgument setAllTabComplete(List> tabComplete) { -+ this.tabComplete.clear(); -+ this.tabComplete.addAll(tabComplete); -+ return this; -+ } -+ -+ public CommandArgumentResult parse(int index, String @NotNull [] args) { -+ Object[] result = new Object[argumentTypes.size()]; -+ Arrays.fill(result, null); -+ for (int i = index, j = 0; i < args.length && j < result.length; i++, j++) { -+ result[j] = argumentTypes.get(j).pasre(args[i]); -+ } -+ return new CommandArgumentResult(new ArrayList<>(Arrays.asList(result))); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java b/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java -new file mode 100644 -index 0000000000000000000000000000000000000000..46aa6eaa75b65aad6bdbe4a5f517b42e87bcca77 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgumentResult.java -@@ -0,0 +1,69 @@ -+package org.leavesmc.leaves.command; -+ -+import net.minecraft.core.BlockPos; -+import org.bukkit.util.Vector; -+ -+import java.util.List; -+import java.util.Objects; -+ -+public class CommandArgumentResult { -+ -+ private final List result; -+ -+ public CommandArgumentResult(List result) { -+ this.result = result; -+ } -+ -+ public int readInt(int def) { -+ return Objects.requireNonNullElse(read(Integer.class), def); -+ } -+ -+ public double readDouble(double def) { -+ return Objects.requireNonNullElse(read(Double.class), def); -+ } -+ -+ public float readFloat(float def) { -+ return Objects.requireNonNullElse(read(Float.class), def); -+ } -+ -+ public String readString(String def) { -+ return Objects.requireNonNullElse(read(String.class), def); -+ } -+ -+ public boolean readBoolean(boolean def) { -+ return Objects.requireNonNullElse(read(Boolean.class), def); -+ } -+ -+ public BlockPos readPos() { -+ Integer[] pos = {read(Integer.class), read(Integer.class), read(Integer.class)}; -+ for (Integer po : pos) { -+ if (po == null) { -+ return null; -+ } -+ } -+ return new BlockPos(pos[0], pos[1], pos[2]); -+ } -+ -+ public Vector readVector() { -+ Double[] pos = {read(Double.class), read(Double.class), read(Double.class)}; -+ for (Double po : pos) { -+ if (po == null) { -+ return null; -+ } -+ } -+ return new Vector(pos[0], pos[1], pos[2]); -+ } -+ -+ public T read(Class tClass) { -+ if (result.isEmpty()) { -+ return null; -+ } -+ -+ Object obj = result.remove(0); -+ if (tClass.isInstance(obj)) { -+ return tClass.cast(obj); -+ } else { -+ return null; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java b/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4ca3508475bbd9771768704e300fe12b717489d6 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/CommandArgumentType.java -@@ -0,0 +1,55 @@ -+package org.leavesmc.leaves.command; -+ -+import org.jetbrains.annotations.NotNull; -+ -+public abstract class CommandArgumentType { -+ -+ public static final CommandArgumentType INTEGER = new CommandArgumentType<>() { -+ @Override -+ public Integer pasre(@NotNull String arg) { -+ try { -+ return Integer.parseInt(arg); -+ } catch (NumberFormatException e) { -+ return null; -+ } -+ } -+ }; -+ -+ public static final CommandArgumentType DOUBLE = new CommandArgumentType<>() { -+ @Override -+ public Double pasre(@NotNull String arg) { -+ try { -+ return Double.parseDouble(arg); -+ } catch (NumberFormatException e) { -+ return null; -+ } -+ } -+ }; -+ -+ public static final CommandArgumentType FLOAT = new CommandArgumentType<>() { -+ @Override -+ public Float pasre(@NotNull String arg) { -+ try { -+ return Float.parseFloat(arg); -+ } catch (NumberFormatException e) { -+ return null; -+ } -+ } -+ }; -+ -+ public static final CommandArgumentType STRING = new CommandArgumentType<>() { -+ @Override -+ public String pasre(@NotNull String arg) { -+ return arg; -+ } -+ }; -+ -+ public static final CommandArgumentType BOOLEAN = new CommandArgumentType<>() { -+ @Override -+ public Boolean pasre(@NotNull String arg) { -+ return Boolean.parseBoolean(arg); -+ } -+ }; -+ -+ public abstract E pasre(@NotNull String arg); -+} -diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..393163c80b9e2ce04b089e90d20eefd7b7ad8366 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -@@ -0,0 +1,117 @@ -+package org.leavesmc.leaves.command; -+ -+import it.unimi.dsi.fastutil.Pair; -+import net.minecraft.Util; -+import org.bukkit.Bukkit; -+import org.bukkit.Location; -+import org.bukkit.command.Command; -+import org.bukkit.command.CommandSender; -+import org.bukkit.permissions.Permission; -+import org.bukkit.permissions.PermissionDefault; -+import org.bukkit.plugin.PluginManager; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.command.subcommands.*; -+ -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Collections; -+import java.util.HashMap; -+import java.util.List; -+import java.util.Locale; -+import java.util.Map; -+import java.util.Set; -+import java.util.stream.Collectors; -+ -+import static net.kyori.adventure.text.Component.text; -+import static net.kyori.adventure.text.format.NamedTextColor.RED; -+ -+public final class LeavesCommand extends Command { -+ static final String BASE_PERM = "bukkit.command.leaves."; -+ // subcommand label -> subcommand -+ private static final Map SUBCOMMANDS = Util.make(() -> { -+ final Map, LeavesSubcommand> commands = new HashMap<>(); -+ commands.put(Set.of("config"), new ConfigCommand()); -+ -+ return commands.entrySet().stream() -+ .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) -+ .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); -+ }); -+ private static final Set COMPLETABLE_SUBCOMMANDS = SUBCOMMANDS.entrySet().stream().filter(entry -> entry.getValue().tabCompletes()).map(Map.Entry::getKey).collect(Collectors.toSet()); -+ -+ public LeavesCommand(final String name) { -+ super(name); -+ this.description = "Leaves related commands"; -+ this.usageMessage = "/leaves [" + String.join(" | ", SUBCOMMANDS.keySet()) + "]"; -+ final List permissions = new ArrayList<>(); -+ permissions.add("bukkit.command.leaves"); -+ permissions.addAll(SUBCOMMANDS.keySet().stream().map(s -> BASE_PERM + s).toList()); -+ this.setPermission(String.join(";", permissions)); -+ final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); -+ for (final String perm : permissions) { -+ if (pluginManager.getPermission(perm) == null) { -+ pluginManager.addPermission(new Permission(perm, PermissionDefault.OP)); -+ } -+ } -+ } -+ -+ private static boolean testPermission(final CommandSender sender, final String permission) { -+ if (sender.hasPermission(BASE_PERM + permission) || sender.hasPermission("bukkit.command.leaves")) { -+ return true; -+ } -+ sender.sendMessage(Bukkit.permissionMessage()); -+ return false; -+ } -+ -+ @NotNull -+ -+ @Override -+ public List tabComplete(final @NotNull CommandSender sender, final @NotNull String alias, final String[] args, final @Nullable Location location) throws IllegalArgumentException { -+ if (args.length <= 1) { -+ return LeavesCommandUtil.getListMatchingLast(sender, args, COMPLETABLE_SUBCOMMANDS); -+ } -+ -+ final @Nullable Pair subCommand = resolveCommand(args[0]); -+ if (subCommand != null) { -+ return subCommand.second().tabComplete(sender, subCommand.first(), Arrays.copyOfRange(args, 1, args.length)); -+ } -+ -+ return Collections.emptyList(); -+ } -+ -+ @Override -+ public boolean execute(final @NotNull CommandSender sender, final @NotNull String commandLabel, final String[] args) { -+ if (!testPermission(sender)) { -+ return true; -+ } -+ -+ if (args.length == 0) { -+ sender.sendMessage(text("Usage: " + this.usageMessage, RED)); -+ return false; -+ } -+ final Pair subCommand = resolveCommand(args[0]); -+ -+ if (subCommand == null) { -+ sender.sendMessage(text("Usage: " + this.usageMessage, RED)); -+ return false; -+ } -+ -+ if (!testPermission(sender, subCommand.first())) { -+ return true; -+ } -+ final String[] choppedArgs = Arrays.copyOfRange(args, 1, args.length); -+ return subCommand.second().execute(sender, subCommand.first(), choppedArgs); -+ } -+ -+ @Nullable -+ private static Pair resolveCommand(String label) { -+ label = label.toLowerCase(Locale.ENGLISH); -+ LeavesSubcommand subCommand = SUBCOMMANDS.get(label); -+ -+ if (subCommand != null) { -+ return Pair.of(label, subCommand); -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4b61fccc71d98a7b69bb7f88fb88ea0a55ca1ed2 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/LeavesSubcommand.java -@@ -0,0 +1,18 @@ -+package org.leavesmc.leaves.command; -+ -+import org.bukkit.command.CommandSender; -+ -+import java.util.Collections; -+import java.util.List; -+ -+public interface LeavesSubcommand { -+ boolean execute(CommandSender sender, String subCommand, String[] args); -+ -+ default List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { -+ return Collections.emptyList(); -+ } -+ -+ default boolean tabCompletes() { -+ return true; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java b/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..56ad63ae93e1322140d59efc234ba90f0d5c7ab8 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/subcommands/ConfigCommand.java -@@ -0,0 +1,83 @@ -+package org.leavesmc.leaves.command.subcommands; -+ -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.JoinConfiguration; -+import net.kyori.adventure.text.format.NamedTextColor; -+import org.bukkit.command.CommandSender; -+import org.leavesmc.leaves.command.LeavesCommandUtil; -+import org.leavesmc.leaves.command.LeavesSubcommand; -+import org.leavesmc.leaves.config.GlobalConfigManager; -+ -+import java.util.ArrayList; -+import java.util.Collections; -+import java.util.List; -+ -+public class ConfigCommand implements LeavesSubcommand { -+ -+ @Override -+ public boolean execute(CommandSender sender, String subCommand, String[] args) { -+ if (args.length < 1) { -+ sender.sendMessage(Component.text("Leaves Config", NamedTextColor.GRAY)); -+ return true; -+ } -+ -+ GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(args[0]); -+ if (verifiedConfig == null) { -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Config ", NamedTextColor.GRAY), -+ Component.text(args[0], NamedTextColor.RED), -+ Component.text(" is Not Found.", NamedTextColor.GRAY) -+ )); -+ return true; -+ } -+ -+ if (args.length > 1) { -+ try { -+ verifiedConfig.set(args[1]); -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Config ", NamedTextColor.GRAY), -+ Component.text(args[0], NamedTextColor.AQUA), -+ Component.text(" changed to ", NamedTextColor.GRAY), -+ Component.text(verifiedConfig.getString(), NamedTextColor.AQUA) -+ )); -+ } catch (IllegalArgumentException exception) { -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Config ", NamedTextColor.GRAY), -+ Component.text(args[0], NamedTextColor.RED), -+ Component.text(" modify error by ", NamedTextColor.GRAY), -+ Component.text(exception.getMessage(), NamedTextColor.RED) -+ )); -+ } -+ } else { -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Config ", NamedTextColor.GRAY), -+ Component.text(args[0], NamedTextColor.AQUA), -+ Component.text(" value is ", NamedTextColor.GRAY), -+ Component.text(verifiedConfig.getString(), NamedTextColor.AQUA) -+ )); -+ } -+ -+ return true; -+ } -+ -+ @Override -+ public List tabComplete(CommandSender sender, String subCommand, String[] args) { -+ switch (args.length) { -+ case 1 -> { -+ List list = new ArrayList<>(GlobalConfigManager.getVerifiedConfigPaths()); -+ return LeavesCommandUtil.getListMatchingLast(sender, args, list); -+ } -+ -+ case 2 -> { -+ GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.getVerifiedConfig(args[0]); -+ if (verifiedConfig != null) { -+ return LeavesCommandUtil.getListMatchingLast(sender, args, verifiedConfig.validator().valueSuggest()); -+ } else { -+ return Collections.singletonList(""); -+ } -+ } -+ } -+ -+ return Collections.emptyList(); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/config/ConfigConverter.java b/src/main/java/org/leavesmc/leaves/config/ConfigConverter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..178f7e8636d135b084444b3b0c476631aac66dcc ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/config/ConfigConverter.java -@@ -0,0 +1,21 @@ -+package org.leavesmc.leaves.config; -+ -+public interface ConfigConverter { -+ -+ E convert(String value) throws IllegalArgumentException; -+ -+ @SuppressWarnings("unchecked") -+ default E loadConvert(Object value) throws IllegalArgumentException { -+ try { -+ return (E) value; -+ } catch (ClassCastException e) { -+ throw new IllegalArgumentException(e); -+ } -+ } -+ -+ default Object saveConvert(E value) { -+ return value; -+ } -+ -+ Class getFieldClass(); -+} -diff --git a/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java b/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d343ae8d44a78e1d1536a2611b4bbbf9143c218e ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/config/ConfigValidator.java -@@ -0,0 +1,16 @@ -+package org.leavesmc.leaves.config; -+ -+import java.util.List; -+ -+public interface ConfigValidator extends ConfigConverter { -+ -+ default void verify(E old, E value) throws IllegalArgumentException { -+ } -+ -+ default List valueSuggest() { -+ return List.of(""); -+ } -+ -+ default void runAfterLoader(E value, boolean firstLoad) { -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/config/ConfigValidatorImpl.java b/src/main/java/org/leavesmc/leaves/config/ConfigValidatorImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0e9cdc0fa9380ad07ad74a3933733fe4053b678b ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/config/ConfigValidatorImpl.java -@@ -0,0 +1,108 @@ -+package org.leavesmc.leaves.config; -+ -+import org.jetbrains.annotations.NotNull; -+ -+import java.lang.reflect.ParameterizedType; -+import java.lang.reflect.Type; -+import java.util.ArrayList; -+import java.util.List; -+import java.util.Locale; -+ -+public abstract class ConfigValidatorImpl implements ConfigValidator { -+ -+ private Class fieldClass; -+ -+ @SuppressWarnings("unchecked") -+ public ConfigValidatorImpl() { -+ Type superClass = getClass().getGenericSuperclass(); -+ if (superClass instanceof ParameterizedType) { -+ Type[] actualTypeArguments = ((ParameterizedType) superClass).getActualTypeArguments(); -+ if (actualTypeArguments[0] instanceof Class) { -+ this.fieldClass = (Class) actualTypeArguments[0]; -+ } -+ } -+ } -+ -+ @Override -+ public Class getFieldClass() { -+ return fieldClass; -+ } -+ -+ public static class BooleanConfigValidator extends ConfigValidatorImpl { -+ -+ @Override -+ public Boolean convert(String value) throws IllegalArgumentException { -+ return Boolean.parseBoolean(value); -+ } -+ -+ @Override -+ public List valueSuggest() { -+ return List.of("false", "true"); -+ } -+ } -+ -+ public static class IntConfigValidator extends ConfigValidatorImpl { -+ @Override -+ public Integer convert(String value) throws IllegalArgumentException { -+ return Integer.parseInt(value); -+ } -+ } -+ -+ public static class StringConfigValidator extends ConfigValidatorImpl { -+ @Override -+ public String convert(String value) throws IllegalArgumentException { -+ return value; -+ } -+ } -+ -+ public static class DoubleConfigValidator extends ConfigValidatorImpl { -+ @Override -+ public Double convert(String value) throws IllegalArgumentException { -+ return Double.parseDouble(value); -+ } -+ } -+ -+ public abstract static class ListConfigValidator extends ConfigValidatorImpl> { -+ public static class STRING extends ListConfigValidator { -+ } -+ -+ @Override -+ public List convert(String value) throws IllegalArgumentException { -+ throw new IllegalArgumentException("not support"); // TODO -+ } -+ } -+ -+ public abstract static class EnumConfigValidator> extends ConfigValidatorImpl { -+ -+ private final List enumValues; -+ -+ public EnumConfigValidator() { -+ super(); -+ this.enumValues = new ArrayList<>() {{ -+ for (E e : getFieldClass().getEnumConstants()) { -+ add(e.name().toLowerCase(Locale.ROOT)); -+ } -+ }}; -+ } -+ -+ @Override -+ public E convert(@NotNull String value) throws IllegalArgumentException { -+ return Enum.valueOf(getFieldClass(), value.toUpperCase(Locale.ROOT)); -+ } -+ -+ @Override -+ public E loadConvert(@NotNull Object value) throws IllegalArgumentException { -+ return this.convert(value.toString()); -+ } -+ -+ @Override -+ public Object saveConvert(@NotNull E value) { -+ return value.toString().toUpperCase(Locale.ROOT); -+ } -+ -+ @Override -+ public List valueSuggest() { -+ return enumValues; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java b/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d6af8fd7552c5560f4397b9961dd5c39014e12ae ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/config/GlobalConfig.java -@@ -0,0 +1,17 @@ -+package org.leavesmc.leaves.config; -+ -+import java.lang.annotation.ElementType; -+import java.lang.annotation.Retention; -+import java.lang.annotation.RetentionPolicy; -+import java.lang.annotation.Target; -+ -+@Target(ElementType.FIELD) -+@Retention(RetentionPolicy.RUNTIME) -+public @interface GlobalConfig { -+ -+ String value(); -+ -+ boolean lock() default false; -+ -+ Class> validator() default ConfigValidatorImpl.BooleanConfigValidator.class; -+} -diff --git a/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java b/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1e109a8b95f7dd25f68f7b3d2115c8cf3c43e58c ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/config/GlobalConfigCategory.java -@@ -0,0 +1,12 @@ -+package org.leavesmc.leaves.config; -+ -+import java.lang.annotation.ElementType; -+import java.lang.annotation.Retention; -+import java.lang.annotation.RetentionPolicy; -+import java.lang.annotation.Target; -+ -+@Target(ElementType.TYPE) -+@Retention(RetentionPolicy.RUNTIME) -+public @interface GlobalConfigCategory { -+ String value(); -+} -diff --git a/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java b/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dfc38bd1fbec54da52188e9ea860448e14342a99 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/config/GlobalConfigCreator.java -@@ -0,0 +1,84 @@ -+package org.leavesmc.leaves.config; -+ -+import org.bukkit.configuration.file.YamlConfiguration; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+ -+import java.io.File; -+import java.io.IOException; -+import java.lang.reflect.Field; -+import java.lang.reflect.Modifier; -+import java.util.Collections; -+ -+import static org.leavesmc.leaves.config.GlobalConfigManager.CONFIG_START; -+ -+@SuppressWarnings("CallToPrintStackTrace") -+public class GlobalConfigCreator { -+ -+ private static YamlConfiguration config; -+ -+ @SuppressWarnings("ResultOfMethodCallIgnored") -+ public static void main(String[] args) { -+ config = new YamlConfiguration(); -+ config.options().setHeader(Collections.singletonList(LeavesConfig.CONFIG_HEADER)); -+ -+ config.set("config-version", LeavesConfig.CURRENT_CONFIG_VERSION); -+ -+ for (Field field : LeavesConfig.class.getDeclaredFields()) { -+ initField(field, null, CONFIG_START); -+ } -+ -+ config.set("settings.protocol.xaero-map-server-id", 0); -+ -+ try { -+ File file = new File("leaves.yml"); -+ if (file.exists()) { -+ file.delete(); -+ } -+ file.createNewFile(); -+ config.save(file); -+ } catch (IOException e) { -+ e.printStackTrace(); -+ } -+ } -+ -+ private static void initCategory(@NotNull Field categoryField, @NotNull GlobalConfigCategory globalCategory, @Nullable Object upstreamField, @NotNull String upstreamPath) { -+ try { -+ Object category = categoryField.get(upstreamField); -+ String categoryPath = upstreamPath + globalCategory.value() + "."; -+ for (Field field : categoryField.getType().getDeclaredFields()) { -+ initField(field, category, categoryPath); -+ } -+ } catch (Exception e) { -+ e.printStackTrace(); -+ } -+ } -+ -+ private static void initField(@NotNull Field field, @Nullable Object upstreamField, @NotNull String upstreamPath) { -+ if (upstreamField != null || Modifier.isStatic(field.getModifiers())) { -+ field.setAccessible(true); -+ -+ GlobalConfig globalConfig = field.getAnnotation(GlobalConfig.class); -+ if (globalConfig != null) { -+ initConfig(field, globalConfig, upstreamField, upstreamPath); -+ return; -+ } -+ -+ GlobalConfigCategory globalCategory = field.getType().getAnnotation(GlobalConfigCategory.class); -+ if (globalCategory != null) { -+ initCategory(field, globalCategory, upstreamField, upstreamPath); -+ } -+ } -+ } -+ -+ -+ private static void initConfig(@NotNull Field field, GlobalConfig globalConfig, @Nullable Object upstreamField, @NotNull String upstreamPath) { -+ try { -+ GlobalConfigManager.VerifiedConfig verifiedConfig = GlobalConfigManager.VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath); -+ config.set(verifiedConfig.path(), verifiedConfig.validator().saveConvert(field.get(upstreamField))); -+ } catch (Exception e) { -+ e.printStackTrace(); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java b/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..adc023d5b669d712ae7eae740f5ab43a7bd5731e ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/config/GlobalConfigManager.java -@@ -0,0 +1,232 @@ -+package org.leavesmc.leaves.config; -+ -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+ -+import java.lang.reflect.Constructor; -+import java.lang.reflect.Field; -+import java.lang.reflect.Modifier; -+import java.util.HashMap; -+import java.util.Map; -+import java.util.Set; -+ -+public class GlobalConfigManager { -+ -+ public final static String CONFIG_START = "settings."; -+ -+ private static boolean firstLoad = true; -+ private static final Map verifiedConfigs = new HashMap<>(); -+ -+ public static void init() { -+ verifiedConfigs.clear(); -+ -+ for (Field field : LeavesConfig.class.getDeclaredFields()) { -+ initField(field, null, CONFIG_START); -+ } -+ -+ verifiedConfigs.forEach((path, config) -> config.validator.runAfterLoader(config.get(), firstLoad)); -+ -+ firstLoad = false; -+ LeavesConfig.save(); -+ } -+ -+ private static void initCategory(@NotNull Field categoryField, @NotNull GlobalConfigCategory globalCategory, @Nullable Object upstreamField, @NotNull String upstreamPath) { -+ try { -+ Object category = categoryField.get(upstreamField); -+ String categoryPath = upstreamPath + globalCategory.value() + "."; -+ for (Field field : categoryField.getType().getDeclaredFields()) { -+ initField(field, category, categoryPath); -+ } -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.severe("Failure to load leaves config" + upstreamPath, e); -+ } -+ } -+ -+ private static void initField(@NotNull Field field, @Nullable Object upstreamField, @NotNull String upstreamPath) { -+ if (upstreamField != null || Modifier.isStatic(field.getModifiers())) { -+ field.setAccessible(true); -+ -+ for (RemovedConfig config : field.getAnnotationsByType(RemovedConfig.class)) { -+ RemovedVerifiedConfig.build(config, field, upstreamField).run(); -+ } -+ -+ GlobalConfig globalConfig = field.getAnnotation(GlobalConfig.class); -+ if (globalConfig != null) { -+ initConfig(field, globalConfig, upstreamField, upstreamPath); -+ return; -+ } -+ -+ GlobalConfigCategory globalCategory = field.getType().getAnnotation(GlobalConfigCategory.class); -+ if (globalCategory != null) { -+ initCategory(field, globalCategory, upstreamField, upstreamPath); -+ } -+ } -+ } -+ -+ private static void initConfig(@NotNull Field field, GlobalConfig globalConfig, @Nullable Object upstreamField, @NotNull String upstreamPath) { -+ try { -+ VerifiedConfig verifiedConfig = VerifiedConfig.build(globalConfig, field, upstreamField, upstreamPath); -+ -+ if (globalConfig.lock() && !firstLoad) { -+ verifiedConfigs.put(verifiedConfig.path.substring(CONFIG_START.length()), verifiedConfig); -+ } -+ -+ ConfigValidator validator = verifiedConfig.validator; -+ -+ Object defValue = validator.saveConvert(field.get(upstreamField)); -+ LeavesConfig.config.addDefault(verifiedConfig.path, defValue); -+ -+ try { -+ Object savedValue = LeavesConfig.config.get(verifiedConfig.path); -+ if (savedValue == null) { -+ throw new IllegalArgumentException("?"); -+ } -+ -+ if (savedValue.getClass() != validator.getFieldClass()) { -+ savedValue = validator.loadConvert(savedValue); -+ } -+ -+ validator.verify(null, savedValue); -+ -+ field.set(upstreamField, savedValue); -+ } catch (IllegalArgumentException | ClassCastException e) { -+ LeavesConfig.config.set(verifiedConfig.path, defValue); -+ LeavesLogger.LOGGER.warning(e.getMessage() + ", reset to " + defValue); -+ } -+ -+ verifiedConfigs.put(verifiedConfig.path.substring(CONFIG_START.length()), verifiedConfig); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.severe("Failure to load leaves config", e); -+ throw new RuntimeException(); -+ } -+ } -+ -+ public static VerifiedConfig getVerifiedConfig(String path) { -+ return verifiedConfigs.get(path); -+ } -+ -+ @Contract(pure = true) -+ public static @NotNull Set getVerifiedConfigPaths() { -+ return verifiedConfigs.keySet(); -+ } -+ -+ public record RemovedVerifiedConfig(ConfigConverter convert, boolean transform, Field field, Object upstreamField, String path) { -+ -+ public void run() { -+ if (transform) { -+ if (LeavesConfig.config.contains(path)) { -+ Object savedValue = LeavesConfig.config.get(path); -+ if (savedValue != null) { -+ try { -+ if (savedValue.getClass() != convert.getFieldClass()) { -+ savedValue = convert.loadConvert(savedValue); -+ } -+ field.set(upstreamField, savedValue); -+ } catch (IllegalAccessException | IllegalArgumentException e) { -+ LeavesLogger.LOGGER.warning("Failure to load leaves config" + path, e); -+ } -+ } else { -+ LeavesLogger.LOGGER.warning("Failed to convert saved value for " + path + ", reset to default"); -+ } -+ } -+ } -+ LeavesConfig.config.set(path, null); -+ } -+ -+ @Contract("_, _, _ -> new") -+ public static @NotNull RemovedVerifiedConfig build(@NotNull RemovedConfig config, @NotNull Field field, @Nullable Object upstreamField) { -+ StringBuilder path = new StringBuilder("settings."); -+ for (String category : config.category()) { -+ path.append(category).append("."); -+ } -+ path.append(config.name()); -+ -+ ConfigConverter converter = null; -+ try { -+ Constructor> constructor = config.convert().getDeclaredConstructor(); -+ constructor.setAccessible(true); -+ converter = constructor.newInstance(); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.warning("Failure to load leaves config" + path, e); -+ } -+ -+ return new RemovedVerifiedConfig(converter, config.transform(), field, upstreamField, path.toString()); -+ } -+ } -+ -+ public record VerifiedConfig(ConfigValidator validator, boolean lock, Field field, Object upstreamField, String path) { -+ -+ public void set(String stringValue) throws IllegalArgumentException { -+ Object value; -+ try { -+ value = validator.convert(stringValue); -+ } catch (IllegalArgumentException e) { -+ throw new IllegalArgumentException("value parse error: " + e.getMessage()); -+ } -+ -+ validator.verify(this.get(), value); -+ -+ try { -+ LeavesConfig.config.set(path, validator.saveConvert(value)); -+ LeavesConfig.save(); -+ if (lock) { -+ throw new IllegalArgumentException("locked, will load after restart"); -+ } -+ field.set(upstreamField, value); -+ } catch (IllegalAccessException e) { -+ throw new IllegalArgumentException(e.getMessage()); -+ } -+ } -+ -+ public Object get() { -+ try { -+ return field.get(upstreamField); -+ } catch (IllegalAccessException e) { -+ LeavesLogger.LOGGER.severe("Failure to get " + path + " value", e); -+ return ""; -+ } -+ } -+ -+ public String getString() { -+ Object value = this.get(); -+ -+ Object savedValue = LeavesConfig.config.get(path); -+ try { -+ if (savedValue != null) { -+ if (validator.getFieldClass() != savedValue.getClass()) { -+ savedValue = validator.loadConvert(savedValue); -+ } -+ -+ if (!savedValue.equals(value)) { -+ return value.toString() + "(" + savedValue + " after restart)"; -+ } -+ } -+ } catch (IllegalArgumentException e) { -+ LeavesLogger.LOGGER.severe("Failure to get " + path + " value", e); -+ } -+ return value.toString(); -+ } -+ -+ @SuppressWarnings("unchecked") -+ @NotNull -+ @Contract("_, _, _, _ -> new") -+ public static VerifiedConfig build(@NotNull GlobalConfig config, @NotNull Field field, @Nullable Object upstreamField, @NotNull String upstreamPath) { -+ String path = upstreamPath + config.value(); -+ -+ ConfigValidator validator; -+ try { -+ Constructor> constructor = config.validator().getDeclaredConstructor(); -+ constructor.setAccessible(true); -+ validator = (ConfigValidator) constructor.newInstance(); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.severe("Failure to load leaves config" + path, e); -+ throw new RuntimeException(); -+ } -+ -+ return new VerifiedConfig(validator, config.lock(), field, upstreamField, path); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java b/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a0364c2a5c4d56d30500233cd8294793309c7978 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/config/RemovedConfig.java -@@ -0,0 +1,27 @@ -+package org.leavesmc.leaves.config; -+ -+import java.lang.annotation.ElementType; -+import java.lang.annotation.Repeatable; -+import java.lang.annotation.Retention; -+import java.lang.annotation.RetentionPolicy; -+import java.lang.annotation.Target; -+ -+@Target(ElementType.FIELD) -+@Retention(RetentionPolicy.RUNTIME) -+@Repeatable(RemovedConfig.Array.class) -+public @interface RemovedConfig { -+ -+ String name(); -+ -+ String[] category(); -+ -+ boolean transform() default false; -+ -+ Class> convert() default ConfigValidatorImpl.BooleanConfigValidator.class; -+ -+ @Target(ElementType.FIELD) -+ @Retention(RetentionPolicy.RUNTIME) -+ @interface Array { -+ RemovedConfig[] value(); -+ } -+} diff --git a/patches/server/0007-Leaves-Protocol-Core.patch b/patches/server/0007-Leaves-Protocol-Core.patch deleted file mode 100644 index 85f34a95..00000000 --- a/patches/server/0007-Leaves-Protocol-Core.patch +++ /dev/null @@ -1,738 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 26 Sep 2023 19:00:41 +0800 -Subject: [PATCH] Leaves Protocol Core - - -diff --git a/src/main/java/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java b/src/main/java/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java -index 7655987d061bdb2839b30f926efb034046feaea3..7ae6b2bb868cc3d391bae87fa5e141cf64cc6c78 100644 ---- a/src/main/java/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java -+++ b/src/main/java/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java -@@ -40,13 +40,23 @@ public interface CustomPacketPayload { - - @Override - public void encode(B friendlyByteBuf, CustomPacketPayload customPacketPayload) { -+ // Leaves start - protocol core -+ if (customPacketPayload instanceof org.leavesmc.leaves.protocol.core.LeavesCustomPayload leavesCustomPayload) { -+ friendlyByteBuf.writeResourceLocation(leavesCustomPayload.id()); -+ leavesCustomPayload.write(friendlyByteBuf); -+ return; -+ } -+ // Leaves end - protocol core - this.writeCap(friendlyByteBuf, customPacketPayload.type(), customPacketPayload); - } - - @Override - public CustomPacketPayload decode(B friendlyByteBuf) { - ResourceLocation resourceLocation = friendlyByteBuf.readResourceLocation(); -- return (CustomPacketPayload)this.findCodec(resourceLocation).decode(friendlyByteBuf); -+ // Leaves start - protocol core -+ var leavesCustomPayload = org.leavesmc.leaves.protocol.core.LeavesProtocolManager.decode(resourceLocation, friendlyByteBuf); -+ return java.util.Objects.requireNonNullElseGet(leavesCustomPayload, () -> (CustomPacketPayload) this.findCodec(resourceLocation).decode(friendlyByteBuf)); -+ // Leaves end - protocol core - } - }; - } -diff --git a/src/main/java/net/minecraft/resources/ResourceLocation.java b/src/main/java/net/minecraft/resources/ResourceLocation.java -index 1967c43ee3a12e63365cc40ee6565307e2fd73cf..6e376d0db5321d8e9b6e0b54617ffd171bf4ee73 100644 ---- a/src/main/java/net/minecraft/resources/ResourceLocation.java -+++ b/src/main/java/net/minecraft/resources/ResourceLocation.java -@@ -36,7 +36,7 @@ public final class ResourceLocation implements Comparable { - private final String namespace; - private final String path; - -- private ResourceLocation(String namespace, String path) { -+ public ResourceLocation(String namespace, String path) { // Leaves - private -> public - assert isValidNamespace(namespace); - - assert isValidPath(path); -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 36f32afc62d9ab9ab48b4b5a25539333297dfa54..94ee464e8706245f37aed622a71907d6983a1636 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1894,6 +1894,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop leavesPayload) { -+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePayload(player, leavesPayload); -+ } -+ // Leaves end - protocol - // Paper start - Brand support - if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.BrandPayload brandPayload) { - this.player.clientBrandName = brandPayload.brand(); -@@ -174,6 +179,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack - String channels = payload.toString(com.google.common.base.Charsets.UTF_8); - for (String channel : channels.split("\0")) { - this.getCraftPlayer().addChannel(channel); -+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleMinecraftRegister(channel, player); // Leaves - protocol - } - } catch (Exception ex) { - ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t register custom payload", ex); -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 1a956249828156fdc273888de59128c3d1a0b898..e0eb02f60fd29610c7c5fde1123801ed890bd903 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -353,6 +353,8 @@ public abstract class PlayerList { - return; - } - -+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol -+ - final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); - - if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure -@@ -539,6 +541,7 @@ public abstract class PlayerList { - return this.remove(entityplayer, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(entityplayer.getDisplayName()))); - } - public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer, net.kyori.adventure.text.Component leaveMessage) { -+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerLeave(entityplayer); // Leaves - protocol - // Paper end - Fix kick event leave message not being sent - ServerLevel worldserver = entityplayer.serverLevel(); - -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 3d18ffbf3604705d8b99f69df156392dfed1863b..3435629606def598ca998fb41ef303677c4cbea4 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -491,6 +491,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 -+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.init(); // Leaves - protocol - } - - public boolean getCommandBlockOverride(String command) { -@@ -1121,6 +1122,7 @@ public final class CraftServer implements Server { - this.spark.registerCommandBeforePlugins(this); // Paper - spark - this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); - this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); -+ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleServerReload(); // Leaves - protocol - - int pollCount = 0; - -diff --git a/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java b/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b09a7bfe4cbff89acfaf4aec498a5b4e5f911cf6 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java -@@ -0,0 +1,29 @@ -+package org.leavesmc.leaves.protocol.core; -+ -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.protocol.common.custom.CustomPacketPayload; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.NotNull; -+ -+import java.lang.annotation.ElementType; -+import java.lang.annotation.Retention; -+import java.lang.annotation.RetentionPolicy; -+import java.lang.annotation.Target; -+ -+public interface LeavesCustomPayload> extends CustomPacketPayload { -+ -+ @Target({ElementType.METHOD, ElementType.CONSTRUCTOR}) -+ @Retention(RetentionPolicy.RUNTIME) -+ @interface New { -+ } -+ -+ void write(FriendlyByteBuf buf); -+ -+ ResourceLocation id(); -+ -+ @Override -+ @NotNull -+ default Type type() { -+ return new CustomPacketPayload.Type<>(id()); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..986d2a6641ff8017dddf3e5f2655adfc2866c119 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java -@@ -0,0 +1,12 @@ -+package org.leavesmc.leaves.protocol.core; -+ -+import java.lang.annotation.ElementType; -+import java.lang.annotation.Retention; -+import java.lang.annotation.RetentionPolicy; -+import java.lang.annotation.Target; -+ -+@Target(ElementType.TYPE) -+@Retention(RetentionPolicy.RUNTIME) -+public @interface LeavesProtocol { -+ String[] namespace(); -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java b/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ea94da3a2332cf23fc8753129e546a6f5a5af395 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java -@@ -0,0 +1,434 @@ -+package org.leavesmc.leaves.protocol.core; -+ -+import com.google.common.collect.ImmutableSet; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.chat.Component; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.level.ServerPlayer; -+import org.apache.commons.lang.ArrayUtils; -+import org.bukkit.event.player.PlayerKickEvent; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesLogger; -+ -+import java.io.File; -+import java.io.IOException; -+import java.lang.reflect.Constructor; -+import java.lang.reflect.Executable; -+import java.lang.reflect.InvocationTargetException; -+import java.lang.reflect.Method; -+import java.lang.reflect.Modifier; -+import java.net.JarURLConnection; -+import java.net.URL; -+import java.net.URLDecoder; -+import java.nio.charset.StandardCharsets; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Collections; -+import java.util.Enumeration; -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Iterator; -+import java.util.LinkedHashSet; -+import java.util.List; -+import java.util.Map; -+import java.util.Set; -+import java.util.jar.JarEntry; -+import java.util.jar.JarFile; -+ -+public class LeavesProtocolManager { -+ -+ private static final Class[] PAYLOAD_PARAMETER_TYPES = {ResourceLocation.class, FriendlyByteBuf.class}; -+ -+ private static final LeavesLogger LOGGER = LeavesLogger.LOGGER; -+ -+ private static final Map> KNOWN_TYPES = new HashMap<>(); -+ private static final Map> KNOW_RECEIVERS = new HashMap<>(); -+ private static Set ALL_KNOWN_ID = new HashSet<>(); -+ -+ private static final List TICKERS = new ArrayList<>(); -+ private static final List PLAYER_JOIN = new ArrayList<>(); -+ private static final List PLAYER_LEAVE = new ArrayList<>(); -+ private static final List RELOAD_SERVER = new ArrayList<>(); -+ private static final Map> MINECRAFT_REGISTER = new HashMap<>(); -+ -+ public static void init() { -+ for (Class clazz : getClasses("org.leavesmc.leaves.protocol")) { -+ final LeavesProtocol protocol = clazz.getAnnotation(LeavesProtocol.class); -+ if (protocol != null) { -+ Set methods; -+ try { -+ Method[] publicMethods = clazz.getMethods(); -+ Method[] privateMethods = clazz.getDeclaredMethods(); -+ methods = new HashSet<>(publicMethods.length + privateMethods.length, 1.0f); -+ Collections.addAll(methods, publicMethods); -+ Collections.addAll(methods, privateMethods); -+ } catch (NoClassDefFoundError error) { -+ LOGGER.severe("Failed to load class " + clazz.getName() + " due to missing dependencies, " + error.getCause() + ": " + error.getMessage()); -+ return; -+ } -+ -+ Map map = KNOWN_TYPES.getOrDefault(protocol, new HashMap<>()); -+ for (final Method method : methods) { -+ if (method.isBridge() || method.isSynthetic() || !Modifier.isStatic(method.getModifiers())) { -+ continue; -+ } -+ -+ method.setAccessible(true); -+ -+ final ProtocolHandler.Init init = method.getAnnotation(ProtocolHandler.Init.class); -+ if (init != null) { -+ try { -+ method.invoke(null); -+ } catch (InvocationTargetException | IllegalAccessException exception) { -+ LOGGER.severe("Failed to invoke init method " + method.getName() + " in " + clazz.getName() + ", " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ continue; -+ } -+ -+ final ProtocolHandler.PayloadReceiver receiver = method.getAnnotation(ProtocolHandler.PayloadReceiver.class); -+ if (receiver != null) { -+ try { -+ boolean found = false; -+ for (Method payloadMethod : receiver.payload().getDeclaredMethods()) { -+ if (payloadMethod.isAnnotationPresent(LeavesCustomPayload.New.class)) { -+ if (Arrays.equals(payloadMethod.getParameterTypes(), PAYLOAD_PARAMETER_TYPES) && payloadMethod.getReturnType() == receiver.payload() && Modifier.isStatic(payloadMethod.getModifiers())) { -+ payloadMethod.setAccessible(true); -+ map.put(receiver, payloadMethod); -+ found = true; -+ break; -+ } -+ } -+ } -+ -+ if (!found) { -+ Constructor> constructor = receiver.payload().getConstructor(PAYLOAD_PARAMETER_TYPES); -+ if (constructor.isAnnotationPresent(LeavesCustomPayload.New.class)) { -+ constructor.setAccessible(true); -+ map.put(receiver, constructor); -+ } else { -+ throw new NoSuchMethodException(); -+ } -+ } -+ } catch (NoSuchMethodException exception) { -+ LOGGER.severe("Failed to find constructor for " + receiver.payload().getName() + ", " + exception.getCause() + ": " + exception.getMessage()); -+ continue; -+ } -+ -+ if (!KNOW_RECEIVERS.containsKey(protocol)) { -+ KNOW_RECEIVERS.put(protocol, new HashMap<>()); -+ } -+ -+ KNOW_RECEIVERS.get(protocol).put(receiver, method); -+ continue; -+ } -+ -+ final ProtocolHandler.Ticker ticker = method.getAnnotation(ProtocolHandler.Ticker.class); -+ if (ticker != null) { -+ TICKERS.add(method); -+ continue; -+ } -+ -+ final ProtocolHandler.PlayerJoin playerJoin = method.getAnnotation(ProtocolHandler.PlayerJoin.class); -+ if (playerJoin != null) { -+ PLAYER_JOIN.add(method); -+ continue; -+ } -+ -+ final ProtocolHandler.PlayerLeave playerLeave = method.getAnnotation(ProtocolHandler.PlayerLeave.class); -+ if (playerLeave != null) { -+ PLAYER_LEAVE.add(method); -+ continue; -+ } -+ -+ final ProtocolHandler.ReloadServer reloadServer = method.getAnnotation(ProtocolHandler.ReloadServer.class); -+ if (reloadServer != null) { -+ RELOAD_SERVER.add(method); -+ continue; -+ } -+ -+ final ProtocolHandler.MinecraftRegister minecraftRegister = method.getAnnotation(ProtocolHandler.MinecraftRegister.class); -+ if (minecraftRegister != null) { -+ if (!MINECRAFT_REGISTER.containsKey(protocol)) { -+ MINECRAFT_REGISTER.put(protocol, new HashMap<>()); -+ } -+ -+ MINECRAFT_REGISTER.get(protocol).put(minecraftRegister, method); -+ } -+ } -+ KNOWN_TYPES.put(protocol, map); -+ } -+ } -+ -+ for (LeavesProtocol protocol : KNOWN_TYPES.keySet()) { -+ Map map = KNOWN_TYPES.get(protocol); -+ for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) { -+ if (receiver.sendFabricRegister() && !receiver.ignoreId()) { -+ for (String payloadId : receiver.payloadId()) { -+ for (String namespace : protocol.namespace()) { -+ ALL_KNOWN_ID.add(new ResourceLocation(namespace, payloadId)); -+ } -+ } -+ } -+ } -+ } -+ ALL_KNOWN_ID = ImmutableSet.copyOf(ALL_KNOWN_ID); -+ } -+ -+ public static LeavesCustomPayload decode(ResourceLocation id, FriendlyByteBuf buf) { -+ for (LeavesProtocol protocol : KNOWN_TYPES.keySet()) { -+ if (!ArrayUtils.contains(protocol.namespace(), id.getNamespace())) { -+ continue; -+ } -+ -+ Map map = KNOWN_TYPES.get(protocol); -+ for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) { -+ if (receiver.ignoreId() || ArrayUtils.contains(receiver.payloadId(), id.getPath())) { -+ try { -+ if (map.get(receiver) instanceof Constructor constructor) { -+ return (LeavesCustomPayload) constructor.newInstance(id, buf); -+ } else if (map.get(receiver) instanceof Method method) { -+ return (LeavesCustomPayload) method.invoke(null, id, buf); -+ } -+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException exception) { -+ LOGGER.warning("Failed to create payload for " + id + " in " + ArrayUtils.toString(protocol.namespace()) + ", " + exception.getCause() + ": " + exception.getMessage()); -+ buf.readBytes(buf.readableBytes()); -+ return new ErrorPayload(id, protocol.namespace(), receiver.payloadId()); -+ } -+ } -+ } -+ } -+ return null; -+ } -+ -+ public static void handlePayload(ServerPlayer player, LeavesCustomPayload payload) { -+ if (payload instanceof ErrorPayload errorPayload) { -+ player.connection.disconnect(Component.literal("Payload " + Arrays.toString(errorPayload.packetID) + " from " + Arrays.toString(errorPayload.protocolID) + " error"), PlayerKickEvent.Cause.INVALID_PAYLOAD); -+ return; -+ } -+ -+ for (LeavesProtocol protocol : KNOW_RECEIVERS.keySet()) { -+ if (!ArrayUtils.contains(protocol.namespace(), payload.type().id().getNamespace())) { -+ continue; -+ } -+ -+ Map map = KNOW_RECEIVERS.get(protocol); -+ for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) { -+ if (payload.getClass() == receiver.payload()) { -+ if (receiver.ignoreId() || ArrayUtils.contains(receiver.payloadId(), payload.type().id().getPath())) { -+ try { -+ map.get(receiver).invoke(null, player, payload); -+ } catch (InvocationTargetException | IllegalAccessException exception) { -+ LOGGER.warning("Failed to handle payload " + payload.type().id() + " in " + ArrayUtils.toString(protocol.namespace()) + ", " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ public static void handleTick() { -+ if (!TICKERS.isEmpty()) { -+ try { -+ for (Method method : TICKERS) { -+ method.invoke(null); -+ } -+ } catch (InvocationTargetException | IllegalAccessException exception) { -+ LOGGER.warning("Failed to tick, " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ } -+ -+ public static void handlePlayerJoin(ServerPlayer player) { -+ if (!PLAYER_JOIN.isEmpty()) { -+ try { -+ for (Method method : PLAYER_JOIN) { -+ method.invoke(null, player); -+ } -+ } catch (InvocationTargetException | IllegalAccessException exception) { -+ LOGGER.warning("Failed to handle player join, " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ -+ ProtocolUtils.sendPayloadPacket(player, new FabricRegisterPayload(ALL_KNOWN_ID)); -+ } -+ -+ public static void handlePlayerLeave(ServerPlayer player) { -+ if (!PLAYER_LEAVE.isEmpty()) { -+ try { -+ for (Method method : PLAYER_LEAVE) { -+ method.invoke(null, player); -+ } -+ } catch (InvocationTargetException | IllegalAccessException exception) { -+ LOGGER.warning("Failed to handle player leave, " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ } -+ -+ public static void handleServerReload() { -+ if (!RELOAD_SERVER.isEmpty()) { -+ try { -+ for (Method method : RELOAD_SERVER) { -+ method.invoke(null); -+ } -+ } catch (InvocationTargetException | IllegalAccessException exception) { -+ LOGGER.warning("Failed to handle server reload, " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ } -+ -+ public static void handleMinecraftRegister(String channelId, ServerPlayer player) { -+ for (LeavesProtocol protocol : MINECRAFT_REGISTER.keySet()) { -+ String[] channel = channelId.split(":"); -+ if (!ArrayUtils.contains(protocol.namespace(), channel[0])) { -+ continue; -+ } -+ -+ Map map = MINECRAFT_REGISTER.get(protocol); -+ for (ProtocolHandler.MinecraftRegister register : map.keySet()) { -+ if (register.ignoreId() || ArrayUtils.contains(register.channelId(), channel[1])) { -+ try { -+ map.get(register).invoke(null, player, channel[1]); -+ } catch (InvocationTargetException | IllegalAccessException exception) { -+ LOGGER.warning("Failed to handle minecraft register, " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ } -+ } -+ } -+ -+ public static Set> getClasses(String pack) { -+ Set> classes = new LinkedHashSet<>(); -+ String packageDirName = pack.replace('.', '/'); -+ Enumeration dirs; -+ try { -+ dirs = Thread.currentThread().getContextClassLoader().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 exception) { -+ LOGGER.warning("Failed to load jar file, " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ } -+ } catch (IOException exception) { -+ LOGGER.warning("Failed to load classes, " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ 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 exception) { -+ LOGGER.warning("Failed to load class " + className + ", " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ } -+ } -+ } -+ -+ 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 exception) { -+ LOGGER.warning("Failed to load class " + className + ", " + exception.getCause() + ": " + exception.getMessage()); -+ } -+ } -+ } -+ } -+ } -+ -+ public record ErrorPayload(ResourceLocation id, String[] protocolID, String[] packetID) implements LeavesCustomPayload { -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ } -+ } -+ -+ public record EmptyPayload(ResourceLocation id) implements LeavesCustomPayload { -+ -+ @New -+ public EmptyPayload(ResourceLocation location, FriendlyByteBuf buf) { -+ this(location); -+ } -+ -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ } -+ } -+ -+ public record LeavesPayload(FriendlyByteBuf data, ResourceLocation id) implements LeavesCustomPayload { -+ -+ @New -+ public LeavesPayload(ResourceLocation location, FriendlyByteBuf buf) { -+ this(new FriendlyByteBuf(buf.readBytes(buf.readableBytes())), location); -+ } -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ buf.writeBytes(data); -+ } -+ } -+ -+ public record FabricRegisterPayload(Set channels) implements LeavesCustomPayload { -+ -+ public static final ResourceLocation CHANNEL = ResourceLocation.withDefaultNamespace("register"); -+ -+ @New -+ public FabricRegisterPayload(ResourceLocation location, FriendlyByteBuf buf) { -+ this(buf.readCollection(HashSet::new, FriendlyByteBuf::readResourceLocation)); -+ } -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ boolean first = true; -+ -+ ResourceLocation channel; -+ for (Iterator var3 = this.channels.iterator(); var3.hasNext(); buf.writeBytes(channel.toString().getBytes(StandardCharsets.US_ASCII))) { -+ channel = var3.next(); -+ if (first) { -+ first = false; -+ } else { -+ buf.writeByte(0); -+ } -+ } -+ } -+ -+ @Override -+ public ResourceLocation id() { -+ return CHANNEL; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java b/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..202fb0717734aa8948ff8307420bf9aaf77d63ca ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java -@@ -0,0 +1,55 @@ -+package org.leavesmc.leaves.protocol.core; -+ -+import java.lang.annotation.ElementType; -+import java.lang.annotation.Retention; -+import java.lang.annotation.RetentionPolicy; -+import java.lang.annotation.Target; -+ -+public class ProtocolHandler { -+ -+ @Target(ElementType.METHOD) -+ @Retention(RetentionPolicy.RUNTIME) -+ public @interface Init { -+ } -+ -+ @Target(ElementType.METHOD) -+ @Retention(RetentionPolicy.RUNTIME) -+ public @interface PayloadReceiver { -+ Class> payload(); -+ -+ String[] payloadId() default ""; -+ -+ boolean ignoreId() default false; -+ -+ boolean sendFabricRegister() default true; -+ } -+ -+ @Target(ElementType.METHOD) -+ @Retention(RetentionPolicy.RUNTIME) -+ public @interface Ticker { -+ int delay() default 0; -+ } -+ -+ @Target(ElementType.METHOD) -+ @Retention(RetentionPolicy.RUNTIME) -+ public @interface PlayerJoin { -+ } -+ -+ @Target(ElementType.METHOD) -+ @Retention(RetentionPolicy.RUNTIME) -+ public @interface PlayerLeave { -+ } -+ -+ @Target(ElementType.METHOD) -+ @Retention(RetentionPolicy.RUNTIME) -+ public @interface ReloadServer { -+ } -+ -+ @Target(ElementType.METHOD) -+ @Retention(RetentionPolicy.RUNTIME) -+ public @interface MinecraftRegister { -+ String[] channelId() default ""; -+ -+ boolean ignoreId() default false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java b/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java -new file mode 100644 -index 0000000000000000000000000000000000000000..72fb1e6517e266146cb1828875e7b7e6ae9b140d ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java -@@ -0,0 +1,52 @@ -+package org.leavesmc.leaves.protocol.core; -+ -+import io.netty.buffer.ByteBuf; -+import io.papermc.paper.ServerBuildInfo; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; -+import net.minecraft.network.protocol.common.custom.CustomPacketPayload; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.function.Consumer; -+import java.util.function.Function; -+ -+public class ProtocolUtils { -+ -+ private static final Function bufDecorator = RegistryFriendlyByteBuf.decorator(MinecraftServer.getServer().registryAccess()); -+ -+ public static String buildProtocolVersion(String protocol) { -+ return protocol + "-leaves-" + ServerBuildInfo.buildInfo().asString(ServerBuildInfo.StringRepresentation.VERSION_SIMPLE); -+ } -+ -+ public static void sendEmptyPayloadPacket(ServerPlayer player, ResourceLocation id) { -+ player.connection.send(new ClientboundCustomPayloadPacket(new LeavesProtocolManager.EmptyPayload(id))); -+ } -+ -+ @SuppressWarnings("all") -+ public static void sendPayloadPacket(@NotNull ServerPlayer player, ResourceLocation id, Consumer consumer) { -+ player.connection.send(new ClientboundCustomPayloadPacket(new LeavesCustomPayload() { -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ consumer.accept(buf); -+ } -+ -+ @Override -+ @NotNull -+ public ResourceLocation id() { -+ return id; -+ } -+ })); -+ } -+ -+ public static void sendPayloadPacket(ServerPlayer player, CustomPacketPayload payload) { -+ player.connection.send(new ClientboundCustomPayloadPacket(payload)); -+ } -+ -+ public static RegistryFriendlyByteBuf decorate(ByteBuf buf) { -+ return bufDecorator.apply(buf); -+ } -+} diff --git a/patches/server/0010-Fakeplayer-support.patch b/patches/server/0010-Fakeplayer-support.patch deleted file mode 100644 index 926a6394..00000000 --- a/patches/server/0010-Fakeplayer-support.patch +++ /dev/null @@ -1,4736 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 3 Feb 2022 12:28:15 +0800 -Subject: [PATCH] Fakeplayer support - - -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 a1c9726d25479b5326fe2fa2b0f5a98d6b2da4c5..0724bd95143cb5dc69b5f1eb2e67ecd679e09a99 100644 ---- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java -+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java -@@ -41,6 +41,12 @@ class PaperEventManager { - throw new IllegalStateException(event.getEventName() + " may only be triggered synchronously."); - } - -+ // Leaves start - skip bot -+ if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Bot) { -+ return; -+ } -+ // Leaves end - skip bot -+ - HandlerList handlers = event.getHandlers(); - RegisteredListener[] listeners = handlers.getRegisteredListeners(); - -diff --git a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java -index 35772110e9318df46a2729dbc0b5879b290011b7..f26989a44cdda9baabf337d573436c6c115c9884 100644 ---- a/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java -+++ b/src/main/java/net/minecraft/advancements/critereon/SimpleCriterionTrigger.java -@@ -39,6 +39,7 @@ public abstract class SimpleCriterionTrigger predicate) { -+ if (player instanceof org.leavesmc.leaves.bot.ServerBot) return; // Leaves - bot skip - PlayerAdvancements playerAdvancements = player.getAdvancements(); - Set> set = (Set) playerAdvancements.criterionData.get(this); // Paper - fix AdvancementDataPlayer leak - if (set != null && !set.isEmpty()) { -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index e693a003ea8f022eef8b49e4332025b769333b30..49e03ba7c04381e263aaee5cda9ed6c042bf6c0e 100644 ---- a/src/main/java/net/minecraft/network/Connection.java -+++ b/src/main/java/net/minecraft/network/Connection.java -@@ -104,7 +104,7 @@ public class Connection extends SimpleChannelInboundHandler> { - @Nullable - private volatile PacketListener disconnectListener; - @Nullable -- private volatile PacketListener packetListener; -+ protected volatile PacketListener packetListener; // Leaves - private -> protected - @Nullable - private DisconnectionDetails disconnectionDetails; - private boolean encrypted; -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 94ee464e8706245f37aed622a71907d6983a1636..186813428916c93545b7bad706b8584a4e338627 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -331,6 +331,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginsBlockingSleep = new java.util.HashSet<>(); // Paper - API to allow/disallow tick sleeping - -+ private org.leavesmc.leaves.bot.BotList botList; // Leaves - fakeplayer -+ - public static S spin(Function serverFactory) { - AtomicReference atomicreference = new AtomicReference(); - Thread thread = new ca.spottedleaf.moonrise.common.util.TickThread(() -> { // Paper - rewrite chunk system -@@ -762,6 +764,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop realPlayers; // Leaves - skip - - public LevelChunk getChunkIfLoaded(int x, int z) { - return this.chunkSource.getChunkAtIfLoadedImmediately(x, z); // Paper - Use getChunkIfLoadedImmediately -@@ -676,6 +677,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - this.chunkDataController = new ca.spottedleaf.moonrise.patches.chunk_system.io.datacontroller.ChunkDataController((ServerLevel)(Object)this, this.chunkTaskScheduler); - // Paper end - rewrite chunk system - this.getCraftServer().addWorld(this.getWorld()); // CraftBukkit -+ this.realPlayers = Lists.newArrayList(); // Leaves - skip - } - - // Paper start -@@ -2187,6 +2189,12 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - return this.players; - } - -+ // Leaves start - fakeplayer skip -+ public List realPlayers() { -+ return this.realPlayers; -+ } -+ // Leaves end - fakeplayer skip -+ - @Override - public void onBlockStateChange(BlockPos pos, BlockState oldBlock, BlockState newBlock) { - Optional> optional = PoiTypes.forState(oldBlock); -@@ -2693,6 +2701,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - // 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 entityplayer) { - ServerLevel.this.players.add(entityplayer); -+ // Leaves start - skip -+ if (!(entityplayer instanceof org.leavesmc.leaves.bot.ServerBot)) { -+ ServerLevel.this.realPlayers.add(entityplayer); -+ } -+ // Leaves end - skip - ServerLevel.this.updateSleepingPlayerList(); - } - -@@ -2770,6 +2783,11 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - ServerLevel.this.getChunkSource().removeEntity(entity); - if (entity instanceof ServerPlayer entityplayer) { - ServerLevel.this.players.remove(entityplayer); -+ // Leaves start - skip -+ if (!(entityplayer instanceof org.leavesmc.leaves.bot.ServerBot)) { -+ ServerLevel.this.realPlayers.remove(entityplayer); -+ } -+ // Leaves end - skip - ServerLevel.this.updateSleepingPlayerList(); - } - -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index 5a8f396d47577f087abb415c972fd4f51e50faba..ebae6c704844755c75aa0c6f460603c5d909b5cf 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -234,7 +234,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - private static final AttributeModifier CREATIVE_ENTITY_INTERACTION_RANGE_MODIFIER = new AttributeModifier(ResourceLocation.withDefaultNamespace("creative_mode_entity_range"), 2.0D, AttributeModifier.Operation.ADD_VALUE); - public ServerGamePacketListenerImpl connection; - public final MinecraftServer server; -- public final ServerPlayerGameMode gameMode; -+ public ServerPlayerGameMode gameMode; // Leaves - final -> null - private final PlayerAdvancements advancements; - private final ServerStatsCounter stats; - private float lastRecordedHealthAndAbsorption = Float.MIN_VALUE; -@@ -998,16 +998,20 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - --this.invulnerableTime; - } - -- // Paper start - Configurable container update tick rate -- if (--containerUpdateDelay <= 0) { -- this.containerMenu.broadcastChanges(); -- containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate; -- } -- // Paper end - Configurable container update tick rate -- if (!this.level().isClientSide && this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen -- this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason -- this.containerMenu = this.inventoryMenu; -+ // Leaves start - skip bot -+ if (!(this instanceof org.leavesmc.leaves.bot.ServerBot)) { -+ // Paper start - Configurable container update tick rate -+ if (--containerUpdateDelay <= 0) { -+ this.containerMenu.broadcastChanges(); -+ containerUpdateDelay = this.level().paperConfig().tickRates.containerUpdate; -+ } -+ // Paper end - Configurable container update tick rate -+ if (!this.level().isClientSide && this.containerMenu != this.inventoryMenu && (this.isImmobile() || !this.containerMenu.stillValid(this))) { // Paper - Prevent opening inventories when frozen -+ this.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.CANT_USE); // Paper - Inventory close reason -+ this.containerMenu = this.inventoryMenu; -+ } - } -+ // Leaves end - skip bot - - Entity entity = this.getCamera(); - -@@ -1023,7 +1027,7 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - } - } - -- CriteriaTriggers.TICK.trigger(this); -+ if (!(this instanceof org.leavesmc.leaves.bot.ServerBot)) CriteriaTriggers.TICK.trigger(this); // Leaves - skip bot - if (this.levitationStartPos != null) { - CriteriaTriggers.LEVITATION.trigger(this, this.levitationStartPos, this.tickCount - this.levitationStartTime); - } -@@ -1680,6 +1684,12 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - this.lastSentHealth = -1.0F; - this.lastSentFood = -1; - -+ // Leaves start - bot support -+ if (org.leavesmc.leaves.LeavesConfig.modify.fakeplayer.enable) { -+ this.server.getBotList().bots.forEach(bot -> bot.sendFakeDataIfNeed(this, true)); // Leaves - render bot -+ } -+ // Leaves end - bot support -+ - // CraftBukkit start - PlayerChangedWorldEvent changeEvent = new PlayerChangedWorldEvent(this.getBukkitEntity(), worldserver1.getWorld()); - this.level().getCraftServer().getPluginManager().callEvent(changeEvent); -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index e0eb02f60fd29610c7c5fde1123801ed890bd903..bc8764fcf0aedcf11d4826738a89300262cf299c 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -124,6 +124,8 @@ import org.bukkit.event.player.PlayerRespawnEvent.RespawnReason; - import org.bukkit.event.player.PlayerSpawnChangeEvent; - // CraftBukkit end - -+import org.leavesmc.leaves.bot.ServerBot; -+ - public abstract class PlayerList { - - public static final File USERBANLIST_FILE = new File("banned-players.json"); -@@ -355,6 +357,19 @@ public abstract class PlayerList { - - org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol - -+ // Leaves start - bot support -+ if (org.leavesmc.leaves.LeavesConfig.modify.fakeplayer.enable) { -+ ServerBot bot = this.server.getBotList().getBotByName(player.getScoreboardName()); -+ if (bot != null) { -+ this.server.getBotList().removeBot(bot, org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.INTERNAL, player.getBukkitEntity(), false); -+ } -+ this.server.getBotList().bots.forEach(bot1 -> { -+ bot1.sendPlayerInfo(player); -+ bot1.sendFakeDataIfNeed(player, true); -+ }); // Leaves - render bot -+ } -+ // Leaves end - bot support -+ - final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); - - if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure -@@ -907,6 +922,12 @@ public abstract class PlayerList { - } - // Paper end - Add PlayerPostRespawnEvent - -+ // Leaves start - bot support -+ if (org.leavesmc.leaves.LeavesConfig.modify.fakeplayer.enable) { -+ this.server.getBotList().bots.forEach(bot -> bot.sendFakeDataIfNeed(entityplayer1, true)); // Leaves - render bot -+ } -+ // Leaves end - bot support -+ - // CraftBukkit end - - return entityplayer1; -@@ -1044,11 +1065,16 @@ public abstract class PlayerList { - } - - public String[] getPlayerNamesArray() { -- String[] astring = new String[this.players.size()]; -+ String[] astring = new String[this.players.size() + this.server.getBotList().bots.size()]; // Leaves - fakeplayer support - - for (int i = 0; i < this.players.size(); ++i) { - astring[i] = ((ServerPlayer) this.players.get(i)).getGameProfile().getName(); - } -+ // Leaves start - fakeplayer support -+ for (int i = this.players.size(); i < astring.length; ++i) { -+ astring[i] = ((ServerPlayer) this.server.getBotList().bots.get(i - this.players.size())).getGameProfile().getName(); -+ } -+ // Leaves end - fakeplayer support - - return astring; - } -@@ -1135,7 +1161,13 @@ public abstract class PlayerList { - - @Nullable - public ServerPlayer getPlayerByName(String name) { -- return this.playersByName.get(name.toLowerCase(java.util.Locale.ROOT)); // Spigot -+ // Leaves start - fakeplayer support -+ ServerPlayer player = this.playersByName.get(name.toLowerCase(java.util.Locale.ROOT)); -+ if (player == null) { -+ player = this.server.getBotList().getBotByName(name); -+ } -+ return player; // Spigot -+ // Leaves end - fakeplayer support - } - - public void broadcast(@Nullable net.minecraft.world.entity.player.Player player, double x, double y, double z, double distance, ResourceKey worldKey, Packet packet) { -@@ -1476,7 +1508,13 @@ public abstract class PlayerList { - - @Nullable - public ServerPlayer getPlayer(UUID uuid) { -- return (ServerPlayer) this.playersByUUID.get(uuid); -+ // Leaves start - fakeplayer support -+ ServerPlayer player = this.playersByUUID.get(uuid); -+ if (player == null) { -+ player = this.server.getBotList().getBot(uuid); -+ } -+ return player; -+ // Leaves start - fakeplayer support - } - - public boolean canBypassPlayerLimit(GameProfile profile) { -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 922996adcf2b85e55a934191e90a12c42f169b0f..7e61b008c9429f6ef60fcef4887bb50db4face91 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -1522,7 +1522,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return offsetFactor; - } - -- private Vec3 collide(Vec3 movement) { -+ public Vec3 collide(Vec3 movement) { // Leaves - private -> public - // Paper start - optimise collisions - final boolean xZero = movement.x == 0.0; - final boolean yZero = movement.y == 0.0; -diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index 61d412c4f1ebd55661cc3f0260468e3ac0efe0bb..c7bc8f493305cca9bac60c79e4e35cf5e369d355 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Player.java -+++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -183,7 +183,7 @@ public abstract class Player extends LivingEntity { - private int lastLevelUpTime; - public GameProfile gameProfile; - private boolean reducedDebugInfo; -- private ItemStack lastItemInMainHand; -+ protected ItemStack lastItemInMainHand; // Leaves - private -> protected - private final ItemCooldowns cooldowns; - private Optional lastDeathLocation; - @Nullable -@@ -340,6 +340,12 @@ public abstract class Player extends LivingEntity { - - } - -+ // Leaves start - fakeplayer -+ protected void livingEntityTick() { -+ super.tick(); -+ } -+ // Leaves end - fakeplayer -+ - @Override - protected float getMaxHeadRotationRelativeToBody() { - return this.isBlocking() ? 15.0F : super.getMaxHeadRotationRelativeToBody(); -@@ -661,7 +667,7 @@ public abstract class Player extends LivingEntity { - - } - -- private void touch(Entity entity) { -+ public void touch(Entity entity) { // Leaves - private -> public - entity.playerTouch(this); - } - -diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -index 4daa69c6be6d48563e30343a7e40e4da9ec7e5ad..d5cb0d8ad7f7ed18ce38b39f245f5ec2c67042d5 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -@@ -63,7 +63,7 @@ public class FishingHook extends Projectile { - public static final EntityDataAccessor DATA_HOOKED_ENTITY = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.INT); - private static final EntityDataAccessor DATA_BITING = SynchedEntityData.defineId(FishingHook.class, EntityDataSerializers.BOOLEAN); - private int life; -- private int nibble; -+ public int nibble; // Leaves - private -> public - public int timeUntilLured; - public int timeUntilHooked; - public float fishAngle; -diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -index 4680f77a275d8d2b226018db89a571ac25998dd8..a21c658343ab6e1eb3a98ff10369b490bd7d52da 100644 ---- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -@@ -443,6 +443,8 @@ public abstract class AbstractContainerMenu { - ItemStack itemstack1; - int l; - -+ if (!doClickCheck(slotIndex, button, actionType, player)) return; // Leaves - doClick check -+ - if (actionType == ClickType.QUICK_CRAFT) { - int i1 = this.quickcraftStatus; - -@@ -733,6 +735,22 @@ public abstract class AbstractContainerMenu { - - } - -+ // Leaves start - doClick check -+ private boolean doClickCheck(int slotIndex, int button, ClickType actionType, Player player) { -+ if (slotIndex < 0) { -+ return true; -+ } -+ -+ Slot slot = getSlot(slotIndex); -+ ItemStack itemStack = slot.getItem(); -+ net.minecraft.world.item.component.CustomData customData = itemStack.get(net.minecraft.core.component.DataComponents.CUSTOM_DATA); -+ if (customData != null && customData.contains("Leaves.Gui.Placeholder")) { -+ return !customData.copyTag().getBoolean("Leaves.Gui.Placeholder"); -+ } -+ return true; -+ } -+ // Leaves end - doClick check -+ - private boolean tryItemClickBehaviourOverride(Player player, ClickAction clickType, Slot slot, ItemStack stack, ItemStack cursorStack) { - FeatureFlagSet featureflagset = player.level().enabledFeatures(); - -diff --git a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java -index 021221da5d0315f6e371380a705ac6b3f6ac18d3..9c13b31e3a0ce6542ef2b2dd2361b906fdb359b8 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/PhantomSpawner.java -@@ -67,6 +67,11 @@ public class PhantomSpawner implements CustomSpawner { - ServerStatsCounter serverstatisticmanager = entityplayer.getStats(); - int j = Mth.clamp(serverstatisticmanager.getValue(Stats.CUSTOM.get(Stats.TIME_SINCE_REST)), 1, Integer.MAX_VALUE); - boolean flag2 = true; -+ // Leaves start - fakeplayer spawn -+ if (entityplayer instanceof org.leavesmc.leaves.bot.ServerBot bot && bot.getConfigValue(org.leavesmc.leaves.bot.agent.Configs.SPAWN_PHANTOM)) { -+ j = Math.max(bot.notSleepTicks, 1); -+ } -+ // Leaves end - fakeplayer spawn - - if (randomsource.nextInt(j) >= world.paperConfig().entities.behavior.playerInsomniaStartTicks) { // Paper - Ability to control player's insomnia and phantoms - BlockPos blockposition1 = blockposition.above(20 + randomsource.nextInt(15)).east(-10 + randomsource.nextInt(21)).south(-10 + randomsource.nextInt(21)); -diff --git a/src/main/java/net/minecraft/world/level/storage/LevelResource.java b/src/main/java/net/minecraft/world/level/storage/LevelResource.java -index fee8367d2812db559b15970f0a60023bedaaefc5..f6b59b00bb1611aff8d161d1ad03df7fc911f994 100644 ---- a/src/main/java/net/minecraft/world/level/storage/LevelResource.java -+++ b/src/main/java/net/minecraft/world/level/storage/LevelResource.java -@@ -15,7 +15,7 @@ public class LevelResource { - public static final LevelResource ROOT = new LevelResource("."); - private final String id; - -- private LevelResource(String relativePath) { -+ public LevelResource(String relativePath) { // Leaves - private -> public - this.id = relativePath; - } - -diff --git a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java -index b148cf247acdd36f856d0495cde4cc5ad32b5a2f..011d6c813781251c7f4041ad3a8396fbde28ff75 100644 ---- a/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java -+++ b/src/main/java/net/minecraft/world/level/storage/PlayerDataStorage.java -@@ -21,7 +21,7 @@ import net.minecraft.world.entity.player.Player; - import org.bukkit.craftbukkit.entity.CraftPlayer; - import org.slf4j.Logger; - --public class PlayerDataStorage { -+public class PlayerDataStorage implements org.leavesmc.leaves.bot.IPlayerDataStorage { - - private static final Logger LOGGER = LogUtils.getLogger(); - private final File playerDir; -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java -index adc6741e0e017660fbd39a62b69be1e67e0e143f..9bc40b07b8eebded4f748fd053b45571df6286a5 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftRegionAccessor.java -@@ -434,6 +434,7 @@ public abstract class CraftRegionAccessor implements RegionAccessor { - @SuppressWarnings("unchecked") - public T addEntity(T entity) { - Preconditions.checkArgument(!entity.isInWorld(), "Entity has already been added to a world"); -+ Preconditions.checkState(!(entity instanceof org.leavesmc.leaves.entity.CraftBot), "[Leaves] Fakeplayers do not support changing world, Please use leaves fakeplayer-api instead!"); - net.minecraft.world.entity.Entity nmsEntity = ((CraftEntity) entity).getHandle(); - if (nmsEntity.level() != this.getHandle().getLevel()) { - nmsEntity = nmsEntity.teleport(new TeleportTransition(this.getHandle().getLevel(), nmsEntity, TeleportTransition.DO_NOTHING)); -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 3435629606def598ca998fb41ef303677c4cbea4..26ef09c86315c1125167af044323dbd3dbcfc6f0 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -313,6 +313,7 @@ public final class CraftServer implements Server { - private final io.papermc.paper.logging.SysoutCatcher sysoutCatcher = new io.papermc.paper.logging.SysoutCatcher(); // Paper - private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes - public final io.papermc.paper.SparksFly spark; // Paper - spark -+ private final org.leavesmc.leaves.entity.CraftBotManager botManager; // Leaves - - // Paper start - Folia region threading API - private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); -@@ -492,6 +493,7 @@ public final class CraftServer implements Server { - datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper - this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark - org.leavesmc.leaves.protocol.core.LeavesProtocolManager.init(); // Leaves - protocol -+ this.botManager = new org.leavesmc.leaves.entity.CraftBotManager(); // Leaves - } - - public boolean getCommandBlockOverride(String command) { -@@ -1479,7 +1481,7 @@ public final class CraftServer implements Server { - return false; - } - -- if (handle.players().size() > 0) { -+ if (handle.realPlayers().size() > 0) { // Leaves - skip - return false; - } - -@@ -3281,4 +3283,11 @@ public final class CraftServer implements Server { - this.console.addPluginAllowingSleep(plugin.getName(), value); - } - // Paper end - API to check if the server is sleeping -+ -+ // Leaves start - Bot API -+ @Override -+ public org.leavesmc.leaves.entity.CraftBotManager getBotManager() { -+ return botManager; -+ } -+ // Leaves end - Bot API - } -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index 57da11c0da7322e74810e7108e9c8000b0c36520..d47bab785d7be71155c964ef13e839768797c9cb 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -242,7 +242,7 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - @Override - public int getPlayerCount() { -- return world.players().size(); -+ return world.realPlayers().size(); // Leaves - skip - } - - @Override -@@ -1286,9 +1286,9 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - @Override - public List getPlayers() { -- List list = new ArrayList(this.world.players().size()); -+ List list = new ArrayList(this.world.realPlayers().size()); // Leaves - skip - -- for (net.minecraft.world.entity.player.Player human : this.world.players()) { -+ for (net.minecraft.world.entity.player.Player human : this.world.realPlayers()) { // Leaves - skip - HumanEntity bukkitEntity = human.getBukkitEntity(); - - if ((bukkitEntity != null) && (bukkitEntity instanceof Player)) { -@@ -1965,7 +1965,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); -- for (ServerPlayer player : this.getHandle().players()) { -+ for (ServerPlayer player : this.getHandle().realPlayers()) { // Leaves - skip - player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player.getX(), player.getY(), player.getZ(), seed, null)); - } - } -@@ -1993,7 +1993,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()) { -- for (ServerPlayer player : this.getHandle().players()) { -+ for (ServerPlayer player : this.getHandle().realPlayers()) { // Leaves - skip - player.connection.send(io.papermc.paper.adventure.PaperAdventure.asSoundPacket(sound, player, seed, null)); - } - } else if (emitter instanceof CraftEntity craftEntity) { -@@ -2217,7 +2217,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().sendParticles( -- receivers == null ? this.getHandle().players() : receivers.stream().map(player -> ((CraftPlayer) player).getHandle()).collect(java.util.stream.Collectors.toList()), // Paper - Particle API -+ receivers == null ? this.getHandle().realPlayers() : receivers.stream().map(player -> ((CraftPlayer) player).getHandle()).collect(java.util.stream.Collectors.toList()), // Paper - Particle API // Leaves - skip - sender != null ? ((CraftPlayer) sender).getHandle() : null, // Sender // Paper - Particle API - CraftParticle.createParticleParam(particle, data), // Particle - x, y, z, // Position -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index ddabaed899c755925ad8618b78c33dacaf2126ac..dea387f418cd173980be2e6e24797b55f9f58409 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -95,6 +95,8 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { - return new CraftHumanEntity(server, (net.minecraft.world.entity.player.Player) entity); - } - -+ if (entity instanceof org.leavesmc.leaves.bot.ServerBot bot) { return new org.leavesmc.leaves.entity.CraftBot(server, bot); } -+ - // Special case complex part, since there is no extra entity type for them - if (entity instanceof EnderDragonPart complexPart) { - if (complexPart.parentMob instanceof EnderDragon) { -diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -index e37aaf77f94b97b736cc20ef070cefdff0400188..efc92e321bb78702388fb572197b7e442c463170 100644 ---- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -+++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -@@ -1025,7 +1025,10 @@ public class CraftEventFactory { - event.setKeepInventory(keepInventory); - event.setKeepLevel(victim.keepLevel); // SPIGOT-2222: pre-set keepLevel - populateFields(victim, event); // Paper - make cancellable -- Bukkit.getServer().getPluginManager().callEvent(event); -+ // Leaves start - disable bot death event -+ if (!(victim instanceof org.leavesmc.leaves.bot.ServerBot)) { -+ Bukkit.getServer().getPluginManager().callEvent(event); -+ } // Leaves end - // Paper start - make cancellable - if (event.isCancelled()) { - return event; -diff --git a/src/main/java/org/leavesmc/leaves/bot/BotCommand.java b/src/main/java/org/leavesmc/leaves/bot/BotCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4bf25cb267a8d01c2d04a2a9e34b9fc6a433cade ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/BotCommand.java -@@ -0,0 +1,544 @@ -+package org.leavesmc.leaves.bot; -+ -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.format.NamedTextColor; -+import org.bukkit.Bukkit; -+import org.bukkit.Location; -+import org.bukkit.World; -+import org.bukkit.command.Command; -+import org.bukkit.command.CommandSender; -+import org.bukkit.command.ConsoleCommandSender; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.entity.Player; -+import org.bukkit.generator.WorldInfo; -+import org.bukkit.permissions.Permission; -+import org.bukkit.permissions.PermissionDefault; -+import org.bukkit.plugin.PluginManager; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.bot.agent.Actions; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.bot.agent.BotConfig; -+import org.leavesmc.leaves.bot.agent.Configs; -+import org.leavesmc.leaves.bot.agent.actions.CraftCustomBotAction; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.command.LeavesCommandUtil; -+import org.leavesmc.leaves.entity.Bot; -+import org.leavesmc.leaves.event.bot.BotActionStopEvent; -+import org.leavesmc.leaves.event.bot.BotConfigModifyEvent; -+import org.leavesmc.leaves.event.bot.BotCreateEvent; -+import org.leavesmc.leaves.event.bot.BotRemoveEvent; -+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; -+ -+import java.util.*; -+ -+import static net.kyori.adventure.text.Component.text; -+ -+public class BotCommand extends Command { -+ -+ private final Component unknownMessage; -+ -+ public BotCommand(String name) { -+ super(name); -+ this.description = "FakePlayer Command"; -+ this.usageMessage = "/bot [create | remove | action | list | config]"; -+ this.unknownMessage = text("Usage: " + usageMessage, NamedTextColor.RED); -+ this.setPermission("bukkit.command.bot"); -+ final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); -+ if (pluginManager.getPermission("bukkit.command.bot") == null) { -+ pluginManager.addPermission(new Permission("bukkit.command.bot", PermissionDefault.OP)); -+ } -+ } -+ -+ @Override -+ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, String @NotNull [] args, Location location) throws IllegalArgumentException { -+ List list = new ArrayList<>(); -+ BotList botList = BotList.INSTANCE; -+ -+ if (args.length <= 1) { -+ list.add("create"); -+ list.add("remove"); -+ if (LeavesConfig.modify.fakeplayer.canUseAction) { -+ list.add("action"); -+ } -+ if (LeavesConfig.modify.fakeplayer.canModifyConfig) { -+ list.add("config"); -+ } -+ if (LeavesConfig.modify.fakeplayer.canManualSaveAndLoad) { -+ list.add("save"); -+ list.add("load"); -+ } -+ list.add("list"); -+ } -+ -+ if (args.length == 2) { -+ switch (args[0]) { -+ case "create" -> list.add(""); -+ case "remove", "action", "config", "save" -> list.addAll(botList.bots.stream().map(e -> e.getName().getString()).toList()); -+ case "list" -> list.addAll(Bukkit.getWorlds().stream().map(WorldInfo::getName).toList()); -+ case "load" -> list.addAll(botList.getSavedBotList().getAllKeys()); -+ } -+ } -+ -+ if (args.length == 3) { -+ switch (args[0]) { -+ case "action" -> { -+ list.add("list"); -+ list.add("stop"); -+ list.addAll(Actions.getNames()); -+ } -+ case "create" -> list.add(""); -+ case "config" -> list.addAll(acceptConfig); -+ case "remove" -> list.addAll(List.of("cancel", "[hour]")); -+ } -+ } -+ -+ if (args[0].equals("remove") && args.length >= 3) { -+ if (!Objects.equals(args[3], "cancel")) { -+ switch (args.length) { -+ case 4 -> list.add("[minute]"); -+ case 5 -> list.add("[second]"); -+ } -+ } -+ } -+ -+ if (args.length >= 4 && args[0].equals("action")) { -+ ServerBot bot = botList.getBotByName(args[1]); -+ -+ if (bot == null) { -+ return Collections.singletonList("<" + args[1] + " not found>"); -+ } -+ -+ if (args[2].equals("stop")) { -+ list.add("all"); -+ for (int i = 0; i < bot.getBotActions().size(); i++) { -+ list.add(String.valueOf(i)); -+ } -+ } else { -+ BotAction action = Actions.getForName(args[2]); -+ if (action != null) { -+ list.addAll(action.getArgument().tabComplete(args.length - 4)); -+ } -+ } -+ } -+ -+ if (args.length >= 4 && args[0].equals("config")) { -+ Configs config = Configs.getConfig(args[2]); -+ if (config != null) { -+ list.addAll(config.config.getArgument().tabComplete(args.length - 4)); -+ } -+ } -+ -+ return LeavesCommandUtil.getListMatchingLast(sender, args, list, "bukkit.command.bot.", "bukkit.command.bot"); -+ } -+ -+ @Override -+ public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, String[] args) { -+ if (!testPermission(sender) || !LeavesConfig.modify.fakeplayer.enable) return true; -+ -+ if (args.length == 0) { -+ sender.sendMessage(unknownMessage); -+ return false; -+ } -+ -+ switch (args[0]) { -+ case "create" -> this.onCreate(sender, args); -+ case "remove" -> this.onRemove(sender, args); -+ case "action" -> this.onAction(sender, args); -+ case "config" -> this.onConfig(sender, args); -+ case "list" -> this.onList(sender, args); -+ case "save" -> this.onSave(sender, args); -+ case "load" -> this.onLoad(sender, args); -+ default -> { -+ sender.sendMessage(unknownMessage); -+ return false; -+ } -+ } -+ -+ return true; -+ } -+ -+ private void onCreate(CommandSender sender, String @NotNull [] args) { -+ if (args.length < 2) { -+ sender.sendMessage(text("Use /bot create [skin_name] to create a fakeplayer", NamedTextColor.RED)); -+ return; -+ } -+ -+ String botName = args[1]; -+ if (this.canCreate(sender, botName)) { -+ BotCreateState.Builder builder = BotCreateState.builder(botName, Bukkit.getWorlds().getFirst().getSpawnLocation()).createReason(BotCreateEvent.CreateReason.COMMAND).creator(sender); -+ -+ if (args.length >= 3) { -+ builder.skinName(args[2]); -+ } -+ -+ if (sender instanceof Player player) { -+ builder.location(player.getLocation()); -+ } else if (sender instanceof ConsoleCommandSender) { -+ if (args.length >= 7) { -+ try { -+ World world = Bukkit.getWorld(args[3]); -+ double x = Double.parseDouble(args[4]); -+ double y = Double.parseDouble(args[5]); -+ double z = Double.parseDouble(args[6]); -+ if (world != null) { -+ builder.location(new Location(world, x, y, z)); -+ } -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.warning("Can't build location", e); -+ } -+ } -+ } -+ -+ builder.spawnWithSkin(null); -+ } -+ } -+ -+ private boolean canCreate(CommandSender sender, @NotNull String name) { -+ BotList botList = BotList.INSTANCE; -+ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { -+ sender.sendMessage(text("This name is illegal", NamedTextColor.RED)); -+ return false; -+ } -+ -+ if (Bukkit.getPlayerExact(name) != null || botList.getBotByName(name) != null) { -+ sender.sendMessage(text("This player is in server", NamedTextColor.RED)); -+ return false; -+ } -+ -+ if (LeavesConfig.modify.fakeplayer.unableNames.contains(name)) { -+ sender.sendMessage(text("This name is not allowed", NamedTextColor.RED)); -+ return false; -+ } -+ -+ if (botList.bots.size() >= LeavesConfig.modify.fakeplayer.limit) { -+ sender.sendMessage(text("Fakeplayer limit is full", NamedTextColor.RED)); -+ return false; -+ } -+ -+ return true; -+ } -+ -+ private void onRemove(CommandSender sender, String @NotNull [] args) { -+ if (args.length < 2 || args.length > 5) { -+ sender.sendMessage(text("Use /bot remove [hour] [minute] [second] to remove a fakeplayer", NamedTextColor.RED)); -+ return; -+ } -+ -+ BotList botList = BotList.INSTANCE; -+ ServerBot bot = botList.getBotByName(args[1]); -+ -+ if (bot == null) { -+ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED)); -+ return; -+ } -+ -+ if (args.length > 2) { -+ if (args[2].equals("cancel")) { -+ if (bot.removeTaskId == -1) { -+ sender.sendMessage(text("This fakeplayer is not scheduled to be removed", NamedTextColor.RED)); -+ return; -+ } -+ Bukkit.getScheduler().cancelTask(bot.removeTaskId); -+ bot.removeTaskId = -1; -+ sender.sendMessage(text("Remove cancel")); -+ return; -+ } -+ -+ long time = 0; -+ int h; // Preventing out-of-range -+ long s = 0; -+ long m = 0; -+ -+ try { -+ h = Integer.parseInt(args[2]); -+ if (h < 0) { -+ throw new NumberFormatException(); -+ } -+ time += ((long) h) * 3600 * 20; -+ if (args.length > 3) { -+ m = Long.parseLong(args[3]); -+ if (m > 59 || m < 0) { -+ throw new NumberFormatException(); -+ } -+ time += m * 60 * 20; -+ } -+ if (args.length > 4) { -+ s = Long.parseLong(args[4]); -+ if (s > 59 || s < 0) { -+ throw new NumberFormatException(); -+ } -+ time += s * 20; -+ } -+ } catch (NumberFormatException e) { -+ sender.sendMessage(text("This fakeplayer is not scheduled to be removed", NamedTextColor.RED)); -+ return; -+ } -+ -+ boolean isReschedule = bot.removeTaskId != -1; -+ -+ if (isReschedule) { -+ Bukkit.getScheduler().cancelTask(bot.removeTaskId); -+ } -+ bot.removeTaskId = Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> { -+ bot.removeTaskId = -1; -+ botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, false); -+ }, time).getTaskId(); -+ -+ sender.sendMessage("This fakeplayer will be removed in " + h + "h " + m + "m " + s + "s" + (isReschedule ? " (rescheduled)" : "")); -+ -+ return; -+ } -+ -+ botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, false); -+ } -+ -+ private void onAction(CommandSender sender, String @NotNull [] args) { -+ if (!LeavesConfig.modify.fakeplayer.canUseAction) { -+ return; -+ } -+ -+ if (args.length < 3) { -+ sender.sendMessage(text("Use /bot action to make fakeplayer do action", NamedTextColor.RED)); -+ return; -+ } -+ -+ ServerBot bot = BotList.INSTANCE.getBotByName(args[1]); -+ if (bot == null) { -+ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED)); -+ return; -+ } -+ -+ if (args[2].equals("list")) { -+ sender.sendMessage(bot.getScoreboardName() + "'s action list:"); -+ for (int i = 0; i < bot.getBotActions().size(); i++) { -+ sender.sendMessage(i + " " + bot.getBotActions().get(i).getName()); -+ } -+ return; -+ } -+ -+ if (args[2].equals("stop")) { -+ if (args.length < 4) { -+ sender.sendMessage(text("Invalid index", NamedTextColor.RED)); -+ return; -+ } -+ -+ String index = args[3]; -+ if (index.equals("all")) { -+ Set> forRemoval = new HashSet<>(); -+ for (int i = 0; i < bot.getBotActions().size(); i++) { -+ BotAction action = bot.getBotActions().get(i); -+ BotActionStopEvent event = new BotActionStopEvent( -+ bot.getBukkitEntity(), action.getName(), action.getUUID(), BotActionStopEvent.Reason.COMMAND, sender -+ ); -+ event.callEvent(); -+ if (!event.isCancelled()) { -+ forRemoval.add(action); -+ } -+ } -+ bot.getBotActions().removeAll(forRemoval); -+ sender.sendMessage(bot.getScoreboardName() + "'s action list cleared."); -+ } else { -+ try { -+ int i = Integer.parseInt(index); -+ if (i < 0 || i >= bot.getBotActions().size()) { -+ sender.sendMessage(text("Invalid index", NamedTextColor.RED)); -+ return; -+ } -+ -+ BotAction action = bot.getBotActions().get(i); -+ BotActionStopEvent event = new BotActionStopEvent( -+ bot.getBukkitEntity(), action.getName(), action.getUUID(), BotActionStopEvent.Reason.COMMAND, sender -+ ); -+ event.callEvent(); -+ if (!event.isCancelled()) { -+ bot.getBotActions().remove(i); -+ sender.sendMessage(bot.getScoreboardName() + "'s " + action.getName() + " stopped."); -+ -+ } -+ } catch (NumberFormatException e) { -+ sender.sendMessage(text("Invalid index", NamedTextColor.RED)); -+ } -+ } -+ return; -+ } -+ -+ BotAction action = Actions.getForName(args[2]); -+ if (action == null) { -+ sender.sendMessage(text("Invalid action", NamedTextColor.RED)); -+ return; -+ } -+ -+ CraftPlayer player; -+ if (sender instanceof CraftPlayer) { -+ player = (CraftPlayer) sender; -+ } else { -+ player = bot.getBukkitEntity(); -+ } -+ -+ String[] realArgs = new String[args.length - 3]; -+ if (realArgs.length != 0) { -+ System.arraycopy(args, 3, realArgs, 0, realArgs.length); -+ } -+ -+ BotAction newAction; -+ try { -+ if (action instanceof CraftCustomBotAction customBotAction) { -+ newAction = customBotAction.createCraft(player, realArgs); -+ } else { -+ newAction = action.create(); -+ newAction.loadCommand(player.getHandle(), action.getArgument().parse(0, realArgs)); -+ } -+ } catch (IllegalArgumentException e) { -+ sender.sendMessage(text("Action create error, please check your arguments, " + e.getMessage(), NamedTextColor.RED)); -+ return; -+ } -+ -+ if (newAction == null) { -+ return; -+ } -+ -+ if (bot.addBotAction(newAction, sender)) { -+ sender.sendMessage("Action " + action.getName() + " has been issued to " + bot.getName().getString()); -+ } -+ } -+ -+ private static final List acceptConfig = Configs.getConfigs().stream().map(config -> config.config.getName()).toList(); -+ -+ private void onConfig(CommandSender sender, String @NotNull [] args) { -+ if (!LeavesConfig.modify.fakeplayer.canModifyConfig) { -+ return; -+ } -+ -+ if (args.length < 3) { -+ sender.sendMessage(text("Use /bot config to modify fakeplayer's config", NamedTextColor.RED)); -+ return; -+ } -+ -+ ServerBot bot = BotList.INSTANCE.getBotByName(args[1]); -+ if (bot == null) { -+ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED)); -+ return; -+ } -+ -+ if (!acceptConfig.contains(args[2])) { -+ sender.sendMessage(text("This config is not accept", NamedTextColor.RED)); -+ return; -+ } -+ -+ BotConfig config = Objects.requireNonNull(Configs.getConfig(args[2])).config; -+ if (args.length < 4) { -+ config.getMessage().forEach(sender::sendMessage); -+ } else { -+ String[] realArgs = new String[args.length - 3]; -+ System.arraycopy(args, 3, realArgs, 0, realArgs.length); -+ -+ BotConfigModifyEvent event = new BotConfigModifyEvent(bot.getBukkitEntity(), config.getName(), realArgs, sender); -+ Bukkit.getPluginManager().callEvent(event); -+ -+ if (event.isCancelled()) { -+ return; -+ } -+ CommandArgumentResult result = config.getArgument().parse(0, realArgs); -+ -+ try { -+ config.setValue(result); -+ config.getChangeMessage().forEach(sender::sendMessage); -+ } catch (IllegalArgumentException e) { -+ sender.sendMessage(text(e.getMessage(), NamedTextColor.RED)); -+ } -+ } -+ } -+ -+ private void onSave(CommandSender sender, String @NotNull [] args) { -+ if (!LeavesConfig.modify.fakeplayer.canManualSaveAndLoad) { -+ return; -+ } -+ -+ if (args.length < 2) { -+ sender.sendMessage(text("Use /bot save to save a fakeplayer", NamedTextColor.RED)); -+ return; -+ } -+ -+ BotList botList = BotList.INSTANCE; -+ ServerBot bot = botList.getBotByName(args[1]); -+ -+ if (bot == null) { -+ sender.sendMessage(text("This fakeplayer is not in server", NamedTextColor.RED)); -+ return; -+ } -+ -+ if (botList.removeBot(bot, BotRemoveEvent.RemoveReason.COMMAND, sender, true)) { -+ sender.sendMessage(bot.getScoreboardName() + " saved to " + bot.createState.realName()); -+ } -+ } -+ -+ private void onLoad(CommandSender sender, String @NotNull [] args) { -+ if (!LeavesConfig.modify.fakeplayer.canManualSaveAndLoad) { -+ return; -+ } -+ -+ if (args.length < 2) { -+ sender.sendMessage(text("Use /bot save to save a fakeplayer", NamedTextColor.RED)); -+ return; -+ } -+ -+ String realName = args[1]; -+ BotList botList = BotList.INSTANCE; -+ if (!botList.getSavedBotList().contains(realName)) { -+ sender.sendMessage(text("This fakeplayer is not saved", NamedTextColor.RED)); -+ return; -+ } -+ -+ if (botList.loadNewBot(realName) == null) { -+ sender.sendMessage(text("Can't load bot, please check", NamedTextColor.RED)); -+ } -+ } -+ -+ private void onList(CommandSender sender, String @NotNull [] args) { -+ BotList botList = BotList.INSTANCE; -+ if (args.length < 2) { -+ Map> botMap = new HashMap<>(); -+ for (World world : Bukkit.getWorlds()) { -+ botMap.put(world, new ArrayList<>()); -+ } -+ -+ for (ServerBot bot : botList.bots) { -+ Bot bukkitBot = bot.getBukkitEntity(); -+ botMap.get(bukkitBot.getWorld()).add(bukkitBot.getName()); -+ } -+ -+ sender.sendMessage("Total number: (" + botList.bots.size() + "/" + LeavesConfig.modify.fakeplayer.limit + ")"); -+ for (World world : botMap.keySet()) { -+ sender.sendMessage(world.getName() + "(" + botMap.get(world).size() + "): " + formatPlayerNameList(botMap.get(world))); -+ } -+ } else { -+ World world = Bukkit.getWorld(args[1]); -+ -+ if (world == null) { -+ sender.sendMessage(text("Unknown world", NamedTextColor.RED)); -+ return; -+ } -+ -+ List snowBotList = new ArrayList<>(); -+ for (ServerBot bot : botList.bots) { -+ Bot bukkitBot = bot.getBukkitEntity(); -+ if (bukkitBot.getWorld() == world) { -+ snowBotList.add(bukkitBot.getName()); -+ } -+ } -+ -+ sender.sendMessage(world.getName() + "(" + botList.bots.size() + "): " + formatPlayerNameList(snowBotList)); -+ } -+ } -+ -+ @NotNull -+ private static String formatPlayerNameList(@NotNull List list) { -+ if (list.isEmpty()) { -+ return ""; -+ } -+ String string = list.toString(); -+ return string.substring(1, string.length() - 1); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java b/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bc09bb05fdccc7e6e51f441b0c23237145317e89 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/BotCreateState.java -@@ -0,0 +1,120 @@ -+package org.leavesmc.leaves.bot; -+ -+import net.minecraft.server.MinecraftServer; -+import org.bukkit.Bukkit; -+import org.bukkit.Location; -+import org.bukkit.command.CommandSender; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.entity.Bot; -+import org.leavesmc.leaves.entity.BotCreator; -+import org.leavesmc.leaves.entity.CraftBot; -+import org.leavesmc.leaves.event.bot.BotCreateEvent; -+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; -+ -+import java.util.Objects; -+import java.util.function.Consumer; -+ -+public record BotCreateState(String realName, String name, String skinName, String[] skin, Location location, BotCreateEvent.CreateReason createReason, CommandSender creator) { -+ -+ private static final MinecraftServer server = MinecraftServer.getServer(); -+ -+ public ServerBot createNow() { -+ return server.getBotList().createNewBot(this); -+ } -+ -+ @NotNull -+ public static Builder builder(@NotNull String realName, @Nullable Location location) { -+ return new Builder(realName, location); -+ } -+ -+ public static class Builder implements BotCreator { -+ -+ private final String realName; -+ -+ private String name; -+ private Location location; -+ -+ private String skinName; -+ private String[] skin; -+ -+ private BotCreateEvent.CreateReason createReason; -+ private CommandSender creator; -+ -+ private Builder(@NotNull String realName, @Nullable Location location) { -+ Objects.requireNonNull(realName); -+ -+ this.realName = realName; -+ this.location = location; -+ -+ this.name = LeavesConfig.modify.fakeplayer.prefix + realName + LeavesConfig.modify.fakeplayer.suffix; -+ this.skinName = this.realName; -+ this.skin = null; -+ this.createReason = BotCreateEvent.CreateReason.UNKNOWN; -+ this.creator = null; -+ } -+ -+ public Builder name(@NotNull String name) { -+ Objects.requireNonNull(name); -+ this.name = name; -+ return this; -+ } -+ -+ public Builder skinName(@Nullable String skinName) { -+ this.skinName = skinName; -+ return this; -+ } -+ -+ public Builder skin(@Nullable String[] skin) { -+ this.skin = skin; -+ return this; -+ } -+ -+ public Builder mojangAPISkin() { -+ if (this.skinName != null) { -+ this.skin = MojangAPI.getSkin(this.skinName); -+ } -+ return this; -+ } -+ -+ public Builder location(@NotNull Location location) { -+ this.location = location; -+ return this; -+ } -+ -+ public Builder createReason(@NotNull BotCreateEvent.CreateReason createReason) { -+ Objects.requireNonNull(createReason); -+ this.createReason = createReason; -+ return this; -+ } -+ -+ public Builder creator(CommandSender creator) { -+ this.creator = creator; -+ return this; -+ } -+ -+ public BotCreateState build() { -+ return new BotCreateState(realName, name, skinName, skin, location, createReason, creator); -+ } -+ -+ public void spawnWithSkin(Consumer consumer) { -+ Bukkit.getScheduler().runTaskAsynchronously(MinecraftInternalPlugin.INSTANCE, () -> { -+ this.mojangAPISkin(); -+ Bukkit.getScheduler().runTask(MinecraftInternalPlugin.INSTANCE, () -> { -+ CraftBot bot = this.spawn(); -+ if (bot != null && consumer != null) { -+ consumer.accept(bot); -+ } -+ }); -+ }); -+ } -+ -+ @Nullable -+ public CraftBot spawn() { -+ Objects.requireNonNull(this.location); -+ ServerBot bot = this.build().createNow(); -+ return bot != null ? bot.getBukkitEntity() : null; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java b/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java -new file mode 100644 -index 0000000000000000000000000000000000000000..53bece66534df40ef8cf559c12e2c472a791b9c3 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/BotDataStorage.java -@@ -0,0 +1,121 @@ -+package org.leavesmc.leaves.bot; -+ -+import com.mojang.logging.LogUtils; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.NbtAccounter; -+import net.minecraft.nbt.NbtIo; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.level.storage.LevelResource; -+import net.minecraft.world.level.storage.LevelStorageSource; -+import org.jetbrains.annotations.NotNull; -+import org.slf4j.Logger; -+ -+import java.io.File; -+import java.io.IOException; -+import java.util.Optional; -+ -+public class BotDataStorage implements IPlayerDataStorage { -+ -+ private static final LevelResource BOT_DATA_DIR = new LevelResource("fakeplayerdata"); -+ private static final LevelResource BOT_LIST_FILE = new LevelResource("fakeplayer.dat"); -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ private final File botDir; -+ private final File botListFile; -+ -+ private CompoundTag savedBotList; -+ -+ public BotDataStorage(LevelStorageSource.@NotNull LevelStorageAccess session) { -+ this.botDir = session.getLevelPath(BOT_DATA_DIR).toFile(); -+ this.botListFile = session.getLevelPath(BOT_LIST_FILE).toFile(); -+ this.botDir.mkdirs(); -+ -+ this.savedBotList = new CompoundTag(); -+ if (this.botListFile.exists() && this.botListFile.isFile()) { -+ try { -+ Optional.of(NbtIo.readCompressed(this.botListFile.toPath(), NbtAccounter.unlimitedHeap())).ifPresent(tag -> this.savedBotList = tag); -+ } catch (Exception exception) { -+ BotDataStorage.LOGGER.warn("Failed to load player data list"); -+ } -+ } -+ } -+ -+ @Override -+ public void save(Player player) { -+ boolean flag = true; -+ try { -+ CompoundTag nbt = player.saveWithoutId(new CompoundTag()); -+ File file = new File(this.botDir, player.getStringUUID() + ".dat"); -+ -+ if (file.exists() && file.isFile()) { -+ if (!file.delete()) { -+ throw new IOException("Failed to delete file: " + file); -+ } -+ } -+ if (!file.createNewFile()) { -+ throw new IOException("Failed to create nbt file: " + file); -+ } -+ NbtIo.writeCompressed(nbt, file.toPath()); -+ } catch (Exception exception) { -+ BotDataStorage.LOGGER.warn("Failed to save fakeplayer data for {}", player.getScoreboardName(), exception); -+ flag = false; -+ } -+ -+ if (flag && player instanceof ServerBot bot) { -+ CompoundTag nbt = new CompoundTag(); -+ nbt.putString("name", bot.createState.name()); -+ nbt.putUUID("uuid", bot.getUUID()); -+ nbt.putBoolean("resume", bot.resume); -+ this.savedBotList.put(bot.createState.realName(), nbt); -+ this.saveBotList(); -+ } -+ } -+ -+ @Override -+ public Optional load(Player player) { -+ return this.load(player.getScoreboardName(), player.getStringUUID()).map((nbt) -> { -+ player.load(nbt); -+ return nbt; -+ }); -+ } -+ -+ private Optional load(String name, String uuid) { -+ File file = new File(this.botDir, uuid + ".dat"); -+ -+ if (file.exists() && file.isFile()) { -+ try { -+ Optional optional = Optional.of(NbtIo.readCompressed(file.toPath(), NbtAccounter.unlimitedHeap())); -+ if (!file.delete()) { -+ throw new IOException("Failed to delete fakeplayer data"); -+ } -+ this.savedBotList.remove(name); -+ this.saveBotList(); -+ return optional; -+ } catch (Exception exception) { -+ BotDataStorage.LOGGER.warn("Failed to load fakeplayer data for {}", name); -+ } -+ } -+ -+ return Optional.empty(); -+ } -+ -+ private void saveBotList() { -+ try { -+ if (this.botListFile.exists() && this.botListFile.isFile()) { -+ if (!this.botListFile.delete()) { -+ throw new IOException("Failed to delete file: " + this.botListFile); -+ } -+ } -+ if (!this.botListFile.createNewFile()) { -+ throw new IOException("Failed to create nbt file: " + this.botListFile); -+ } -+ NbtIo.writeCompressed(this.savedBotList, this.botListFile.toPath()); -+ } catch (Exception exception) { -+ BotDataStorage.LOGGER.warn("Failed to save player data list"); -+ } -+ } -+ -+ public CompoundTag getSavedBotList() { -+ return savedBotList; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java b/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4f5e6e5c1b9d8bd38c98e97fd31b38338f35faa6 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/BotInventoryContainer.java -@@ -0,0 +1,191 @@ -+package org.leavesmc.leaves.bot; -+ -+import com.google.common.collect.ImmutableList; -+import com.mojang.datafixers.util.Pair; -+import net.minecraft.core.NonNullList; -+import net.minecraft.core.component.DataComponentPatch; -+import net.minecraft.core.component.DataComponents; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.chat.Component; -+import net.minecraft.world.ContainerHelper; -+import net.minecraft.world.SimpleContainer; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.Items; -+import net.minecraft.world.item.component.CustomData; -+ -+import javax.annotation.Nonnull; -+import java.util.List; -+ -+// Power by gugle-carpet-addition(https://github.com/Gu-ZT/gugle-carpet-addition) -+public class BotInventoryContainer extends SimpleContainer { -+ -+ public final NonNullList items; -+ public final NonNullList armor; -+ public final NonNullList offhand; -+ private final List> compartments; -+ private final NonNullList buttons = NonNullList.withSize(13, ItemStack.EMPTY); -+ private final ServerBot player; -+ -+ public BotInventoryContainer(ServerBot player) { -+ this.player = player; -+ this.items = this.player.getInventory().items; -+ this.armor = this.player.getInventory().armor; -+ this.offhand = this.player.getInventory().offhand; -+ this.compartments = ImmutableList.of(this.items, this.armor, this.offhand, this.buttons); -+ createButton(); -+ } -+ -+ @Override -+ public int getContainerSize() { -+ return this.items.size() + this.armor.size() + this.offhand.size() + this.buttons.size(); -+ } -+ -+ @Override -+ public boolean isEmpty() { -+ for (ItemStack itemStack : this.items) { -+ if (itemStack.isEmpty()) { -+ continue; -+ } -+ return false; -+ } -+ for (ItemStack itemStack : this.armor) { -+ if (itemStack.isEmpty()) { -+ continue; -+ } -+ return false; -+ } -+ for (ItemStack itemStack : this.offhand) { -+ if (itemStack.isEmpty()) { -+ continue; -+ } -+ return false; -+ } -+ return true; -+ } -+ -+ @Override -+ @Nonnull -+ public ItemStack getItem(int slot) { -+ Pair, Integer> pair = getItemSlot(slot); -+ if (pair != null) { -+ return pair.getFirst().get(pair.getSecond()); -+ } else { -+ return ItemStack.EMPTY; -+ } -+ } -+ -+ public Pair, Integer> getItemSlot(int slot) { -+ switch (slot) { -+ case 0 -> { -+ return new Pair<>(buttons, 0); -+ } -+ case 1, 2, 3, 4 -> { -+ return new Pair<>(armor, 4 - slot); -+ } -+ case 5, 6 -> { -+ return new Pair<>(buttons, slot - 4); -+ } -+ case 7 -> { -+ return new Pair<>(offhand, 0); -+ } -+ case 8, 9, 10, 11, 12, 13, 14, 15, 16, 17 -> { -+ return new Pair<>(buttons, slot - 5); -+ } -+ case 18, 19, 20, 21, 22, 23, 24, 25, 26, -+ 27, 28, 29, 30, 31, 32, 33, 34, 35, -+ 36, 37, 38, 39, 40, 41, 42, 43, 44 -> { -+ return new Pair<>(items, slot - 9); -+ } -+ case 45, 46, 47, 48, 49, 50, 51, 52, 53 -> { -+ return new Pair<>(items, slot - 45); -+ } -+ default -> { -+ return null; -+ } -+ } -+ } -+ -+ @Override -+ @Nonnull -+ public ItemStack removeItem(int slot, int amount) { -+ Pair, Integer> pair = getItemSlot(slot); -+ NonNullList list = null; -+ ItemStack itemStack = ItemStack.EMPTY; -+ if (pair != null) { -+ list = pair.getFirst(); -+ slot = pair.getSecond(); -+ } -+ if (list != null && !list.get(slot).isEmpty()) { -+ itemStack = ContainerHelper.removeItem(list, slot, amount); -+ player.detectEquipmentUpdatesPublic(); -+ } -+ return itemStack; -+ } -+ -+ @Override -+ @Nonnull -+ public ItemStack removeItemNoUpdate(int slot) { -+ Pair, Integer> pair = getItemSlot(slot); -+ NonNullList list = null; -+ if (pair != null) { -+ list = pair.getFirst(); -+ slot = pair.getSecond(); -+ } -+ if (list != null && !list.get(slot).isEmpty()) { -+ ItemStack itemStack = list.get(slot); -+ list.set(slot, ItemStack.EMPTY); -+ return itemStack; -+ } -+ return ItemStack.EMPTY; -+ } -+ -+ @Override -+ public void setItem(int slot, @Nonnull ItemStack stack) { -+ Pair, Integer> pair = getItemSlot(slot); -+ NonNullList list = null; -+ if (pair != null) { -+ list = pair.getFirst(); -+ slot = pair.getSecond(); -+ } -+ if (list != null) { -+ list.set(slot, stack); -+ player.detectEquipmentUpdatesPublic(); -+ } -+ } -+ -+ @Override -+ public void setChanged() { -+ } -+ -+ @Override -+ public boolean stillValid(@Nonnull Player player) { -+ if (this.player.isRemoved()) { -+ return false; -+ } -+ return !(player.distanceToSqr(this.player) > 64.0); -+ } -+ -+ @Override -+ public void clearContent() { -+ for (List list : this.compartments) { -+ list.clear(); -+ } -+ } -+ -+ private void createButton() { -+ CompoundTag customData = new CompoundTag(); -+ customData.putBoolean("Leaves.Gui.Placeholder", true); -+ -+ DataComponentPatch patch = DataComponentPatch.builder() -+ .set(DataComponents.CUSTOM_NAME, Component.empty()) -+ .set(DataComponents.CUSTOM_DATA, CustomData.of(customData)) -+ .build(); -+ -+ for (int i = 0; i < 13; i++) { -+ ItemStack button = new ItemStack(Items.STRUCTURE_VOID); -+ button.applyComponents(patch); -+ buttons.set(i, button); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/BotList.java b/src/main/java/org/leavesmc/leaves/bot/BotList.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3881aa5c5bd18a8859715ad31624dbd5e92e5721 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/BotList.java -@@ -0,0 +1,297 @@ -+package org.leavesmc.leaves.bot; -+ -+import com.google.common.collect.Maps; -+import com.mojang.authlib.GameProfile; -+import com.mojang.authlib.properties.Property; -+import com.mojang.logging.LogUtils; -+import io.papermc.paper.adventure.PaperAdventure; -+import net.kyori.adventure.text.format.NamedTextColor; -+import net.kyori.adventure.text.format.Style; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.chat.Component; -+import net.minecraft.network.protocol.game.ClientboundRemoveEntitiesPacket; -+import net.minecraft.resources.ResourceKey; -+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.Level; -+import org.bukkit.Bukkit; -+import org.bukkit.Location; -+import org.bukkit.command.CommandSender; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.event.bot.BotCreateEvent; -+import org.leavesmc.leaves.event.bot.BotJoinEvent; -+import org.leavesmc.leaves.event.bot.BotLoadEvent; -+import org.leavesmc.leaves.event.bot.BotRemoveEvent; -+import org.leavesmc.leaves.event.bot.BotSpawnLocationEvent; -+import org.slf4j.Logger; -+ -+import java.util.List; -+import java.util.Locale; -+import java.util.Map; -+import java.util.Optional; -+import java.util.UUID; -+import java.util.concurrent.CopyOnWriteArrayList; -+ -+public class BotList { -+ -+ public static BotList INSTANCE; -+ -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ -+ private final MinecraftServer server; -+ -+ public final List bots = new CopyOnWriteArrayList<>(); -+ private final BotDataStorage dataStorage; -+ -+ private final Map botsByUUID = Maps.newHashMap(); -+ private final Map botsByName = Maps.newHashMap(); -+ -+ public BotList(MinecraftServer server) { -+ this.server = server; -+ this.dataStorage = new BotDataStorage(server.storageSource); -+ INSTANCE = this; -+ } -+ -+ public ServerBot createNewBot(BotCreateState state) { -+ BotCreateEvent event = new BotCreateEvent(state.name(), state.skinName(), state.location(), state.createReason(), state.creator()); -+ event.setCancelled(!isCreateLegal(state.name())); -+ this.server.server.getPluginManager().callEvent(event); -+ -+ if (event.isCancelled()) { -+ return null; -+ } -+ -+ Location location = event.getCreateLocation(); -+ ServerLevel world = ((CraftWorld) location.getWorld()).getHandle(); -+ -+ CustomGameProfile profile = new CustomGameProfile(BotUtil.getBotUUID(state), state.name(), state.skin()); -+ ServerBot bot = new ServerBot(this.server, world, profile); -+ bot.createState = state; -+ if (event.getCreator() instanceof org.bukkit.entity.Player player) { -+ bot.createPlayer = player.getUniqueId(); -+ } -+ -+ return this.placeNewBot(bot, world, location, null); -+ } -+ -+ public ServerBot loadNewBot(String realName) { -+ return this.loadNewBot(realName, this.dataStorage); -+ } -+ -+ public ServerBot loadNewBot(String realName, IPlayerDataStorage playerIO) { -+ UUID uuid = BotUtil.getBotUUID(realName); -+ -+ BotLoadEvent event = new BotLoadEvent(realName, uuid); -+ this.server.server.getPluginManager().callEvent(event); -+ if (event.isCancelled()) { -+ return null; -+ } -+ -+ ServerBot bot = new ServerBot(this.server, this.server.getLevel(Level.OVERWORLD), new GameProfile(uuid, realName)); -+ bot.connection = new ServerBotPacketListenerImpl(this.server, bot); -+ Optional optional = playerIO.load(bot); -+ -+ if (optional.isEmpty()) { -+ return null; -+ } -+ -+ ResourceKey resourcekey = null; -+ if (optional.get().contains("WorldUUIDMost") && optional.get().contains("WorldUUIDLeast")) { -+ org.bukkit.World bWorld = Bukkit.getServer().getWorld(new UUID(optional.get().getLong("WorldUUIDMost"), optional.get().getLong("WorldUUIDLeast"))); -+ if (bWorld != null) { -+ resourcekey = ((CraftWorld) bWorld).getHandle().dimension(); -+ } -+ } -+ if (resourcekey == null) { -+ return null; -+ } -+ -+ ServerLevel world = this.server.getLevel(resourcekey); -+ return this.placeNewBot(bot, world, bot.getLocation(), optional.get()); -+ } -+ -+ public ServerBot placeNewBot(ServerBot bot, ServerLevel world, Location location, @Nullable CompoundTag nbt) { -+ Optional optional = Optional.ofNullable(nbt); -+ -+ bot.isRealPlayer = true; -+ bot.loginTime = System.currentTimeMillis(); -+ bot.connection = new ServerBotPacketListenerImpl(this.server, bot); -+ bot.setServerLevel(world); -+ -+ BotSpawnLocationEvent event = new BotSpawnLocationEvent(bot.getBukkitEntity(), location); -+ this.server.server.getPluginManager().callEvent(event); -+ location = event.getSpawnLocation(); -+ -+ bot.spawnIn(world); -+ bot.gameMode.setLevel(bot.serverLevel()); -+ -+ bot.setPosRaw(location.getX(), location.getY(), location.getZ()); -+ bot.setRot(location.getYaw(), location.getPitch()); -+ -+ bot.connection.teleport(bot.getX(), bot.getY(), bot.getZ(), bot.getYRot(), bot.getXRot()); -+ -+ this.bots.add(bot); -+ this.botsByName.put(bot.getScoreboardName().toLowerCase(Locale.ROOT), bot); -+ this.botsByUUID.put(bot.getUUID(), bot); -+ -+ bot.supressTrackerForLogin = true; -+ world.addNewPlayer(bot); -+ bot.loadAndSpawnEnderpearls(optional); -+ bot.loadAndSpawnParentVehicle(optional); -+ -+ BotJoinEvent event1 = new BotJoinEvent(bot.getBukkitEntity(), PaperAdventure.asAdventure(Component.translatable("multiplayer.player.joined", bot.getDisplayName())).style(Style.style(NamedTextColor.YELLOW))); -+ this.server.server.getPluginManager().callEvent(event1); -+ -+ net.kyori.adventure.text.Component joinMessage = event1.joinMessage(); -+ if (joinMessage != null && !joinMessage.equals(net.kyori.adventure.text.Component.empty())) { -+ this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(joinMessage), false); -+ } -+ -+ bot.renderAll(); -+ bot.supressTrackerForLogin = false; -+ -+ bot.serverLevel().getChunkSource().chunkMap.addEntity(bot); -+ BotList.LOGGER.info("{}[{}] logged in with entity id {} at ([{}]{}, {}, {})", bot.getName().getString(), "Local", bot.getId(), bot.serverLevel().serverLevelData.getLevelName(), bot.getX(), bot.getY(), bot.getZ()); -+ return bot; -+ } -+ -+ public boolean removeBot(@NotNull ServerBot bot, @NotNull BotRemoveEvent.RemoveReason reason, @Nullable CommandSender remover, boolean saved) { -+ return this.removeBot(bot, reason, remover, saved, this.dataStorage); -+ } -+ -+ public boolean removeBot(@NotNull ServerBot bot, @NotNull BotRemoveEvent.RemoveReason reason, @Nullable CommandSender remover, boolean saved, IPlayerDataStorage playerIO) { -+ BotRemoveEvent event = new BotRemoveEvent(bot.getBukkitEntity(), reason, remover, PaperAdventure.asAdventure(Component.translatable("multiplayer.player.left", bot.getDisplayName())).style(Style.style(NamedTextColor.YELLOW)), saved); -+ this.server.server.getPluginManager().callEvent(event); -+ -+ if (event.isCancelled() && event.getReason() != BotRemoveEvent.RemoveReason.INTERNAL) { -+ return event.isCancelled(); -+ } -+ -+ if (bot.removeTaskId != -1) { -+ Bukkit.getScheduler().cancelTask(bot.removeTaskId); -+ bot.removeTaskId = -1; -+ } -+ -+ if (this.server.isSameThread()) { -+ bot.doTick(); -+ } -+ -+ if (event.shouldSave()) { -+ playerIO.save(bot); -+ } else { -+ bot.dropAll(); -+ } -+ -+ if (bot.isPassenger()) { -+ Entity entity = bot.getRootVehicle(); -+ if (entity.hasExactlyOnePlayerPassenger()) { -+ bot.stopRiding(); -+ entity.getPassengersAndSelf().forEach((entity1) -> { -+ if (entity1 instanceof net.minecraft.world.entity.npc.AbstractVillager villager) { -+ final net.minecraft.world.entity.player.Player human = villager.getTradingPlayer(); -+ if (human != null) { -+ villager.setTradingPlayer(null); -+ } -+ } -+ entity1.setRemoved(Entity.RemovalReason.UNLOADED_WITH_PLAYER); -+ }); -+ } -+ } -+ -+ bot.unRide(); -+ bot.serverLevel().removePlayerImmediately(bot, Entity.RemovalReason.UNLOADED_WITH_PLAYER); -+ this.bots.remove(bot); -+ this.botsByName.remove(bot.getScoreboardName().toLowerCase(Locale.ROOT)); -+ -+ UUID uuid = bot.getUUID(); -+ ServerBot bot1 = this.botsByUUID.get(uuid); -+ if (bot1 == bot) { -+ this.botsByUUID.remove(uuid); -+ } -+ -+ bot.removeTab(); -+ for (ServerPlayer player : bot.serverLevel().players()) { -+ if (!(player instanceof ServerBot) && !bot.needSendFakeData(player)) { -+ player.connection.send(new ClientboundRemoveEntitiesPacket(bot.getId())); -+ } -+ } -+ -+ net.kyori.adventure.text.Component removeMessage = event.removeMessage(); -+ if (removeMessage != null && !removeMessage.equals(net.kyori.adventure.text.Component.empty())) { -+ this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(removeMessage), false); -+ } -+ return true; -+ } -+ -+ public void removeAll() { -+ for (ServerBot bot : this.bots) { -+ bot.resume = LeavesConfig.modify.fakeplayer.canResident; -+ this.removeBot(bot, BotRemoveEvent.RemoveReason.INTERNAL, null, LeavesConfig.modify.fakeplayer.canResident); -+ } -+ } -+ -+ public void loadResume() { -+ if (LeavesConfig.modify.fakeplayer.enable && LeavesConfig.modify.fakeplayer.canResident) { -+ CompoundTag savedBotList = this.getSavedBotList().copy(); -+ for (String realName : savedBotList.getAllKeys()) { -+ CompoundTag nbt = savedBotList.getCompound(realName); -+ if (nbt.getBoolean("resume")) { -+ this.loadNewBot(realName); -+ } -+ } -+ } -+ } -+ -+ public void networkTick() { -+ this.bots.forEach(ServerBot::doTick); -+ } -+ -+ @Nullable -+ public ServerBot getBot(@NotNull UUID uuid) { -+ return this.botsByUUID.get(uuid); -+ } -+ -+ @Nullable -+ public ServerBot getBotByName(@NotNull String name) { -+ return this.botsByName.get(name.toLowerCase(Locale.ROOT)); -+ } -+ -+ public CompoundTag getSavedBotList() { -+ return this.dataStorage.getSavedBotList(); -+ } -+ -+ public boolean isCreateLegal(@NotNull String name) { -+ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { -+ return false; -+ } -+ -+ if (Bukkit.getPlayerExact(name) != null || this.getBotByName(name) != null) { -+ return false; -+ } -+ -+ if (LeavesConfig.modify.fakeplayer.unableNames.contains(name)) { -+ return false; -+ } -+ -+ return this.bots.size() < LeavesConfig.modify.fakeplayer.limit; -+ } -+ -+ public static class CustomGameProfile extends GameProfile { -+ -+ public CustomGameProfile(UUID uuid, String name, String[] skin) { -+ super(uuid, name); -+ this.setSkin(skin); -+ } -+ -+ public void setSkin(String[] skin) { -+ if (skin != null) { -+ this.getProperties().put("textures", new Property("textures", skin[0], skin[1])); -+ } -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java b/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..10494446f915bc1720a18cfe75b2cab2404646e9 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/BotStatsCounter.java -@@ -0,0 +1,36 @@ -+package org.leavesmc.leaves.bot; -+ -+import com.mojang.datafixers.DataFixer; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.stats.ServerStatsCounter; -+import net.minecraft.stats.Stat; -+import net.minecraft.world.entity.player.Player; -+import org.jetbrains.annotations.NotNull; -+ -+import java.io.File; -+ -+public class BotStatsCounter extends ServerStatsCounter { -+ -+ private static final File UNKOWN_FILE = new File("BOT_STATS_REMOVE_THIS"); -+ -+ public BotStatsCounter(MinecraftServer server) { -+ super(server, UNKOWN_FILE); -+ } -+ -+ @Override -+ public void save() { -+ } -+ -+ @Override -+ public void setValue(@NotNull Player player, @NotNull Stat stat, int value) { -+ } -+ -+ @Override -+ public void parseLocal(@NotNull DataFixer dataFixer, @NotNull String json) { -+ } -+ -+ @Override -+ public int getValue(@NotNull Stat stat) { -+ return 0; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/BotUtil.java b/src/main/java/org/leavesmc/leaves/bot/BotUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..78414d1f53328cdc2963264ecb4f5a65e9783798 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/BotUtil.java -@@ -0,0 +1,73 @@ -+package org.leavesmc.leaves.bot; -+ -+import com.google.common.base.Charsets; -+import net.minecraft.core.NonNullList; -+import net.minecraft.world.entity.EquipmentSlot; -+import net.minecraft.world.item.ItemStack; -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.UUID; -+ -+public class BotUtil { -+ -+ public static void replenishment(@NotNull ItemStack itemStack, NonNullList itemStackList) { -+ int count = itemStack.getMaxStackSize() / 2; -+ if (itemStack.getCount() <= 8 && count > 8) { -+ for (ItemStack itemStack1 : itemStackList) { -+ if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) { -+ continue; -+ } -+ -+ if (ItemStack.isSameItemSameComponents(itemStack1, itemStack)) { -+ if (itemStack1.getCount() > count) { -+ itemStack.setCount(itemStack.getCount() + count); -+ itemStack1.setCount(itemStack1.getCount() - count); -+ } else { -+ itemStack.setCount(itemStack.getCount() + itemStack1.getCount()); -+ itemStack1.setCount(0); -+ } -+ break; -+ } -+ } -+ } -+ } -+ -+ public static void replaceTool(@NotNull EquipmentSlot slot, @NotNull ServerBot bot) { -+ ItemStack itemStack = bot.getItemBySlot(slot); -+ for (int i = 0; i < 36; i++) { -+ ItemStack itemStack1 = bot.getInventory().getItem(i); -+ if (itemStack1 == ItemStack.EMPTY || itemStack1 == itemStack) { -+ continue; -+ } -+ -+ if (itemStack1.getItem().getClass() == itemStack.getItem().getClass() && !isDamage(itemStack1, 10)) { -+ ItemStack itemStack2 = itemStack1.copy(); -+ bot.getInventory().setItem(i, itemStack); -+ bot.setItemSlot(slot, itemStack2); -+ return; -+ } -+ } -+ -+ for (int i = 0; i < 36; i++) { -+ ItemStack itemStack1 = bot.getInventory().getItem(i); -+ if (itemStack1 == ItemStack.EMPTY && itemStack1 != itemStack) { -+ bot.getInventory().setItem(i, itemStack); -+ bot.setItemSlot(slot, ItemStack.EMPTY); -+ return; -+ } -+ } -+ } -+ -+ public static boolean isDamage(@NotNull ItemStack item, int minDamage) { -+ return item.isDamageableItem() && (item.getMaxDamage() - item.getDamageValue()) <= minDamage; -+ } -+ -+ @NotNull -+ public static UUID getBotUUID(@NotNull BotCreateState state) { -+ return getBotUUID(state.realName()); -+ } -+ -+ public static UUID getBotUUID(@NotNull String realName) { -+ return UUID.nameUUIDFromBytes(("Fakeplayer:" + realName).getBytes(Charsets.UTF_8)); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java b/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7ebe4d6c71e90be92387a585ea581c6b2c4af89d ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/IPlayerDataStorage.java -@@ -0,0 +1,13 @@ -+package org.leavesmc.leaves.bot; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.world.entity.player.Player; -+ -+import java.util.Optional; -+ -+public interface IPlayerDataStorage { -+ -+ void save(Player player); -+ -+ Optional load(Player player); -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java b/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4162df8802b1af73d9a0a76f846ab9ad953a1921 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/MojangAPI.java -@@ -0,0 +1,39 @@ -+package org.leavesmc.leaves.bot; -+ -+import com.google.gson.JsonObject; -+import com.google.gson.JsonParser; -+import org.leavesmc.leaves.LeavesConfig; -+ -+import java.io.IOException; -+import java.io.InputStreamReader; -+import java.net.URI; -+import java.util.HashMap; -+import java.util.Map; -+ -+public class MojangAPI { -+ -+ private static final Map CACHE = new HashMap<>(); -+ -+ public static String[] getSkin(String name) { -+ if (LeavesConfig.modify.fakeplayer.useSkinCache && CACHE.containsKey(name)) { -+ return CACHE.get(name); -+ } -+ -+ String[] values = pullFromAPI(name); -+ CACHE.put(name, values); -+ return values; -+ } -+ -+ // Laggggggggggggggggggggggggggggggggggggggggg -+ public static String[] pullFromAPI(String name) { -+ try { -+ String uuid = JsonParser.parseReader(new InputStreamReader(URI.create("https://api.mojang.com/users/profiles/minecraft/" + name).toURL().openStream())) -+ .getAsJsonObject().get("id").getAsString(); -+ JsonObject property = JsonParser.parseReader(new InputStreamReader(URI.create("https://sessionserver.mojang.com/session/minecraft/profile/" + uuid + "?unsigned=false").toURL().openStream())) -+ .getAsJsonObject().get("properties").getAsJsonArray().get(0).getAsJsonObject(); -+ return new String[]{property.get("value").getAsString(), property.get("signature").getAsString()}; -+ } catch (IOException | IllegalStateException | IllegalArgumentException e) { -+ return null; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/ServerBot.java b/src/main/java/org/leavesmc/leaves/bot/ServerBot.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f83b787a6827bdcf3927dbf2a0a0fa6e998959ac ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/ServerBot.java -@@ -0,0 +1,556 @@ -+package org.leavesmc.leaves.bot; -+ -+import com.google.common.collect.ImmutableMap; -+import com.mojang.authlib.GameProfile; -+import io.papermc.paper.adventure.PaperAdventure; -+import io.papermc.paper.event.entity.EntityKnockbackEvent; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.particles.BlockParticleOption; -+import net.minecraft.core.particles.ParticleTypes; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.ListTag; -+import net.minecraft.nbt.StringTag; -+import net.minecraft.network.chat.Component; -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.game.ClientboundPlayerInfoRemovePacket; -+import net.minecraft.network.protocol.game.ClientboundPlayerInfoUpdatePacket; -+import net.minecraft.network.protocol.game.ClientboundRotateHeadPacket; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ChunkMap; -+import net.minecraft.server.level.ClientInformation; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.network.ServerPlayerConnection; -+import net.minecraft.stats.ServerStatsCounter; -+import net.minecraft.util.Mth; -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.InteractionResult; -+import net.minecraft.world.SimpleMenuProvider; -+import net.minecraft.world.damagesource.DamageSource; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.EquipmentSlot; -+import net.minecraft.world.entity.PositionMoveRotation; -+import net.minecraft.world.entity.ai.attributes.Attributes; -+import net.minecraft.world.entity.item.ItemEntity; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.inventory.ChestMenu; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.GameRules; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.gameevent.GameEvent; -+import net.minecraft.world.level.portal.TeleportTransition; -+import net.minecraft.world.phys.EntityHitResult; -+import net.minecraft.world.phys.Vec3; -+import org.bukkit.Bukkit; -+import org.bukkit.Location; -+import org.bukkit.command.CommandSender; -+import org.bukkit.util.Vector; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.bot.agent.Actions; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.bot.agent.BotConfig; -+import org.leavesmc.leaves.bot.agent.Configs; -+import org.leavesmc.leaves.entity.CraftBot; -+import org.leavesmc.leaves.event.bot.BotActionScheduleEvent; -+import org.leavesmc.leaves.event.bot.BotCreateEvent; -+import org.leavesmc.leaves.event.bot.BotDeathEvent; -+import org.leavesmc.leaves.event.bot.BotInventoryOpenEvent; -+import org.leavesmc.leaves.event.bot.BotRemoveEvent; -+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; -+import org.leavesmc.leaves.util.MathUtils; -+ -+import java.util.ArrayList; -+import java.util.EnumSet; -+import java.util.List; -+import java.util.Map; -+import java.util.Objects; -+import java.util.UUID; -+import java.util.function.Predicate; -+ -+// TODO test -+public class ServerBot extends ServerPlayer { -+ -+ private final Map, BotConfig> configs; -+ private final List> actions; -+ -+ public boolean resume = false; -+ public BotCreateState createState; -+ public UUID createPlayer; -+ -+ private final int tracingRange; -+ private final ServerStatsCounter stats; -+ private final BotInventoryContainer container; -+ -+ public int notSleepTicks; -+ -+ public int removeTaskId = -1; -+ -+ private Vec3 knockback = Vec3.ZERO; -+ -+ public ServerBot(MinecraftServer server, ServerLevel world, GameProfile profile) { -+ super(server, world, profile, ClientInformation.createDefault()); -+ this.entityData.set(Player.DATA_PLAYER_MODE_CUSTOMISATION, (byte) -2); -+ -+ this.gameMode = new ServerBotGameMode(this); -+ this.actions = new ArrayList<>(); -+ -+ ImmutableMap.Builder, BotConfig> configBuilder = ImmutableMap.builder(); -+ for (Configs config : Configs.getConfigs()) { -+ configBuilder.put(config, config.config.create(this)); -+ } -+ this.configs = configBuilder.build(); -+ -+ this.stats = new BotStatsCounter(server); -+ this.container = new BotInventoryContainer(this); -+ this.tracingRange = world.spigotConfig.playerTrackingRange * world.spigotConfig.playerTrackingRange; -+ -+ this.notSleepTicks = 0; -+ this.fauxSleeping = LeavesConfig.modify.fakeplayer.canSkipSleep; -+ } -+ -+ public void sendPlayerInfo(ServerPlayer player) { -+ player.connection.send(new ClientboundPlayerInfoUpdatePacket(EnumSet.of(ClientboundPlayerInfoUpdatePacket.Action.ADD_PLAYER, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_LISTED, ClientboundPlayerInfoUpdatePacket.Action.UPDATE_DISPLAY_NAME), List.of(this))); -+ } -+ -+ public boolean needSendFakeData(ServerPlayer player) { -+ return this.getConfigValue(Configs.ALWAYS_SEND_DATA) && (player.level() == this.level() && player.position().distanceToSqr(this.position()) > this.tracingRange); -+ } -+ -+ public void sendFakeDataIfNeed(ServerPlayer player, boolean login) { -+ if (needSendFakeData(player)) { -+ this.sendFakeData(player.connection, login); -+ } -+ } -+ -+ public void sendFakeData(ServerPlayerConnection playerConnection, boolean login) { -+ ChunkMap.TrackedEntity entityTracker = ((ServerLevel) this.level()).getChunkSource().chunkMap.entityMap.get(this.getId()); -+ -+ if (entityTracker == null) { -+ LeavesLogger.LOGGER.warning("Fakeplayer cant get entity tracker for " + this.getId()); -+ return; -+ } -+ -+ playerConnection.send(this.getAddEntityPacket(entityTracker.serverEntity)); -+ if (login) { -+ Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> playerConnection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f))), 10); -+ } else { -+ playerConnection.send(new ClientboundRotateHeadPacket(this, (byte) ((getYRot() * 256f) / 360f))); -+ } -+ } -+ -+ public void renderAll() { -+ this.server.getPlayerList().getPlayers().forEach( -+ player -> { -+ this.sendPlayerInfo(player); -+ this.sendFakeDataIfNeed(player, false); -+ } -+ ); -+ } -+ -+ private void sendPacket(Packet packet) { -+ this.server.getPlayerList().getPlayers().forEach(player -> player.connection.send(packet)); -+ } -+ -+ @Override -+ public void die(@NotNull DamageSource damageSource) { -+ boolean flag = this.serverLevel().getGameRules().getBoolean(GameRules.RULE_SHOWDEATHMESSAGES); -+ Component defaultMessage = this.getCombatTracker().getDeathMessage(); -+ -+ BotDeathEvent event = new BotDeathEvent(this.getBukkitEntity(), PaperAdventure.asAdventure(defaultMessage), flag); -+ this.server.server.getPluginManager().callEvent(event); -+ -+ if (event.isCancelled()) { -+ if (this.getHealth() <= 0) { -+ this.setHealth(0.1f); -+ } -+ return; -+ } -+ -+ this.gameEvent(GameEvent.ENTITY_DIE); -+ -+ net.kyori.adventure.text.Component deathMessage = event.deathMessage(); -+ if (event.isSendDeathMessage() && deathMessage != null && !deathMessage.equals(net.kyori.adventure.text.Component.empty())) { -+ this.server.getPlayerList().broadcastSystemMessage(PaperAdventure.asVanilla(deathMessage), false); -+ } -+ -+ this.server.getBotList().removeBot(this, BotRemoveEvent.RemoveReason.DEATH, null, false); -+ } -+ -+ public void removeTab() { -+ this.sendPacket(new ClientboundPlayerInfoRemovePacket(List.of(this.getUUID()))); -+ } -+ -+ @Override -+ public @Nullable ServerBot teleport(@NotNull TeleportTransition teleportTarget) { -+ if (this.isSleeping() || this.isRemoved()) { -+ return null; -+ } -+ if (teleportTarget.newLevel().dimension() != this.serverLevel().dimension()) { -+ return null; -+ } else { -+ if (!teleportTarget.asPassenger()) { -+ this.stopRiding(); -+ } -+ -+ this.connection.internalTeleport(PositionMoveRotation.of(teleportTarget), teleportTarget.relatives()); -+ this.connection.resetPosition(); -+ teleportTarget.postTeleportTransition().onTransition(this); -+ return this; -+ } -+ } -+ -+ @Override -+ public void handlePortal() { -+ } -+ -+ @Override -+ public void tick() { -+ if (!this.isAlive()) { -+ return; -+ } -+ super.tick(); -+ -+ if (this.getConfigValue(Configs.SPAWN_PHANTOM)) { -+ notSleepTicks++; -+ } -+ -+ if (LeavesConfig.modify.fakeplayer.regenAmount > 0.0 && server.getTickCount() % 20 == 0) { -+ float health = getHealth(); -+ float maxHealth = getMaxHealth(); -+ float regenAmount = (float) (LeavesConfig.modify.fakeplayer.regenAmount * 20); -+ float amount; -+ -+ if (health < maxHealth - regenAmount) { -+ amount = health + regenAmount; -+ } else { -+ amount = maxHealth; -+ } -+ -+ this.setHealth(amount); -+ } -+ } -+ -+ @Override -+ public void onItemPickup(@NotNull ItemEntity item) { -+ super.onItemPickup(item); -+ this.updateItemInHand(InteractionHand.MAIN_HAND); -+ } -+ -+ public void updateItemInHand(InteractionHand hand) { -+ ItemStack item = this.getItemInHand(hand); -+ -+ if (!item.isEmpty()) { -+ BotUtil.replenishment(item, getInventory().items); -+ if (BotUtil.isDamage(item, 10)) { -+ BotUtil.replaceTool(hand == InteractionHand.MAIN_HAND ? EquipmentSlot.MAINHAND : EquipmentSlot.OFFHAND, this); -+ } -+ } -+ this.detectEquipmentUpdatesPublic(); -+ } -+ -+ @Override -+ public @NotNull InteractionResult interact(@NotNull Player player, @NotNull InteractionHand hand) { -+ if (LeavesConfig.modify.fakeplayer.canOpenInventory) { -+ if (player instanceof ServerPlayer player1 && player.getMainHandItem().isEmpty()) { -+ BotInventoryOpenEvent event = new BotInventoryOpenEvent(this.getBukkitEntity(), player1.getBukkitEntity()); -+ this.server.server.getPluginManager().callEvent(event); -+ if (!event.isCancelled()) { -+ player.openMenu(new SimpleMenuProvider((i, inventory, p) -> ChestMenu.sixRows(i, inventory, this.container), this.getDisplayName())); -+ return InteractionResult.SUCCESS; -+ } -+ } -+ } -+ return super.interact(player, hand); -+ } -+ -+ @Override -+ public void checkFallDamage(double heightDifference, boolean onGround, @NotNull BlockState state, @NotNull BlockPos landedPosition) { -+ if (onGround && this.fallDistance > 0.0F) { -+ this.onChangedBlock(this.serverLevel(), landedPosition); -+ double d1 = this.getAttributeValue(Attributes.SAFE_FALL_DISTANCE); -+ -+ if ((double) this.fallDistance > d1 && !state.isAir()) { -+ double d2 = this.getX(); -+ double d3 = this.getY(); -+ double d4 = this.getZ(); -+ BlockPos blockposition = this.blockPosition(); -+ -+ if (landedPosition.getX() != blockposition.getX() || landedPosition.getZ() != blockposition.getZ()) { -+ double d5 = d2 - (double) landedPosition.getX() - 0.5D; -+ double d6 = d4 - (double) landedPosition.getZ() - 0.5D; -+ double d7 = Math.max(Math.abs(d5), Math.abs(d6)); -+ -+ d2 = (double) landedPosition.getX() + 0.5D + d5 / d7 * 0.5D; -+ d4 = (double) landedPosition.getZ() + 0.5D + d6 / d7 * 0.5D; -+ } -+ -+ float f = (float) Mth.ceil((double) this.fallDistance - d1); -+ double d8 = Math.min(0.2F + f / 15.0F, 2.5D); -+ int i = (int) (150.0D * d8); -+ -+ this.serverLevel().sendParticles(this, new BlockParticleOption(ParticleTypes.BLOCK, state), d2, d3, d4, i, 0.0D, 0.0D, 0.0D, 0.15000000596046448D, false); -+ } -+ } -+ -+ if (onGround) { -+ if (this.fallDistance > 0.0F) { -+ state.getBlock().fallOn(this.level(), state, landedPosition, this, this.fallDistance); -+ this.level().gameEvent(GameEvent.HIT_GROUND, this.position(), GameEvent.Context.of(this, this.mainSupportingBlockPos.map((blockposition1) -> { -+ return this.level().getBlockState(blockposition1); -+ }).orElse(state))); -+ } -+ -+ this.resetFallDistance(); -+ } else if (heightDifference < 0.0D) { -+ this.fallDistance -= (float) heightDifference; -+ } -+ } -+ -+ @Override -+ public void doTick() { -+ this.absMoveTo(this.getX(), this.getY(), this.getZ(), this.getYRot(), this.getXRot()); -+ -+ if (this.takeXpDelay > 0) { -+ --this.takeXpDelay; -+ } -+ -+ if (this.isSleeping()) { -+ ++this.sleepCounter; -+ if (this.sleepCounter > 100) { -+ this.sleepCounter = 100; -+ this.notSleepTicks = 0; -+ } -+ -+ if (!this.level().isClientSide && this.level().isDay()) { -+ this.stopSleepInBed(false, true); -+ } -+ } else if (this.sleepCounter > 0) { -+ ++this.sleepCounter; -+ if (this.sleepCounter >= 110) { -+ this.sleepCounter = 0; -+ } -+ } -+ -+ this.updateIsUnderwater(); -+ -+ this.addDeltaMovement(knockback); -+ this.knockback = Vec3.ZERO; -+ -+ this.server.scheduleOnMain(this::runAction); -+ -+ this.livingEntityTick(); -+ -+ this.foodData.tick(this); -+ -+ ++this.attackStrengthTicker; -+ ItemStack itemstack = this.getMainHandItem(); -+ if (!ItemStack.matches(this.lastItemInMainHand, itemstack)) { -+ if (!ItemStack.isSameItem(this.lastItemInMainHand, itemstack)) { -+ this.resetAttackStrengthTicker(); -+ } -+ -+ this.lastItemInMainHand = itemstack.copy(); -+ } -+ -+ this.getCooldowns().tick(); -+ this.updatePlayerPose(); -+ -+ if (this.hurtTime > 0) { -+ this.hurtTime -= 1; -+ } -+ } -+ -+ @Override -+ public void knockback(double strength, double x, double z, @Nullable Entity attacker, @NotNull EntityKnockbackEvent.Cause cause) { -+ strength *= 1.0D - this.getAttributeValue(Attributes.KNOCKBACK_RESISTANCE); -+ if (strength > 0.0D) { -+ Vec3 vec3d = this.getDeltaMovement(); -+ Vec3 vec3d1 = (new Vec3(x, 0.0D, z)).normalize().scale(strength); -+ this.hasImpulse = true; -+ this.knockback = new Vec3(vec3d.x / 2.0D - vec3d1.x, this.onGround() ? Math.min(0.4D, vec3d.y / 2.0D + strength) : vec3d.y, vec3d.z / 2.0D - vec3d1.z).subtract(vec3d); -+ } -+ } -+ -+ @Override -+ public void attack(@NotNull Entity target) { -+ super.attack(target); -+ this.swing(InteractionHand.MAIN_HAND); -+ } -+ -+ @Override -+ public void addAdditionalSaveData(@NotNull CompoundTag nbt) { -+ super.addAdditionalSaveData(nbt); -+ nbt.putBoolean("isShiftKeyDown", this.isShiftKeyDown()); -+ -+ CompoundTag createNbt = new CompoundTag(); -+ createNbt.putString("realName", this.createState.realName()); -+ createNbt.putString("name", this.createState.name()); -+ -+ createNbt.putString("skinName", this.createState.skinName()); -+ if (this.createState.skin() != null) { -+ ListTag skin = new ListTag(); -+ for (String s : this.createState.skin()) { -+ skin.add(StringTag.valueOf(s)); -+ } -+ createNbt.put("skin", skin); -+ } -+ -+ nbt.put("createStatus", createNbt); -+ -+ if (!this.actions.isEmpty()) { -+ ListTag actionNbt = new ListTag(); -+ for (BotAction action : this.actions) { -+ actionNbt.add(action.save(new CompoundTag())); -+ } -+ nbt.put("actions", actionNbt); -+ } -+ -+ if (!this.configs.isEmpty()) { -+ ListTag configNbt = new ListTag(); -+ for (BotConfig config : this.configs.values()) { -+ configNbt.add(config.save(new CompoundTag())); -+ } -+ nbt.put("configs", configNbt); -+ } -+ } -+ -+ @Override -+ public void readAdditionalSaveData(@NotNull CompoundTag nbt) { -+ super.readAdditionalSaveData(nbt); -+ this.setShiftKeyDown(nbt.getBoolean("isShiftKeyDown")); -+ -+ CompoundTag createNbt = nbt.getCompound("createStatus"); -+ BotCreateState.Builder createBuilder = BotCreateState.builder(createNbt.getString("realName"), null).name(createNbt.getString("name")); -+ -+ String[] skin = null; -+ if (createNbt.contains("skin")) { -+ ListTag skinTag = createNbt.getList("skin", 8); -+ skin = new String[skinTag.size()]; -+ for (int i = 0; i < skinTag.size(); i++) { -+ skin[i] = skinTag.getString(i); -+ } -+ } -+ -+ createBuilder.skinName(createNbt.getString("skinName")).skin(skin); -+ createBuilder.createReason(BotCreateEvent.CreateReason.INTERNAL).creator(null); -+ -+ this.createState = createBuilder.build(); -+ this.gameProfile = new BotList.CustomGameProfile(this.getUUID(), this.createState.name(), this.createState.skin()); -+ -+ -+ if (nbt.contains("actions")) { -+ ListTag actionNbt = nbt.getList("actions", 10); -+ for (int i = 0; i < actionNbt.size(); i++) { -+ CompoundTag actionTag = actionNbt.getCompound(i); -+ BotAction action = Actions.getForName(actionTag.getString("actionName")); -+ if (action != null) { -+ BotAction newAction = action.create(); -+ newAction.load(actionTag); -+ this.actions.add(newAction); -+ } -+ } -+ } -+ -+ if (nbt.contains("configs")) { -+ ListTag configNbt = nbt.getList("configs", 10); -+ for (int i = 0; i < configNbt.size(); i++) { -+ CompoundTag configTag = configNbt.getCompound(i); -+ Configs configKey = Configs.getConfig(configTag.getString("configName")); -+ if (configKey != null) { -+ this.configs.get(configKey).load(configTag); -+ } -+ } -+ } -+ } -+ -+ public void faceLocation(@NotNull Location loc) { -+ this.look(loc.toVector().subtract(getLocation().toVector()), false); -+ } -+ -+ public void look(Vector dir, boolean keepYaw) { -+ float yaw, pitch; -+ -+ if (keepYaw) { -+ yaw = this.getYHeadRot(); -+ pitch = MathUtils.fetchPitch(dir); -+ } else { -+ float[] vals = MathUtils.fetchYawPitch(dir); -+ yaw = vals[0]; -+ pitch = vals[1]; -+ -+ this.sendPacket(new ClientboundRotateHeadPacket(this, (byte) (yaw * 256 / 360f))); -+ } -+ -+ this.setRot(yaw, pitch); -+ } -+ -+ public Location getLocation() { -+ return this.getBukkitEntity().getLocation(); -+ } -+ -+ public Entity getTargetEntity(int maxDistance, Predicate predicate) { -+ List entities = this.level().getEntities((Entity) null, this.getBoundingBox(), (e -> e != this && (predicate == null || predicate.test(e)))); -+ if (!entities.isEmpty()) { -+ return entities.getFirst(); -+ } else { -+ EntityHitResult result = this.getBukkitEntity().rayTraceEntity(maxDistance, false); -+ if (result != null && (predicate == null || predicate.test(result.getEntity()))) { -+ return result.getEntity(); -+ } -+ } -+ return null; -+ } -+ -+ public void dropAll() { -+ this.getInventory().dropAll(); -+ this.detectEquipmentUpdatesPublic(); -+ } -+ -+ private void runAction() { -+ if (LeavesConfig.modify.fakeplayer.canUseAction) { -+ this.actions.forEach(action -> action.tryTick(this)); -+ this.actions.removeIf(BotAction::isCancelled); -+ } -+ } -+ -+ public boolean addBotAction(BotAction action, CommandSender sender) { -+ if (!LeavesConfig.modify.fakeplayer.canUseAction) { -+ return false; -+ } -+ -+ if (!new BotActionScheduleEvent(this.getBukkitEntity(), action.getName(), action.getUUID(), sender).callEvent()) { -+ return false; -+ } -+ -+ action.init(); -+ this.actions.add(action); -+ return true; -+ } -+ -+ public List> getBotActions() { -+ return actions; -+ } -+ -+ @Override -+ public @NotNull ServerStatsCounter getStats() { -+ return stats; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public BotConfig getConfig(Configs config) { -+ return (BotConfig) Objects.requireNonNull(this.configs.get(config)); -+ } -+ -+ public E getConfigValue(Configs config) { -+ return this.getConfig(config).getValue(); -+ } -+ -+ @Override -+ @NotNull -+ public CraftBot getBukkitEntity() { -+ return (CraftBot) super.getBukkitEntity(); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java b/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java -new file mode 100644 -index 0000000000000000000000000000000000000000..87ea98ab920a604bdd51281e0656c65160d30118 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/ServerBotGameMode.java -@@ -0,0 +1,137 @@ -+package org.leavesmc.leaves.bot; -+ -+import net.kyori.adventure.text.Component; -+import net.minecraft.core.BlockPos; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.server.level.ServerPlayerGameMode; -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.InteractionResult; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.context.UseOnContext; -+import net.minecraft.world.level.GameType; -+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.state.BlockState; -+import net.minecraft.world.phys.BlockHitResult; -+import org.bukkit.event.player.PlayerGameModeChangeEvent; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+ -+public class ServerBotGameMode extends ServerPlayerGameMode { -+ -+ public ServerBotGameMode(ServerBot bot) { -+ super(bot); -+ super.setGameModeForPlayer(GameType.SURVIVAL, null); -+ } -+ -+ @Override -+ public boolean changeGameModeForPlayer(@NotNull GameType gameMode) { -+ return false; -+ } -+ -+ @Nullable -+ @Override -+ public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) { -+ return null; -+ } -+ -+ @Override -+ protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) { -+ } -+ -+ @Override -+ public void tick() { -+ } -+ -+ @Override -+ public void destroyAndAck(@NotNull BlockPos pos, int sequence, @NotNull String reason) { -+ this.destroyBlock(pos); -+ } -+ -+ @Override -+ public boolean destroyBlock(@NotNull BlockPos pos) { -+ BlockState iblockdata = this.level.getBlockState(pos); -+ BlockEntity tileentity = this.level.getBlockEntity(pos); -+ Block block = iblockdata.getBlock(); -+ -+ if (this.player.blockActionRestricted(this.level, pos, this.getGameModeForPlayer())) { -+ return false; -+ } else { -+ this.level.captureDrops = null; -+ BlockState iblockdata1 = block.playerWillDestroy(this.level, pos, iblockdata, this.player); -+ boolean flag = this.level.removeBlock(pos, false); -+ -+ if (flag) { -+ block.destroy(this.level, pos, iblockdata1); -+ } -+ -+ ItemStack itemstack = this.player.getMainHandItem(); -+ ItemStack itemstack1 = itemstack.copy(); -+ -+ boolean flag1 = this.player.hasCorrectToolForDrops(iblockdata1); -+ -+ itemstack.mineBlock(this.level, iblockdata1, pos, this.player); -+ if (flag && flag1) { -+ Block.dropResources(iblockdata1, this.level, pos, tileentity, this.player, itemstack1, true); -+ } -+ -+ if (flag) { -+ iblockdata.getBlock().popExperience(this.level, pos, block.getExpDrop(iblockdata, this.level, pos, itemstack, true), this.player); -+ } -+ -+ return true; -+ } -+ } -+ -+ @NotNull -+ @Override -+ public InteractionResult useItemOn(@NotNull ServerPlayer player, Level world, @NotNull ItemStack stack, @NotNull InteractionHand hand, BlockHitResult hitResult) { -+ BlockPos blockposition = hitResult.getBlockPos(); -+ BlockState iblockdata = world.getBlockState(blockposition); -+ InteractionResult enuminteractionresult = InteractionResult.PASS; -+ -+ if (!iblockdata.getBlock().isEnabled(world.enabledFeatures())) { -+ return InteractionResult.FAIL; -+ } -+ -+ if (player.getCooldowns().isOnCooldown(stack)) { -+ return InteractionResult.PASS; -+ } -+ -+ this.firedInteract = true; -+ this.interactResult = false; -+ this.interactPosition = blockposition.immutable(); -+ this.interactHand = hand; -+ this.interactItemStack = stack.copy(); -+ -+ boolean flag = !player.getMainHandItem().isEmpty() || !player.getOffhandItem().isEmpty(); -+ boolean flag1 = player.isSecondaryUseActive() && flag; -+ -+ if (!flag1) { -+ InteractionResult iteminteractionresult = iblockdata.useItemOn(player.getItemInHand(hand), world, player, hand, hitResult); -+ -+ if (iteminteractionresult.consumesAction()) { -+ return iteminteractionresult; -+ } -+ -+ if (iteminteractionresult == InteractionResult.PASS && hand == InteractionHand.MAIN_HAND) { -+ enuminteractionresult = iblockdata.useWithoutItem(world, player, hitResult); -+ if (enuminteractionresult.consumesAction()) { -+ return enuminteractionresult; -+ } -+ } -+ } -+ -+ if (!stack.isEmpty() && enuminteractionresult != InteractionResult.SUCCESS && !this.interactResult) { -+ UseOnContext itemactioncontext = new UseOnContext(player, hand, hitResult); -+ return stack.useOn(itemactioncontext); -+ } -+ return enuminteractionresult; -+ } -+ -+ @Override -+ public void setLevel(@NotNull ServerLevel world) { -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java b/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c62f9258e4114ff686642b7f472d0e14151f37d5 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/ServerBotPacketListenerImpl.java -@@ -0,0 +1,85 @@ -+package org.leavesmc.leaves.bot; -+ -+import net.minecraft.network.Connection; -+import net.minecraft.network.DisconnectionDetails; -+import net.minecraft.network.PacketSendListener; -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.PacketFlow; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.network.CommonListenerCookie; -+import net.minecraft.server.network.ServerGamePacketListenerImpl; -+import org.bukkit.event.player.PlayerKickEvent; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+ -+public class ServerBotPacketListenerImpl extends ServerGamePacketListenerImpl { -+ -+ public ServerBotPacketListenerImpl(MinecraftServer server, ServerBot bot) { -+ super(server, BotConnection.INSTANCE, bot, CommonListenerCookie.createInitial(bot.gameProfile, false)); -+ } -+ -+ @Override -+ public void sendPacket(@NotNull Packet packet) { -+ } -+ -+ @Override -+ public void send(@NotNull Packet packet) { -+ } -+ -+ @Override -+ public void send(@NotNull Packet packet, @Nullable PacketSendListener callbacks) { -+ } -+ -+ @Override -+ public void disconnect(@NotNull DisconnectionDetails disconnectionInfo, PlayerKickEvent.@NotNull Cause cause) { -+ } -+ -+ @Override -+ public boolean isAcceptingMessages() { -+ return true; -+ } -+ -+ @Override -+ public void tick() { -+ } -+ -+ public static class BotConnection extends Connection { -+ -+ private static final BotConnection INSTANCE = new BotConnection(); -+ -+ public BotConnection() { -+ super(PacketFlow.SERVERBOUND); -+ } -+ -+ @Override -+ public void tick() { -+ } -+ -+ @Override -+ public boolean isConnected() { -+ return true; -+ } -+ -+ @Override -+ public boolean isConnecting() { -+ return false; -+ } -+ -+ @Override -+ public boolean isMemoryConnection() { -+ return false; -+ } -+ -+ @Override -+ public void send(@NotNull Packet packet) { -+ } -+ -+ @Override -+ public void send(@NotNull Packet packet, @Nullable PacketSendListener packetsendlistener) { -+ } -+ -+ @Override -+ public void send(@NotNull Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java b/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a37513e1ba8443c702ab0c01fbe5e052e5f0f2ab ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/Actions.java -@@ -0,0 +1,67 @@ -+package org.leavesmc.leaves.bot.agent; -+ -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.agent.actions.*; -+ -+import java.util.Collection; -+import java.util.HashMap; -+import java.util.Map; -+import java.util.Set; -+ -+public class Actions { -+ -+ private static final Map> actions = new HashMap<>(); -+ -+ public static void registerAll() { -+ register(new AttackAction()); -+ register(new BreakBlockAction()); -+ register(new DropAction()); -+ register(new JumpAction()); -+ register(new RotateAction()); -+ register(new SneakAction()); -+ register(new UseItemAction()); -+ register(new UseItemOnAction()); -+ register(new UseItemToAction()); -+ register(new LookAction()); -+ register(new FishAction()); -+ register(new SwimAction()); -+ register(new UseItemOffHandAction()); -+ register(new UseItemOnOffhandAction()); -+ register(new UseItemToOffhandAction()); -+ register(new RotationAction()); -+ } -+ -+ public static boolean register(@NotNull BotAction action) { -+ if (!actions.containsKey(action.getName())) { -+ actions.put(action.getName(), action); -+ return true; -+ } -+ return false; -+ } -+ -+ public static boolean unregister(@NotNull String name) { -+ if (actions.containsKey(name)) { -+ actions.remove(name); -+ return true; -+ } -+ return false; -+ } -+ -+ @NotNull -+ @Contract(pure = true) -+ public static Collection> getAll() { -+ return actions.values(); -+ } -+ -+ @NotNull -+ public static Set getNames() { -+ return actions.keySet(); -+ } -+ -+ @Nullable -+ public static BotAction getForName(String name) { -+ return actions.get(name); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3bd512b436b32f240466a406a101a051b4b07817 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/BotAction.java -@@ -0,0 +1,163 @@ -+package org.leavesmc.leaves.bot.agent; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.event.bot.BotActionExecuteEvent; -+import org.leavesmc.leaves.event.bot.BotActionStopEvent; -+ -+import java.util.List; -+import java.util.UUID; -+import java.util.function.Supplier; -+ -+public abstract class BotAction> { -+ -+ private final String name; -+ private final CommandArgument argument; -+ private final Supplier creator; -+ -+ private boolean cancel; -+ private int tickDelay; -+ private int number; -+ private UUID uuid; -+ -+ private int needWaitTick; -+ private int canDoNumber; -+ -+ public BotAction(String name, CommandArgument argument, Supplier creator) { -+ this.name = name; -+ this.argument = argument; -+ this.uuid = UUID.randomUUID(); -+ this.creator = creator; -+ -+ this.cancel = false; -+ this.tickDelay = 20; -+ this.number = -1; -+ } -+ -+ public void init() { -+ this.needWaitTick = 0; -+ this.canDoNumber = this.getNumber(); -+ this.setCancelled(false); -+ } -+ -+ public String getName() { -+ return this.name; -+ } -+ -+ public UUID getUUID() { -+ return uuid; -+ } -+ -+ public int getTickDelay() { -+ return this.tickDelay; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public E setTickDelay(int tickDelay) { -+ this.tickDelay = Math.max(0, tickDelay); -+ return (E) this; -+ } -+ -+ public int getNumber() { -+ return this.number; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public E setNumber(int number) { -+ this.number = Math.max(-1, number); -+ return (E) this; -+ } -+ -+ public int getCanDoNumber() { -+ return this.canDoNumber; -+ } -+ -+ public boolean isCancelled() { -+ return cancel; -+ } -+ -+ public void setCancelled(boolean cancel) { -+ this.cancel = cancel; -+ } -+ -+ public void stop(@NotNull ServerBot bot, BotActionStopEvent.Reason reason) { -+ new BotActionStopEvent(bot.getBukkitEntity(), this.name, this.uuid, reason, null).callEvent(); -+ this.setCancelled(true); -+ } -+ -+ public CommandArgument getArgument() { -+ return this.argument; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public E setTabComplete(int index, List list) { -+ this.argument.setTabComplete(index, list); -+ return (E) this; -+ } -+ -+ public void tryTick(ServerBot bot) { -+ if (this.canDoNumber == 0) { -+ this.stop(bot, BotActionStopEvent.Reason.DONE); -+ return; -+ } -+ -+ if (this.needWaitTick <= 0) { -+ BotActionExecuteEvent event = new BotActionExecuteEvent(bot.getBukkitEntity(), name, uuid); -+ -+ event.callEvent(); -+ if (event.getResult() == BotActionExecuteEvent.Result.SOFT_CANCEL) { -+ this.needWaitTick = this.getTickDelay(); -+ return; -+ } else if (event.getResult() == BotActionExecuteEvent.Result.HARD_CANCEL) { -+ if (this.canDoNumber > 0) { -+ this.canDoNumber--; -+ } -+ this.needWaitTick = this.getTickDelay(); -+ return; -+ } -+ -+ if (this.doTick(bot)) { -+ if (this.canDoNumber > 0) { -+ this.canDoNumber--; -+ } -+ this.needWaitTick = this.getTickDelay(); -+ } -+ } else { -+ this.needWaitTick--; -+ } -+ } -+ -+ @NotNull -+ public E create() { -+ return this.creator.get(); -+ } -+ -+ @NotNull -+ public CompoundTag save(@NotNull CompoundTag nbt) { -+ if (!this.cancel) { -+ nbt.putString("actionName", this.name); -+ nbt.putUUID("actionUUID", this.uuid); -+ -+ nbt.putInt("canDoNumber", this.canDoNumber); -+ nbt.putInt("needWaitTick", this.needWaitTick); -+ nbt.putInt("tickDelay", this.tickDelay); -+ } -+ return nbt; -+ } -+ -+ public void load(@NotNull CompoundTag nbt) { -+ this.tickDelay = nbt.getInt("tickDelay"); -+ this.needWaitTick = nbt.getInt("needWaitTick"); -+ this.canDoNumber = nbt.getInt("canDoNumber"); -+ this.uuid = nbt.getUUID("actionUUID"); -+ } -+ -+ public abstract void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result); -+ -+ public abstract boolean doTick(@NotNull ServerBot bot); -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c889a2409d8b9f5979a10b61c98638054bd8f5bc ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/BotConfig.java -@@ -0,0 +1,62 @@ -+package org.leavesmc.leaves.bot.agent; -+ -+import net.minecraft.nbt.CompoundTag; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+ -+import java.util.List; -+import java.util.function.Supplier; -+ -+public abstract class BotConfig { -+ -+ private final String name; -+ private final CommandArgument argument; -+ private final Supplier> creator; -+ protected ServerBot bot; -+ -+ public BotConfig(String name, CommandArgument argument, Supplier> creator) { -+ this.name = name; -+ this.argument = argument; -+ this.creator = creator; -+ } -+ -+ public BotConfig setBot(ServerBot bot) { -+ this.bot = bot; -+ return this; -+ } -+ -+ public abstract E getValue(); -+ -+ public abstract void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException; -+ -+ public List getMessage() { -+ return List.of(this.bot.getScoreboardName() + "'s " + this.getName() + ": " + this.getValue()); -+ } -+ -+ public List getChangeMessage() { -+ return List.of(this.bot.getScoreboardName() + "'s " + this.getName() + " changed: " + this.getValue()); -+ } -+ -+ public String getName() { -+ return name; -+ } -+ -+ public CommandArgument getArgument() { -+ return argument; -+ } -+ -+ @NotNull -+ public BotConfig create(ServerBot bot) { -+ return this.creator.get().setBot(bot); -+ } -+ -+ @NotNull -+ public CompoundTag save(@NotNull CompoundTag nbt) { -+ nbt.putString("configName", this.name); -+ return nbt; -+ } -+ -+ public abstract void load(@NotNull CompoundTag nbt); -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java b/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d99f459b2e323474174cfd5d892cb7573a32ca12 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/Configs.java -@@ -0,0 +1,44 @@ -+package org.leavesmc.leaves.bot.agent; -+ -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.agent.configs.*; -+ -+import java.util.Collection; -+import java.util.HashMap; -+import java.util.Map; -+ -+public class Configs { -+ -+ private static final Map> configs = new HashMap<>(); -+ -+ public static final Configs SKIP_SLEEP = register(new SkipSleepConfig()); -+ public static final Configs ALWAYS_SEND_DATA = register(new AlwaysSendDataConfig()); -+ public static final Configs SPAWN_PHANTOM = register(new SpawnPhantomConfig()); -+ public static final Configs SIMULATION_DISTANCE = register(new SimulationDistanceConfig()); -+ -+ public final BotConfig config; -+ -+ private Configs(BotConfig config) { -+ this.config = config; -+ } -+ -+ @NotNull -+ @Contract(pure = true) -+ public static Collection> getConfigs() { -+ return configs.values(); -+ } -+ -+ @Nullable -+ public static Configs getConfig(String name) { -+ return configs.get(name); -+ } -+ -+ @NotNull -+ private static Configs register(BotConfig botConfig) { -+ Configs config = new Configs<>(botConfig); -+ configs.put(botConfig.getName(), config); -+ return config; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..be55a3085a53542c08e7f0209883a4f3f72602e7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AbstractTimerAction.java -@@ -0,0 +1,25 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.command.CommandArgumentType; -+ -+import java.util.List; -+import java.util.function.Supplier; -+ -+public abstract class AbstractTimerAction> extends BotAction { -+ -+ public AbstractTimerAction(String name, Supplier creator) { -+ super(name, CommandArgument.of(CommandArgumentType.INTEGER, CommandArgumentType.INTEGER), creator); -+ this.setTabComplete(0, List.of("[TickDelay]")).setTabComplete(1, List.of("[DoNumber]")); -+ } -+ -+ @Override -+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { -+ this.setTickDelay(result.readInt(20)).setNumber(result.readInt(-1)); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6c66ef29e702a49d1b8569aa0942e22e40843343 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/AttackAction.java -@@ -0,0 +1,22 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.world.entity.Entity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+public class AttackAction extends AbstractTimerAction { -+ -+ public AttackAction() { -+ super("attack", AttackAction::new); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ Entity entity = bot.getTargetEntity(3, target -> target.isAttackable() && !target.skipAttackInteraction(bot)); -+ if (entity != null) { -+ bot.attack(entity); -+ return true; -+ } -+ return false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bf7d20374cd7bff7cb7e09d209c6da5d297fe1f1 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/BreakBlockAction.java -@@ -0,0 +1,75 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.level.block.state.BlockState; -+import org.bukkit.block.Block; -+import org.bukkit.craftbukkit.block.CraftBlock; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+public class BreakBlockAction extends AbstractTimerAction { -+ -+ public BreakBlockAction() { -+ super("break", BreakBlockAction::new); -+ } -+ -+ private BlockPos lastPos = null; -+ private int destroyProgressTime = 0; -+ private int lastSentState = -1; -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ Block block = bot.getBukkitEntity().getTargetBlockExact(5); -+ if (block != null) { -+ BlockPos pos = ((CraftBlock) block).getPosition(); -+ -+ if (lastPos == null || !lastPos.equals(pos)) { -+ lastPos = pos; -+ destroyProgressTime = 0; -+ lastSentState = -1; -+ } -+ -+ BlockState iblockdata = bot.level().getBlockState(pos); -+ if (!iblockdata.isAir()) { -+ bot.swing(InteractionHand.MAIN_HAND); -+ -+ if (iblockdata.getDestroyProgress(bot, bot.level(), pos) >= 1.0F) { -+ bot.gameMode.destroyAndAck(pos, 0, "insta mine"); -+ bot.level().destroyBlockProgress(bot.getId(), pos, -1); -+ bot.updateItemInHand(InteractionHand.MAIN_HAND); -+ finalBreak(); -+ return true; -+ } -+ -+ float damage = this.incrementDestroyProgress(bot, iblockdata, pos); -+ if (damage >= 1.0F) { -+ bot.gameMode.destroyAndAck(pos, 0, "destroyed"); -+ bot.level().destroyBlockProgress(bot.getId(), pos, -1); -+ bot.updateItemInHand(InteractionHand.MAIN_HAND); -+ finalBreak(); -+ return true; -+ } -+ } -+ } -+ return false; -+ } -+ -+ private void finalBreak() { -+ lastPos = null; -+ destroyProgressTime = 0; -+ lastSentState = -1; -+ } -+ -+ private float incrementDestroyProgress(ServerBot bot, @NotNull BlockState state, BlockPos pos) { -+ float f = state.getDestroyProgress(bot, bot.level(), pos) * (float) (++destroyProgressTime); -+ int k = (int) (f * 10.0F); -+ -+ if (k != lastSentState) { -+ bot.level().destroyBlockProgress(bot.getId(), pos, k); -+ lastSentState = k; -+ } -+ -+ return f; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d96fc7b97ff826efe1bd36988f2d1a9ea04654cb ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftBotAction.java -@@ -0,0 +1,54 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.agent.Actions; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.entity.botaction.BotActionType; -+import org.leavesmc.leaves.entity.botaction.LeavesBotAction; -+ -+public class CraftBotAction extends LeavesBotAction { -+ -+ private final BotAction handle; -+ -+ public CraftBotAction(@NotNull BotAction action) { -+ super(BotActionType.valueOf(action.getName()), action.getTickDelay(), action.getCanDoNumber()); -+ this.handle = action; -+ } -+ -+ @Contract("_ -> new") -+ @NotNull -+ public static LeavesBotAction asAPICopy(BotAction action) { -+ return new CraftBotAction(action); -+ } -+ -+ @NotNull -+ public static BotAction asInternalCopy(@NotNull LeavesBotAction action) { -+ BotAction act = Actions.getForName(action.getActionName()); -+ if (act == null) { -+ throw new IllegalArgumentException("Invalid action name!"); -+ } -+ -+ BotAction newAction = null; -+ String[] args = new String[]{String.valueOf(action.getExecuteInterval()), String.valueOf(action.getRemainingExecuteTime())}; -+ try { -+ if (act instanceof CraftCustomBotAction customBotAction) { -+ newAction = customBotAction.createCraft(action.getActionPlayer(), args); -+ } else { -+ newAction = act.create(); -+ newAction.loadCommand(action.getActionPlayer() == null ? null : ((CraftPlayer) action.getActionPlayer()).getHandle(), act.getArgument().parse(0, args)); -+ } -+ } catch (IllegalArgumentException ignore) { -+ } -+ -+ if (newAction == null) { -+ throw new IllegalArgumentException("Invalid action!"); // TODO look action -+ } -+ return newAction; -+ } -+ -+ public BotAction getHandle() { -+ return handle; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7b149243b08a44f1181e82217a8645ccab7732d7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/CraftCustomBotAction.java -@@ -0,0 +1,49 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.server.level.ServerPlayer; -+import org.bukkit.entity.Player; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.entity.botaction.CustomBotAction; -+ -+public class CraftCustomBotAction extends BotAction { -+ -+ private final CustomBotAction realAction; -+ -+ public CraftCustomBotAction(String name, @NotNull CustomBotAction realAction) { -+ super(name, CommandArgument.of().setAllTabComplete(realAction.getTabComplete()), null); -+ this.realAction = realAction; -+ } -+ -+ @Override -+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { -+ throw new UnsupportedOperationException("Not supported."); -+ } -+ -+ public CraftCustomBotAction createCraft(@Nullable Player player, String[] args) { -+ CustomBotAction newRealAction = realAction.getNew(player, args); -+ if (newRealAction != null) { -+ return new CraftCustomBotAction(this.getName(), newRealAction); -+ } -+ return null; -+ } -+ -+ @Override -+ public int getNumber() { -+ return realAction.getNumber(); -+ } -+ -+ @Override -+ public int getTickDelay() { -+ return realAction.getTickDelay(); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ return realAction.doTick(bot.getBukkitEntity()); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c71e483e8938ef3b181c95d8e297e54203b5b914 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/DropAction.java -@@ -0,0 +1,25 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+ -+public class DropAction extends AbstractTimerAction { -+ -+ public DropAction() { -+ super("drop", DropAction::new); -+ } -+ -+ @Override -+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { -+ this.setTickDelay(result.readInt(100)).setNumber(result.readInt(1)); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ bot.dropAll(); -+ return true; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3a13f8ac73e042d939496fb5602e4aa4ea368e0d ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/FishAction.java -@@ -0,0 +1,73 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.entity.projectile.FishingHook; -+import net.minecraft.world.item.FishingRodItem; -+import net.minecraft.world.item.ItemStack; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+public class FishAction extends AbstractTimerAction { -+ -+ public FishAction() { -+ super("fish", FishAction::new); -+ } -+ -+ private int delay = 0; -+ private int nowDelay = 0; -+ -+ @Override -+ public FishAction setTickDelay(int tickDelay) { -+ super.setTickDelay(0); -+ this.delay = tickDelay; -+ return this; -+ } -+ -+ @Override -+ @NotNull -+ public CompoundTag save(@NotNull CompoundTag nbt) { -+ super.save(nbt); -+ nbt.putInt("fishDelay", this.delay); -+ nbt.putInt("fishNowDelay", this.nowDelay); -+ return nbt; -+ } -+ -+ @Override -+ public void load(@NotNull CompoundTag nbt) { -+ super.load(nbt); -+ this.delay = nbt.getInt("fishDelay"); -+ this.nowDelay = nbt.getInt("fishNowDelay"); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ if (this.nowDelay > 0) { -+ this.nowDelay--; -+ return false; -+ } -+ -+ ItemStack mainHand = bot.getMainHandItem(); -+ if (mainHand == ItemStack.EMPTY || mainHand.getItem().getClass() != FishingRodItem.class) { -+ return false; -+ } -+ -+ FishingHook fishingHook = bot.fishing; -+ if (fishingHook != null) { -+ if (fishingHook.currentState == FishingHook.FishHookState.HOOKED_IN_ENTITY) { -+ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); -+ this.nowDelay = 20; -+ return false; -+ } -+ if (fishingHook.nibble > 0) { -+ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); -+ this.nowDelay = this.delay; -+ return true; -+ } -+ } else { -+ mainHand.use(bot.level(), bot, InteractionHand.MAIN_HAND); -+ } -+ -+ return false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6fc9ba9bf94cb19ed32cfafa3a44fad0201b14a6 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/JumpAction.java -@@ -0,0 +1,21 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+public class JumpAction extends AbstractTimerAction { -+ -+ public JumpAction() { -+ super("jump", JumpAction::new); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ if (bot.onGround()) { -+ bot.jumpFromGround(); -+ return true; -+ } else { -+ return false; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8be962cf7dc273ccb6a6754684a9be8353865225 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/LookAction.java -@@ -0,0 +1,63 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ServerPlayer; -+import org.bukkit.util.Vector; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.command.CommandArgumentType; -+ -+import java.util.List; -+ -+public class LookAction extends BotAction { -+ -+ public LookAction() { -+ super("look", CommandArgument.of(CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE, CommandArgumentType.DOUBLE), LookAction::new); -+ this.setTabComplete(0, List.of("")); -+ this.setTabComplete(1, List.of("")); -+ this.setTabComplete(2, List.of("")); -+ } -+ -+ private Vector pos; -+ -+ @Override -+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) throws IllegalArgumentException { -+ Vector pos = result.readVector(); -+ if (pos != null) { -+ this.setPos(pos).setTickDelay(0).setNumber(1); -+ } else { -+ throw new IllegalArgumentException("pos?"); -+ } -+ } -+ -+ @Override -+ @NotNull -+ public CompoundTag save(@NotNull CompoundTag nbt) { -+ super.save(nbt); -+ nbt.putDouble("x", this.pos.getX()); -+ nbt.putDouble("y", this.pos.getY()); -+ nbt.putDouble("z", this.pos.getZ()); -+ return nbt; -+ } -+ -+ @Override -+ public void load(@NotNull CompoundTag nbt) { -+ super.load(nbt); -+ this.setPos(new Vector(nbt.getDouble("x"), nbt.getDouble("y"), nbt.getDouble("z"))); -+ } -+ -+ public LookAction setPos(Vector pos) { -+ this.pos = pos; -+ return this; -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ bot.look(pos.subtract(bot.getLocation().toVector()), false); -+ return true; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..84eb7bd727a0085d005a6ee518dfbb8b44fce991 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotateAction.java -@@ -0,0 +1,51 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+ -+public class RotateAction extends BotAction { -+ -+ public RotateAction() { -+ super("rotate", CommandArgument.EMPTY, RotateAction::new); -+ } -+ -+ private ServerPlayer player; -+ -+ @Override -+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { -+ this.setPlayer(player).setTickDelay(0).setNumber(1); -+ } -+ -+ public RotateAction setPlayer(ServerPlayer player) { -+ this.player = player; -+ return this; -+ } -+ -+ @Override -+ @NotNull -+ public CompoundTag save(@NotNull CompoundTag nbt) { -+ super.save(nbt); -+ nbt.putString("actionName", "look"); // to player loc -+ nbt.putDouble("x", player.getX()); -+ nbt.putDouble("y", player.getY()); -+ nbt.putDouble("z", player.getZ()); -+ return nbt; -+ } -+ -+ @Override -+ public void load(@NotNull CompoundTag nbt) { -+ throw new UnsupportedOperationException("Not supported."); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ bot.faceLocation(player.getBukkitEntity().getLocation()); -+ return true; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6f6ea32fd78c634467e431572957711034aa6529 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/RotationAction.java -@@ -0,0 +1,65 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.command.CommandArgumentType; -+ -+import java.util.List; -+ -+public class RotationAction extends BotAction { -+ -+ public RotationAction() { -+ super("rotation", CommandArgument.of(CommandArgumentType.FLOAT, CommandArgumentType.FLOAT), RotationAction::new); -+ this.setTabComplete(0, List.of("")); -+ this.setTabComplete(1, List.of("")); -+ } -+ -+ private float yaw; -+ private float pitch; -+ -+ @Override -+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { -+ if (player == null) { -+ return; -+ } -+ -+ this.setYaw(result.readFloat(player.getYRot())).setPitch(result.readFloat(player.getXRot())).setTickDelay(0).setNumber(1); -+ } -+ -+ public RotationAction setYaw(float yaw) { -+ this.yaw = yaw; -+ return this; -+ } -+ -+ public RotationAction setPitch(float pitch) { -+ this.pitch = pitch; -+ return this; -+ } -+ -+ @Override -+ @NotNull -+ public CompoundTag save(@NotNull CompoundTag nbt) { -+ super.save(nbt); -+ nbt.putFloat("yaw", this.yaw); -+ nbt.putFloat("pitch", this.pitch); -+ return nbt; -+ } -+ -+ @Override -+ public void load(@NotNull CompoundTag nbt) { -+ super.load(nbt); -+ this.setYaw(nbt.getFloat("yaw")).setPitch(nbt.getFloat("pitch")); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ bot.setRot(yaw, pitch); -+ return true; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..923cf55d81fce5cf9db9a1c7adc6f3aed5753b16 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SneakAction.java -@@ -0,0 +1,27 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+ -+public class SneakAction extends BotAction { -+ -+ public SneakAction() { -+ super("sneak", CommandArgument.EMPTY, SneakAction::new); -+ } -+ -+ @Override -+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { -+ this.setTickDelay(0).setNumber(1); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ bot.setShiftKeyDown(!bot.isShiftKeyDown()); -+ return true; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b5ccedee17857bc955301512ee965d81fd12017f ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/SwimAction.java -@@ -0,0 +1,30 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.phys.Vec3; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+ -+public class SwimAction extends BotAction { -+ -+ public SwimAction() { -+ super("swim", CommandArgument.EMPTY, SwimAction::new); -+ } -+ -+ @Override -+ public void loadCommand(@Nullable ServerPlayer player, @NotNull CommandArgumentResult result) { -+ this.setTickDelay(0).setNumber(-1); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ if (bot.isInWater()) { -+ bot.addDeltaMovement(new Vec3(0, 0.03, 0)); -+ } -+ return true; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c511ed17e9d5df6d2b961c24ca6f8c157a2baf07 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemAction.java -@@ -0,0 +1,26 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.world.InteractionHand; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+public class UseItemAction extends AbstractTimerAction { -+ -+ public UseItemAction() { -+ super("use", UseItemAction::new); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ if (bot.isUsingItem()) { -+ return false; -+ } -+ -+ boolean flag = bot.gameMode.useItem(bot, bot.level(), bot.getItemInHand(InteractionHand.MAIN_HAND), InteractionHand.MAIN_HAND).consumesAction(); -+ if (flag) { -+ bot.swing(InteractionHand.MAIN_HAND); -+ bot.updateItemInHand(InteractionHand.MAIN_HAND); -+ } -+ return flag; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..26d7286fe9ca9885a02f4f13a8d648d59c7550cd ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOffHandAction.java -@@ -0,0 +1,26 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.world.InteractionHand; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+public class UseItemOffHandAction extends AbstractTimerAction { -+ -+ public UseItemOffHandAction() { -+ super("use_offhand", UseItemOffHandAction::new); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ if (bot.isUsingItem()) { -+ return false; -+ } -+ -+ boolean flag = bot.gameMode.useItem(bot, bot.level(), bot.getItemInHand(InteractionHand.OFF_HAND), InteractionHand.OFF_HAND).consumesAction(); -+ if (flag) { -+ bot.swing(InteractionHand.OFF_HAND); -+ bot.updateItemInHand(InteractionHand.OFF_HAND); -+ } -+ return flag; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..232d0abecb871d3e48c6833f839c921f405b7be7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnAction.java -@@ -0,0 +1,45 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.level.ClipContext; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import net.minecraft.world.level.block.entity.TrappedChestBlockEntity; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.phys.BlockHitResult; -+import net.minecraft.world.phys.HitResult; -+import org.bukkit.Bukkit; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; -+ -+public class UseItemOnAction extends AbstractTimerAction { -+ -+ public UseItemOnAction() { -+ super("use_on", UseItemOnAction::new); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ HitResult result = bot.getRayTrace(5, ClipContext.Fluid.NONE); -+ if (result instanceof BlockHitResult blockHitResult) { -+ BlockState state = bot.serverLevel().getBlockState(blockHitResult.getBlockPos()); -+ if (state.isAir()) { -+ return false; -+ } -+ bot.swing(InteractionHand.MAIN_HAND); -+ if (state.getBlock() == Blocks.TRAPPED_CHEST) { -+ BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos()); -+ if (entity instanceof TrappedChestBlockEntity chestBlockEntity) { -+ chestBlockEntity.startOpen(bot); -+ Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> chestBlockEntity.stopOpen(bot), 1); -+ return true; -+ } -+ } else { -+ bot.updateItemInHand(InteractionHand.MAIN_HAND); -+ return bot.gameMode.useItemOn(bot, bot.level(), bot.getItemInHand(InteractionHand.MAIN_HAND), InteractionHand.MAIN_HAND, (BlockHitResult) result).consumesAction(); -+ } -+ } -+ return false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3616802c37908372cb7e30c61d6d343bcd3c1cc8 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemOnOffhandAction.java -@@ -0,0 +1,45 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.level.ClipContext; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import net.minecraft.world.level.block.entity.TrappedChestBlockEntity; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.phys.BlockHitResult; -+import net.minecraft.world.phys.HitResult; -+import org.bukkit.Bukkit; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; -+ -+public class UseItemOnOffhandAction extends AbstractTimerAction { -+ -+ public UseItemOnOffhandAction() { -+ super("use_on_offhand", UseItemOnOffhandAction::new); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ HitResult result = bot.getRayTrace(5, ClipContext.Fluid.NONE); -+ if (result instanceof BlockHitResult blockHitResult) { -+ BlockState state = bot.serverLevel().getBlockState(blockHitResult.getBlockPos()); -+ if (state.isAir()) { -+ return false; -+ } -+ bot.swing(InteractionHand.OFF_HAND); -+ if (state.getBlock() == Blocks.TRAPPED_CHEST) { -+ BlockEntity entity = bot.serverLevel().getBlockEntity(blockHitResult.getBlockPos()); -+ if (entity instanceof TrappedChestBlockEntity chestBlockEntity) { -+ chestBlockEntity.startOpen(bot); -+ Bukkit.getScheduler().runTaskLater(MinecraftInternalPlugin.INSTANCE, () -> chestBlockEntity.stopOpen(bot), 1); -+ return true; -+ } -+ } else { -+ bot.updateItemInHand(InteractionHand.OFF_HAND); -+ return bot.gameMode.useItemOn(bot, bot.level(), bot.getItemInHand(InteractionHand.OFF_HAND), InteractionHand.OFF_HAND, (BlockHitResult) result).consumesAction(); -+ } -+ } -+ return false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..05be3dd5ca71a7cd81cd150b9588c60e86b54b73 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToAction.java -@@ -0,0 +1,27 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.entity.Entity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+public class UseItemToAction extends AbstractTimerAction { -+ -+ public UseItemToAction() { -+ super("use_to", UseItemToAction::new); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ Entity entity = bot.getTargetEntity(3, null); -+ if (entity != null) { -+ boolean flag = bot.interactOn(entity, InteractionHand.MAIN_HAND).consumesAction(); -+ if (flag) { -+ bot.swing(InteractionHand.MAIN_HAND); -+ bot.updateItemInHand(InteractionHand.MAIN_HAND); -+ } -+ return flag; -+ } -+ return false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f8334858a7a0572d1c3dcf5f04696fbbec552a84 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/actions/UseItemToOffhandAction.java -@@ -0,0 +1,27 @@ -+package org.leavesmc.leaves.bot.agent.actions; -+ -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.entity.Entity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+public class UseItemToOffhandAction extends AbstractTimerAction { -+ -+ public UseItemToOffhandAction() { -+ super("use_to_offhand", UseItemToOffhandAction::new); -+ } -+ -+ @Override -+ public boolean doTick(@NotNull ServerBot bot) { -+ Entity entity = bot.getTargetEntity(3, null); -+ if (entity != null) { -+ boolean flag = bot.interactOn(entity, InteractionHand.OFF_HAND).consumesAction(); -+ if (flag) { -+ bot.swing(InteractionHand.OFF_HAND); -+ bot.updateItemInHand(InteractionHand.OFF_HAND); -+ } -+ return flag; -+ } -+ return false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java -new file mode 100644 -index 0000000000000000000000000000000000000000..31f68872652b3ac217d3b4f56e3bdd778314c27a ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/AlwaysSendDataConfig.java -@@ -0,0 +1,45 @@ -+package org.leavesmc.leaves.bot.agent.configs; -+ -+import net.minecraft.nbt.CompoundTag; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+ -+import org.leavesmc.leaves.bot.agent.BotConfig; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.command.CommandArgumentType; -+ -+import java.util.List; -+ -+public class AlwaysSendDataConfig extends BotConfig { -+ -+ private boolean value; -+ -+ public AlwaysSendDataConfig() { -+ super("always_send_data", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("true", "false")), AlwaysSendDataConfig::new); -+ this.value = LeavesConfig.modify.fakeplayer.canSendDataAlways; -+ } -+ -+ @Override -+ public Boolean getValue() { -+ return value; -+ } -+ -+ @Override -+ public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException { -+ this.value = result.readBoolean(this.value); -+ } -+ -+ @Override -+ @NotNull -+ public CompoundTag save(@NotNull CompoundTag nbt) { -+ super.save(nbt); -+ nbt.putBoolean("always_send_data", this.getValue()); -+ return nbt; -+ } -+ -+ @Override -+ public void load(@NotNull CompoundTag nbt) { -+ this.value = nbt.getBoolean("always_send_data"); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c8a2243361cd03e9c64b6a04b37725b549e5b87f ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SimulationDistanceConfig.java -@@ -0,0 +1,47 @@ -+package org.leavesmc.leaves.bot.agent.configs; -+ -+import net.minecraft.nbt.CompoundTag; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.agent.BotConfig; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.command.CommandArgumentType; -+ -+import java.util.ArrayList; -+import java.util.List; -+ -+public class SimulationDistanceConfig extends BotConfig { -+ -+ public SimulationDistanceConfig() { -+ super("simulation_distance", CommandArgument.of(CommandArgumentType.INTEGER).setTabComplete(0, List.of("2", "10", "")), SimulationDistanceConfig::new); -+ } -+ -+ @Override -+ public Integer getValue() { -+ return this.bot.getBukkitEntity().getSimulationDistance(); -+ } -+ -+ @Override -+ public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException { -+ int realValue = result.readInt(this.bot.getBukkitEntity().getSimulationDistance()); -+ if (realValue < 2 || realValue > 32) { -+ throw new IllegalArgumentException("simulation_distance must be a number between 2 and 32, got: " + result); -+ } -+ this.bot.getBukkitEntity().setSimulationDistance(realValue); -+ } -+ -+ @Override -+ @NotNull -+ public CompoundTag save(@NotNull CompoundTag nbt) { -+ super.save(nbt); -+ nbt.putInt("simulation_distance", this.getValue()); -+ return nbt; -+ } -+ -+ @Override -+ public void load(@NotNull CompoundTag nbt) { -+ this.setValue(new CommandArgumentResult(new ArrayList<>(){{ -+ add(nbt.getInt("simulation_distance")); -+ }})); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0d934910cff745ea9a53d651e20079635ea6781c ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SkipSleepConfig.java -@@ -0,0 +1,42 @@ -+package org.leavesmc.leaves.bot.agent.configs; -+ -+import net.minecraft.nbt.CompoundTag; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.bot.agent.BotConfig; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.command.CommandArgumentType; -+ -+import java.util.ArrayList; -+import java.util.List; -+ -+public class SkipSleepConfig extends BotConfig { -+ -+ public SkipSleepConfig() { -+ super("skip_sleep", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("true", "false")), SkipSleepConfig::new); -+ } -+ -+ @Override -+ public Boolean getValue() { -+ return bot.fauxSleeping; -+ } -+ -+ @Override -+ public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException { -+ bot.fauxSleeping = result.readBoolean(bot.fauxSleeping); -+ } -+ -+ @Override -+ public @NotNull CompoundTag save(@NotNull CompoundTag nbt) { -+ super.save(nbt); -+ nbt.putBoolean("skip_sleep", this.getValue()); -+ return nbt; -+ } -+ -+ @Override -+ public void load(@NotNull CompoundTag nbt) { -+ this.setValue(new CommandArgumentResult(new ArrayList<>() {{ -+ add(nbt.getBoolean("skip_sleep")); -+ }})); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java -new file mode 100644 -index 0000000000000000000000000000000000000000..983b358a5cc03f07d36a3924d55275294b9a727e ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bot/agent/configs/SpawnPhantomConfig.java -@@ -0,0 +1,51 @@ -+package org.leavesmc.leaves.bot.agent.configs; -+ -+import net.minecraft.nbt.CompoundTag; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.bot.agent.BotConfig; -+import org.leavesmc.leaves.command.CommandArgument; -+import org.leavesmc.leaves.command.CommandArgumentResult; -+import org.leavesmc.leaves.command.CommandArgumentType; -+ -+import java.util.List; -+ -+public class SpawnPhantomConfig extends BotConfig { -+ -+ private boolean value; -+ -+ public SpawnPhantomConfig() { -+ super("spawn_phantom", CommandArgument.of(CommandArgumentType.BOOLEAN).setTabComplete(0, List.of("true", "false")), SpawnPhantomConfig::new); -+ this.value = LeavesConfig.modify.fakeplayer.canSpawnPhantom; -+ } -+ -+ @Override -+ public Boolean getValue() { -+ return value; -+ } -+ -+ @Override -+ public void setValue(@NotNull CommandArgumentResult result) throws IllegalArgumentException { -+ this.value = result.readBoolean(this.value); -+ } -+ -+ @Override -+ public List getMessage() { -+ return List.of( -+ bot.getScoreboardName() + "'s spawn_phantom: " + this.getValue(), -+ bot.getScoreboardName() + "'s not_sleeping_ticks: " + bot.notSleepTicks -+ ); -+ } -+ -+ @Override -+ public @NotNull CompoundTag save(@NotNull CompoundTag nbt) { -+ super.save(nbt); -+ nbt.putBoolean("spawn_phantom", this.getValue()); -+ return nbt; -+ } -+ -+ @Override -+ public void load(@NotNull CompoundTag nbt) { -+ this.value = nbt.getBoolean("spawn_phantom"); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java -index f50c3871e3ab435abc6de5bfb67b85b09d235733..d110bf39788503ec662d6f0c737ce9aa2ab809e9 100644 ---- a/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java -+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommandUtil.java -@@ -3,7 +3,6 @@ package org.leavesmc.leaves.command; - import com.google.common.base.Functions; - import com.google.common.collect.Iterables; - import com.google.common.collect.Lists; --import io.papermc.paper.command.PaperCommand; - import net.minecraft.resources.ResourceLocation; - import org.bukkit.command.CommandSender; - import org.checkerframework.checker.nullness.qual.NonNull; -diff --git a/src/main/java/org/leavesmc/leaves/entity/CraftBot.java b/src/main/java/org/leavesmc/leaves/entity/CraftBot.java -new file mode 100644 -index 0000000000000000000000000000000000000000..aaa5f8a3a70cac4483003c4a4a7ac2db72791ef9 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/entity/CraftBot.java -@@ -0,0 +1,94 @@ -+package org.leavesmc.leaves.entity; -+ -+import com.google.common.base.Preconditions; -+import org.bukkit.Location; -+import org.bukkit.craftbukkit.CraftServer; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.event.player.PlayerTeleportEvent; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.BotList; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.bot.agent.actions.CraftBotAction; -+import org.leavesmc.leaves.entity.botaction.LeavesBotAction; -+import org.leavesmc.leaves.event.bot.BotActionStopEvent; -+import org.leavesmc.leaves.event.bot.BotRemoveEvent; -+ -+import java.util.UUID; -+ -+public class CraftBot extends CraftPlayer implements Bot { -+ -+ public CraftBot(CraftServer server, ServerBot entity) { -+ super(server, entity); -+ } -+ -+ @Override -+ public String getSkinName() { -+ return this.getHandle().createState.skinName(); -+ } -+ -+ @Override -+ public @NotNull String getRealName() { -+ return this.getHandle().createState.realName(); -+ } -+ -+ @Override -+ public @Nullable UUID getCreatePlayerUUID() { -+ return this.getHandle().createPlayer; -+ } -+ -+ @Override -+ public void addAction(@NotNull LeavesBotAction action) { -+ this.getHandle().addBotAction(CraftBotAction.asInternalCopy(action), null); -+ } -+ -+ @Override -+ public LeavesBotAction getAction(int index) { -+ return CraftBotAction.asAPICopy(this.getHandle().getBotActions().get(index)); -+ } -+ -+ @Override -+ public int getActionSize() { -+ return this.getHandle().getBotActions().size(); -+ } -+ -+ @Override -+ public void stopAction(int index) { -+ this.getHandle().getBotActions().get(index).stop(this.getHandle(), BotActionStopEvent.Reason.PLUGIN); -+ } -+ -+ @Override -+ public void stopAllActions() { -+ for (BotAction action : this.getHandle().getBotActions()) { -+ action.stop(this.getHandle(), BotActionStopEvent.Reason.PLUGIN); -+ } -+ } -+ -+ @Override -+ public boolean remove(boolean save) { -+ BotList.INSTANCE.removeBot(this.getHandle(), BotRemoveEvent.RemoveReason.PLUGIN, null, save); -+ return true; -+ } -+ -+ @Override -+ public boolean teleport(Location location, PlayerTeleportEvent.TeleportCause cause, io.papermc.paper.entity.TeleportFlag... flags) { -+ Preconditions.checkArgument(location != null, "location cannot be null"); -+ Preconditions.checkState(location.getWorld().equals(this.getWorld()), "[Leaves] Fakeplayers do not support changing world, Please use leaves fakeplayer-api instead!"); -+ return super.teleport(location, cause, flags); -+ } -+ -+ @Override -+ public ServerBot getHandle() { -+ return (ServerBot) entity; -+ } -+ -+ public void setHandle(final ServerBot entity) { -+ super.setHandle(entity); -+ } -+ -+ @Override -+ public String toString() { -+ return "CraftBot{" + "name=" + getName() + '}'; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java b/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..422640df346ccae612b2d3492780efa59d8b4d17 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/entity/CraftBotManager.java -@@ -0,0 +1,80 @@ -+package org.leavesmc.leaves.entity; -+ -+import com.google.common.base.Function; -+import com.google.common.collect.Lists; -+import net.minecraft.server.MinecraftServer; -+import org.bukkit.Location; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.BotCreateState; -+import org.leavesmc.leaves.bot.BotList; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.bot.agent.Actions; -+import org.leavesmc.leaves.bot.agent.BotAction; -+import org.leavesmc.leaves.bot.agent.actions.CraftCustomBotAction; -+import org.leavesmc.leaves.entity.botaction.CustomBotAction; -+import org.leavesmc.leaves.event.bot.BotCreateEvent; -+ -+import java.util.Collection; -+import java.util.Collections; -+import java.util.UUID; -+ -+public class CraftBotManager implements BotManager { -+ -+ private final BotList botList; -+ private final Collection botViews; -+ -+ public CraftBotManager() { -+ this.botList = MinecraftServer.getServer().getBotList(); -+ this.botViews = Collections.unmodifiableList(Lists.transform(botList.bots, new Function() { -+ @Override -+ public CraftBot apply(ServerBot bot) { -+ return bot.getBukkitEntity(); -+ } -+ })); -+ } -+ -+ @Override -+ public @Nullable Bot getBot(@NotNull UUID uuid) { -+ ServerBot bot = botList.getBot(uuid); -+ if (bot != null) { -+ return bot.getBukkitEntity(); -+ } else { -+ return null; -+ } -+ } -+ -+ @Override -+ public @Nullable Bot getBot(@NotNull String name) { -+ ServerBot bot = botList.getBotByName(name); -+ if (bot != null) { -+ return bot.getBukkitEntity(); -+ } else { -+ return null; -+ } -+ } -+ -+ @Override -+ public Collection getBots() { -+ return botViews; -+ } -+ -+ @Override -+ public boolean registerCustomBotAction(String name, CustomBotAction action) { -+ return Actions.register(new CraftCustomBotAction(name, action)); -+ } -+ -+ @Override -+ public boolean unregisterCustomBotAction(String name) { -+ BotAction action = Actions.getForName(name); -+ if (action instanceof CraftCustomBotAction) { -+ return Actions.unregister(name); -+ } -+ return false; -+ } -+ -+ @Override -+ public BotCreator botCreator(@NotNull String realName, @NotNull Location location) { -+ return BotCreateState.builder(realName, location).createReason(BotCreateEvent.CreateReason.PLUGIN); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java b/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java -new file mode 100644 -index 0000000000000000000000000000000000000000..de06c854a9a5242cf632b38806e8e710496b7e4e ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/plugin/MinecraftInternalPlugin.java -@@ -0,0 +1,151 @@ -+package org.leavesmc.leaves.plugin; -+ -+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 MinecraftInternalPlugin extends PluginBase { -+ -+ public static final MinecraftInternalPlugin INSTANCE = new MinecraftInternalPlugin(); -+ -+ private boolean enabled = true; -+ -+ private final PluginDescriptionFile pdf; -+ -+ public MinecraftInternalPlugin() { -+ String pluginName = "Minecraft"; -+ pdf = new PluginDescriptionFile(pluginName, "1.0", "nms"); -+ } -+ -+ public void setEnabled(boolean enabled) { -+ this.enabled = enabled; -+ } -+ -+ @Override -+ public File getDataFolder() { -+ throw new UnsupportedOperationException("Not supported."); -+ } -+ -+ @Override -+ public PluginDescriptionFile getDescription() { -+ return pdf; -+ } -+ -+ @Override -+ public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() { -+ return pdf; -+ } -+ -+ @Override -+ public FileConfiguration getConfig() { -+ throw new UnsupportedOperationException("Not supported."); -+ } -+ -+ @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; -+ } -+ -+ @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."); -+ } -+ -+ @Override -+ public @NotNull io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager getLifecycleManager() { -+ throw new UnsupportedOperationException("Not supported."); -+ } -+} diff --git a/patches/server/0011-Make-shears-in-dispenser-can-unlimited-use.patch b/patches/server/0011-Make-shears-in-dispenser-can-unlimited-use.patch deleted file mode 100644 index e1ccecd8..00000000 --- a/patches/server/0011-Make-shears-in-dispenser-can-unlimited-use.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sun, 27 Feb 2022 14:07:57 +0800 -Subject: [PATCH] Make shears in dispenser can unlimited use - - -diff --git a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -index 65ed3d77a51b8299517e0c165403b0c5ac413475..4708560d4c749f3c1039beaa2779c0036fb3d42e 100644 ---- a/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -+++ b/src/main/java/net/minecraft/core/dispenser/ShearsDispenseItemBehavior.java -@@ -64,7 +64,7 @@ public class ShearsDispenseItemBehavior extends OptionalDispenseItemBehavior { - BlockPos blockposition = pointer.pos().relative((Direction) pointer.state().getValue(DispenserBlock.FACING)); - - this.setSuccess(ShearsDispenseItemBehavior.tryShearBeehive(worldserver, blockposition) || ShearsDispenseItemBehavior.tryShearLivingEntity(worldserver, blockposition, stack, bukkitBlock, craftItem)); // CraftBukkit -- if (this.isSuccess()) { -+ if (this.isSuccess() && !org.leavesmc.leaves.LeavesConfig.modify.oldMC.shearsInDispenserCanZeroAmount) { // Leaves - Make shears in dispenser can unlimited use - stack.hurtAndBreak(1, worldserver, (ServerPlayer) null, (item) -> { - }); - } diff --git a/patches/server/0012-Redstone-Shears-Wrench.patch b/patches/server/0012-Redstone-Shears-Wrench.patch deleted file mode 100644 index 12845b3b..00000000 --- a/patches/server/0012-Redstone-Shears-Wrench.patch +++ /dev/null @@ -1,102 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sun, 27 Mar 2022 12:48:40 +0800 -Subject: [PATCH] Redstone Shears Wrench - - -diff --git a/src/main/java/net/minecraft/world/item/ShearsItem.java b/src/main/java/net/minecraft/world/item/ShearsItem.java -index 374d51da03ce47ff744b64b8cfe98ad6d72ebdb4..5ae43ecff45239086ab8aad3b5c2e0774b999544 100644 ---- a/src/main/java/net/minecraft/world/item/ShearsItem.java -+++ b/src/main/java/net/minecraft/world/item/ShearsItem.java -@@ -23,6 +23,23 @@ import net.minecraft.world.level.block.GrowingPlantHeadBlock; - import net.minecraft.world.level.block.state.BlockState; - import net.minecraft.world.level.gameevent.GameEvent; - -+// Leaves start - shears wrench -+import net.minecraft.network.chat.Component; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.Util; -+import net.minecraft.world.level.block.ComparatorBlock; -+import net.minecraft.world.level.block.DispenserBlock; -+import net.minecraft.world.level.block.HopperBlock; -+import net.minecraft.world.level.block.ObserverBlock; -+import net.minecraft.world.level.block.RepeaterBlock; -+import net.minecraft.world.level.block.CrafterBlock; -+import net.minecraft.world.level.block.LeverBlock; -+import net.minecraft.world.level.block.CocoaBlock; -+import net.minecraft.world.level.block.piston.PistonBaseBlock; -+import net.minecraft.world.level.block.state.StateDefinition; -+import net.minecraft.world.level.block.state.properties.Property; -+// Leaves end - shears wrench -+ - public class ShearsItem extends Item { - public ShearsItem(Item.Properties settings) { - super(settings); -@@ -82,6 +99,67 @@ public class ShearsItem extends Item { - return InteractionResult.SUCCESS; - } - -+ // Leaves start - shears wrench -+ Block block = blockState.getBlock(); -+ if (org.leavesmc.leaves.LeavesConfig.modify.redstoneShearsWrench && block instanceof ObserverBlock || block instanceof DispenserBlock || -+ block instanceof PistonBaseBlock || block instanceof HopperBlock || block instanceof RepeaterBlock || block instanceof ComparatorBlock || -+ block instanceof CrafterBlock || block instanceof LeverBlock || block instanceof CocoaBlock) { -+ StateDefinition blockstatelist = block.getStateDefinition(); -+ Property iblockstate = block instanceof CrafterBlock ? blockstatelist.getProperty("orientation") : blockstatelist.getProperty("facing"); -+ Player player = context.getPlayer(); -+ -+ if (iblockstate == null || player == null) { -+ return InteractionResult.FAIL; -+ } -+ -+ if (block instanceof PistonBaseBlock) { -+ if (getNameHelper(blockState, PistonBaseBlock.EXTENDED).equals("true")) { -+ return InteractionResult.FAIL; -+ } -+ } -+ -+ if (block instanceof RepeaterBlock || block instanceof ComparatorBlock) { -+ if (getNameHelper(blockState, ComparatorBlock.POWERED).equals("true")) { -+ return InteractionResult.FAIL; -+ } -+ if (block instanceof RepeaterBlock) { -+ if (getNameHelper(blockState, RepeaterBlock.LOCKED).equals("true")) { -+ return InteractionResult.FAIL; -+ } -+ } -+ } -+ -+ if (block instanceof CrafterBlock) { -+ if (getNameHelper(blockState, CrafterBlock.CRAFTING).equals("true")) { -+ return InteractionResult.FAIL; -+ } -+ } -+ -+ BlockState iblockdata1 = ShearsItem.cycleState(blockState, iblockstate, player.isSecondaryUseActive()); -+ level.setBlock(blockPos, iblockdata1, 18); -+ ShearsItem.message(player, Component.translatable("item.minecraft.debug_stick.update", iblockstate.getName(), ShearsItem.getNameHelper(iblockdata1, iblockstate))); -+ return InteractionResult.CONSUME; -+ } -+ // Leaves end - shears wrench -+ - return super.useOn(context); - } -+ -+ // Leaves start - shears wrench -+ private static > BlockState cycleState(BlockState state, Property property, boolean inverse) { -+ return state.setValue(property, ShearsItem.getRelative(property.getPossibleValues(), state.getValue(property), inverse)); // CraftBukkit - decompile error -+ } -+ -+ private static T getRelative(Iterable elements, T current, boolean inverse) { -+ return inverse ? Util.findPreviousInIterable(elements, current) : Util.findNextInIterable(elements, current); -+ } -+ -+ private static void message(Player player, Component message) { -+ ((ServerPlayer) player).sendSystemMessage(message, true); -+ } -+ -+ private static > String getNameHelper(BlockState state, Property property) { -+ return property.getName(state.getValue(property)); -+ } -+ // Leaves end - shears wrench - } diff --git a/patches/server/0013-Add-isShrink-to-EntityResurrectEvent.patch b/patches/server/0013-Add-isShrink-to-EntityResurrectEvent.patch deleted file mode 100644 index c563343f..00000000 --- a/patches/server/0013-Add-isShrink-to-EntityResurrectEvent.patch +++ /dev/null @@ -1,30 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 30 Mar 2022 08:58:45 +0000 -Subject: [PATCH] Add isShrink to EntityResurrectEvent - - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index f36a075dbee2b96d01899e02460b1d8443e91749..d487bf7c9d425e5bc82c7efd7b901752ef915f16 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -1703,12 +1703,12 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - org.bukkit.inventory.EquipmentSlot handSlot = (hand != null) ? org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(hand) : null; -- EntityResurrectEvent event = new EntityResurrectEvent((org.bukkit.entity.LivingEntity) this.getBukkitEntity(), handSlot); -+ EntityResurrectEvent event = new EntityResurrectEvent((org.bukkit.entity.LivingEntity) this.getBukkitEntity(), handSlot, true); // Leaves - can dont shrink - event.setCancelled(itemstack == null); - this.level().getCraftServer().getPluginManager().callEvent(event); - - if (!event.isCancelled()) { -- if (!itemstack1.isEmpty() && itemstack != null) { // Paper - only reduce item if actual totem was found -+ if (!itemstack1.isEmpty() && itemstack != null && event.isShrink()) { // Paper - only reduce item if actual totem was found // Leaves - can dont shrink - itemstack1.shrink(1); - } - // Paper start - fix NPE when pre-cancelled EntityResurrectEvent is uncancelled -@@ -4804,3 +4804,4 @@ public abstract class LivingEntity extends Entity implements Attackable { - - } - } -+ diff --git a/patches/server/0016-Stick-can-change-ArmorStand-arm-status.patch b/patches/server/0016-Stick-can-change-ArmorStand-arm-status.patch deleted file mode 100644 index 817ed945..00000000 --- a/patches/server/0016-Stick-can-change-ArmorStand-arm-status.patch +++ /dev/null @@ -1,23 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sat, 25 Jun 2022 19:54:23 +0800 -Subject: [PATCH] Stick can change ArmorStand arm status - - -diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -index 70b8023c3badc745f342d5b0ab54699e3923826a..49e8a9d5df9b20bced385019f5e7fb622536213a 100644 ---- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -+++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -@@ -397,6 +397,12 @@ public class ArmorStand extends LivingEntity { - return InteractionResult.SUCCESS_SERVER; - } - } else { -+ // Leaves start - stick can change ArmorStand arm status -+ if (org.leavesmc.leaves.LeavesConfig.modify.stickChangeArmorStandArmStatus && itemstack.is(Items.STICK) && player.isShiftKeyDown()) { -+ setShowArms(!showArms()); -+ } -+ // Leaves end - stick can change ArmorStand arm status -+ - if (this.isDisabled(enumitemslot)) { - return InteractionResult.FAIL; - } diff --git a/patches/server/0017-Configurable-MC-59471.patch b/patches/server/0017-Configurable-MC-59471.patch deleted file mode 100644 index 2841fe07..00000000 --- a/patches/server/0017-Configurable-MC-59471.patch +++ /dev/null @@ -1,42 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sat, 30 Jul 2022 01:11:30 +0800 -Subject: [PATCH] Configurable MC-59471 - - -diff --git a/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java b/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java -index c2589f42c467ca672417c24076313da51bb2dcbb..99caf34456385565d8a985409690c656ab3d3c3d 100644 ---- a/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/TripWireHookBlock.java -@@ -191,7 +191,7 @@ public class TripWireHookBlock extends Block { - - TripWireHookBlock.emitState(world, pos, flag4, flag5, flag2, flag3); - if (!flag) { -- if (world.getBlockState(pos).getBlock() == Blocks.TRIPWIRE_HOOK) // Paper - Validate tripwire hook placement before update -+ // if (world.getBlockState(pos).getBlock() == Blocks.TRIPWIRE_HOOK) // Paper - Validate tripwire hook placement before update // Leaves - vanilla lol - world.setBlock(pos, (BlockState) iblockdata3.setValue(TripWireHookBlock.FACING, enumdirection), 3); - if (flag1) { - TripWireHookBlock.notifyNeighbors(block, world, pos, enumdirection); -@@ -204,11 +204,18 @@ public class TripWireHookBlock extends Block { - BlockState iblockdata4 = aiblockdata[l]; - - if (iblockdata4 != null) { -- BlockState iblockdata5 = world.getBlockState(blockposition2); -- -- if (iblockdata5.is(Blocks.TRIPWIRE) || iblockdata5.is(Blocks.TRIPWIRE_HOOK)) { -- world.setBlock(blockposition2, (BlockState) iblockdata4.trySetValue(TripWireHookBlock.ATTACHED, flag4), 3); -+ // Leaves start - MC-59471 -+ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.stringTripwireHookDuplicate) { -+ world.setBlock(blockposition2, iblockdata4.trySetValue(TripWireHookBlock.ATTACHED, flag4), 3); -+ world.getBlockState(blockposition2); -+ } else { -+ BlockState iblockdata5 = world.getBlockState(blockposition2); -+ -+ if (iblockdata5.is(Blocks.TRIPWIRE) || iblockdata5.is(Blocks.TRIPWIRE_HOOK)) { -+ world.setBlock(blockposition2, (BlockState) iblockdata4.trySetValue(TripWireHookBlock.ATTACHED, flag4), 3); -+ } - } -+ // Leaves end - MC-59471 - } - } - } diff --git a/patches/server/0018-No-chat-sign.patch b/patches/server/0018-No-chat-sign.patch deleted file mode 100644 index d4c9ffa0..00000000 --- a/patches/server/0018-No-chat-sign.patch +++ /dev/null @@ -1,163 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 3 Aug 2022 11:20:51 +0800 -Subject: [PATCH] No chat sign - - -diff --git a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java -index 14e412ebf75b0e06ab53a1c8f9dd1be6ad1e2680..73d239536b373e292ee2883edac0049541b59ba0 100644 ---- a/src/main/java/io/papermc/paper/adventure/ChatProcessor.java -+++ b/src/main/java/io/papermc/paper/adventure/ChatProcessor.java -@@ -317,7 +317,7 @@ public final class ChatProcessor { - - private void sendToServer(final ChatType.Bound chatType, final @Nullable Function msgFunction) { - final PlayerChatMessage toConsoleMessage = msgFunction == null ? ChatProcessor.this.message : ChatProcessor.this.message.withUnsignedContent(msgFunction.apply(ChatProcessor.this.server.console)); -- ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) ? null : "Not Secure"); -+ ChatProcessor.this.server.logChatMessage(toConsoleMessage.decoratedContent(), chatType, ChatProcessor.this.server.getPlayerList().verifyChatTrusted(toConsoleMessage) || org.leavesmc.leaves.LeavesConfig.mics.noChatSign ? null : "Not Secure"); // Leaves - No Not Secure - } - } - -diff --git a/src/main/java/net/minecraft/commands/arguments/ArgumentSignatures.java b/src/main/java/net/minecraft/commands/arguments/ArgumentSignatures.java -index 479e6e2aa88a22ef7f8fccb06add6806f5b71d9d..5a103b7c4d2566d93b18a2e8db32a3444ba44e36 100644 ---- a/src/main/java/net/minecraft/commands/arguments/ArgumentSignatures.java -+++ b/src/main/java/net/minecraft/commands/arguments/ArgumentSignatures.java -@@ -13,10 +13,17 @@ public record ArgumentSignatures(List entries) { - private static final int MAX_ARGUMENT_COUNT = 8; - private static final int MAX_ARGUMENT_NAME_LENGTH = 16; - -+ // Leaves start - no chat sign - public ArgumentSignatures(FriendlyByteBuf buf) { -- this(buf.readCollection(FriendlyByteBuf.limitValue(ArrayList::new, 8), ArgumentSignatures.Entry::new)); -+ this(readSign(buf)); - } - -+ private static List readSign(FriendlyByteBuf buf) { -+ var entries = buf.readCollection(FriendlyByteBuf.limitValue(ArrayList::new, 8), Entry::new); -+ return org.leavesmc.leaves.LeavesConfig.mics.noChatSign ? List.of() : entries; -+ } -+ // Leaves end - no chat sign -+ - public void write(FriendlyByteBuf buf) { - buf.writeCollection(this.entries, (buf2, entry) -> entry.write(buf2)); - } -diff --git a/src/main/java/net/minecraft/network/FriendlyByteBuf.java b/src/main/java/net/minecraft/network/FriendlyByteBuf.java -index a523a83aec3a6ecbec4d60a187edc0c0167d15b4..e4ac9fac52cec522d679a1ffe8b3263a204f5d7f 100644 ---- a/src/main/java/net/minecraft/network/FriendlyByteBuf.java -+++ b/src/main/java/net/minecraft/network/FriendlyByteBuf.java -@@ -129,6 +129,16 @@ public class FriendlyByteBuf extends ByteBuf { - // Paper end - Adventure; add max length parameter - DataResult dataresult = codec.encodeStart(JsonOps.INSTANCE, value); - -+ // Leaves start - no chat sign -+ if (codec == net.minecraft.network.protocol.status.ServerStatus.CODEC) { -+ JsonElement element = dataresult.getOrThrow(string -> new EncoderException("Failed to encode: " + string + " " + value)); -+ element.getAsJsonObject().addProperty("preventsChatReports", org.leavesmc.leaves.LeavesConfig.mics.noChatSign); -+ -+ this.writeUtf(GSON.toJson(element)); -+ return; -+ } -+ // Leaves end - no chat sign -+ - this.writeUtf(FriendlyByteBuf.GSON.toJson((JsonElement) dataresult.getOrThrow((s) -> { - return new EncoderException("Failed to encode: " + s + " " + String.valueOf(value)); - })), maxLength); // Paper - Adventure; add max length parameter -diff --git a/src/main/java/net/minecraft/network/protocol/game/ServerboundChatPacket.java b/src/main/java/net/minecraft/network/protocol/game/ServerboundChatPacket.java -index 07df3299f1d1aa5506e1f6f146347d53e0278d9c..342f23250f22771d8b395cece087d277fd99eaae 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ServerboundChatPacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ServerboundChatPacket.java -@@ -15,8 +15,9 @@ public record ServerboundChatPacket(String message, Instant timeStamp, long salt - ServerboundChatPacket::write, ServerboundChatPacket::new - ); - -+ // Leaves start - no chat sign - private ServerboundChatPacket(FriendlyByteBuf buf) { -- this(buf.readUtf(256), buf.readInstant(), buf.readLong(), buf.readNullable(MessageSignature::read), new LastSeenMessages.Update(buf)); -+ this(buf.readUtf(256), buf.readInstant(), buf.readLong(), buf.readNullable(ServerboundChatPacket::readSign), new LastSeenMessages.Update(buf)); - } - - private void write(FriendlyByteBuf buf) { -@@ -27,6 +28,13 @@ public record ServerboundChatPacket(String message, Instant timeStamp, long salt - this.lastSeenMessages.write(buf); - } - -+ private static MessageSignature readSign(FriendlyByteBuf buf) { -+ byte[] bs = new byte[256]; -+ buf.readBytes(bs); -+ return org.leavesmc.leaves.LeavesConfig.mics.noChatSign ? null : new MessageSignature(bs); -+ } -+ // Leaves end - no chat sign -+ - @Override - public PacketType type() { - return GamePacketTypes.SERVERBOUND_CHAT; -diff --git a/src/main/java/net/minecraft/network/protocol/game/ServerboundChatSessionUpdatePacket.java b/src/main/java/net/minecraft/network/protocol/game/ServerboundChatSessionUpdatePacket.java -index 5705cb920084b775cce4b361683b32c6b6e003ed..eb0caad2613e8fdd7dbcc2a295cc9201a225f3f2 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ServerboundChatSessionUpdatePacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ServerboundChatSessionUpdatePacket.java -@@ -26,6 +26,11 @@ public record ServerboundChatSessionUpdatePacket(RemoteChatSession.Data chatSess - - @Override - public void handle(ServerGamePacketListener listener) { -+ // Leaves start - no chat report -+ if (org.leavesmc.leaves.LeavesConfig.mics.noChatSign) { -+ return; -+ } -+ // Leaves end - no chat report - listener.handleChatSessionUpdate(this); - } - } -diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -index 9b1e6effb8bf852957dba92a54688932ddb0d565..679a40aeedc7e259401a6c244ed54136d6c75a40 100644 ---- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -@@ -650,7 +650,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - // Paper start - Add setting for proxy online mode status - return dedicatedserverproperties.enforceSecureProfile - && io.papermc.paper.configuration.GlobalConfiguration.get().proxies.isProxyOnlineMode() -- && this.services.canValidateProfileKeys(); -+ && this.services.canValidateProfileKeys() && !org.leavesmc.leaves.LeavesConfig.mics.noChatSign; // Leaves - no chat sign - // Paper end - Add setting for proxy online mode status - } - -diff --git a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java -index 8344c7f953fc465ee9f43e1f81e2eb062fd3c432..93404b7e1aaf74d6acc7031116dc8f63fc70f964 100644 ---- a/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerCommonPacketListenerImpl.java -@@ -304,10 +304,24 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack - } - - public void send(Packet packet) { -+ // Leaves start - rebuild ClientboundPlayerChatPacket -+ if (org.leavesmc.leaves.LeavesConfig.mics.noChatSign) { -+ if (this instanceof ServerGamePacketListenerImpl && packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket chat) { -+ packet = new net.minecraft.network.protocol.game.ClientboundSystemChatPacket(chat.chatType().decorate(chat.unsignedContent() != null ? chat.unsignedContent() : Component.literal(chat.body().content())), false); -+ } -+ } -+ // Leaves end - rebuild ClientboundPlayerChatPacket - this.send(packet, (PacketSendListener) null); - } - - public void send(Packet packet, @Nullable PacketSendListener callbacks) { -+ // Leaves start - no ClientboundPlayerChatHeaderPacket and rebuild ClientboundPlayerChatPacket -+ if (org.leavesmc.leaves.LeavesConfig.mics.noChatSign) { -+ if (packet instanceof net.minecraft.network.protocol.game.ClientboundPlayerChatPacket chat && callbacks != null) { -+ callbacks = null; -+ } -+ } -+ // Leaves end - no ClientboundPlayerChatHeaderPacket and rebuild ClientboundPlayerChatPacket - // CraftBukkit start - if (packet == null || this.processedDisconnect) { // Spigot - return; -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 9b3fbc13779ded07c696e151772d834ff8d61a7d..b83b903381ca6c426d2f26f9459e769083eb5876 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -1423,7 +1423,7 @@ public abstract class PlayerList { - } - - public boolean verifyChatTrusted(PlayerChatMessage message) { // Paper - private -> public -- return message.hasSignature() && !message.hasExpiredServer(Instant.now()); -+ return org.leavesmc.leaves.LeavesConfig.mics.noChatSign || (message.hasSignature() && !message.hasExpiredServer(Instant.now())); // Leaves - No Not Secure - } - - // CraftBukkit start diff --git a/patches/server/0020-Optimize-suffocation.patch b/patches/server/0020-Optimize-suffocation.patch deleted file mode 100644 index afe1f83d..00000000 --- a/patches/server/0020-Optimize-suffocation.patch +++ /dev/null @@ -1,36 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sun, 14 Aug 2022 08:25:24 +0800 -Subject: [PATCH] Optimize suffocation - -This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 7610b2961d59af006440476f7c91038b9b1c5432..8c34c7f74779774e8627c2eb9d5230ed1980df36 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -467,7 +467,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - - if (world1 instanceof ServerLevel) { - worldserver1 = (ServerLevel) world1; -- if (this.isInWall()) { -+ if ((!org.leavesmc.leaves.LeavesConfig.performance.enableSuffocationOptimization || this instanceof WitherBoss || (tickCount % 10 == 0 && couldPossiblyBeHurt(1.0F))) && this.isInWall()) { // Leaves - optimize suffocation - this.hurtServer(worldserver1, this.damageSources().inWall(), 1.0F); - } else if (flag && !this.level().getWorldBorder().isWithinBounds(this.getBoundingBox())) { - double d1 = this.level().getWorldBorder().getDistanceToBorder(this) + this.level().getWorldBorder().getDamageSafeZone(); -@@ -1437,6 +1437,15 @@ public abstract class LivingEntity extends Entity implements Attackable { - return this.getHealth() <= 0.0F; - } - -+ // Leaves start - optimize suffocation -+ public boolean couldPossiblyBeHurt(float amount) { -+ if ((float) this.invulnerableTime > (float) this.invulnerableDuration / 2.0F && amount <= this.lastHurt) { -+ return false; -+ } -+ return true; -+ } -+ // Leaves end - optimize suffocation -+ - @Override - public boolean hurtServer(ServerLevel world, DamageSource source, float amount) { - if (this.isInvulnerableTo(world, source)) { diff --git a/patches/server/0022-Config-to-disable-method-profiler.patch b/patches/server/0022-Config-to-disable-method-profiler.patch deleted file mode 100644 index 9be611da..00000000 --- a/patches/server/0022-Config-to-disable-method-profiler.patch +++ /dev/null @@ -1,32 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 15 Aug 2022 10:18:36 +0800 -Subject: [PATCH] Config to disable method profiler - -This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 186813428916c93545b7bad706b8584a4e338627..10d56397dbdf98668489b6968dc66ff138aa9d5f 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1322,7 +1322,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop -Date: Wed, 17 Aug 2022 10:56:49 +0800 -Subject: [PATCH] Remove lambda from ticking guard - -This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 1b1350485ba333e9bda048b01273de9744ce16a9..1b2ba3ff09b27ea74afc96735e48a474cf6ebddd 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -811,7 +811,23 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - gameprofilerfiller.push("tick"); -- this.guardEntityTick(this::tickNonPassenger, entity); -+ // Leaves start - copied from this.guardEntityTick -+ if (org.leavesmc.leaves.LeavesConfig.performance.remove.tickGuardLambda) { -+ try { -+ this.tickNonPassenger(entity); // Leaves - changed -+ } catch (Throwable throwable) { -+ if (throwable instanceof ThreadDeath) throw throwable; // Paper -+ // Paper start - Prevent block entity and entity crashes -+ final String msg = String.format("Entity threw exception at %s:%s,%s,%s", entity.level().getWorld().getName(), entity.getX(), entity.getY(), entity.getZ()); -+ MinecraftServer.LOGGER.error(msg, throwable); -+ getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(msg, throwable))); // Paper - ServerExceptionEvent -+ entity.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DISCARD); -+ // Paper end - Prevent block entity and entity crashes -+ } -+ } else { -+ this.guardEntityTick(this::tickNonPassenger, entity); -+ } -+ // Leaves end - copied from this.guardEntityTick - gameprofilerfiller.pop(); - } - } diff --git a/patches/server/0027-Cache-climbing-check-for-activation.patch b/patches/server/0027-Cache-climbing-check-for-activation.patch deleted file mode 100644 index 35614163..00000000 --- a/patches/server/0027-Cache-climbing-check-for-activation.patch +++ /dev/null @@ -1,47 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 18 Aug 2022 16:31:08 +0800 -Subject: [PATCH] Cache climbing check for activation - -This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index d5614e702137b52700873a8301d9ec3c87f47040..44180a5fddaafec623875676ab54cbd62b1268a8 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -2129,6 +2129,22 @@ public abstract class LivingEntity extends Entity implements Attackable { - return this.lastClimbablePos; - } - -+ // Leaves start - cache climbing check -+ private boolean cachedOnClimable = false; -+ private BlockPos lastClimbingPosition = null; -+ -+ public boolean onClimableCached() { -+ if (!org.leavesmc.leaves.LeavesConfig.performance.cacheClimbCheck) { -+ return this.onClimbable(); -+ } -+ if (!this.blockPosition().equals(this.lastClimbingPosition)) { -+ this.cachedOnClimable = this.onClimbable(); -+ this.lastClimbingPosition = this.blockPosition(); -+ } -+ return this.cachedOnClimable; -+ } -+ // Leaves end - cache climbing check -+ - public boolean onClimbable() { - if (this.isSpectator()) { - return false; -diff --git a/src/main/java/org/spigotmc/ActivationRange.java b/src/main/java/org/spigotmc/ActivationRange.java -index 1d438ef44cbe4d1eedfba36d8fe5d2ad53464921..4f6f42fb965d76c8360eb7c67936d2c3d104a700 100644 ---- a/src/main/java/org/spigotmc/ActivationRange.java -+++ b/src/main/java/org/spigotmc/ActivationRange.java -@@ -295,7 +295,7 @@ public class ActivationRange - if ( entity instanceof LivingEntity ) - { - LivingEntity living = (LivingEntity) entity; -- if ( living.onClimbable() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 || living.isFreezing()) // Paper -+ if ( living.onClimableCached() || living.jumping || living.hurtTime > 0 || living.activeEffects.size() > 0 || living.isFreezing()) // Paper // Leaves - use cached - { - return 1; // Paper - } diff --git a/patches/server/0028-Reduce-chunk-loading-lookups.patch b/patches/server/0028-Reduce-chunk-loading-lookups.patch deleted file mode 100644 index b2bb3b40..00000000 --- a/patches/server/0028-Reduce-chunk-loading-lookups.patch +++ /dev/null @@ -1,43 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sun, 21 Aug 2022 08:29:15 +0800 -Subject: [PATCH] Reduce chunk loading & lookups - -This patch is Powered by Pufferfish(https://github.com/pufferfish-gg/Pufferfish) - -diff --git a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java -index 48dcd2bc12ce1d08cc5195bff5460dc0dd9902d3..2b8e1a8e233071821411eb1f95c705efb4a6e816 100644 ---- a/src/main/java/net/minecraft/world/entity/monster/EnderMan.java -+++ b/src/main/java/net/minecraft/world/entity/monster/EnderMan.java -@@ -307,11 +307,28 @@ public class EnderMan extends Monster implements NeutralMob { - private boolean teleport(double x, double y, double z) { - BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(x, y, z); - -- while (blockposition_mutableblockposition.getY() > this.level().getMinY() && !this.level().getBlockState(blockposition_mutableblockposition).blocksMotion()) { -- blockposition_mutableblockposition.move(Direction.DOWN); -+ // Leaves start - single chunk lookup -+ BlockState iblockdata; -+ if (org.leavesmc.leaves.LeavesConfig.performance.reduceChuckLoadAndLookup) { -+ net.minecraft.world.level.chunk.LevelChunk chunk = this.level().getChunkIfLoaded(blockposition_mutableblockposition); -+ if (chunk == null) { -+ return false; -+ } -+ -+ while (blockposition_mutableblockposition.getY() > this.level().getMinY() && !chunk.getBlockState(blockposition_mutableblockposition).blocksMotion()) { -+ blockposition_mutableblockposition.move(Direction.DOWN); -+ } -+ -+ iblockdata = chunk.getBlockState(blockposition_mutableblockposition); -+ } else { -+ while (blockposition_mutableblockposition.getY() > this.level().getMinY() && !this.level().getBlockState(blockposition_mutableblockposition).blocksMotion()) { -+ blockposition_mutableblockposition.move(Direction.DOWN); -+ } -+ -+ iblockdata = this.level().getBlockState(blockposition_mutableblockposition); - } -+ // Leaves end - single chunk lookup - -- BlockState iblockdata = this.level().getBlockState(blockposition_mutableblockposition); - boolean flag = iblockdata.blocksMotion(); - boolean flag1 = iblockdata.getFluidState().is(FluidTags.WATER); - diff --git a/patches/server/0030-Random-flatten-triangular-distribution.patch b/patches/server/0030-Random-flatten-triangular-distribution.patch deleted file mode 100644 index 420ad4de..00000000 --- a/patches/server/0030-Random-flatten-triangular-distribution.patch +++ /dev/null @@ -1,27 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 30 Nov 2022 07:32:05 +0800 -Subject: [PATCH] Random flatten triangular distribution - -This patch is Powered by Carpet-TIS-Addition(https://github.com/plusls/Carpet-TIS-Addition) - -diff --git a/src/main/java/net/minecraft/util/RandomSource.java b/src/main/java/net/minecraft/util/RandomSource.java -index 252aef3ffe0fecd47ebea1ed7df48e14fa873eb9..bbfe68a7e860fff5c43feb2dc02c3c664a29bd09 100644 ---- a/src/main/java/net/minecraft/util/RandomSource.java -+++ b/src/main/java/net/minecraft/util/RandomSource.java -@@ -53,7 +53,14 @@ public interface RandomSource { - double nextGaussian(); - - default double triangle(double mode, double deviation) { -- return mode + deviation * (this.nextDouble() - this.nextDouble()); -+ // Leaves start - flattenTriangularDistribution -+ if (org.leavesmc.leaves.LeavesConfig.modify.flattenTriangularDistribution) { -+ this.nextDouble(); -+ return mode + deviation * (-1 + this.nextDouble() * 2); -+ } else { -+ return mode + deviation * (this.nextDouble() - this.nextDouble()); -+ } -+ // Leaves end - flattenTriangularDistribution - } - - default float triangle(float mode, float deviation) { diff --git a/patches/server/0031-BBOR-Protocol.patch b/patches/server/0031-BBOR-Protocol.patch deleted file mode 100644 index 1e6b5974..00000000 --- a/patches/server/0031-BBOR-Protocol.patch +++ /dev/null @@ -1,267 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 29 Nov 2022 09:51:16 +0800 -Subject: [PATCH] BBOR Protocol - - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index b83b903381ca6c426d2f26f9459e769083eb5876..7a741442ee2373f1a400214047bbf5844efecce6 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -1566,6 +1566,7 @@ public abstract class PlayerList { - entityplayer.getRecipeBook().sendInitialRecipeBook(entityplayer); - } - -+ org.leavesmc.leaves.protocol.BBORProtocol.onDataPackReload(); // Leaves - bbor - } - - public boolean isAllowCommandsForAllPlayers() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 4640baec5bed6c2d53cc0f8ca1d273cc115abe9b..2fea5a46bba27578366c36f594472c3e38395bee 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -813,6 +813,11 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - - public void setLoaded(boolean loadedToWorld) { - this.loaded = loadedToWorld; -+ // Leaves start - bbor -+ if (loaded) { -+ org.leavesmc.leaves.protocol.BBORProtocol.onChunkLoaded(this); -+ } -+ // Leaves end - bbor - } - - public Level getLevel() { -diff --git a/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c4ba09a998966fd8563f592920774cba2ea61c31 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java -@@ -0,0 +1,227 @@ -+package org.leavesmc.leaves.protocol; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Registry; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.levelgen.structure.BoundingBox; -+import net.minecraft.world.level.levelgen.structure.Structure; -+import net.minecraft.world.level.levelgen.structure.StructurePiece; -+import net.minecraft.world.level.levelgen.structure.StructureStart; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+import java.util.concurrent.ConcurrentHashMap; -+import java.util.stream.Collectors; -+ -+import static org.leavesmc.leaves.protocol.core.LeavesProtocolManager.EmptyPayload; -+ -+@LeavesProtocol(namespace = "bbor") -+public class BBORProtocol { -+ -+ public static final String PROTOCOL_ID = "bbor"; -+ -+ // send -+ private static final ResourceLocation INITIALIZE_CLIENT = id("initialize"); -+ private static final ResourceLocation ADD_BOUNDING_BOX = id("add_bounding_box_v2"); -+ private static final ResourceLocation STRUCTURE_LIST_SYNC = id("structure_list_sync_v1"); -+ // call -+ private static final Map players = new ConcurrentHashMap<>(); -+ private static final Map> playerBoundingBoxesCache = new HashMap<>(); -+ private static final Map>> dimensionCache = new ConcurrentHashMap<>(); -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation id(String path) { -+ return new ResourceLocation(PROTOCOL_ID, path); -+ } -+ -+ @ProtocolHandler.Ticker -+ public static void tick() { -+ if (LeavesConfig.protocol.bborProtocol) { -+ for (var playerEntry : players.entrySet()) { -+ sendBoundingToPlayer(playerEntry.getKey(), playerEntry.getValue()); -+ } -+ } -+ } -+ -+ @ProtocolHandler.ReloadServer -+ public static void onServerReload() { -+ if (LeavesConfig.protocol.bborProtocol) { -+ initAllPlayer(); -+ } else { -+ loggedOutAllPlayer(); -+ } -+ } -+ -+ @ProtocolHandler.PlayerJoin -+ public static void onPlayerLoggedIn(@NotNull ServerPlayer player) { -+ if (LeavesConfig.protocol.bborProtocol) { -+ ServerLevel overworld = MinecraftServer.getServer().overworld(); -+ ProtocolUtils.sendPayloadPacket(player, INITIALIZE_CLIENT, buf -> { -+ buf.writeLong(overworld.getSeed()); -+ buf.writeInt(overworld.levelData.getSpawnPos().getX()); -+ buf.writeInt(overworld.levelData.getSpawnPos().getZ()); -+ }); -+ sendStructureList(player); -+ } -+ } -+ -+ @ProtocolHandler.PlayerLeave -+ public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { -+ if (LeavesConfig.protocol.bborProtocol) { -+ players.remove(player.getId()); -+ playerBoundingBoxesCache.remove(player.getId()); -+ } -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = EmptyPayload.class, payloadId = "subscribe") -+ public static void onPlayerSubscribed(@NotNull ServerPlayer player, EmptyPayload payload) { -+ if (LeavesConfig.protocol.bborProtocol) { -+ players.put(player.getId(), player); -+ sendBoundingToPlayer(player.getId(), player); -+ } -+ } -+ -+ public static void onDataPackReload() { -+ if (LeavesConfig.protocol.bborProtocol) { -+ players.values().forEach(BBORProtocol::sendStructureList); -+ } -+ } -+ -+ public static void onChunkLoaded(@NotNull LevelChunk chunk) { -+ if (LeavesConfig.protocol.bborProtocol) { -+ Map structures = new HashMap<>(); -+ final Registry structureFeatureRegistry = chunk.getLevel().registryAccess().lookupOrThrow(Registries.STRUCTURE); -+ for (var es : chunk.getAllStarts().entrySet()) { -+ final var optional = structureFeatureRegistry.getResourceKey(es.getKey()); -+ optional.ifPresent(key -> structures.put(key.location().toString(), es.getValue())); -+ } -+ if (!structures.isEmpty()) { -+ onStructuresLoaded(chunk.getLevel().dimension().location(), structures); -+ } -+ } -+ } -+ -+ public static void onStructuresLoaded(@NotNull ResourceLocation dimensionID, @NotNull Map structures) { -+ Map> cache = getOrCreateCache(dimensionID); -+ for (var entry : structures.entrySet()) { -+ StructureStart structureStart = entry.getValue(); -+ if (structureStart == null) { -+ return; -+ } -+ -+ String type = "structure:" + entry.getKey(); -+ BoundingBox bb = structureStart.getBoundingBox(); -+ BBoundingBox boundingBox = buildStructure(bb, type); -+ if (cache.containsKey(boundingBox)) { -+ return; -+ } -+ -+ Set structureBoundingBoxes = new HashSet<>(); -+ for (StructurePiece structureComponent : structureStart.getPieces()) { -+ structureBoundingBoxes.add(buildStructure(structureComponent.getBoundingBox(), type)); -+ } -+ cache.put(boundingBox, structureBoundingBoxes); -+ } -+ } -+ -+ private static @NotNull BBoundingBox buildStructure(@NotNull BoundingBox bb, String type) { -+ BlockPos min = new BlockPos(bb.minX(), bb.minY(), bb.minZ()); -+ BlockPos max = new BlockPos(bb.maxX(), bb.maxY(), bb.maxZ()); -+ return new BBoundingBox(type, min, max); -+ } -+ -+ private static void sendStructureList(@NotNull ServerPlayer player) { -+ final Registry structureRegistry = player.server.registryAccess().lookupOrThrow(Registries.STRUCTURE); -+ final Set structureIds = structureRegistry.entrySet().stream() -+ .map(e -> e.getKey().location().toString()).collect(Collectors.toSet()); -+ ProtocolUtils.sendPayloadPacket(player, STRUCTURE_LIST_SYNC, buf -> { -+ buf.writeVarInt(structureIds.size()); -+ structureIds.forEach(buf::writeUtf); -+ }); -+ } -+ -+ private static void sendBoundingToPlayer(int id, ServerPlayer player) { -+ for (var entry : dimensionCache.entrySet()) { -+ if (entry.getValue() == null) { -+ return; -+ } -+ -+ Set playerBoundingBoxes = playerBoundingBoxesCache.computeIfAbsent(id, k -> new HashSet<>()); -+ Map> boundingBoxMap = entry.getValue(); -+ for (BBoundingBox key : boundingBoxMap.keySet()) { -+ if (playerBoundingBoxes.contains(key)) { -+ continue; -+ } -+ -+ Set boundingBoxes = boundingBoxMap.get(key); -+ ProtocolUtils.sendPayloadPacket(player, ADD_BOUNDING_BOX, buf -> { -+ buf.writeResourceLocation(entry.getKey()); -+ key.serialize(buf); -+ if (boundingBoxes != null && boundingBoxes.size() > 1) { -+ for (BBoundingBox box : boundingBoxes) { -+ box.serialize(buf); -+ } -+ } -+ }); -+ playerBoundingBoxes.add(key); -+ } -+ } -+ } -+ -+ public static void initAllPlayer() { -+ for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { -+ onPlayerLoggedIn(player); -+ } -+ } -+ -+ public static void loggedOutAllPlayer() { -+ players.clear(); -+ playerBoundingBoxesCache.clear(); -+ for (var cache : dimensionCache.values()) { -+ cache.clear(); -+ } -+ dimensionCache.clear(); -+ } -+ -+ private static Map> getOrCreateCache(ResourceLocation dimensionId) { -+ return dimensionCache.computeIfAbsent(dimensionId, dt -> new ConcurrentHashMap<>()); -+ } -+ -+ private record BBoundingBox(String type, BlockPos min, BlockPos max) { -+ -+ public void serialize(@NotNull FriendlyByteBuf buf) { -+ buf.writeChar('S'); -+ buf.writeInt(type.hashCode()); -+ buf.writeVarInt(min.getX()).writeVarInt(min.getY()).writeVarInt(min.getZ()); -+ buf.writeVarInt(max.getX()).writeVarInt(max.getY()).writeVarInt(max.getZ()); -+ } -+ -+ @Override -+ public int hashCode() { -+ return combineHashCodes(min.hashCode(), max.hashCode()); -+ } -+ -+ private static int combineHashCodes(int @NotNull ... hashCodes) { -+ final int prime = 31; -+ int result = 0; -+ for (int hashCode : hashCodes) { -+ result = prime * result + hashCode; -+ } -+ return result; -+ } -+ } -+} diff --git a/patches/server/0032-PCA-sync-protocol.patch b/patches/server/0032-PCA-sync-protocol.patch deleted file mode 100644 index 192f5d21..00000000 --- a/patches/server/0032-PCA-sync-protocol.patch +++ /dev/null @@ -1,733 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 28 Nov 2022 15:34:15 +0800 -Subject: [PATCH] PCA sync protocol - -This patch is Powered by plusls-carpet-addition(https://github.com/plusls/plusls-carpet-addition) - -diff --git a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java -index 8aed30cdbbfdd42c20dcd4c8773c8a0ee21a980d..b2c83534446f95503b9fd8bd09be90575622c395 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java -+++ b/src/main/java/net/minecraft/world/entity/animal/horse/AbstractHorse.java -@@ -449,6 +449,11 @@ public abstract class AbstractHorse extends Animal implements ContainerListener, - - @Override - public void containerChanged(Container sender) { -+ // Leaves start - pca -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(this); -+ } -+ // Leaves end - pca - boolean flag = this.isSaddled(); - - this.syncSaddleToClients(); -diff --git a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -index 5f656fc726a1dc5f42657095a2f2b7cf85b92d7c..6c0cece094d36ddb2ae8d67d3c847a2c8faa3da8 100644 ---- a/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -+++ b/src/main/java/net/minecraft/world/entity/npc/AbstractVillager.java -@@ -69,6 +69,15 @@ public abstract class AbstractVillager extends AgeableMob implements InventoryCa - super(type, world); - this.setPathfindingMalus(PathType.DANGER_FIRE, 16.0F); - this.setPathfindingMalus(PathType.DAMAGE_FIRE, -1.0F); -+ // Leaves start - pca -+ if (!this.level().isClientSide()) { -+ this.inventory.addListener(inventory -> { -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(this); -+ } -+ }); -+ } -+ // Leaves end - pca - } - - @Override -diff --git a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java -index d528e8e4aea266c495377365f01e314001eb1970..2472eb9f8f0b6872f7b98cb2da511c6a2527b8fc 100644 ---- a/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java -+++ b/src/main/java/net/minecraft/world/entity/vehicle/AbstractMinecartContainer.java -@@ -122,7 +122,13 @@ public abstract class AbstractMinecartContainer extends AbstractMinecart impleme - } - - @Override -- public void setChanged() {} -+ public void setChanged() { -+ // Leaves start - pca -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncEntityToClient(this); -+ } -+ // Leaves end - pca -+ } - - @Override - public boolean stillValid(Player player) { -diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -index 15e0861486a2bda3e2f4049b1b5a299c870acd31..82e93a63ea64adbf648ea7b8a844a376bbe63597 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -@@ -440,6 +440,16 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit - } - } - -+ // Leaves start - pca -+ @Override -+ public void setChanged() { -+ super.setChanged(); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ } -+ // Leaves end - pca -+ - @Override - public boolean canPlaceItem(int slot, ItemStack stack) { - if (slot == 2) { -diff --git a/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java -index 618552afbdacc919c33b30a6bf4834fb71ab3d5b..c14e7805e849e56912d50b61d5f2f3d802ac9bde 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/BarrelBlockEntity.java -@@ -132,6 +132,16 @@ public class BarrelBlockEntity extends RandomizableContainerBlockEntity { - this.items = inventory; - } - -+ // Leaves start - pca -+ @Override -+ public void setChanged() { -+ super.setChanged(); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ } -+ // Leaves end - pca -+ - @Override - protected Component getDefaultName() { - return Component.translatable("container.barrel"); -diff --git a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java -index 83ad45aed0894e90825d22e078632352c3a06816..58319df10af098db7cb74f08651880dab64ae4e1 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/BeehiveBlockEntity.java -@@ -144,6 +144,11 @@ public class BeehiveBlockEntity extends BlockEntity { - super.setChanged(); - } - -+ // Leaves start - pca -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ // Leaves end - pca - return list; - } - -@@ -198,6 +203,12 @@ public class BeehiveBlockEntity extends BlockEntity { - this.level.gameEvent((Holder) GameEvent.BLOCK_CHANGE, blockposition, GameEvent.Context.of(entity, this.getBlockState())); - } - -+ // Leaves start - pca -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ // Leaves end - pca -+ - entity.discard(EntityRemoveEvent.Cause.ENTER_BLOCK); // CraftBukkit - add Bukkit remove cause - super.setChanged(); - } -@@ -313,6 +324,11 @@ public class BeehiveBlockEntity extends BlockEntity { - if (BeehiveBlockEntity.releaseOccupant(world, pos, state, tileentitybeehive_hivebee.toOccupant(), (List) null, tileentitybeehive_releasestatus, flowerPos)) { - flag = true; - iterator.remove(); -+ // Leaves start - pca -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(Objects.requireNonNull(world.getBlockEntity(pos))); -+ } -+ // Leaves end - pca - // CraftBukkit start - } else { - tileentitybeehive_hivebee.exitTickCounter = tileentitybeehive_hivebee.occupant.minTicksInHive / 2; // Not strictly Vanilla behaviour in cases where bees cannot spawn but still reasonable // Paper - Fix bees aging inside hives; use exitTickCounter to keep actual bee life -@@ -358,6 +374,11 @@ public class BeehiveBlockEntity extends BlockEntity { - this.maxBees = nbt.getInt("Bukkit.MaxEntities"); - } - // CraftBukkit end -+ // Leaves start - pca -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ // Leaves end - pca - } - - @Override -diff --git a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java -index 02fc9ce21c7d367055da350d21be4870d4242f3a..974a7fbec9ec347ea16a07d7c857a27e3b25f76d 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/BrewingStandBlockEntity.java -@@ -345,6 +345,16 @@ public class BrewingStandBlockEntity extends BaseContainerBlockEntity implements - return this.canPlaceItem(slot, stack); - } - -+ // Leaves start - pca -+ @Override -+ public void setChanged() { -+ super.setChanged(); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ } -+ // Leaves end - pca -+ - @Override - public boolean canTakeItemThroughFace(int slot, ItemStack stack, Direction dir) { - return slot == 3 ? stack.is(Items.GLASS_BOTTLE) : true; -diff --git a/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java -index 8847617f6a23e6d2fe9bf7444a2072dc53f741b8..ef91101fb9e44b5db58272e4c2fdada1fa9e1686 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/ChestBlockEntity.java -@@ -191,6 +191,16 @@ public class ChestBlockEntity extends RandomizableContainerBlockEntity implement - this.items = inventory; - } - -+ // Leaves start - pca -+ @Override -+ public void setChanged() { -+ super.setChanged(); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ } -+ // Leaves end - pca -+ - @Override - public float getOpenNess(float tickDelta) { - return this.chestLidController.getOpenness(tickDelta); -diff --git a/src/main/java/net/minecraft/world/level/block/entity/ComparatorBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ComparatorBlockEntity.java -index 9db5826420d693628ad74614f4cee79e1ebd88d9..0e14b15b262f1ca5e12e31d2f18bc90bb94baf79 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/ComparatorBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/ComparatorBlockEntity.java -@@ -24,6 +24,16 @@ public class ComparatorBlockEntity extends BlockEntity { - this.output = nbt.getInt("OutputSignal"); - } - -+ // Leaves start - pca -+ @Override -+ public void setChanged() { -+ super.setChanged(); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ } -+ // Leaves end - pca -+ - public int getOutputSignal() { - return this.output; - } -diff --git a/src/main/java/net/minecraft/world/level/block/entity/DispenserBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/DispenserBlockEntity.java -index c7f1937b0f171eee967388ab4699703dcdcfbd2b..c3d41360b2c5dab04f519459bd855045e8bec7c1 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/DispenserBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/DispenserBlockEntity.java -@@ -109,6 +109,16 @@ public class DispenserBlockEntity extends RandomizableContainerBlockEntity { - return stack; - } - -+ // Leaves start - pca -+ @Override -+ public void setChanged() { -+ super.setChanged(); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ } -+ // Leaves end - pca -+ - @Override - protected Component getDefaultName() { - return Component.translatable("container.dispenser"); -diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -index 5ebbdb94d9b91c442ff60eb6872f740ebd790fa0..02bda85189fd57bd3f6d18cfd573553b2e607300 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -@@ -134,6 +134,16 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - this.facing = (Direction) state.getValue(HopperBlock.FACING); - } - -+ // Leaves start - pca -+ @Override -+ public void setChanged() { -+ super.setChanged(); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ } -+ // Leaves end - pca -+ - @Override - protected Component getDefaultName() { - return Component.translatable("container.hopper"); -@@ -212,6 +222,11 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - if (flag) { - blockEntity.setCooldown(world.spigotConfig.hopperTransfer); // Spigot - setChanged(world, pos, state); -+ // Leaves start - pca -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(blockEntity); -+ } -+ // Leaves end - pca - return true; - } - } -diff --git a/src/main/java/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java -index 6291ebf3426bddb0e6d13159ce20936b6e9ba6bd..3784e4c14bb59073f941b966f616ddac3f80a497 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java -@@ -270,6 +270,16 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl - this.itemStacks = inventory; - } - -+ // Leaves start - pca -+ @Override -+ public void setChanged() { -+ super.setChanged(); -+ if (org.leavesmc.leaves.LeavesConfig.protocol.pca.enable) { -+ org.leavesmc.leaves.protocol.PcaSyncProtocol.syncBlockEntityToClient(this); -+ } -+ } -+ // Leaves end - pca -+ - @Override - public int[] getSlotsForFace(Direction side) { - return ShulkerBoxBlockEntity.SLOTS; -diff --git a/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a38318506aeb1632ba7903671bcfad1e871c8fc7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/PcaSyncProtocol.java -@@ -0,0 +1,432 @@ -+package org.leavesmc.leaves.protocol; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.ChestBlock; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.block.state.properties.ChestType; -+import org.apache.commons.lang3.tuple.ImmutablePair; -+import org.apache.commons.lang3.tuple.MutablePair; -+import org.apache.commons.lang3.tuple.Pair; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+import java.util.concurrent.locks.ReentrantLock; -+ -+import static org.leavesmc.leaves.protocol.core.LeavesProtocolManager.EmptyPayload; -+ -+@LeavesProtocol(namespace = "pca") -+public class PcaSyncProtocol { -+ -+ public static final String PROTOCOL_ID = "pca"; -+ -+ public static final ReentrantLock lock = new ReentrantLock(true); -+ public static final ReentrantLock pairLock = new ReentrantLock(true); -+ -+ // send -+ private static final ResourceLocation ENABLE_PCA_SYNC_PROTOCOL = id("enable_pca_sync_protocol"); -+ private static final ResourceLocation DISABLE_PCA_SYNC_PROTOCOL = id("disable_pca_sync_protocol"); -+ -+ private static final Map> playerWatchBlockPos = new HashMap<>(); -+ private static final Map> playerWatchEntity = new HashMap<>(); -+ private static final Map, Set> blockPosWatchPlayerSet = new HashMap<>(); -+ private static final Map, Set> entityWatchPlayerSet = new HashMap<>(); -+ private static final MutablePair ResourceLocationEntityPair = new MutablePair<>(); -+ private static final MutablePair ResourceLocationBlockPosPair = new MutablePair<>(); -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation id(String path) { -+ return new ResourceLocation(PROTOCOL_ID, path); -+ } -+ -+ @ProtocolHandler.PlayerJoin -+ private static void onJoin(ServerPlayer player) { -+ if (LeavesConfig.protocol.pca.enable) { -+ enablePcaSyncProtocol(player); -+ } -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = EmptyPayload.class, payloadId = "cancel_sync_block_entity") -+ private static void cancelSyncBlockEntityHandler(ServerPlayer player, EmptyPayload payload) { -+ if (!LeavesConfig.protocol.pca.enable) { -+ return; -+ } -+ PcaSyncProtocol.clearPlayerWatchBlock(player); -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = EmptyPayload.class, payloadId = "cancel_sync_entity") -+ private static void cancelSyncEntityHandler(ServerPlayer player, EmptyPayload payload) { -+ if (!LeavesConfig.protocol.pca.enable) { -+ return; -+ } -+ PcaSyncProtocol.clearPlayerWatchEntity(player); -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = SyncBlockEntityPayload.class, payloadId = "sync_block_entity") -+ private static void syncBlockEntityHandler(ServerPlayer player, SyncBlockEntityPayload payload) { -+ if (!LeavesConfig.protocol.pca.enable) { -+ return; -+ } -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ BlockPos pos = payload.pos; -+ ServerLevel world = player.serverLevel(); -+ -+ server.execute(() -> { -+ BlockState blockState = world.getBlockState(pos); -+ clearPlayerWatchData(player); -+ -+ BlockEntity blockEntityAdj = null; -+ if (blockState.getBlock() instanceof ChestBlock) { -+ if (blockState.getValue(ChestBlock.TYPE) != ChestType.SINGLE) { -+ BlockPos posAdj = pos.relative(ChestBlock.getConnectedDirection(blockState)); -+ // The method in World now checks that the caller is from the same thread... -+ blockEntityAdj = world.getChunk(posAdj).getBlockEntity(posAdj); -+ } -+ } -+ -+ if (blockEntityAdj != null) { -+ updateBlockEntity(player, blockEntityAdj); -+ } -+ -+ // The method in World now checks that the caller is from the same thread... -+ BlockEntity blockEntity = world.getChunk(pos).getBlockEntity(pos); -+ if (blockEntity != null) { -+ updateBlockEntity(player, blockEntity); -+ } -+ -+ Pair pair = new ImmutablePair<>(player.level().dimension().location(), pos); -+ lock.lock(); -+ playerWatchBlockPos.put(player, pair); -+ if (!blockPosWatchPlayerSet.containsKey(pair)) { -+ blockPosWatchPlayerSet.put(pair, new HashSet<>()); -+ } -+ blockPosWatchPlayerSet.get(pair).add(player); -+ lock.unlock(); -+ }); -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = SyncEntityPayload.class, payloadId = "sync_entity") -+ private static void syncEntityHandler(ServerPlayer player, SyncEntityPayload payload) { -+ if (!LeavesConfig.protocol.pca.enable) { -+ return; -+ } -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ int entityId = payload.entityId; -+ ServerLevel world = player.serverLevel(); -+ -+ server.execute(() -> { -+ Entity entity = world.getEntity(entityId); -+ -+ if (entity != null) { -+ clearPlayerWatchData(player); -+ -+ if (entity instanceof Player) { -+ switch (LeavesConfig.protocol.pca.syncPlayerEntity) { -+ case NOBODY -> { -+ return; -+ } -+ case BOT -> { -+ if (!(entity instanceof ServerBot)) { -+ return; -+ } -+ } -+ case OPS -> { -+ if (!(entity instanceof ServerBot) && server.getPlayerList().isOp(player.gameProfile)) { -+ return; -+ } -+ } -+ case OPS_AND_SELF -> { -+ if (!(entity instanceof ServerBot) && server.getPlayerList().isOp(player.gameProfile) && entity != player) { -+ return; -+ } -+ } -+ case EVERYONE -> {} -+ case null -> LeavesLogger.LOGGER.warning("pcaSyncPlayerEntity wtf???"); -+ } -+ } -+ updateEntity(player, entity); -+ -+ Pair pair = new ImmutablePair<>(entity.level().dimension().location(), entity); -+ lock.lock(); -+ playerWatchEntity.put(player, pair); -+ if (!entityWatchPlayerSet.containsKey(pair)) { -+ entityWatchPlayerSet.put(pair, new HashSet<>()); -+ } -+ entityWatchPlayerSet.get(pair).add(player); -+ lock.unlock(); -+ } -+ }); -+ } -+ -+ public static void onConfigModify(boolean enable) { -+ if (enable) { -+ enablePcaSyncProtocolGlobal(); -+ } else { -+ disablePcaSyncProtocolGlobal(); -+ } -+ } -+ -+ public static void enablePcaSyncProtocol(@NotNull ServerPlayer player) { -+ ProtocolUtils.sendEmptyPayloadPacket(player, ENABLE_PCA_SYNC_PROTOCOL); -+ lock.lock(); -+ lock.unlock(); -+ } -+ -+ public static void disablePcaSyncProtocol(@NotNull ServerPlayer player) { -+ ProtocolUtils.sendEmptyPayloadPacket(player, DISABLE_PCA_SYNC_PROTOCOL); -+ } -+ -+ public static void updateEntity(@NotNull ServerPlayer player, @NotNull Entity entity) { -+ CompoundTag nbt = entity.saveWithoutId(new CompoundTag()); -+ ProtocolUtils.sendPayloadPacket(player, new UpdateEntityPayload(entity.level().dimension().location(), entity.getId(), nbt)); -+ } -+ -+ public static void updateBlockEntity(@NotNull ServerPlayer player, @NotNull BlockEntity blockEntity) { -+ Level world = blockEntity.getLevel(); -+ -+ if (world == null) { -+ return; -+ } -+ -+ ProtocolUtils.sendPayloadPacket(player, new UpdateBlockEntityPayload(world.dimension().location(), blockEntity.getBlockPos(), blockEntity.saveWithoutMetadata(world.registryAccess()))); -+ } -+ -+ private static MutablePair getResourceLocationEntityPair(ResourceLocation ResourceLocation, Entity entity) { -+ pairLock.lock(); -+ ResourceLocationEntityPair.setLeft(ResourceLocation); -+ ResourceLocationEntityPair.setRight(entity); -+ pairLock.unlock(); -+ return ResourceLocationEntityPair; -+ } -+ -+ private static MutablePair getResourceLocationBlockPosPair(ResourceLocation ResourceLocation, BlockPos pos) { -+ pairLock.lock(); -+ ResourceLocationBlockPosPair.setLeft(ResourceLocation); -+ ResourceLocationBlockPosPair.setRight(pos); -+ pairLock.unlock(); -+ return ResourceLocationBlockPosPair; -+ } -+ -+ private static @Nullable Set getWatchPlayerList(@NotNull Entity entity) { -+ return entityWatchPlayerSet.get(getResourceLocationEntityPair(entity.level().dimension().location(), entity)); -+ } -+ -+ private static @Nullable Set getWatchPlayerList(@NotNull Level world, @NotNull BlockPos blockPos) { -+ return blockPosWatchPlayerSet.get(getResourceLocationBlockPosPair(world.dimension().location(), blockPos)); -+ } -+ -+ public static boolean syncEntityToClient(@NotNull Entity entity) { -+ if (entity.level().isClientSide()) { -+ return false; -+ } -+ lock.lock(); -+ Set playerList = getWatchPlayerList(entity); -+ boolean ret = false; -+ if (playerList != null) { -+ for (ServerPlayer player : playerList) { -+ updateEntity(player, entity); -+ ret = true; -+ } -+ } -+ lock.unlock(); -+ return ret; -+ } -+ -+ public static boolean syncBlockEntityToClient(@NotNull BlockEntity blockEntity) { -+ boolean ret = false; -+ Level world = blockEntity.getLevel(); -+ BlockPos pos = blockEntity.getBlockPos(); -+ if (world != null) { -+ if (world.isClientSide()) { -+ return false; -+ } -+ BlockState blockState = world.getBlockState(pos); -+ lock.lock(); -+ Set playerList = getWatchPlayerList(world, blockEntity.getBlockPos()); -+ -+ Set playerListAdj = null; -+ -+ if (blockState.getBlock() instanceof ChestBlock) { -+ if (blockState.getValue(ChestBlock.TYPE) != ChestType.SINGLE) { -+ BlockPos posAdj = pos.relative(ChestBlock.getConnectedDirection(blockState)); -+ playerListAdj = getWatchPlayerList(world, posAdj); -+ } -+ } -+ if (playerListAdj != null) { -+ if (playerList == null) { -+ playerList = playerListAdj; -+ } else { -+ playerList.addAll(playerListAdj); -+ } -+ } -+ -+ if (playerList != null) { -+ for (ServerPlayer player : playerList) { -+ updateBlockEntity(player, blockEntity); -+ ret = true; -+ } -+ } -+ lock.unlock(); -+ } -+ return ret; -+ } -+ -+ private static void clearPlayerWatchEntity(ServerPlayer player) { -+ lock.lock(); -+ Pair pair = playerWatchEntity.get(player); -+ if (pair != null) { -+ Set playerSet = entityWatchPlayerSet.get(pair); -+ playerSet.remove(player); -+ if (playerSet.isEmpty()) { -+ entityWatchPlayerSet.remove(pair); -+ } -+ playerWatchEntity.remove(player); -+ } -+ lock.unlock(); -+ } -+ -+ private static void clearPlayerWatchBlock(ServerPlayer player) { -+ lock.lock(); -+ Pair pair = playerWatchBlockPos.get(player); -+ if (pair != null) { -+ Set playerSet = blockPosWatchPlayerSet.get(pair); -+ playerSet.remove(player); -+ if (playerSet.isEmpty()) { -+ blockPosWatchPlayerSet.remove(pair); -+ } -+ playerWatchBlockPos.remove(player); -+ } -+ lock.unlock(); -+ } -+ -+ public static void disablePcaSyncProtocolGlobal() { -+ lock.lock(); -+ playerWatchBlockPos.clear(); -+ playerWatchEntity.clear(); -+ blockPosWatchPlayerSet.clear(); -+ entityWatchPlayerSet.clear(); -+ lock.unlock(); -+ for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { -+ disablePcaSyncProtocol(player); -+ } -+ } -+ -+ public static void enablePcaSyncProtocolGlobal() { -+ for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { -+ enablePcaSyncProtocol(player); -+ } -+ } -+ -+ -+ public static void clearPlayerWatchData(ServerPlayer player) { -+ PcaSyncProtocol.clearPlayerWatchBlock(player); -+ PcaSyncProtocol.clearPlayerWatchEntity(player); -+ } -+ -+ public record UpdateEntityPayload(ResourceLocation dimension, int entityId, CompoundTag tag) implements LeavesCustomPayload { -+ -+ public static final ResourceLocation UPDATE_ENTITY = PcaSyncProtocol.id("update_entity"); -+ -+ @New -+ public UpdateEntityPayload(ResourceLocation location, FriendlyByteBuf byteBuf) { -+ this(byteBuf.readResourceLocation(), byteBuf.readInt(), byteBuf.readNbt()); -+ } -+ -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ buf.writeResourceLocation(this.dimension); -+ buf.writeInt(this.entityId); -+ buf.writeNbt(this.tag); -+ } -+ -+ @Override -+ public ResourceLocation id() { -+ return UPDATE_ENTITY; -+ } -+ } -+ -+ public record UpdateBlockEntityPayload(ResourceLocation dimension, BlockPos blockPos, CompoundTag tag) implements LeavesCustomPayload { -+ -+ private static final ResourceLocation UPDATE_BLOCK_ENTITY = PcaSyncProtocol.id("update_block_entity"); -+ -+ @New -+ public UpdateBlockEntityPayload(ResourceLocation location, @NotNull FriendlyByteBuf byteBuf) { -+ this(byteBuf.readResourceLocation(), byteBuf.readBlockPos(), byteBuf.readNbt()); -+ } -+ -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ buf.writeResourceLocation(this.dimension); -+ buf.writeBlockPos(this.blockPos); -+ buf.writeNbt(this.tag); -+ } -+ -+ @Override -+ public ResourceLocation id() { -+ return UPDATE_BLOCK_ENTITY; -+ } -+ } -+ -+ public record SyncBlockEntityPayload(BlockPos pos) implements LeavesCustomPayload { -+ -+ public static final ResourceLocation SYNC_BLOCK_ENTITY = PcaSyncProtocol.id("sync_block_entity"); -+ -+ @New -+ public SyncBlockEntityPayload(ResourceLocation id, @NotNull FriendlyByteBuf buf) { -+ this(buf.readBlockPos()); -+ } -+ -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ buf.writeBlockPos(pos); -+ } -+ -+ @Override -+ public @NotNull ResourceLocation id() { -+ return SYNC_BLOCK_ENTITY; -+ } -+ } -+ -+ public record SyncEntityPayload(int entityId) implements LeavesCustomPayload { -+ -+ public static final ResourceLocation SYNC_ENTITY = PcaSyncProtocol.id("sync_entity"); -+ -+ @New -+ public SyncEntityPayload(ResourceLocation id, @NotNull FriendlyByteBuf buf) { -+ this(buf.readInt()); -+ } -+ -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ buf.writeInt(entityId); -+ } -+ -+ @Override -+ public @NotNull ResourceLocation id() { -+ return SYNC_ENTITY; -+ } -+ } -+} diff --git a/patches/server/0033-Jade-Protocol.patch b/patches/server/0033-Jade-Protocol.patch deleted file mode 100644 index 1d45d19b..00000000 --- a/patches/server/0033-Jade-Protocol.patch +++ /dev/null @@ -1,3540 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sat, 3 Dec 2022 08:57:15 +0800 -Subject: [PATCH] Jade Protocol - -This patch is Powered by Jade(https://github.com/Snownee/Jade) - -diff --git a/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java b/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java -index c1ef714096159608752d744b98f615cd45fe459a..142bc508f5b816407d219944b9354db649a10ce0 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java -+++ b/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java -@@ -63,7 +63,7 @@ public class Armadillo extends Animal { - public final AnimationState rollOutAnimationState = new AnimationState(); - public final AnimationState rollUpAnimationState = new AnimationState(); - public final AnimationState peekAnimationState = new AnimationState(); -- private int scuteTime; -+ public int scuteTime; // Leaves - private -> public - private boolean peekReceivedClient = false; - - public Armadillo(EntityType type, Level world) { -diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java -index 48ac8c3f6e00c3c2dc67b6c994be7c0ac6dfcf81..ac306874dc94ec59427b353beec273f9f7d54432 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java -+++ b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java -@@ -246,7 +246,7 @@ public class Tadpole extends AbstractFish { - - } - -- private int getTicksLeftUntilAdult() { -+ public int getTicksLeftUntilAdult() { // Leaves - private -> public - return Math.max(0, Tadpole.ticksToBeFrog - this.age); - } - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java b/src/main/java/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java -index a49f83784f85f5420091692aae588ef067aa5fcd..137f777d324d0a0addc2e43913e196bc9f09e004 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java -@@ -73,7 +73,7 @@ public class TrialSpawnerData { - }); - public final Set detectedPlayers; - public final Set currentMobs; -- protected long cooldownEndsAt; -+ public long cooldownEndsAt; // Leaves - protected -> public - protected long nextMobSpawnsAt; - protected int totalMobsSpawned; - public Optional nextSpawnData; -diff --git a/src/main/java/net/minecraft/world/level/storage/loot/LootPool.java b/src/main/java/net/minecraft/world/level/storage/loot/LootPool.java -index 38078c44b35e917d1d243a5f8599aa858d8611de..13dbadfb50278b79b33d9dce10413c93c9e4ff31 100644 ---- a/src/main/java/net/minecraft/world/level/storage/loot/LootPool.java -+++ b/src/main/java/net/minecraft/world/level/storage/loot/LootPool.java -@@ -36,7 +36,7 @@ public class LootPool { - ) - .apply(instance, LootPool::new) - ); -- private final List entries; -+ public final List entries; // Leaves - private -> public - private final List conditions; - private final Predicate compositeCondition; - private final List functions; -diff --git a/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java b/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java -index c2bded5094097f5615a2ddb0718942486ede93b5..78fd2b33174a0c8b1dec99009bc2c8d41f6cbfe0 100644 ---- a/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java -+++ b/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java -@@ -58,7 +58,7 @@ public class LootTable { - public static final Codec> CODEC = RegistryFileCodec.create(Registries.LOOT_TABLE, LootTable.DIRECT_CODEC); - private final ContextKeySet paramSet; - private final Optional randomSequence; -- private final List pools; -+ public final List pools; // Leaves - private -> public - private final List functions; - private final BiFunction compositeFunction; - public CraftLootTable craftLootTable; // CraftBukkit -diff --git a/src/main/java/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java b/src/main/java/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java -index 128792f76f02d74c1ccf84beb8e7973453424639..775fbddf3e3b133e33f54aaa8e8a07d131095e34 100644 ---- a/src/main/java/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java -+++ b/src/main/java/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java -@@ -9,7 +9,7 @@ import net.minecraft.world.level.storage.loot.ValidationContext; - import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; - - public abstract class CompositeEntryBase extends LootPoolEntryContainer { -- protected final List children; -+ public final List children; // Leaves - private -> public - private final ComposableEntryContainer composedChildren; - - protected CompositeEntryBase(List terms, List conditions) { -diff --git a/src/main/java/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java b/src/main/java/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java -index 1d2f2bb352abf6772cd20293575fc79e8e64ce3b..b157dfaf1efffebd3f2ae8cb8fcf0972fe742258 100644 ---- a/src/main/java/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java -+++ b/src/main/java/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java -@@ -13,7 +13,7 @@ import net.minecraft.world.level.storage.loot.predicates.ConditionUserBuilder; - import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; - - public abstract class LootPoolEntryContainer implements ComposableEntryContainer { -- protected final List conditions; -+ public final List conditions; // Leaves - private -> public - private final Predicate compositeCondition; - - protected LootPoolEntryContainer(List conditions) { -diff --git a/src/main/java/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java b/src/main/java/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java -index 61d47a1e86a26bdee49d0ae931aeb92417ab02c2..1116f194b457ef736bf9e836c190817610008ef6 100644 ---- a/src/main/java/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java -+++ b/src/main/java/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java -@@ -25,7 +25,7 @@ public class NestedLootTable extends LootPoolSingletonContainer { - .and(singletonFields(instance)) - .apply(instance, NestedLootTable::new) - ); -- private final Either, LootTable> contents; -+ public final Either, LootTable> contents; // Leaves - private -> public - - private NestedLootTable( - Either, LootTable> value, int weight, int quality, List conditions, List functions -diff --git a/src/main/java/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java b/src/main/java/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java -index 30d0133a42ce990352f5c492fcf9beb105364848..1ab2eab686b3a89d406f127a6036c0e2932db4a6 100644 ---- a/src/main/java/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java -+++ b/src/main/java/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java -@@ -11,7 +11,7 @@ import net.minecraft.world.level.storage.loot.LootContext; - import net.minecraft.world.level.storage.loot.ValidationContext; - - public abstract class CompositeLootItemCondition implements LootItemCondition { -- protected final List terms; -+ public final List terms; // Leaves - private -> public - private final Predicate composedPredicate; - - protected CompositeLootItemCondition(List terms, Predicate predicate) { -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6fbd71ba1158b4451396936e4c08f8aaed8f4158 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java -@@ -0,0 +1,280 @@ -+package org.leavesmc.leaves.protocol.jade; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.AgeableMob; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.LivingEntity; -+import net.minecraft.world.entity.animal.Animal; -+import net.minecraft.world.entity.animal.Chicken; -+import net.minecraft.world.entity.animal.allay.Allay; -+import net.minecraft.world.entity.animal.armadillo.Armadillo; -+import net.minecraft.world.entity.animal.frog.Tadpole; -+import net.minecraft.world.entity.monster.ZombieVillager; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.Items; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.CampfireBlock; -+import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; -+import net.minecraft.world.level.block.entity.BeehiveBlockEntity; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; -+import net.minecraft.world.level.block.entity.CalibratedSculkSensorBlockEntity; -+import net.minecraft.world.level.block.entity.ChiseledBookShelfBlockEntity; -+import net.minecraft.world.level.block.entity.CommandBlockEntity; -+import net.minecraft.world.level.block.entity.ComparatorBlockEntity; -+import net.minecraft.world.level.block.entity.HopperBlockEntity; -+import net.minecraft.world.level.block.entity.JukeboxBlockEntity; -+import net.minecraft.world.level.block.entity.LecternBlockEntity; -+import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.payload.ReceiveDataPayload; -+import org.leavesmc.leaves.protocol.jade.payload.RequestBlockPayload; -+import org.leavesmc.leaves.protocol.jade.payload.RequestEntityPayload; -+import org.leavesmc.leaves.protocol.jade.payload.ServerPingPayload; -+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; -+import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; -+import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; -+import org.leavesmc.leaves.protocol.jade.provider.ItemStorageExtensionProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.BeehiveProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.BrewingStandProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.CampfireProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.ChiseledBookshelfProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.CommandBlockProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.FurnaceProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.HopperLockProvider; -+import org.leavesmc.leaves.protocol.jade.provider.ItemStorageProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.JukeboxProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.LecternProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.MobSpawnerCooldownProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.ObjectNameProvider; -+import org.leavesmc.leaves.protocol.jade.provider.block.RedstoneProvider; -+import org.leavesmc.leaves.protocol.jade.provider.entity.AnimalOwnerProvider; -+import org.leavesmc.leaves.protocol.jade.provider.entity.MobBreedingProvider; -+import org.leavesmc.leaves.protocol.jade.provider.entity.MobGrowthProvider; -+import org.leavesmc.leaves.protocol.jade.provider.entity.NextEntityDropProvider; -+import org.leavesmc.leaves.protocol.jade.provider.entity.StatusEffectsProvider; -+import org.leavesmc.leaves.protocol.jade.provider.entity.ZombieVillagerProvider; -+import org.leavesmc.leaves.protocol.jade.util.HierarchyLookup; -+import org.leavesmc.leaves.protocol.jade.util.LootTableMineableCollector; -+import org.leavesmc.leaves.protocol.jade.util.PairHierarchyLookup; -+import org.leavesmc.leaves.protocol.jade.util.PriorityStore; -+import org.leavesmc.leaves.protocol.jade.util.WrappedHierarchyLookup; -+ -+import java.util.Collections; -+import java.util.List; -+ -+@LeavesProtocol(namespace = "jade") -+public class JadeProtocol { -+ -+ public static PriorityStore priorities; -+ private static List shearableBlocks = null; -+ -+ public static final String PROTOCOL_ID = "jade"; -+ -+ public static final HierarchyLookup> entityDataProviders = new HierarchyLookup<>(Entity.class); -+ public static final PairHierarchyLookup> blockDataProviders = new PairHierarchyLookup<>(new HierarchyLookup<>(Block.class), new HierarchyLookup<>(BlockEntity.class)); -+ public static final WrappedHierarchyLookup> itemStorageProviders = WrappedHierarchyLookup.forAccessor(); -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation id(String path) { -+ return new ResourceLocation(PROTOCOL_ID, path); -+ } -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation mc_id(String path) { -+ return ResourceLocation.withDefaultNamespace(path); -+ } -+ -+ @ProtocolHandler.Init -+ public static void init() { -+ priorities = new PriorityStore<>(IJadeProvider::getDefaultPriority, IJadeProvider::getUid); -+ -+ // core plugin -+ blockDataProviders.register(BlockEntity.class, ObjectNameProvider.ForBlock.INSTANCE); -+ -+ // universal plugin -+ entityDataProviders.register(Entity.class, ItemStorageProvider.getEntity()); -+ blockDataProviders.register(Block.class, ItemStorageProvider.getBlock()); -+ -+ itemStorageProviders.register(Object.class, ItemStorageExtensionProvider.INSTANCE); -+ itemStorageProviders.register(Block.class, ItemStorageExtensionProvider.INSTANCE); -+ -+ // vanilla plugin -+ entityDataProviders.register(Entity.class, AnimalOwnerProvider.INSTANCE); -+ entityDataProviders.register(LivingEntity.class, StatusEffectsProvider.INSTANCE); -+ entityDataProviders.register(AgeableMob.class, MobGrowthProvider.INSTANCE); -+ entityDataProviders.register(Tadpole.class, MobGrowthProvider.INSTANCE); -+ entityDataProviders.register(Animal.class, MobBreedingProvider.INSTANCE); -+ entityDataProviders.register(Allay.class, MobBreedingProvider.INSTANCE); -+ -+ entityDataProviders.register(Chicken.class, NextEntityDropProvider.INSTANCE); -+ entityDataProviders.register(Armadillo.class, NextEntityDropProvider.INSTANCE); -+ -+ entityDataProviders.register(ZombieVillager.class, ZombieVillagerProvider.INSTANCE); -+ -+ blockDataProviders.register(BrewingStandBlockEntity.class, BrewingStandProvider.INSTANCE); -+ blockDataProviders.register(BeehiveBlockEntity.class, BeehiveProvider.INSTANCE); -+ blockDataProviders.register(CommandBlockEntity.class, CommandBlockProvider.INSTANCE); -+ blockDataProviders.register(JukeboxBlockEntity.class, JukeboxProvider.INSTANCE); -+ blockDataProviders.register(LecternBlockEntity.class, LecternProvider.INSTANCE); -+ -+ blockDataProviders.register(ComparatorBlockEntity.class, RedstoneProvider.INSTANCE); -+ blockDataProviders.register(HopperBlockEntity.class, HopperLockProvider.INSTANCE); -+ blockDataProviders.register(CalibratedSculkSensorBlockEntity.class, RedstoneProvider.INSTANCE); -+ -+ blockDataProviders.register(AbstractFurnaceBlockEntity.class, FurnaceProvider.INSTANCE); -+ blockDataProviders.register(ChiseledBookShelfBlockEntity.class, ChiseledBookshelfProvider.INSTANCE); -+ blockDataProviders.register(TrialSpawnerBlockEntity.class, MobSpawnerCooldownProvider.INSTANCE); -+ -+ blockDataProviders.idMapped(); -+ entityDataProviders.idMapped(); -+ itemStorageProviders.register(CampfireBlock.class, CampfireProvider.INSTANCE); -+ -+ blockDataProviders.loadComplete(priorities); -+ entityDataProviders.loadComplete(priorities); -+ itemStorageProviders.loadComplete(priorities); -+ -+ try { -+ shearableBlocks = Collections.unmodifiableList(LootTableMineableCollector.execute( -+ MinecraftServer.getServer().reloadableRegistries().lookup().lookupOrThrow(Registries.LOOT_TABLE), -+ Items.SHEARS.getDefaultInstance() -+ )); -+ } catch (Throwable ignore) { -+ shearableBlocks = List.of(); -+ LeavesLogger.LOGGER.severe("Failed to collect shearable blocks"); -+ } -+ } -+ -+ @ProtocolHandler.PlayerJoin -+ public static void onPlayerJoin(ServerPlayer player) { -+ if (!LeavesConfig.protocol.jadeProtocol) { -+ return; -+ } -+ -+ sendPingPacket(player); -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = RequestEntityPayload.class, payloadId = "request_entity") -+ public static void requestEntityData(ServerPlayer player, RequestEntityPayload payload) { -+ if (!LeavesConfig.protocol.jadeProtocol) { -+ return; -+ } -+ -+ MinecraftServer.getServer().execute(() -> { -+ EntityAccessor accessor = payload.data().unpack(player); -+ if (accessor == null) { -+ return; -+ } -+ -+ Entity entity = accessor.getEntity(); -+ double maxDistance = Mth.square(player.entityInteractionRange() + 21); -+ if (entity == null || player.distanceToSqr(entity) > maxDistance) { -+ return; -+ } -+ -+ List> providers = entityDataProviders.get(entity); -+ if (providers.isEmpty()) { -+ return; -+ } -+ -+ CompoundTag tag = new CompoundTag(); -+ for (IServerDataProvider provider : providers) { -+ if (!payload.dataProviders().contains(provider)) { -+ continue; -+ } -+ try { -+ provider.appendServerData(tag, accessor); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.warning("Error while saving data for entity " + entity); -+ } -+ } -+ tag.putInt("EntityId", entity.getId()); -+ -+ ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag)); -+ }); -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = RequestBlockPayload.class, payloadId = "request_block") -+ public static void requestBlockData(ServerPlayer player, RequestBlockPayload payload) { -+ if (!LeavesConfig.protocol.jadeProtocol) { -+ return; -+ } -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ server.execute(() -> { -+ BlockAccessor accessor = payload.data().unpack(player); -+ if (accessor == null) { -+ return; -+ } -+ -+ BlockPos pos = accessor.getPosition(); -+ Block block = accessor.getBlock(); -+ BlockEntity blockEntity = accessor.getBlockEntity(); -+ double maxDistance = Mth.square(player.blockInteractionRange() + 21); -+ if (pos.distSqr(player.blockPosition()) > maxDistance || !accessor.getLevel().isLoaded(pos)) { -+ return; -+ } -+ -+ List> providers; -+ if (blockEntity != null) { -+ providers = blockDataProviders.getMerged(block, blockEntity); -+ } else { -+ providers = blockDataProviders.first.get(block); -+ } -+ -+ if (providers.isEmpty()) { -+ return; -+ } -+ -+ CompoundTag tag = new CompoundTag(); -+ for (IServerDataProvider provider : providers) { -+ if (!payload.dataProviders().contains(provider)) { -+ continue; -+ } -+ try { -+ provider.appendServerData(tag, accessor); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.warning("Error while saving data for block " + accessor.getBlockState()); -+ } -+ } -+ tag.putInt("x", pos.getX()); -+ tag.putInt("y", pos.getY()); -+ tag.putInt("z", pos.getZ()); -+ tag.putString("BlockId", BuiltInRegistries.BLOCK.getKey(block).toString()); -+ -+ ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag)); -+ }); -+ } -+ -+ @ProtocolHandler.ReloadServer -+ public static void onServerReload() { -+ if (LeavesConfig.protocol.jadeProtocol) { -+ enableAllPlayer(); -+ } -+ } -+ -+ public static void enableAllPlayer() { -+ for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) { -+ sendPingPacket(player); -+ } -+ } -+ -+ public static void sendPingPacket(ServerPlayer player) { -+ ProtocolUtils.sendPayloadPacket(player, new ServerPingPayload(Collections.emptyMap(), shearableBlocks, blockDataProviders.mappedIds(), entityDataProviders.mappedIds())); -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1a637045d9375ae357ca1e592ec0dc7b45fa47fa ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java -@@ -0,0 +1,32 @@ -+package org.leavesmc.leaves.protocol.jade.accessor; -+ -+import net.minecraft.nbt.Tag; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.StreamEncoder; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.phys.HitResult; -+import org.jetbrains.annotations.Nullable; -+ -+public interface Accessor { -+ -+ Level getLevel(); -+ -+ Player getPlayer(); -+ -+ Tag encodeAsNbt(StreamEncoder codec, D value); -+ -+ T getHitResult(); -+ -+ /** -+ * @return {@code true} if the dedicated server has Jade installed. -+ */ -+ boolean isServerConnected(); -+ -+ boolean showDetails(); -+ -+ @Nullable -+ Object getTarget(); -+ -+ float tickRate(); -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a04fbb45b466d8999b40717d8d48f650b81fe82a ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java -@@ -0,0 +1,83 @@ -+package org.leavesmc.leaves.protocol.jade.accessor; -+ -+import java.util.function.Supplier; -+ -+import org.apache.commons.lang3.ArrayUtils; -+ -+import io.netty.buffer.Unpooled; -+import net.minecraft.nbt.ByteArrayTag; -+import net.minecraft.nbt.Tag; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.StreamEncoder; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.phys.HitResult; -+ -+public abstract class AccessorImpl implements Accessor { -+ -+ private final Level level; -+ private final Player player; -+ private final Supplier hit; -+ private final boolean serverConnected; -+ private final boolean showDetails; -+ protected boolean verify; -+ private RegistryFriendlyByteBuf buffer; -+ -+ public AccessorImpl(Level level, Player player, Supplier hit, boolean serverConnected, boolean showDetails) { -+ this.level = level; -+ this.player = player; -+ this.hit = hit; -+ this.serverConnected = serverConnected; -+ this.showDetails = showDetails; -+ } -+ -+ @Override -+ public Level getLevel() { -+ return level; -+ } -+ -+ @Override -+ public Player getPlayer() { -+ return player; -+ } -+ -+ private RegistryFriendlyByteBuf buffer() { -+ if (buffer == null) { -+ buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), level.registryAccess()); -+ } -+ buffer.clear(); -+ return buffer; -+ } -+ -+ @Override -+ public Tag encodeAsNbt(StreamEncoder streamCodec, D value) { -+ RegistryFriendlyByteBuf buffer = buffer(); -+ streamCodec.encode(buffer, value); -+ ByteArrayTag tag = new ByteArrayTag(ArrayUtils.subarray(buffer.array(), 0, buffer.readableBytes())); -+ buffer.clear(); -+ return tag; -+ } -+ -+ @Override -+ public T getHitResult() { -+ return hit.get(); -+ } -+ -+ /** -+ * Returns true if dedicated server has Jade installed. -+ */ -+ @Override -+ public boolean isServerConnected() { -+ return serverConnected; -+ } -+ -+ @Override -+ public boolean showDetails() { -+ return showDetails; -+ } -+ -+ @Override -+ public float tickRate() { -+ return getLevel().tickRateManager().tickrate(); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java -new file mode 100644 -index 0000000000000000000000000000000000000000..12d689ca80887dcd5dbf68ea2c38a8adcc5ddee4 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java -@@ -0,0 +1,50 @@ -+package org.leavesmc.leaves.protocol.jade.accessor; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.world.entity.player.Player; -+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.state.BlockState; -+import net.minecraft.world.phys.BlockHitResult; -+import org.jetbrains.annotations.ApiStatus; -+ -+import java.util.function.Supplier; -+ -+public interface BlockAccessor extends Accessor { -+ -+ Block getBlock(); -+ -+ BlockState getBlockState(); -+ -+ BlockEntity getBlockEntity(); -+ -+ BlockPos getPosition(); -+ -+ Direction getSide(); -+ -+ @ApiStatus.NonExtendable -+ interface Builder { -+ Builder level(Level level); -+ -+ Builder player(Player player); -+ -+ Builder showDetails(boolean showDetails); -+ -+ Builder hit(BlockHitResult hit); -+ -+ Builder blockState(BlockState state); -+ -+ default Builder blockEntity(BlockEntity blockEntity) { -+ return blockEntity(() -> blockEntity); -+ } -+ -+ Builder blockEntity(Supplier blockEntity); -+ -+ Builder from(BlockAccessor accessor); -+ -+ BlockAccessor build(); -+ } -+ -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9e4a321b91a8afc480ef506487608255db2b9c89 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java -@@ -0,0 +1,163 @@ -+package org.leavesmc.leaves.protocol.jade.accessor; -+ -+import java.util.function.Supplier; -+ -+import org.jetbrains.annotations.Nullable; -+ -+import com.google.common.base.Suppliers; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.entity.player.Player; -+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.Blocks; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.phys.BlockHitResult; -+ -+/** -+ * Class to get information of block target and context. -+ */ -+public class BlockAccessorImpl extends AccessorImpl implements BlockAccessor { -+ -+ private final BlockState blockState; -+ @Nullable -+ private final Supplier blockEntity; -+ -+ private BlockAccessorImpl(Builder builder) { -+ super(builder.level, builder.player, Suppliers.ofInstance(builder.hit), builder.connected, builder.showDetails); -+ blockState = builder.blockState; -+ blockEntity = builder.blockEntity; -+ } -+ -+ @Override -+ public Block getBlock() { -+ return getBlockState().getBlock(); -+ } -+ -+ @Override -+ public BlockState getBlockState() { -+ return blockState; -+ } -+ -+ @Override -+ public BlockEntity getBlockEntity() { -+ return blockEntity == null ? null : blockEntity.get(); -+ } -+ -+ @Override -+ public BlockPos getPosition() { -+ return getHitResult().getBlockPos(); -+ } -+ -+ @Override -+ public Direction getSide() { -+ return getHitResult().getDirection(); -+ } -+ -+ @Nullable -+ @Override -+ public Object getTarget() { -+ return getBlockEntity(); -+ } -+ -+ public static class Builder implements BlockAccessor.Builder { -+ -+ private Level level; -+ private Player player; -+ private boolean connected; -+ private boolean showDetails; -+ private BlockHitResult hit; -+ private BlockState blockState = Blocks.AIR.defaultBlockState(); -+ private Supplier blockEntity; -+ -+ @Override -+ public Builder level(Level level) { -+ this.level = level; -+ return this; -+ } -+ -+ @Override -+ public Builder player(Player player) { -+ this.player = player; -+ return this; -+ } -+ -+ @Override -+ public Builder showDetails(boolean showDetails) { -+ this.showDetails = showDetails; -+ return this; -+ } -+ -+ @Override -+ public Builder hit(BlockHitResult hit) { -+ this.hit = hit; -+ return this; -+ } -+ -+ @Override -+ public Builder blockState(BlockState blockState) { -+ this.blockState = blockState; -+ return this; -+ } -+ -+ @Override -+ public Builder blockEntity(Supplier blockEntity) { -+ this.blockEntity = blockEntity; -+ return this; -+ } -+ -+ @Override -+ public Builder from(BlockAccessor accessor) { -+ level = accessor.getLevel(); -+ player = accessor.getPlayer(); -+ connected = accessor.isServerConnected(); -+ showDetails = accessor.showDetails(); -+ hit = accessor.getHitResult(); -+ blockEntity = accessor::getBlockEntity; -+ blockState = accessor.getBlockState(); -+ return this; -+ } -+ -+ @Override -+ public BlockAccessor build() { -+ return new BlockAccessorImpl(this); -+ } -+ } -+ -+ public record SyncData(boolean showDetails, BlockHitResult hit, BlockState blockState, ItemStack fakeBlock) { -+ public static final StreamCodec STREAM_CODEC = StreamCodec.composite( -+ ByteBufCodecs.BOOL, -+ SyncData::showDetails, -+ StreamCodec.of(FriendlyByteBuf::writeBlockHitResult, FriendlyByteBuf::readBlockHitResult), -+ SyncData::hit, -+ ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY), -+ SyncData::blockState, -+ ItemStack.OPTIONAL_STREAM_CODEC, -+ SyncData::fakeBlock, -+ SyncData::new -+ ); -+ -+ public BlockAccessor unpack(ServerPlayer player) { -+ Supplier blockEntity = null; -+ if (blockState.hasBlockEntity()) { -+ blockEntity = Suppliers.memoize(() -> player.level().getBlockEntity(hit.getBlockPos())); -+ } -+ return new Builder() -+ .level(player.level()) -+ .player(player) -+ .showDetails(showDetails) -+ .hit(hit) -+ .blockState(blockState) -+ .blockEntity(blockEntity) -+ .build(); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java -new file mode 100644 -index 0000000000000000000000000000000000000000..454360d5e5c01cad3c197b078d536a9f34e2c6a2 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java -@@ -0,0 +1,44 @@ -+package org.leavesmc.leaves.protocol.jade.accessor; -+ -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.phys.EntityHitResult; -+import org.jetbrains.annotations.ApiStatus; -+ -+import java.util.function.Supplier; -+ -+public interface EntityAccessor extends Accessor { -+ -+ Entity getEntity(); -+ -+ /** -+ * For part entity like ender dragon's, getEntity() will return the parent entity. -+ */ -+ Entity getRawEntity(); -+ -+ @ApiStatus.NonExtendable -+ interface Builder { -+ Builder level(Level level); -+ -+ Builder player(Player player); -+ -+ Builder showDetails(boolean showDetails); -+ -+ default Builder hit(EntityHitResult hit) { -+ return hit(() -> hit); -+ } -+ -+ Builder hit(Supplier hit); -+ -+ default Builder entity(Entity entity) { -+ return entity(() -> entity); -+ } -+ -+ Builder entity(Supplier entity); -+ -+ Builder from(EntityAccessor accessor); -+ -+ EntityAccessor build(); -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java -new file mode 100644 -index 0000000000000000000000000000000000000000..65d16c0024372ede4cec230b7aad54de28de15f2 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java -@@ -0,0 +1,123 @@ -+package org.leavesmc.leaves.protocol.jade.accessor; -+ -+import com.google.common.base.Suppliers; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.phys.EntityHitResult; -+import net.minecraft.world.phys.Vec3; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.util.CommonUtil; -+ -+import java.util.function.Supplier; -+ -+public class EntityAccessorImpl extends AccessorImpl implements EntityAccessor { -+ -+ private final Supplier entity; -+ -+ public EntityAccessorImpl(Builder builder) { -+ super(builder.level, builder.player, builder.hit, builder.connected, builder.showDetails); -+ entity = builder.entity; -+ } -+ -+ @Override -+ public Entity getEntity() { -+ return CommonUtil.wrapPartEntityParent(getRawEntity()); -+ } -+ -+ @Override -+ public Entity getRawEntity() { -+ return entity.get(); -+ } -+ -+ @NotNull -+ @Override -+ public Object getTarget() { -+ return getEntity(); -+ } -+ -+ public static class Builder implements EntityAccessor.Builder { -+ -+ public boolean showDetails; -+ private Level level; -+ private Player player; -+ private boolean connected; -+ private Supplier hit; -+ private Supplier entity; -+ -+ @Override -+ public Builder level(Level level) { -+ this.level = level; -+ return this; -+ } -+ -+ @Override -+ public Builder player(Player player) { -+ this.player = player; -+ return this; -+ } -+ -+ @Override -+ public Builder showDetails(boolean showDetails) { -+ this.showDetails = showDetails; -+ return this; -+ } -+ -+ @Override -+ public Builder hit(Supplier hit) { -+ this.hit = hit; -+ return this; -+ } -+ -+ @Override -+ public Builder entity(Supplier entity) { -+ this.entity = entity; -+ return this; -+ } -+ -+ @Override -+ public Builder from(EntityAccessor accessor) { -+ level = accessor.getLevel(); -+ player = accessor.getPlayer(); -+ connected = accessor.isServerConnected(); -+ showDetails = accessor.showDetails(); -+ hit = accessor::getHitResult; -+ entity = accessor::getEntity; -+ return this; -+ } -+ -+ @Override -+ public EntityAccessor build() { -+ return new EntityAccessorImpl(this); -+ } -+ } -+ -+ public record SyncData(boolean showDetails, int id, int partIndex, Vec3 hitVec) { -+ public static final StreamCodec STREAM_CODEC = StreamCodec.composite( -+ ByteBufCodecs.BOOL, -+ SyncData::showDetails, -+ ByteBufCodecs.VAR_INT, -+ SyncData::id, -+ ByteBufCodecs.VAR_INT, -+ SyncData::partIndex, -+ ByteBufCodecs.VECTOR3F.map(Vec3::new, Vec3::toVector3f), -+ SyncData::hitVec, -+ SyncData::new -+ ); -+ -+ public EntityAccessor unpack(ServerPlayer player) { -+ Supplier entity = Suppliers.memoize(() -> CommonUtil.getPartEntity(player.level().getEntity(id), partIndex)); -+ return new EntityAccessorImpl.Builder() -+ .level(player.level()) -+ .player(player) -+ .showDetails(showDetails) -+ .entity(entity) -+ .hit(Suppliers.memoize(() -> new EntityHitResult(entity.get(), hitVec))) -+ .build(); -+ } -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java b/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1b474ea8c1075b3dbaa7cd27e5bd95aa904fbe97 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java -@@ -0,0 +1,28 @@ -+package org.leavesmc.leaves.protocol.jade.payload; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+ -+public record ReceiveDataPayload(CompoundTag tag) implements LeavesCustomPayload { -+ -+ private static final ResourceLocation PACKET_RECEIVE_DATA = JadeProtocol.id("receive_data"); -+ -+ @New -+ public ReceiveDataPayload(ResourceLocation id, FriendlyByteBuf buf) { -+ this(buf.readNbt()); -+ } -+ -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ buf.writeNbt(tag); -+ } -+ -+ @Override -+ public ResourceLocation id() { -+ return PACKET_RECEIVE_DATA; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java b/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java -new file mode 100644 -index 0000000000000000000000000000000000000000..480ec35f28c850bfbe4f787d080fd7bbd84ca24c ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java -@@ -0,0 +1,51 @@ -+package org.leavesmc.leaves.protocol.jade.payload; -+ -+import io.netty.buffer.ByteBuf; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessorImpl; -+import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; -+ -+import java.util.List; -+import java.util.Objects; -+ -+import static org.leavesmc.leaves.protocol.jade.JadeProtocol.blockDataProviders; -+ -+public record RequestBlockPayload(BlockAccessorImpl.SyncData data, List<@Nullable IServerDataProvider> dataProviders) implements LeavesCustomPayload { -+ -+ private static final ResourceLocation PACKET_REQUEST_BLOCK = JadeProtocol.id("request_block"); -+ private static final StreamCodec CODEC = StreamCodec.composite( -+ BlockAccessorImpl.SyncData.STREAM_CODEC, -+ RequestBlockPayload::data, -+ ByteBufCodecs.>list() -+ .apply(ByteBufCodecs.idMapper( -+ $ -> Objects.requireNonNull(blockDataProviders.idMapper()).byId($), -+ $ -> Objects.requireNonNull(blockDataProviders.idMapper()).getIdOrThrow($))), -+ RequestBlockPayload::dataProviders, -+ RequestBlockPayload::new); -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ CODEC.encode(new RegistryFriendlyByteBuf(buf, MinecraftServer.getServer().registryAccess()), this); -+ } -+ -+ @New -+ public static RequestBlockPayload create(ResourceLocation location, FriendlyByteBuf buf) { -+ return CODEC.decode(new RegistryFriendlyByteBuf(buf, MinecraftServer.getServer().registryAccess())); -+ } -+ -+ @Override -+ @NotNull -+ public ResourceLocation id() { -+ return PACKET_REQUEST_BLOCK; -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java b/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java -new file mode 100644 -index 0000000000000000000000000000000000000000..395138d427413e562a5a3df80b33b53cffacc637 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java -@@ -0,0 +1,53 @@ -+package org.leavesmc.leaves.protocol.jade.payload; -+ -+import io.netty.buffer.ByteBuf; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessorImpl; -+import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; -+ -+import java.util.List; -+import java.util.Objects; -+ -+import static org.leavesmc.leaves.protocol.jade.JadeProtocol.entityDataProviders; -+ -+public record RequestEntityPayload(EntityAccessorImpl.SyncData data, List<@Nullable IServerDataProvider> dataProviders) implements LeavesCustomPayload { -+ -+ private static final ResourceLocation PACKET_REQUEST_ENTITY = JadeProtocol.id("request_entity"); -+ private static final StreamCodec CODEC = StreamCodec.composite( -+ EntityAccessorImpl.SyncData.STREAM_CODEC, -+ RequestEntityPayload::data, -+ ByteBufCodecs.>list() -+ .apply(ByteBufCodecs.idMapper( -+ $ -> Objects.requireNonNull(entityDataProviders.idMapper()).byId($), -+ $ -> Objects.requireNonNull(entityDataProviders.idMapper()).getIdOrThrow($) -+ )), -+ RequestEntityPayload::dataProviders, -+ RequestEntityPayload::new); -+ -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ CODEC.encode(new RegistryFriendlyByteBuf(buf, MinecraftServer.getServer().registryAccess()), this); -+ } -+ -+ @New -+ public static RequestEntityPayload create(ResourceLocation location, FriendlyByteBuf buf) { -+ return CODEC.decode(new RegistryFriendlyByteBuf(buf, MinecraftServer.getServer().registryAccess())); -+ } -+ -+ @Override -+ @NotNull -+ public ResourceLocation id() { -+ return PACKET_REQUEST_ENTITY; -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerPingPayload.java b/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerPingPayload.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f4419962a7fedaea05140bbf6eaa01cc94c05049 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerPingPayload.java -@@ -0,0 +1,49 @@ -+package org.leavesmc.leaves.protocol.jade.payload; -+ -+import com.google.common.collect.Maps; -+import io.netty.buffer.ByteBuf; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.world.level.block.Block; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+ -+import java.util.List; -+import java.util.Map; -+ -+import static org.leavesmc.leaves.protocol.jade.util.JadeCodec.PRIMITIVE_STREAM_CODEC; -+ -+public record ServerPingPayload( -+ Map serverConfig, -+ List shearableBlocks, -+ List blockProviderIds, -+ List entityProviderIds) implements LeavesCustomPayload { -+ -+ private static final ResourceLocation PACKET_SERVER_HANDSHAKE = JadeProtocol.id("server_ping_v1"); -+ private static final StreamCodec CODEC = StreamCodec.composite( -+ ByteBufCodecs.map(Maps::newHashMapWithExpectedSize, ResourceLocation.STREAM_CODEC, PRIMITIVE_STREAM_CODEC), -+ ServerPingPayload::serverConfig, -+ ByteBufCodecs.registry(Registries.BLOCK).apply(ByteBufCodecs.list()), -+ ServerPingPayload::shearableBlocks, -+ ByteBufCodecs.list().apply(ResourceLocation.STREAM_CODEC), -+ ServerPingPayload::blockProviderIds, -+ ByteBufCodecs.list().apply(ResourceLocation.STREAM_CODEC), -+ ServerPingPayload::entityProviderIds, -+ ServerPingPayload::new); -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ CODEC.encode(new RegistryFriendlyByteBuf(buf, MinecraftServer.getServer().registryAccess()), this); -+ } -+ -+ @Override -+ public ResourceLocation id() { -+ return PACKET_SERVER_HANDSHAKE; -+ } -+} -+ -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d62fc8f96fcdee7dbb0204d2460ff6fee4074e1a ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java -@@ -0,0 +1,12 @@ -+package org.leavesmc.leaves.protocol.jade.provider; -+ -+import net.minecraft.resources.ResourceLocation; -+ -+public interface IJadeProvider { -+ -+ ResourceLocation getUid(); -+ -+ default int getDefaultPriority() { -+ return 0; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7d839f17601ca4d2b8717222989cf566a0eb6524 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java -@@ -0,0 +1,8 @@ -+package org.leavesmc.leaves.protocol.jade.provider; -+ -+import net.minecraft.nbt.CompoundTag; -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+ -+public interface IServerDataProvider> extends IJadeProvider { -+ void appendServerData(CompoundTag data, T accessor); -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6e32eed15f028020223e2500849b4db3892f68c3 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java -@@ -0,0 +1,10 @@ -+package org.leavesmc.leaves.protocol.jade.provider; -+ -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+import org.leavesmc.leaves.protocol.jade.util.ViewGroup; -+ -+import java.util.List; -+ -+public interface IServerExtensionProvider extends IJadeProvider { -+ List> getGroups(Accessor request); -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3efa3ceb9654bc53adb61ded96c0ad08eb9b083d ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java -@@ -0,0 +1,138 @@ -+package org.leavesmc.leaves.protocol.jade.provider; -+ -+import com.google.common.cache.Cache; -+import com.google.common.cache.CacheBuilder; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.Container; -+import net.minecraft.world.LockCode; -+import net.minecraft.world.RandomizableContainer; -+import net.minecraft.world.WorldlyContainerHolder; -+import net.minecraft.world.entity.animal.horse.AbstractHorse; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.entity.vehicle.ContainerEntity; -+import net.minecraft.world.inventory.PlayerEnderChestContainer; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.block.ChestBlock; -+import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; -+import net.minecraft.world.level.block.entity.ChestBlockEntity; -+import net.minecraft.world.level.block.entity.EnderChestBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.util.ItemCollector; -+import org.leavesmc.leaves.protocol.jade.util.ItemIterator; -+import org.leavesmc.leaves.protocol.jade.util.ViewGroup; -+ -+import java.util.List; -+import java.util.concurrent.ExecutionException; -+import java.util.concurrent.TimeUnit; -+ -+public enum ItemStorageExtensionProvider implements IServerExtensionProvider { -+ INSTANCE; -+ -+ public static final Cache> targetCache = CacheBuilder.newBuilder().weakKeys().expireAfterAccess(60, TimeUnit.SECONDS).build(); -+ -+ private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage.default"); -+ -+ @Override -+ public List> getGroups(Accessor request) { -+ Object target = request.getTarget(); -+ -+ switch (target) { -+ case null -> { -+ return createItemCollector(request).update(request); -+ } -+ case RandomizableContainer te when te.getLootTable() != null -> { -+ return List.of(); -+ } -+ case ContainerEntity containerEntity when containerEntity.getContainerLootTable() != null -> { -+ return List.of(); -+ } -+ default -> { -+ } -+ } -+ -+ Player player = request.getPlayer(); -+ if (!player.isCreative() && !player.isSpectator() && target instanceof BaseContainerBlockEntity te) { -+ if (te.lockKey != LockCode.NO_LOCK) { -+ return List.of(); -+ } -+ } -+ -+ if (target instanceof EnderChestBlockEntity) { -+ PlayerEnderChestContainer inventory = player.getEnderChestInventory(); -+ return new ItemCollector<>(new ItemIterator.ContainerItemIterator(x -> inventory, 0)).update(request); -+ } -+ -+ ItemCollector itemCollector; -+ try { -+ itemCollector = targetCache.get(target, () -> createItemCollector(request)); -+ } catch (ExecutionException e) { -+ LeavesLogger.LOGGER.severe("Failed to get item collector for " + target); -+ return null; -+ } -+ -+ return itemCollector.update(request); -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return UNIVERSAL_ITEM_STORAGE; -+ } -+ -+ public static ItemCollector createItemCollector(Accessor request) { -+ if (request.getTarget() instanceof AbstractHorse) { -+ return new ItemCollector<>(new ItemIterator.ContainerItemIterator(o -> { -+ if (o instanceof AbstractHorse horse) { -+ return horse.inventory; -+ } -+ return null; -+ }, 2)); -+ } -+ -+ // TODO BlockEntity like fabric's ItemStorage -+ -+ final Container container = findContainer(request); -+ if (container != null) { -+ if (container instanceof ChestBlockEntity) { -+ return new ItemCollector<>(new ItemIterator.ContainerItemIterator(o -> { -+ if (o instanceof ChestBlockEntity blockEntity) { -+ if (blockEntity.getBlockState().getBlock() instanceof ChestBlock chestBlock) { -+ Container compound = null; -+ if (blockEntity.getLevel() != null) { -+ compound = ChestBlock.getContainer(chestBlock, blockEntity.getBlockState(), blockEntity.getLevel(), blockEntity.getBlockPos(), false); -+ } -+ if (compound != null) { -+ return compound; -+ } -+ } -+ return blockEntity; -+ } -+ return null; -+ }, 0)); -+ } -+ return new ItemCollector<>(new ItemIterator.ContainerItemIterator(0)); -+ } -+ -+ return ItemCollector.EMPTY; -+ } -+ -+ public static @Nullable Container findContainer(@NotNull Accessor accessor) { -+ Object target = accessor.getTarget(); -+ if (target == null && accessor instanceof BlockAccessor blockAccessor && -+ blockAccessor.getBlock() instanceof WorldlyContainerHolder holder) { -+ return holder.getContainer(blockAccessor.getBlockState(), accessor.getLevel(), blockAccessor.getPosition()); -+ } else if (target instanceof Container container) { -+ return container; -+ } -+ return null; -+ } -+ -+ @Override -+ public int getDefaultPriority() { -+ return IServerExtensionProvider.super.getDefaultPriority() + 1000; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8289b5c48213348a7346dfdbb30ee1b8787d2a2d ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java -@@ -0,0 +1,88 @@ -+package org.leavesmc.leaves.protocol.jade.provider; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.LockCode; -+import net.minecraft.world.RandomizableContainer; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; -+import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.util.CommonUtil; -+import org.leavesmc.leaves.protocol.jade.util.ItemCollector; -+import org.leavesmc.leaves.protocol.jade.util.ViewGroup; -+ -+import java.util.List; -+import java.util.Map; -+ -+public abstract class ItemStorageProvider> implements IServerDataProvider { -+ -+ private static final StreamCodec>>> STREAM_CODEC = ViewGroup.listCodec( -+ ItemStack.OPTIONAL_STREAM_CODEC); -+ -+ private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage"); -+ -+ public static ForBlock getBlock() { -+ return ForBlock.INSTANCE; -+ } -+ -+ public static ForEntity getEntity() { -+ return ForEntity.INSTANCE; -+ } -+ -+ public static class ForBlock extends ItemStorageProvider { -+ private static final ForBlock INSTANCE = new ForBlock(); -+ } -+ -+ public static class ForEntity extends ItemStorageProvider { -+ private static final ForEntity INSTANCE = new ForEntity(); -+ } -+ -+ public static void putData(CompoundTag tag, @NotNull Accessor accessor) { -+ Object target = accessor.getTarget(); -+ Player player = accessor.getPlayer(); -+ Map.Entry>> entry = CommonUtil.getServerExtensionData(accessor, JadeProtocol.itemStorageProviders); -+ if (entry != null) { -+ List> groups = entry.getValue(); -+ for (ViewGroup group : groups) { -+ if (group.views.size() > ItemCollector.MAX_SIZE) { -+ group.views = group.views.subList(0, ItemCollector.MAX_SIZE); -+ } -+ } -+ tag.put(UNIVERSAL_ITEM_STORAGE.toString(), accessor.encodeAsNbt(STREAM_CODEC, entry)); -+ return; -+ } -+ if (target instanceof RandomizableContainer containerEntity && containerEntity.getLootTable() != null) { -+ tag.putBoolean("Loot", true); -+ } else if (!player.isCreative() && !player.isSpectator() && target instanceof BaseContainerBlockEntity te) { -+ if (te.lockKey != LockCode.NO_LOCK) { -+ tag.putBoolean("Locked", true); -+ } -+ } -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return UNIVERSAL_ITEM_STORAGE; -+ } -+ -+ @Override -+ public void appendServerData(CompoundTag tag, @NotNull T accessor) { -+ if (accessor.getTarget() instanceof AbstractFurnaceBlockEntity) { -+ return; -+ } -+ putData(tag, accessor); -+ } -+ -+ @Override -+ public int getDefaultPriority() { -+ return 9999; -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..52887edb3359c5eb1900cd1eec912e52afef2c9f ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java -@@ -0,0 +1,26 @@ -+package org.leavesmc.leaves.protocol.jade.provider; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.Tag; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.StreamCodec; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+ -+import java.util.Optional; -+ -+public interface StreamServerDataProvider, D> extends IServerDataProvider { -+ -+ @Override -+ default void appendServerData(CompoundTag data, T accessor) { -+ D value = streamData(accessor); -+ if (value != null) { -+ data.put(getUid().toString(), accessor.encodeAsNbt(streamCodec(), value)); -+ } -+ } -+ -+ @Nullable -+ D streamData(T accessor); -+ -+ StreamCodec streamCodec(); -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ee92d79bf4d328c95c51178a2ad43beb0a54cd29 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java -@@ -0,0 +1,34 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.level.block.entity.BeehiveBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum BeehiveProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_BEEHIVE = JadeProtocol.mc_id("beehive"); -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return ByteBufCodecs.BYTE.cast(); -+ } -+ -+ @Override -+ public Byte streamData(@NotNull BlockAccessor accessor) { -+ BeehiveBlockEntity beehive = (BeehiveBlockEntity) accessor.getBlockEntity(); -+ int bees = beehive.getOccupantCount(); -+ return (byte) (beehive.isFull() ? bees : -bees); -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_BEEHIVE; -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e6f15f87819d4a7c4d4383db735d8318ea30a03c ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java -@@ -0,0 +1,43 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import io.netty.buffer.ByteBuf; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum BrewingStandProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_BREWING_STAND = JadeProtocol.mc_id("brewing_stand"); -+ -+ @Override -+ public @NotNull Data streamData(@NotNull BlockAccessor accessor) { -+ BrewingStandBlockEntity brewingStand = (BrewingStandBlockEntity) accessor.getBlockEntity(); -+ return new Data(brewingStand.fuel, brewingStand.brewTime); -+ } -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return Data.STREAM_CODEC.cast(); -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_BREWING_STAND; -+ } -+ -+ public record Data(int fuel, int time) { -+ public static final StreamCodec STREAM_CODEC = StreamCodec.composite( -+ ByteBufCodecs.VAR_INT, -+ Data::fuel, -+ ByteBufCodecs.VAR_INT, -+ Data::time, -+ Data::new); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2deb3777a320d6a50168e06f234ba4c21da48e9a ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java -@@ -0,0 +1,55 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import com.google.common.collect.Lists; -+import com.mojang.serialization.Codec; -+import com.mojang.serialization.MapCodec; -+import net.minecraft.core.component.DataComponents; -+import net.minecraft.nbt.NbtOps; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.component.CustomData; -+import net.minecraft.world.level.block.entity.CampfireBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.jetbrains.annotations.Unmodifiable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; -+import org.leavesmc.leaves.protocol.jade.util.ViewGroup; -+ -+import java.util.List; -+ -+public enum CampfireProvider implements IServerExtensionProvider { -+ INSTANCE; -+ -+ private static final MapCodec COOKING_TIME_CODEC = Codec.INT.fieldOf("jade:cooking"); -+ private static final ResourceLocation MC_CAMPFIRE = JadeProtocol.mc_id("campfire"); -+ -+ @Override -+ public @Nullable @Unmodifiable List> getGroups(@NotNull Accessor request) { -+ if (request.getTarget() instanceof CampfireBlockEntity campfire) { -+ List list = Lists.newArrayList(); -+ for (int i = 0; i < campfire.cookingTime.length; i++) { -+ ItemStack stack = campfire.getItems().get(i); -+ if (stack.isEmpty()) { -+ continue; -+ } -+ stack = stack.copy(); -+ -+ CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) -+ .update(NbtOps.INSTANCE, COOKING_TIME_CODEC, campfire.cookingTime[i] - campfire.cookingProgress[i]) -+ .getOrThrow(); -+ stack.set(DataComponents.CUSTOM_DATA, customData); -+ -+ list.add(stack); -+ } -+ return List.of(new ViewGroup<>(list)); -+ } -+ return null; -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_CAMPFIRE; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..bde872cc5ebd9d79af307c8a4b38acd385cec11b ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java -@@ -0,0 +1,39 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.block.ChiseledBookShelfBlock; -+import net.minecraft.world.level.block.entity.ChiseledBookShelfBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum ChiseledBookshelfProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_CHISELED_BOOKSHELF = JadeProtocol.mc_id("chiseled_bookshelf"); -+ -+ @Override -+ public @Nullable ItemStack streamData(@NotNull BlockAccessor accessor) { -+ int slot = ((ChiseledBookShelfBlock) accessor.getBlock()).getHitSlot(accessor.getHitResult(), accessor.getBlockState()).orElse(-1); -+ if (slot == -1) { -+ return null; -+ } -+ return ((ChiseledBookShelfBlockEntity) accessor.getBlockEntity()).getItem(slot); -+ } -+ -+ @Override -+ public StreamCodec streamCodec() { -+ return ItemStack.OPTIONAL_STREAM_CODEC; -+ } -+ -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_CHISELED_BOOKSHELF; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..5f71fadae8fe95e3386e3ee5465eb33f850a37b0 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java -@@ -0,0 +1,40 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.level.block.entity.CommandBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum CommandBlockProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_COMMAND_BLOCK = JadeProtocol.mc_id("command_block"); -+ -+ @Nullable -+ public String streamData(@NotNull BlockAccessor accessor) { -+ if (!accessor.getPlayer().canUseGameMasterBlocks()) { -+ return null; -+ } -+ String command = ((CommandBlockEntity) accessor.getBlockEntity()).getCommandBlock().getCommand(); -+ if (command.length() > 40) { -+ command = command.substring(0, 37) + "..."; -+ } -+ return command; -+ } -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return ByteBufCodecs.STRING_UTF8.cast(); -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_COMMAND_BLOCK; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..090e6a350a5c19c0204ecf9a2c2c42e8d012cb3d ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java -@@ -0,0 +1,60 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+import java.util.List; -+ -+public enum FurnaceProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_FURNACE = JadeProtocol.mc_id("furnace"); -+ -+ @Override -+ public @Nullable Data streamData(@NotNull BlockAccessor accessor) { -+ if (!(accessor.getTarget() instanceof AbstractFurnaceBlockEntity furnace)) { -+ return null; -+ } -+ -+ if (furnace.isEmpty()) { -+ return null; -+ } -+ -+ CompoundTag furnaceTag = furnace.saveWithoutMetadata(accessor.getLevel().registryAccess()); -+ return new Data( -+ furnaceTag.getInt("CookTime"), -+ furnaceTag.getInt("CookTimeTotal"), -+ List.of(furnace.getItem(0), furnace.getItem(1), furnace.getItem(2))); -+ } -+ -+ @Override -+ public StreamCodec streamCodec() { -+ return Data.STREAM_CODEC; -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_FURNACE; -+ } -+ -+ public record Data(int progress, int total, List inventory) { -+ public static final StreamCodec STREAM_CODEC = StreamCodec.composite( -+ ByteBufCodecs.VAR_INT, -+ Data::progress, -+ ByteBufCodecs.VAR_INT, -+ Data::total, -+ ItemStack.OPTIONAL_LIST_STREAM_CODEC, -+ Data::inventory, -+ Data::new); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a3937081bd923d3b6f2ee966dc95aa235c3eb57c ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java -@@ -0,0 +1,32 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.level.block.state.properties.BlockStateProperties; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum HopperLockProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_HOPPER_LOCK = JadeProtocol.mc_id("hopper_lock"); -+ -+ @Override -+ public Boolean streamData(@NotNull BlockAccessor accessor) { -+ return !accessor.getBlockState().getValue(BlockStateProperties.ENABLED); -+ } -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return ByteBufCodecs.BOOL.cast(); -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_HOPPER_LOCK; -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0b6e224ebc8d6acdc29abf51f7d98b667baf0984 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java -@@ -0,0 +1,32 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.block.entity.JukeboxBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum JukeboxProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_JUKEBOX = JadeProtocol.mc_id("jukebox"); -+ -+ @Override -+ public @NotNull ItemStack streamData(BlockAccessor accessor) { -+ return ((JukeboxBlockEntity) accessor.getBlockEntity()).getTheItem(); -+ } -+ -+ @Override -+ public StreamCodec streamCodec() { -+ return ItemStack.OPTIONAL_STREAM_CODEC; -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_JUKEBOX; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c363bd616fa41eca3266ccb485432cfd90ad7473 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java -@@ -0,0 +1,33 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.block.entity.LecternBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum LecternProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_LECTERN = JadeProtocol.mc_id("lectern"); -+ -+ @Override -+ public @NotNull ItemStack streamData(@NotNull BlockAccessor accessor) { -+ return ((LecternBlockEntity) accessor.getBlockEntity()).getBook(); -+ } -+ -+ @Override -+ public StreamCodec streamCodec() { -+ return ItemStack.OPTIONAL_STREAM_CODEC; -+ } -+ -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_LECTERN; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a70f4a81166221ec1971b1fbf06e4c73efffcbe4 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java -@@ -0,0 +1,42 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity; -+import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum MobSpawnerCooldownProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_MOB_SPAWNER_COOLDOWN = JadeProtocol.mc_id("mob_spawner.cooldown"); -+ -+ @Override -+ public @Nullable Integer streamData(@NotNull BlockAccessor accessor) { -+ TrialSpawnerBlockEntity spawner = (TrialSpawnerBlockEntity) accessor.getBlockEntity(); -+ TrialSpawnerData spawnerData = spawner.getTrialSpawner().getData(); -+ ServerLevel level = ((ServerLevel) accessor.getLevel()); -+ if (spawner.getTrialSpawner().canSpawnInLevel(level) && level.getGameTime() < spawnerData.cooldownEndsAt) { -+ return (int) (spawnerData.cooldownEndsAt - level.getGameTime()); -+ } -+ return null; -+ } -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return ByteBufCodecs.VAR_INT.cast(); -+ } -+ -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_MOB_SPAWNER_COOLDOWN; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6a060c8a64f18a3bff232bcb43f20121b1da36b1 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java -@@ -0,0 +1,63 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.core.component.DataComponents; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.chat.Component; -+import net.minecraft.network.chat.ComponentSerialization; -+import net.minecraft.network.chat.contents.TranslatableContents; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.MenuProvider; -+import net.minecraft.world.Nameable; -+import net.minecraft.world.level.block.ChestBlock; -+import net.minecraft.world.level.block.entity.ChestBlockEntity; -+import net.minecraft.world.level.block.state.properties.ChestType; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public abstract class ObjectNameProvider implements StreamServerDataProvider { -+ -+ private static final ResourceLocation CORE_OBJECT_NAME = JadeProtocol.id("object_name"); -+ -+ public static class ForBlock extends ObjectNameProvider implements StreamServerDataProvider { -+ public static final ForBlock INSTANCE = new ForBlock(); -+ -+ @Override -+ @Nullable -+ public Component streamData(@NotNull BlockAccessor accessor) { -+ if (!(accessor.getBlockEntity() instanceof Nameable nameable)) { -+ return null; -+ } -+ if (nameable instanceof ChestBlockEntity && accessor.getBlock() instanceof ChestBlock && accessor.getBlockState().getValue(ChestBlock.TYPE) != ChestType.SINGLE) { -+ MenuProvider menuProvider = accessor.getBlockState().getMenuProvider(accessor.getLevel(), accessor.getPosition()); -+ if (menuProvider != null) { -+ Component name = menuProvider.getDisplayName(); -+ if (!(name.getContents() instanceof TranslatableContents contents) || !"container.chestDouble".equals(contents.getKey())) { -+ return name; -+ } -+ } -+ } else if (nameable.hasCustomName()) { -+ return nameable.getDisplayName(); -+ } -+ return accessor.getBlockEntity().components().get(DataComponents.ITEM_NAME); -+ } -+ -+ @Override -+ public StreamCodec streamCodec() { -+ return ComponentSerialization.STREAM_CODEC; -+ } -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return CORE_OBJECT_NAME; -+ } -+ -+ @Override -+ public int getDefaultPriority() { -+ return -10100; -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1cdcf21ed69744f96f47673100d8bf0114850f4f ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java -@@ -0,0 +1,36 @@ -+package org.leavesmc.leaves.protocol.jade.provider.block; -+ -+import net.minecraft.core.Direction; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.level.block.CalibratedSculkSensorBlock; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import net.minecraft.world.level.block.entity.CalibratedSculkSensorBlockEntity; -+import net.minecraft.world.level.block.entity.ComparatorBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; -+ -+public enum RedstoneProvider implements IServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_REDSTONE = JadeProtocol.mc_id("redstone"); -+ -+ @Override -+ public void appendServerData(CompoundTag data, @NotNull BlockAccessor accessor) { -+ BlockEntity blockEntity = accessor.getBlockEntity(); -+ if (blockEntity instanceof ComparatorBlockEntity comparator) { -+ data.putInt("Signal", comparator.getOutputSignal()); -+ } else if (blockEntity instanceof CalibratedSculkSensorBlockEntity) { -+ Direction direction = accessor.getBlockState().getValue(CalibratedSculkSensorBlock.FACING).getOpposite(); -+ int signal = accessor.getLevel().getSignal(accessor.getPosition().relative(direction), direction); -+ data.putInt("Signal", signal); -+ } -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_REDSTONE; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ff50ef60e3b37ab231161c870c432ef8e3018458 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java -@@ -0,0 +1,43 @@ -+package org.leavesmc.leaves.protocol.jade.provider.entity; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.OwnableEntity; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+import org.leavesmc.leaves.protocol.jade.util.CommonUtil; -+ -+import java.util.UUID; -+ -+public enum AnimalOwnerProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_ANIMAL_OWNER = JadeProtocol.mc_id("animal_owner"); -+ -+ @Override -+ public String streamData(@NotNull EntityAccessor accessor) { -+ return CommonUtil.getLastKnownUsername(getOwnerUUID(accessor.getEntity())); -+ } -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return ByteBufCodecs.STRING_UTF8.cast(); -+ } -+ -+ public static UUID getOwnerUUID(Entity entity) { -+ if (entity instanceof OwnableEntity ownableEntity) { -+ return ownableEntity.getOwnerUUID(); -+ } -+ return null; -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_ANIMAL_OWNER; -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0acba2f9700e4a65e764077b22e65e18d787be2a ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java -@@ -0,0 +1,44 @@ -+package org.leavesmc.leaves.protocol.jade.provider.entity; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.animal.Animal; -+import net.minecraft.world.entity.animal.allay.Allay; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum MobBreedingProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_MOB_BREEDING = JadeProtocol.mc_id("mob_breeding"); -+ -+ @Override -+ public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { -+ int time = 0; -+ Entity entity = accessor.getEntity(); -+ if (entity instanceof Allay allay) { -+ if (allay.duplicationCooldown > 0 && allay.duplicationCooldown < Integer.MAX_VALUE) { -+ time = (int) allay.duplicationCooldown; -+ } -+ } else { -+ time = ((Animal) entity).getAge(); -+ } -+ return time > 0 ? time : null; -+ } -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return ByteBufCodecs.VAR_INT.cast(); -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_MOB_BREEDING; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..44f5f4b8bf3b9fe66b2f8b93b36284c3ae5d1d87 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java -@@ -0,0 +1,43 @@ -+package org.leavesmc.leaves.protocol.jade.provider.entity; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.entity.AgeableMob; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.animal.frog.Tadpole; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum MobGrowthProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_MOB_GROWTH = JadeProtocol.mc_id("mob_growth"); -+ -+ @Override -+ public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { -+ int time = -1; -+ Entity entity = accessor.getEntity(); -+ if (entity instanceof AgeableMob ageable) { -+ time = -ageable.getAge(); -+ } else if (entity instanceof Tadpole tadpole) { -+ time = tadpole.getTicksLeftUntilAdult(); -+ } -+ return time > 0 ? time : null; -+ } -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return ByteBufCodecs.VAR_INT.cast(); -+ } -+ -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_MOB_GROWTH; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..892911a3d0087a5bf48b2df8326e3c5ce27835a0 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java -@@ -0,0 +1,35 @@ -+package org.leavesmc.leaves.protocol.jade.provider.entity; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.entity.animal.Chicken; -+import net.minecraft.world.entity.animal.armadillo.Armadillo; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; -+ -+public enum NextEntityDropProvider implements IServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_NEXT_ENTITY_DROP = JadeProtocol.mc_id("next_entity_drop"); -+ -+ @Override -+ public void appendServerData(CompoundTag tag, @NotNull EntityAccessor accessor) { -+ int max = 24000 * 2; -+ if (accessor.getEntity() instanceof Chicken chicken) { -+ if (!chicken.isBaby() && chicken.eggTime < max) { -+ tag.putInt("NextEggIn", chicken.eggTime); -+ } -+ } else if (accessor.getEntity() instanceof Armadillo armadillo) { -+ if (!armadillo.isBaby() && armadillo.scuteTime < max) { -+ tag.putInt("NextScuteIn", armadillo.scuteTime); -+ } -+ } -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_NEXT_ENTITY_DROP; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ffc571875f620fe53b2210583c246d6579c3df1f ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java -@@ -0,0 +1,45 @@ -+package org.leavesmc.leaves.protocol.jade.provider.entity; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.effect.MobEffectInstance; -+import net.minecraft.world.entity.LivingEntity; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+import java.util.List; -+ -+public enum StatusEffectsProvider implements StreamServerDataProvider> { -+ INSTANCE; -+ -+ -+ private static final StreamCodec> STREAM_CODEC = ByteBufCodecs.list() -+ .apply(MobEffectInstance.STREAM_CODEC); -+ private static final ResourceLocation MC_POTION_EFFECTS = JadeProtocol.mc_id("potion_effects"); -+ -+ @Override -+ @Nullable -+ public List streamData(@NotNull EntityAccessor accessor) { -+ List effects = ((LivingEntity) accessor.getEntity()).getActiveEffects() -+ .stream() -+ .filter(MobEffectInstance::isVisible) -+ .toList(); -+ return effects.isEmpty() ? null : effects; -+ } -+ -+ @Override -+ public StreamCodec> streamCodec() { -+ return STREAM_CODEC; -+ } -+ -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_POTION_EFFECTS; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b7c9afd29f3ddead6871c8f2b1f2b0815605cea5 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java -@@ -0,0 +1,34 @@ -+package org.leavesmc.leaves.protocol.jade.provider.entity; -+ -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.entity.monster.ZombieVillager; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; -+ -+public enum ZombieVillagerProvider implements StreamServerDataProvider { -+ INSTANCE; -+ -+ private static final ResourceLocation MC_ZOMBIE_VILLAGER = JadeProtocol.mc_id("zombie_villager"); -+ -+ @Override -+ public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { -+ int time = ((ZombieVillager) accessor.getEntity()).villagerConversionTime; -+ return time > 0 ? time : null; -+ } -+ -+ @Override -+ public @NotNull StreamCodec streamCodec() { -+ return ByteBufCodecs.VAR_INT.cast(); -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return MC_ZOMBIE_VILLAGER; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6a9cd9f0e71331c4fd3bef7bbeabebcc5d0e80cb ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java -@@ -0,0 +1,32 @@ -+package org.leavesmc.leaves.protocol.jade.tool; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.Items; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.state.BlockState; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+ -+import java.util.List; -+ -+public class ShearsToolHandler extends SimpleToolHandler { -+ -+ private static final ShearsToolHandler INSTANCE = new ShearsToolHandler(); -+ -+ public static ShearsToolHandler getInstance() { -+ return INSTANCE; -+ } -+ -+ public ShearsToolHandler() { -+ super(JadeProtocol.id("shears"), List.of(Items.SHEARS.getDefaultInstance()), true); -+ } -+ -+ @Override -+ public ItemStack test(BlockState state, Level world, BlockPos pos) { -+ if (state.is(Blocks.TRIPWIRE)) { -+ return tools.getFirst(); -+ } -+ return super.test(state, world, pos); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d45ecdb17a78d7e0c5eb280ee584960761ced1d2 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java -@@ -0,0 +1,71 @@ -+package org.leavesmc.leaves.protocol.jade.tool; -+ -+import com.google.common.base.Preconditions; -+import com.google.common.collect.Lists; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.component.DataComponents; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.component.Tool; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.state.BlockState; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.List; -+ -+public class SimpleToolHandler implements ToolHandler { -+ -+ protected final List tools = Lists.newArrayList(); -+ private final ResourceLocation uid; -+ private final boolean skipInstaBreakingBlock; -+ -+ protected SimpleToolHandler(ResourceLocation uid, @NotNull List tools, boolean skipInstaBreakingBlock) { -+ this.uid = uid; -+ Preconditions.checkArgument(!tools.isEmpty(), "tools cannot be empty"); -+ this.tools.addAll(tools); -+ this.skipInstaBreakingBlock = skipInstaBreakingBlock; -+ } -+ -+ @Contract("_, _ -> new") -+ public static @NotNull SimpleToolHandler create(ResourceLocation uid, List tools) { -+ return create(uid, tools, true); -+ } -+ -+ @Contract("_, _, _ -> new") -+ public static @NotNull SimpleToolHandler create(ResourceLocation uid, List tools, boolean skipInstaBreakingBlock) { -+ return new SimpleToolHandler(uid, Lists.transform(tools, Item::getDefaultInstance), skipInstaBreakingBlock); -+ } -+ -+ @Override -+ public ItemStack test(BlockState state, Level world, BlockPos pos) { -+ if (skipInstaBreakingBlock && !state.requiresCorrectToolForDrops() && state.getDestroySpeed(world, pos) == 0) { -+ return ItemStack.EMPTY; -+ } -+ return test(state); -+ } -+ -+ public ItemStack test(BlockState state) { -+ for (ItemStack toolItem : tools) { -+ if (toolItem.isCorrectToolForDrops(state)) { -+ return toolItem; -+ } -+ Tool tool = toolItem.get(DataComponents.TOOL); -+ if (tool != null && tool.getMiningSpeed(state) > tool.defaultMiningSpeed()) { -+ return toolItem; -+ } -+ } -+ return ItemStack.EMPTY; -+ } -+ -+ @Override -+ public List getTools() { -+ return tools; -+ } -+ -+ @Override -+ public ResourceLocation getUid() { -+ return uid; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..18f11e701189ce3615e08c631e31112d53ea5686 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java -@@ -0,0 +1,17 @@ -+package org.leavesmc.leaves.protocol.jade.tool; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.state.BlockState; -+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; -+ -+import java.util.List; -+ -+public interface ToolHandler extends IJadeProvider { -+ -+ ItemStack test(BlockState state, Level world, BlockPos pos); -+ -+ List getTools(); -+ -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a0a85361693980eb5b0bf7f9d486475c77b646f7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java -@@ -0,0 +1,79 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import com.mojang.authlib.GameProfile; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.entity.boss.EnderDragonPart; -+import net.minecraft.world.entity.boss.enderdragon.EnderDragon; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.entity.SkullBlockEntity; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; -+ -+import java.util.List; -+import java.util.Map; -+import java.util.Optional; -+import java.util.UUID; -+ -+public class CommonUtil { -+ -+ public static @NotNull ResourceLocation getId(Block block) { -+ return BuiltInRegistries.BLOCK.getKey(block); -+ } -+ -+ public static Entity wrapPartEntityParent(Entity target) { -+ if (target instanceof EnderDragonPart part) { -+ return part.parentMob; -+ } -+ return target; -+ } -+ -+ public static Entity getPartEntity(Entity parent, int index) { -+ if (parent == null) { -+ return null; -+ } -+ if (index < 0) { -+ return parent; -+ } -+ if (parent instanceof EnderDragon dragon) { -+ EnderDragonPart[] parts = dragon.getSubEntities(); -+ if (index < parts.length) { -+ return parts[index]; -+ } -+ } -+ return parent; -+ } -+ -+ -+ @Nullable -+ public static String getLastKnownUsername(@Nullable UUID uuid) { -+ if (uuid == null) { -+ return null; -+ } -+ Optional optional = SkullBlockEntity.fetchGameProfile(String.valueOf(uuid)).getNow(Optional.empty()); -+ return optional.map(GameProfile::getName).orElse(null); -+ } -+ -+ -+ public static Map.Entry>> getServerExtensionData( -+ Accessor accessor, -+ WrappedHierarchyLookup> lookup) { -+ for (var provider : lookup.wrappedGet(accessor)) { -+ List> groups; -+ try { -+ groups = provider.getGroups(accessor); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.severe(e.toString()); -+ continue; -+ } -+ if (groups != null) { -+ return Map.entry(provider.getUid(), groups); -+ } -+ } -+ return null; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0070fd22b096281a094d1cd19c93fdbccc03a3cc ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java -@@ -0,0 +1,139 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import com.google.common.base.Preconditions; -+import com.google.common.cache.Cache; -+import com.google.common.cache.CacheBuilder; -+import com.google.common.collect.ArrayListMultimap; -+import com.google.common.collect.ImmutableList; -+import com.google.common.collect.ImmutableListMultimap; -+import com.google.common.collect.ListMultimap; -+import com.google.common.collect.Lists; -+import com.google.common.collect.Sets; -+import net.minecraft.core.IdMapper; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; -+ -+import java.util.Collection; -+import java.util.Comparator; -+import java.util.List; -+import java.util.Map; -+import java.util.Objects; -+import java.util.Set; -+import java.util.concurrent.ExecutionException; -+import java.util.stream.Stream; -+ -+public class HierarchyLookup implements IHierarchyLookup { -+ private final Class baseClass; -+ private final Cache, List> resultCache = CacheBuilder.newBuilder().build(); -+ private final boolean singleton; -+ protected boolean idMapped; -+ @Nullable -+ protected IdMapper idMapper; -+ private ListMultimap, T> objects = ArrayListMultimap.create(); -+ -+ public HierarchyLookup(Class baseClass) { -+ this(baseClass, false); -+ } -+ -+ public HierarchyLookup(Class baseClass, boolean singleton) { -+ this.baseClass = baseClass; -+ this.singleton = singleton; -+ } -+ -+ @Override -+ public void idMapped() { -+ this.idMapped = true; -+ } -+ -+ @Override -+ @Nullable -+ public IdMapper idMapper() { -+ return idMapper; -+ } -+ -+ @Override -+ public void register(Class clazz, T provider) { -+ Preconditions.checkArgument(isClassAcceptable(clazz), "Class %s is not acceptable", clazz); -+ Objects.requireNonNull(provider.getUid()); -+ JadeProtocol.priorities.put(provider); -+ objects.put(clazz, provider); -+ } -+ -+ @Override -+ public boolean isClassAcceptable(Class clazz) { -+ return baseClass.isAssignableFrom(clazz); -+ } -+ -+ @Override -+ public List get(Class clazz) { -+ try { -+ return resultCache.get(clazz, () -> { -+ List list = Lists.newArrayList(); -+ getInternal(clazz, list); -+ list = ImmutableList.sortedCopyOf(Comparator.comparingInt(JadeProtocol.priorities::byValue), list); -+ if (singleton && !list.isEmpty()) { -+ return ImmutableList.of(list.getFirst()); -+ } -+ return list; -+ }); -+ } catch (ExecutionException e) { -+ LeavesLogger.LOGGER.warning("HierarchyLookup error", e); -+ } -+ return List.of(); -+ } -+ -+ private void getInternal(Class clazz, List list) { -+ if (clazz != baseClass && clazz != Object.class) { -+ getInternal(clazz.getSuperclass(), list); -+ } -+ list.addAll(objects.get(clazz)); -+ } -+ -+ @Override -+ public boolean isEmpty() { -+ return objects.isEmpty(); -+ } -+ -+ @Override -+ public Stream, Collection>> entries() { -+ return objects.asMap().entrySet().stream(); -+ } -+ -+ @Override -+ public void invalidate() { -+ resultCache.invalidateAll(); -+ } -+ -+ @Override -+ public void loadComplete(PriorityStore priorityStore) { -+ objects.asMap().forEach((clazz, list) -> { -+ if (list.size() < 2) { -+ return; -+ } -+ Set set = Sets.newHashSetWithExpectedSize(list.size()); -+ for (T provider : list) { -+ if (set.contains(provider.getUid())) { -+ throw new IllegalStateException("Duplicate UID: %s for %s".formatted(provider.getUid(), list.stream() -+ .filter(p -> p.getUid().equals(provider.getUid())) -+ .map(p -> p.getClass().getName()) -+ .toList() -+ )); -+ } -+ set.add(provider.getUid()); -+ } -+ }); -+ -+ objects = ImmutableListMultimap., T>builder() -+ .orderValuesBy(Comparator.comparingInt(priorityStore::byValue)) -+ .putAll(objects) -+ .build(); -+ -+ if (idMapped) { -+ idMapper = createIdMapper(); -+ } -+ } -+ -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0536309cb016f05f296daaeb17dd9275777a7333 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java -@@ -0,0 +1,66 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import com.google.common.collect.Streams; -+import net.minecraft.core.IdMapper; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; -+ -+import java.util.Collection; -+import java.util.List; -+import java.util.Map; -+import java.util.Objects; -+import java.util.stream.Stream; -+ -+public interface IHierarchyLookup { -+ default IHierarchyLookup cast() { -+ return this; -+ } -+ -+ void idMapped(); -+ -+ @Nullable -+ IdMapper idMapper(); -+ -+ default List mappedIds() { -+ return Streams.stream(Objects.requireNonNull(idMapper())) -+ .map(IJadeProvider::getUid) -+ .toList(); -+ } -+ -+ void register(Class clazz, T provider); -+ -+ boolean isClassAcceptable(Class clazz); -+ -+ default List get(Object obj) { -+ if (obj == null) { -+ return List.of(); -+ } -+ return get(obj.getClass()); -+ } -+ -+ List get(Class clazz); -+ -+ boolean isEmpty(); -+ -+ Stream, Collection>> entries(); -+ -+ void invalidate(); -+ -+ void loadComplete(PriorityStore priorityStore); -+ -+ default IdMapper createIdMapper() { -+ List list = entries().flatMap(entry -> entry.getValue().stream()).toList(); -+ IdMapper idMapper = idMapper(); -+ if (idMapper == null) { -+ idMapper = new IdMapper<>(list.size()); -+ } -+ for (T provider : list) { -+ if (idMapper.getId(provider) == IdMapper.DEFAULT) { -+ idMapper.add(provider); -+ } -+ } -+ return idMapper; -+ } -+} -+ -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java -new file mode 100644 -index 0000000000000000000000000000000000000000..408c81a0d0762f7d4b32aedc9b67c7ff546b43f3 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java -@@ -0,0 +1,118 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; -+import net.minecraft.core.component.DataComponentPatch; -+import net.minecraft.core.component.DataComponents; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.component.CustomData; -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+ -+import java.util.List; -+import java.util.Locale; -+import java.util.concurrent.atomic.AtomicInteger; -+import java.util.function.Predicate; -+ -+public class ItemCollector { -+ public static final int MAX_SIZE = 54; -+ public static final ItemCollector EMPTY = new ItemCollector<>(null); -+ private static final Predicate NON_EMPTY = stack -> { -+ if (stack.isEmpty()) { -+ return false; -+ } -+ CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY); -+ if (customData.contains("CustomModelData")) { -+ CompoundTag tag = customData.copyTag(); -+ for (String key : tag.getAllKeys()) { -+ if (key.toLowerCase(Locale.ENGLISH).endsWith("clear") && tag.getBoolean(key)) { -+ return false; -+ } -+ } -+ } -+ return true; -+ }; -+ private final Object2IntLinkedOpenHashMap items = new Object2IntLinkedOpenHashMap<>(); -+ private final ItemIterator iterator; -+ public long version; -+ public long lastTimeFinished; -+ public boolean lastTimeIsEmpty; -+ public List> mergedResult; -+ -+ public ItemCollector(ItemIterator iterator) { -+ this.iterator = iterator; -+ } -+ -+ public List> update(Accessor request) { -+ if (iterator == null) { -+ return null; -+ } -+ T container = iterator.find(request.getTarget()); -+ if (container == null) { -+ return null; -+ } -+ long currentVersion = iterator.getVersion(container); -+ long gameTime = request.getLevel().getGameTime(); -+ if (mergedResult != null && iterator.isFinished()) { -+ if (version == currentVersion) { -+ return mergedResult; // content not changed -+ } -+ if (lastTimeFinished + 5 > gameTime) { -+ return mergedResult; // avoid update too frequently -+ } -+ iterator.reset(); -+ } -+ AtomicInteger count = new AtomicInteger(); -+ iterator.populate(container).forEach(stack -> { -+ count.incrementAndGet(); -+ if (NON_EMPTY.test(stack)) { -+ ItemDefinition def = new ItemDefinition(stack); -+ items.addTo(def, stack.getCount()); -+ } -+ }); -+ iterator.afterPopulate(count.get()); -+ if (mergedResult != null && !iterator.isFinished()) { -+ updateCollectingProgress(mergedResult.getFirst()); -+ return mergedResult; -+ } -+ List partialResult = items.object2IntEntrySet().stream().limit(MAX_SIZE ).map(entry -> { -+ ItemDefinition def = entry.getKey(); -+ return def.toStack(entry.getIntValue()); -+ }).toList(); -+ List> groups = List.of(updateCollectingProgress(new ViewGroup<>(partialResult))); -+ if (iterator.isFinished()) { -+ mergedResult = groups; -+ lastTimeIsEmpty = mergedResult.getFirst().views.isEmpty(); -+ version = currentVersion; -+ lastTimeFinished = gameTime; -+ items.clear(); -+ } -+ return groups; -+ } -+ -+ protected ViewGroup updateCollectingProgress(ViewGroup group) { -+ if (lastTimeIsEmpty && group.views.isEmpty()) { -+ return group; -+ } -+ float progress = iterator.getCollectingProgress(); -+ CompoundTag data = group.getExtraData(); -+ if (Float.isNaN(progress) || progress >= 1) { -+ data.remove("Collecting"); -+ } else { -+ data.putFloat("Collecting", progress); -+ } -+ return group; -+ } -+ -+ public record ItemDefinition(Item item, DataComponentPatch components) { -+ ItemDefinition(ItemStack stack) { -+ this(stack.getItem(), stack.getComponentsPatch()); -+ } -+ -+ public ItemStack toStack(int count) { -+ ItemStack itemStack = new ItemStack(item, count); -+ itemStack.applyComponents(components); -+ return itemStack; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4d65e9a8b5224bd268b1bf18bc39a58dc0113850 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java -@@ -0,0 +1,102 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import net.minecraft.world.Container; -+import net.minecraft.world.item.ItemStack; -+import org.jetbrains.annotations.Nullable; -+ -+import java.util.concurrent.atomic.AtomicLong; -+import java.util.function.Function; -+import java.util.stream.IntStream; -+import java.util.stream.Stream; -+ -+public abstract class ItemIterator { -+ public static final AtomicLong version = new AtomicLong(); -+ protected final Function containerFinder; -+ protected final int fromIndex; -+ protected boolean finished; -+ protected int currentIndex; -+ -+ protected ItemIterator(Function containerFinder, int fromIndex) { -+ this.containerFinder = containerFinder; -+ this.currentIndex = this.fromIndex = fromIndex; -+ } -+ -+ public @Nullable T find(Object target) { -+ return containerFinder.apply(target); -+ } -+ -+ public final boolean isFinished() { -+ return finished; -+ } -+ -+ public long getVersion(T container) { -+ return version.getAndIncrement(); -+ } -+ -+ public abstract Stream populate(T container); -+ -+ public void reset() { -+ currentIndex = fromIndex; -+ finished = false; -+ } -+ -+ public void afterPopulate(int count) { -+ currentIndex += count; -+ if (count == 0 || currentIndex >= 10000) { -+ finished = true; -+ } -+ } -+ -+ public float getCollectingProgress() { -+ return Float.NaN; -+ } -+ -+ public static abstract class SlottedItemIterator extends ItemIterator { -+ protected float progress; -+ -+ public SlottedItemIterator(Function containerFinder, int fromIndex) { -+ super(containerFinder, fromIndex); -+ } -+ -+ protected abstract int getSlotCount(T container); -+ -+ protected abstract ItemStack getItemInSlot(T container, int slot); -+ -+ @Override -+ public Stream populate(T container) { -+ int slotCount = getSlotCount(container); -+ int toIndex = currentIndex + ItemCollector.MAX_SIZE * 2; -+ if (toIndex >= slotCount) { -+ toIndex = slotCount; -+ finished = true; -+ } -+ progress = (float) (currentIndex - fromIndex) / (slotCount - fromIndex); -+ return IntStream.range(currentIndex, toIndex).mapToObj(slot -> getItemInSlot(container, slot)); -+ } -+ -+ @Override -+ public float getCollectingProgress() { -+ return progress; -+ } -+ } -+ -+ public static class ContainerItemIterator extends SlottedItemIterator { -+ public ContainerItemIterator(int fromIndex) { -+ this(Container.class::cast, fromIndex); -+ } -+ -+ public ContainerItemIterator(Function containerFinder, int fromIndex) { -+ super(containerFinder, fromIndex); -+ } -+ -+ @Override -+ protected int getSlotCount(Container container) { -+ return container.getContainerSize(); -+ } -+ -+ @Override -+ protected ItemStack getItemInSlot(Container container, int slot) { -+ return container.getItem(slot); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a046ae4e542efcadd0001b7225440c309a7a7570 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java -@@ -0,0 +1,59 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import io.netty.buffer.ByteBuf; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import org.jetbrains.annotations.NotNull; -+ -+public class JadeCodec { -+ public static final StreamCodec PRIMITIVE_STREAM_CODEC = new StreamCodec<>() { -+ @Override -+ public @NotNull Object decode(@NotNull ByteBuf buf) { -+ byte b = buf.readByte(); -+ if (b == 0) { -+ return false; -+ } else if (b == 1) { -+ return true; -+ } else if (b == 2) { -+ return ByteBufCodecs.VAR_INT.decode(buf); -+ } else if (b == 3) { -+ return ByteBufCodecs.FLOAT.decode(buf); -+ } else if (b == 4) { -+ return ByteBufCodecs.STRING_UTF8.decode(buf); -+ } else if (b > 20) { -+ return b - 20; -+ } -+ throw new IllegalArgumentException("Unknown primitive type: " + b); -+ } -+ -+ @Override -+ public void encode(@NotNull ByteBuf buf, @NotNull Object o) { -+ switch (o) { -+ case Boolean b -> buf.writeByte(b ? 1 : 0); -+ case Number n -> { -+ float f = n.floatValue(); -+ if (f != (int) f) { -+ buf.writeByte(3); -+ ByteBufCodecs.FLOAT.encode(buf, f); -+ } -+ int i = n.intValue(); -+ if (i <= Byte.MAX_VALUE - 20 && i >= 0) { -+ buf.writeByte(i + 20); -+ } else { -+ ByteBufCodecs.VAR_INT.encode(buf, i); -+ } -+ } -+ case String s -> { -+ buf.writeByte(4); -+ ByteBufCodecs.STRING_UTF8.encode(buf, s); -+ } -+ case Enum anEnum -> { -+ buf.writeByte(4); -+ ByteBufCodecs.STRING_UTF8.encode(buf, anEnum.name()); -+ } -+ case null -> throw new NullPointerException(); -+ default -> throw new IllegalArgumentException("Unknown primitive type: %s (%s)".formatted(o, o.getClass())); -+ } -+ } -+ }; -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java -new file mode 100644 -index 0000000000000000000000000000000000000000..81575cfa4c5a6f1a1aadbf9ea7743cd1d053f925 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java -@@ -0,0 +1,109 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import com.google.common.collect.Lists; -+import net.minecraft.advancements.critereon.ItemPredicate; -+import net.minecraft.core.Holder; -+import net.minecraft.core.HolderGetter; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.storage.loot.LootPool; -+import net.minecraft.world.level.storage.loot.LootTable; -+import net.minecraft.world.level.storage.loot.entries.AlternativesEntry; -+import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; -+import net.minecraft.world.level.storage.loot.entries.NestedLootTable; -+import net.minecraft.world.level.storage.loot.predicates.AnyOfCondition; -+import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; -+import net.minecraft.world.level.storage.loot.predicates.MatchTool; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.jade.tool.ShearsToolHandler; -+ -+import java.util.List; -+import java.util.Optional; -+import java.util.function.Function; -+ -+public class LootTableMineableCollector { -+ -+ private final HolderGetter lootRegistry; -+ private final ItemStack toolItem; -+ -+ public LootTableMineableCollector(HolderGetter lootRegistry, ItemStack toolItem) { -+ this.lootRegistry = lootRegistry; -+ this.toolItem = toolItem; -+ } -+ -+ public static @NotNull List execute(HolderGetter lootRegistry, ItemStack toolItem) { -+ LootTableMineableCollector collector = new LootTableMineableCollector(lootRegistry, toolItem); -+ List list = Lists.newArrayList(); -+ for (Block block : BuiltInRegistries.BLOCK) { -+ if (!ShearsToolHandler.getInstance().test(block.defaultBlockState()).isEmpty()) { -+ continue; -+ } -+ -+ if (block.getLootTable().isPresent()) { -+ LootTable lootTable = lootRegistry.get(block.getLootTable().get()).map(Holder::value).orElse(null); -+ if (collector.doLootTable(lootTable)) { -+ list.add(block); -+ } -+ } -+ } -+ return list; -+ } -+ -+ private boolean doLootTable(LootTable lootTable) { -+ if (lootTable == null || lootTable == LootTable.EMPTY) { -+ return false; -+ } -+ -+ for (LootPool pool : lootTable.pools) { -+ if (doLootPool(pool)) { -+ return true; -+ } -+ } -+ return false; -+ } -+ -+ private boolean doLootPool(@NotNull LootPool lootPool) { -+ for (LootPoolEntryContainer entry : lootPool.entries) { -+ if (doLootPoolEntry(entry)) { -+ return true; -+ } -+ } -+ return false; -+ } -+ -+ private boolean doLootPoolEntry(LootPoolEntryContainer entry) { -+ if (entry instanceof AlternativesEntry alternativesEntry) { -+ for (LootPoolEntryContainer child : alternativesEntry.children) { -+ if (doLootPoolEntry(child)) { -+ return true; -+ } -+ } -+ } else if (entry instanceof NestedLootTable nestedLootTable) { -+ LootTable lootTable = nestedLootTable.contents.map($ -> lootRegistry.get($).map(Holder::value).orElse(null), Function.identity()); -+ return doLootTable(lootTable); -+ } else { -+ return isCorrectConditions(entry.conditions, toolItem); -+ } -+ return false; -+ } -+ -+ public static boolean isCorrectConditions(@NotNull List conditions, ItemStack toolItem) { -+ if (conditions.size() != 1) { -+ return false; -+ } -+ -+ LootItemCondition condition = conditions.getFirst(); -+ if (condition instanceof MatchTool(Optional predicate)) { -+ ItemPredicate itemPredicate = predicate.orElse(null); -+ return itemPredicate != null && itemPredicate.test(toolItem); -+ } else if (condition instanceof AnyOfCondition anyOfCondition) { -+ for (LootItemCondition child : anyOfCondition.terms) { -+ if (isCorrectConditions(List.of(child), toolItem)) { -+ return true; -+ } -+ } -+ } -+ return false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cb5c8201958b3f444d990082d7aac615090cc2a8 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java -@@ -0,0 +1,120 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import com.google.common.cache.Cache; -+import com.google.common.cache.CacheBuilder; -+import com.google.common.collect.ImmutableList; -+import com.google.common.collect.Iterables; -+import net.minecraft.core.IdMapper; -+import net.minecraft.resources.ResourceLocation; -+import org.apache.commons.lang3.tuple.Pair; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.jade.JadeProtocol; -+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; -+ -+import java.util.Collection; -+import java.util.Comparator; -+import java.util.List; -+import java.util.Map; -+import java.util.Objects; -+import java.util.concurrent.ExecutionException; -+import java.util.stream.Stream; -+ -+public class PairHierarchyLookup implements IHierarchyLookup { -+ public final IHierarchyLookup first; -+ public final IHierarchyLookup second; -+ private final Cache, Class>, List> mergedCache = CacheBuilder.newBuilder().build(); -+ protected boolean idMapped; -+ @Nullable -+ protected IdMapper idMapper; -+ -+ public PairHierarchyLookup(IHierarchyLookup first, IHierarchyLookup second) { -+ this.first = first; -+ this.second = second; -+ } -+ -+ @SuppressWarnings("unchecked") -+ public List getMerged(Object first, Object second) { -+ Objects.requireNonNull(first); -+ Objects.requireNonNull(second); -+ try { -+ return (List) mergedCache.get(Pair.of(first.getClass(), second.getClass()), () -> { -+ List firstList = this.first.get(first); -+ List secondList = this.second.get(second); -+ if (firstList.isEmpty()) { -+ return secondList; -+ } else if (secondList.isEmpty()) { -+ return firstList; -+ } -+ return ImmutableList.sortedCopyOf( -+ Comparator.comparingInt(JadeProtocol.priorities::byValue), -+ Iterables.concat(firstList, secondList) -+ ); -+ }); -+ } catch (ExecutionException e) { -+ LeavesLogger.LOGGER.severe(e.toString()); -+ } -+ return List.of(); -+ } -+ -+ @Override -+ public void idMapped() { -+ idMapped = true; -+ } -+ -+ @Override -+ public @Nullable IdMapper idMapper() { -+ return idMapper; -+ } -+ -+ @Override -+ public void register(Class clazz, T provider) { -+ if (first.isClassAcceptable(clazz)) { -+ first.register(clazz, provider); -+ } else if (second.isClassAcceptable(clazz)) { -+ second.register(clazz, provider); -+ } else { -+ throw new IllegalArgumentException("Class " + clazz + " is not acceptable"); -+ } -+ } -+ -+ @Override -+ public boolean isClassAcceptable(Class clazz) { -+ return first.isClassAcceptable(clazz) || second.isClassAcceptable(clazz); -+ } -+ -+ @Override -+ public List get(Class clazz) { -+ List result = first.get(clazz); -+ if (result.isEmpty()) { -+ result = second.get(clazz); -+ } -+ return result; -+ } -+ -+ @Override -+ public boolean isEmpty() { -+ return first.isEmpty() && second.isEmpty(); -+ } -+ -+ @Override -+ public Stream, Collection>> entries() { -+ return Stream.concat(first.entries(), second.entries()); -+ } -+ -+ @Override -+ public void invalidate() { -+ first.invalidate(); -+ second.invalidate(); -+ mergedCache.invalidateAll(); -+ } -+ -+ @Override -+ public void loadComplete(PriorityStore priorityStore) { -+ first.loadComplete(priorityStore); -+ second.loadComplete(priorityStore); -+ if (idMapped) { -+ idMapper = createIdMapper(); -+ } -+ } -+} -\ No newline at end of file -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java -new file mode 100644 -index 0000000000000000000000000000000000000000..da4d5a7751b1076417e63b63dc1f91c0fcc73ff9 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java -@@ -0,0 +1,40 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Object2IntMap; -+ -+import java.util.Objects; -+import java.util.function.Function; -+import java.util.function.ToIntFunction; -+ -+public class PriorityStore { -+ -+ private final Object2IntMap priorities = new Object2IntLinkedOpenHashMap<>(); -+ private final Function keyGetter; -+ private final ToIntFunction defaultPriorityGetter; -+ -+ public PriorityStore(ToIntFunction defaultPriorityGetter, Function keyGetter) { -+ this.defaultPriorityGetter = defaultPriorityGetter; -+ this.keyGetter = keyGetter; -+ } -+ -+ public void put(V provider) { -+ Objects.requireNonNull(provider); -+ put(provider, defaultPriorityGetter.applyAsInt(provider)); -+ } -+ -+ public void put(V provider, int priority) { -+ Objects.requireNonNull(provider); -+ K uid = keyGetter.apply(provider); -+ Objects.requireNonNull(uid); -+ priorities.put(uid, priority); -+ } -+ -+ public int byValue(V value) { -+ return byKey(keyGetter.apply(value)); -+ } -+ -+ public int byKey(K id) { -+ return priorities.getInt(id); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..520eadbf6de55141524741b4e4063cd542ef7128 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java -@@ -0,0 +1,62 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import io.netty.buffer.ByteBuf; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.codec.ByteBufCodecs; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.Nullable; -+ -+import java.util.List; -+import java.util.Map; -+import java.util.Optional; -+ -+public class ViewGroup { -+ public static StreamCodec> codec(StreamCodec viewCodec) { -+ return StreamCodec.composite( -+ ByteBufCodecs.list().apply(viewCodec), -+ $ -> $.views, -+ ByteBufCodecs.optional(ByteBufCodecs.STRING_UTF8), -+ $ -> Optional.ofNullable($.id), -+ ByteBufCodecs.optional(ByteBufCodecs.COMPOUND_TAG), -+ $ -> Optional.ofNullable($.extraData), -+ ViewGroup::new); -+ } -+ -+ public static StreamCodec>>> listCodec(StreamCodec viewCodec) { -+ return StreamCodec.composite( -+ ResourceLocation.STREAM_CODEC, -+ Map.Entry::getKey, -+ ByteBufCodecs.>list().apply(codec(viewCodec)), -+ Map.Entry::getValue, -+ Map::entry); -+ } -+ -+ public List views; -+ @Nullable -+ public String id; -+ @Nullable -+ protected CompoundTag extraData; -+ -+ public ViewGroup(List views) { -+ this(views, Optional.empty(), Optional.empty()); -+ } -+ -+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") -+ public ViewGroup(List views, Optional id, Optional extraData) { -+ this.views = views; -+ this.id = id.orElse(null); -+ this.extraData = extraData.orElse(null); -+ } -+ -+ public CompoundTag getExtraData() { -+ if (extraData == null) { -+ extraData = new CompoundTag(); -+ } -+ return extraData; -+ } -+ -+ public void setProgress(float progress) { -+ getExtraData().putFloat("Progress", progress); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9b49efd3255e4521824e1b21b88e3625eedcc7a5 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java -@@ -0,0 +1,104 @@ -+package org.leavesmc.leaves.protocol.jade.util; -+ -+import com.google.common.collect.Lists; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.world.level.block.Block; -+import org.apache.commons.lang3.tuple.Pair; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.protocol.jade.accessor.Accessor; -+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; -+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; -+ -+import java.util.Collection; -+import java.util.List; -+import java.util.Map; -+import java.util.function.Function; -+import java.util.stream.Stream; -+ -+public class WrappedHierarchyLookup extends HierarchyLookup { -+ public final List, Function, @Nullable Object>>> overrides = Lists.newArrayList(); -+ private boolean empty = true; -+ -+ public WrappedHierarchyLookup() { -+ super(Object.class); -+ } -+ -+ @NotNull -+ public static WrappedHierarchyLookup forAccessor() { -+ WrappedHierarchyLookup lookup = new WrappedHierarchyLookup<>(); -+ lookup.overrides.add(Pair.of( -+ new HierarchyLookup<>(Block.class), accessor -> { -+ if (accessor instanceof BlockAccessor blockAccessor) { -+ return blockAccessor.getBlock(); -+ } -+ return null; -+ })); -+ return lookup; -+ } -+ -+ public List wrappedGet(Accessor accessor) { -+ List list = Lists.newArrayList(); -+ for (var override : overrides) { -+ Object o = override.getRight().apply(accessor); -+ if (o != null) { -+ list.addAll(override.getLeft().get(o)); -+ } -+ } -+ list.addAll(get(accessor.getTarget())); -+ return list; -+ } -+ -+ @Override -+ public void register(Class clazz, T provider) { -+ for (var override : overrides) { -+ if (override.getLeft().isClassAcceptable(clazz)) { -+ override.getLeft().register(clazz, provider); -+ empty = false; -+ return; -+ } -+ } -+ super.register(clazz, provider); -+ empty = false; -+ } -+ -+ @Override -+ public boolean isClassAcceptable(Class clazz) { -+ for (var override : overrides) { -+ if (override.getLeft().isClassAcceptable(clazz)) { -+ return true; -+ } -+ } -+ return super.isClassAcceptable(clazz); -+ } -+ -+ @Override -+ public void invalidate() { -+ for (var override : overrides) { -+ override.getLeft().invalidate(); -+ } -+ super.invalidate(); -+ } -+ -+ @Override -+ public void loadComplete(PriorityStore priorityStore) { -+ for (var override : overrides) { -+ override.getLeft().loadComplete(priorityStore); -+ } -+ super.loadComplete(priorityStore); -+ } -+ -+ @Override -+ public boolean isEmpty() { -+ return empty; -+ } -+ -+ @Override -+ public Stream, Collection>> entries() { -+ Stream, Collection>> stream = super.entries(); -+ for (var override : overrides) { -+ stream = Stream.concat(stream, override.getLeft().entries()); -+ } -+ return stream; -+ } -+} diff --git a/patches/server/0034-Alternative-block-placement-Protocol.patch b/patches/server/0034-Alternative-block-placement-Protocol.patch deleted file mode 100644 index f37dba8e..00000000 --- a/patches/server/0034-Alternative-block-placement-Protocol.patch +++ /dev/null @@ -1,495 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 8 Dec 2022 19:40:00 +0800 -Subject: [PATCH] Alternative block placement Protocol - -This patch is Powered by -carpet-extra(https://github.com/gnembon/carpet-extra) -MasaGadget(https://github.com/plusls/MasaGadget) -litematica(https://github.com/maruohon/litematica) - -diff --git a/src/main/java/net/minecraft/world/item/BlockItem.java b/src/main/java/net/minecraft/world/item/BlockItem.java -index c816c935ecc74a811ffdffbe6ded73c06e92324a..dba8ee20f9fed3adf26885471897ade154ec1d4d 100644 ---- a/src/main/java/net/minecraft/world/item/BlockItem.java -+++ b/src/main/java/net/minecraft/world/item/BlockItem.java -@@ -157,6 +157,27 @@ public class BlockItem extends Item { - @Nullable - protected BlockState getPlacementState(BlockPlaceContext context) { - BlockState iblockdata = this.getBlock().getStateForPlacement(context); -+ // Leaves start - alternativeBlockPlacement -+ switch (org.leavesmc.leaves.LeavesConfig.protocol.alternativeBlockPlacement) { -+ case CARPET -> { -+ BlockState tryState = org.leavesmc.leaves.protocol.CarpetAlternativeBlockPlacement.alternativeBlockPlacement(getBlock(), context); -+ if (tryState != null) { -+ iblockdata = tryState; -+ } -+ } -+ case CARPET_FIX -> { -+ BlockState tryState = org.leavesmc.leaves.protocol.CarpetAlternativeBlockPlacement.alternativeBlockPlacementFix(getBlock(), context); -+ if (tryState != null) { -+ iblockdata = tryState; -+ } -+ } -+ case LITEMATICA -> { -+ if (iblockdata != null && this.canPlace(context, iblockdata)) { -+ return org.leavesmc.leaves.protocol.LitematicaEasyPlaceProtocol.applyPlacementProtocol(iblockdata, context); -+ } -+ } -+ } -+ // Leaves end - alternativeBlockPlacement - - return iblockdata != null && this.canPlace(context, iblockdata) ? iblockdata : null; - } -diff --git a/src/main/java/net/minecraft/world/item/StandingAndWallBlockItem.java b/src/main/java/net/minecraft/world/item/StandingAndWallBlockItem.java -index 1451b25cedb7a8f01c046c8e1f8c6853aca42283..4390adf843b395db688017eb9034b56a40971473 100644 ---- a/src/main/java/net/minecraft/world/item/StandingAndWallBlockItem.java -+++ b/src/main/java/net/minecraft/world/item/StandingAndWallBlockItem.java -@@ -34,7 +34,7 @@ public class StandingAndWallBlockItem extends BlockItem { - @Nullable - @Override - protected BlockState getPlacementState(BlockPlaceContext context) { -- BlockState iblockdata = this.wallBlock.getStateForPlacement(context); -+ BlockState iblockdata = this.wallBlock.getRealStateForPlacement(context); // Leaves - alternativeBlockPlacement - BlockState iblockdata1 = null; - Level world = context.getLevel(); - BlockPos blockposition = context.getClickedPos(); -@@ -45,7 +45,7 @@ public class StandingAndWallBlockItem extends BlockItem { - Direction enumdirection = aenumdirection[j]; - - if (enumdirection != this.attachmentDirection.getOpposite()) { -- BlockState iblockdata2 = enumdirection == this.attachmentDirection ? this.getBlock().getStateForPlacement(context) : iblockdata; -+ BlockState iblockdata2 = enumdirection == this.attachmentDirection ? this.getBlock().getRealStateForPlacement(context) : iblockdata; // Leaves - carpetAlternativeBlockPlacement - - if (iblockdata2 != null && this.canPlace(world, iblockdata2, blockposition)) { - iblockdata1 = iblockdata2; -diff --git a/src/main/java/net/minecraft/world/level/block/Block.java b/src/main/java/net/minecraft/world/level/block/Block.java -index b3a3388ef62b0622906b2470056cb41f0deb0391..1c9b9f27179f2bd3f24709a763ff264e97cb1dca 100644 ---- a/src/main/java/net/minecraft/world/level/block/Block.java -+++ b/src/main/java/net/minecraft/world/level/block/Block.java -@@ -414,6 +414,33 @@ public class Block extends BlockBehaviour implements ItemLike { - - public void stepOn(Level world, BlockPos pos, BlockState state, Entity entity) {} - -+ // Leaves start - alternativeBlockPlacement -+ @Nullable -+ public BlockState getRealStateForPlacement(BlockPlaceContext ctx) { -+ BlockState vanillaState = getStateForPlacement(ctx); -+ switch (org.leavesmc.leaves.LeavesConfig.protocol.alternativeBlockPlacement) { -+ case CARPET -> { -+ BlockState tryState = org.leavesmc.leaves.protocol.CarpetAlternativeBlockPlacement.alternativeBlockPlacement(this, ctx); -+ if (tryState != null) { -+ return tryState; -+ } -+ } -+ case CARPET_FIX -> { -+ BlockState tryState = org.leavesmc.leaves.protocol.CarpetAlternativeBlockPlacement.alternativeBlockPlacementFix(this, ctx); -+ if (tryState != null) { -+ return tryState; -+ } -+ } -+ case LITEMATICA -> { -+ if (vanillaState != null) { -+ return org.leavesmc.leaves.protocol.LitematicaEasyPlaceProtocol.applyPlacementProtocol(vanillaState, ctx); -+ } -+ } -+ } -+ return vanillaState; -+ } -+ // Leaves end - alternativeBlockPlacement -+ - @Nullable - public BlockState getStateForPlacement(BlockPlaceContext ctx) { - return this.defaultBlockState(); -diff --git a/src/main/java/org/leavesmc/leaves/protocol/CarpetAlternativeBlockPlacement.java b/src/main/java/org/leavesmc/leaves/protocol/CarpetAlternativeBlockPlacement.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e5c5bee2518ff2843270979d36d264f17a5cf0a6 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/CarpetAlternativeBlockPlacement.java -@@ -0,0 +1,167 @@ -+package org.leavesmc.leaves.protocol; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.util.Mth; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.item.context.BlockPlaceContext; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.BedBlock; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.ComparatorBlock; -+import net.minecraft.world.level.block.DirectionalBlock; -+import net.minecraft.world.level.block.DispenserBlock; -+import net.minecraft.world.level.block.GlazedTerracottaBlock; -+import net.minecraft.world.level.block.ObserverBlock; -+import net.minecraft.world.level.block.RepeaterBlock; -+import net.minecraft.world.level.block.StairBlock; -+import net.minecraft.world.level.block.TrapDoorBlock; -+import net.minecraft.world.level.block.piston.PistonBaseBlock; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.block.state.properties.BlockStateProperties; -+import net.minecraft.world.level.block.state.properties.ComparatorMode; -+import net.minecraft.world.level.block.state.properties.EnumProperty; -+import net.minecraft.world.level.block.state.properties.Half; -+import net.minecraft.world.level.block.state.properties.Property; -+import net.minecraft.world.level.block.state.properties.SlabType; -+import net.minecraft.world.phys.Vec3; -+import org.jetbrains.annotations.NotNull; -+ -+import javax.annotation.Nullable; -+import java.util.Objects; -+ -+public class CarpetAlternativeBlockPlacement { -+ -+ @Nullable -+ public static BlockState alternativeBlockPlacement(@NotNull Block block, @NotNull BlockPlaceContext context) { -+ Vec3 hitPos = context.getClickLocation(); -+ BlockPos blockPos = context.getClickedPos(); -+ double relativeHitX = hitPos.x - blockPos.getX(); -+ BlockState state = block.getStateForPlacement(context); -+ Player player = context.getPlayer(); -+ -+ if (relativeHitX < 2 || state == null || player == null) { -+ return null; -+ } -+ -+ EnumProperty directionProp = getFirstDirectionProperty(state); -+ int protocolValue = ((int) relativeHitX - 2) / 2; -+ -+ if (directionProp != null) { -+ Direction origFacing = state.getValue(directionProp); -+ Direction facing = origFacing; -+ int facingIndex = protocolValue & 0xF; -+ -+ if (facingIndex == 6) { -+ facing = facing.getOpposite(); -+ } else if (facingIndex <= 5) { -+ facing = Direction.from3DDataValue(facingIndex); -+ } -+ -+ if (!directionProp.getPossibleValues().contains(facing)) { -+ facing = player.getDirection().getOpposite(); -+ } -+ -+ if (facing != origFacing && directionProp.getPossibleValues().contains(facing)) { -+ if (state.getBlock() instanceof BedBlock) { -+ BlockPos headPos = blockPos.relative(facing); -+ -+ if (!context.getLevel().getBlockState(headPos).canBeReplaced(context)) { -+ return null; -+ } -+ } -+ -+ state = state.setValue(directionProp, facing); -+ } -+ } else if (state.hasProperty(BlockStateProperties.AXIS)) { -+ Direction.Axis axis = Direction.Axis.VALUES[protocolValue % 3]; -+ state = state.setValue(BlockStateProperties.AXIS, axis); -+ } -+ -+ protocolValue &= 0xFFFFFFF0; -+ -+ if (protocolValue >= 16) { -+ if (block instanceof RepeaterBlock) { -+ Integer delay = (protocolValue / 16); -+ -+ if (RepeaterBlock.DELAY.getPossibleValues().contains(delay)) { -+ state = state.setValue(RepeaterBlock.DELAY, delay); -+ } -+ } else if (protocolValue == 16) { -+ if (block instanceof ComparatorBlock) { -+ state = state.setValue(ComparatorBlock.MODE, ComparatorMode.SUBTRACT); -+ } else if (state.hasProperty(BlockStateProperties.HALF) && state.getValue(BlockStateProperties.HALF) == Half.BOTTOM) { -+ state = state.setValue(BlockStateProperties.HALF, Half.TOP); -+ } else if (state.hasProperty(BlockStateProperties.SLAB_TYPE) && state.getValue(BlockStateProperties.SLAB_TYPE) == SlabType.BOTTOM) { -+ state = state.setValue(BlockStateProperties.SLAB_TYPE, SlabType.TOP); -+ } -+ } -+ } -+ -+ return state; -+ } -+ -+ @Nullable -+ public static BlockState alternativeBlockPlacementFix(Block block, @NotNull BlockPlaceContext context) { -+ Direction facing; -+ Vec3 vec3d = context.getClickLocation(); -+ BlockPos pos = context.getClickedPos(); -+ double hitX = vec3d.x - pos.getX(); -+ if (hitX < 2) { -+ return null; -+ } -+ int code = (int) (hitX - 2) / 2; -+ Player placer = Objects.requireNonNull(context.getPlayer()); -+ Level world = context.getLevel(); -+ -+ if (block instanceof GlazedTerracottaBlock) { -+ facing = Direction.from3DDataValue(code); -+ if (facing == Direction.UP || facing == Direction.DOWN) { -+ facing = placer.getDirection().getOpposite(); -+ } -+ return block.defaultBlockState().setValue(GlazedTerracottaBlock.FACING, facing); -+ } else if (block instanceof ObserverBlock) { -+ return block.defaultBlockState().setValue(ObserverBlock.FACING, Direction.from3DDataValue(code)).setValue(ObserverBlock.POWERED, true); -+ } else if (block instanceof RepeaterBlock) { -+ facing = Direction.from3DDataValue(code % 16); -+ if (facing == Direction.UP || facing == Direction.DOWN) { -+ facing = placer.getDirection().getOpposite(); -+ } -+ return block.defaultBlockState().setValue(RepeaterBlock.FACING, facing).setValue(RepeaterBlock.DELAY, Mth.clamp(code / 16, 1, 4)).setValue(RepeaterBlock.LOCKED, Boolean.FALSE); -+ } else if (block instanceof TrapDoorBlock) { -+ facing = Direction.from3DDataValue(code % 16); -+ if (facing == Direction.UP || facing == Direction.DOWN) { -+ facing = placer.getDirection().getOpposite(); -+ } -+ return block.defaultBlockState().setValue(TrapDoorBlock.FACING, facing).setValue(TrapDoorBlock.OPEN, Boolean.FALSE).setValue(TrapDoorBlock.HALF, (code >= 16) ? Half.TOP : Half.BOTTOM).setValue(TrapDoorBlock.OPEN, world.hasNeighborSignal(pos)); -+ } else if (block instanceof ComparatorBlock) { -+ facing = Direction.from3DDataValue(code % 16); -+ if ((facing == Direction.UP) || (facing == Direction.DOWN)) { -+ facing = placer.getDirection().getOpposite(); -+ } -+ ComparatorMode m = (hitX >= 16) ? ComparatorMode.SUBTRACT : ComparatorMode.COMPARE; -+ return block.defaultBlockState().setValue(ComparatorBlock.FACING, facing).setValue(ComparatorBlock.POWERED, Boolean.FALSE).setValue(ComparatorBlock.MODE, m); -+ } else if (block instanceof DispenserBlock) { -+ return block.defaultBlockState().setValue(DispenserBlock.FACING, Direction.from3DDataValue(code)).setValue(DispenserBlock.TRIGGERED, Boolean.FALSE); -+ } else if (block instanceof PistonBaseBlock) { -+ return block.defaultBlockState().setValue(DirectionalBlock.FACING, Direction.from3DDataValue(code)).setValue(PistonBaseBlock.EXTENDED, Boolean.FALSE); -+ } else if (block instanceof StairBlock) { -+ return Objects.requireNonNull(block.getStateForPlacement(context)).setValue(StairBlock.FACING, Direction.from3DDataValue(code % 16)).setValue(StairBlock.HALF, (hitX >= 16) ? Half.TOP : Half.BOTTOM); -+ } -+ return null; -+ } -+ -+ @SuppressWarnings("unchecked") -+ @Nullable -+ public static EnumProperty getFirstDirectionProperty(@NotNull BlockState state) { -+ for (Property prop : state.getProperties()) { -+ if (prop instanceof EnumProperty enumProperty) { -+ if (enumProperty.getValueClass().equals(Direction.class)) { -+ return (EnumProperty) enumProperty; -+ } -+ } -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..eceb1b8322fa50809990636e6caed491828e2fd6 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java -@@ -0,0 +1,214 @@ -+package org.leavesmc.leaves.protocol; -+ -+import com.google.common.collect.ImmutableSet; -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Direction; -+import net.minecraft.util.Mth; -+import net.minecraft.world.InteractionHand; -+import net.minecraft.world.entity.LivingEntity; -+import net.minecraft.world.item.context.BlockPlaceContext; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.BedBlock; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.block.state.properties.BlockStateProperties; -+import net.minecraft.world.level.block.state.properties.EnumProperty; -+import net.minecraft.world.level.block.state.properties.Property; -+import net.minecraft.world.level.block.state.properties.SlabType; -+import net.minecraft.world.phys.Vec3; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesLogger; -+ -+import javax.annotation.Nullable; -+import java.util.ArrayList; -+import java.util.Comparator; -+import java.util.List; -+ -+public class LitematicaEasyPlaceProtocol { -+ -+ public static final ImmutableSet> WHITELISTED_PROPERTIES = ImmutableSet.of( -+ BlockStateProperties.INVERTED, -+ BlockStateProperties.OPEN, -+ BlockStateProperties.BELL_ATTACHMENT, -+ BlockStateProperties.AXIS, -+ BlockStateProperties.BED_PART, -+ BlockStateProperties.HALF, -+ BlockStateProperties.ATTACH_FACE, -+ BlockStateProperties.CHEST_TYPE, -+ BlockStateProperties.MODE_COMPARATOR, -+ BlockStateProperties.DOOR_HINGE, -+ BlockStateProperties.FACING_HOPPER, -+ BlockStateProperties.HORIZONTAL_FACING, -+ BlockStateProperties.ORIENTATION, -+ BlockStateProperties.RAIL_SHAPE, -+ BlockStateProperties.RAIL_SHAPE_STRAIGHT, -+ BlockStateProperties.SLAB_TYPE, -+ BlockStateProperties.STAIRS_SHAPE, -+ BlockStateProperties.BITES, -+ BlockStateProperties.DELAY, -+ BlockStateProperties.NOTE, -+ BlockStateProperties.ROTATION_16 -+ ); -+ -+ public static BlockState applyPlacementProtocol(BlockState state, BlockPlaceContext context) { -+ return applyPlacementProtocolV3(state, UseContext.from(context, context.getHand())); -+ } -+ -+ @Nullable -+ private static > BlockState applyPlacementProtocolV3(BlockState state, @NotNull UseContext context) { -+ int protocolValue = (int) (context.getHitVec().x - (double) context.getPos().getX()) - 2; -+ BlockState oldState = state; -+ if (protocolValue < 0) { -+ return oldState; -+ } -+ -+ EnumProperty property = CarpetAlternativeBlockPlacement.getFirstDirectionProperty(state); -+ -+ if (property != null && property != BlockStateProperties.VERTICAL_DIRECTION) { -+ state = applyDirectionProperty(state, context, property, protocolValue); -+ -+ if (state == null) { -+ return null; -+ } -+ -+ if (state.canSurvive(context.getWorld(), context.getPos())) { -+ oldState = state; -+ } else { -+ state = oldState; -+ } -+ -+ protocolValue >>>= 3; -+ } -+ -+ protocolValue >>>= 1; -+ -+ List> propList = new ArrayList<>(state.getBlock().getStateDefinition().getProperties()); -+ propList.sort(Comparator.comparing(Property::getName)); -+ -+ try { -+ for (Property p : propList) { -+ if (((p instanceof EnumProperty ep) && !ep.getValueClass().equals(Direction.class)) && WHITELISTED_PROPERTIES.contains(p)) { -+ @SuppressWarnings("unchecked") -+ Property prop = (Property) p; -+ List list = new ArrayList<>(prop.getPossibleValues()); -+ list.sort(Comparable::compareTo); -+ -+ int requiredBits = Mth.log2(Mth.smallestEncompassingPowerOfTwo(list.size())); -+ int bitMask = ~(0xFFFFFFFF << requiredBits); -+ int valueIndex = protocolValue & bitMask; -+ -+ if (valueIndex < list.size()) { -+ T value = list.get(valueIndex); -+ -+ if (!state.getValue(prop).equals(value) && value != SlabType.DOUBLE) { -+ state = state.setValue(prop, value); -+ -+ if (state.canSurvive(context.getWorld(), context.getPos())) { -+ oldState = state; -+ } else { -+ state = oldState; -+ } -+ } -+ -+ protocolValue >>>= requiredBits; -+ } -+ } -+ } -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.warning("Exception trying to apply placement protocol value", e); -+ } -+ -+ if (state.canSurvive(context.getWorld(), context.getPos())) { -+ return state; -+ } else { -+ return null; -+ } -+ } -+ -+ private static BlockState applyDirectionProperty(BlockState state, UseContext context, EnumProperty property, int protocolValue) { -+ Direction facingOrig = state.getValue(property); -+ Direction facing = facingOrig; -+ int decodedFacingIndex = (protocolValue & 0xF) >> 1; -+ -+ if (decodedFacingIndex == 6) { -+ facing = facing.getOpposite(); -+ } else if (decodedFacingIndex <= 5) { -+ facing = Direction.from3DDataValue(decodedFacingIndex); -+ -+ if (!property.getPossibleValues().contains(facing)) { -+ facing = context.getEntity().getDirection().getOpposite(); -+ } -+ } -+ -+ if (facing != facingOrig && property.getPossibleValues().contains(facing)) { -+ if (state.getBlock() instanceof BedBlock) { -+ BlockPos headPos = context.pos.relative(facing); -+ BlockPlaceContext ctx = context.getItemPlacementContext(); -+ -+ if (ctx == null || !context.getWorld().getBlockState(headPos).canBeReplaced(ctx)) { -+ return null; -+ } -+ } -+ -+ state = state.setValue(property, facing); -+ } -+ -+ return state; -+ } -+ -+ public static class UseContext { -+ -+ private final Level world; -+ private final BlockPos pos; -+ private final Direction side; -+ private final Vec3 hitVec; -+ private final LivingEntity entity; -+ private final InteractionHand hand; -+ @Nullable -+ private final BlockPlaceContext itemPlacementContext; -+ -+ private UseContext(Level world, BlockPos pos, Direction side, Vec3 hitVec, LivingEntity entity, InteractionHand hand, @Nullable BlockPlaceContext itemPlacementContext) { -+ this.world = world; -+ this.pos = pos; -+ this.side = side; -+ this.hitVec = hitVec; -+ this.entity = entity; -+ this.hand = hand; -+ this.itemPlacementContext = itemPlacementContext; -+ } -+ -+ @NotNull -+ public static UseContext from(@NotNull BlockPlaceContext ctx, InteractionHand hand) { -+ Vec3 pos = ctx.getClickLocation(); -+ return new UseContext(ctx.getLevel(), ctx.getClickedPos(), ctx.getClickedFace(), new Vec3(pos.x, pos.y, pos.z), ctx.getPlayer(), hand, ctx); -+ } -+ -+ public Level getWorld() { -+ return this.world; -+ } -+ -+ public BlockPos getPos() { -+ return this.pos; -+ } -+ -+ public Direction getSide() { -+ return this.side; -+ } -+ -+ public Vec3 getHitVec() { -+ return this.hitVec; -+ } -+ -+ public LivingEntity getEntity() { -+ return this.entity; -+ } -+ -+ public InteractionHand getHand() { -+ return this.hand; -+ } -+ -+ @Nullable -+ public BlockPlaceContext getItemPlacementContext() { -+ return this.itemPlacementContext; -+ } -+ } -+} diff --git a/patches/server/0037-Stackable-ShulkerBoxes.patch b/patches/server/0037-Stackable-ShulkerBoxes.patch deleted file mode 100644 index 135ecf70..00000000 --- a/patches/server/0037-Stackable-ShulkerBoxes.patch +++ /dev/null @@ -1,464 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 14 Dec 2022 14:47:06 +0800 -Subject: [PATCH] Stackable ShulkerBoxes - -This patch is Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet) and plusls-carpet-addition(https://github.com/plusls/plusls-carpet-addition) - -diff --git a/src/main/java/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java b/src/main/java/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java -index 8d5939e03a065197af125d95a10134abbccd07ec..f6b703a7c46da64b7d789dc85b0973e894ced6de 100644 ---- a/src/main/java/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java -+++ b/src/main/java/net/minecraft/network/protocol/game/ClientboundContainerSetContentPacket.java -@@ -23,7 +23,7 @@ public class ClientboundContainerSetContentPacket implements Packet= 1 && packet.slotNum() <= 45; -- boolean flag2 = itemstack.isEmpty() || itemstack.getCount() <= itemstack.getMaxStackSize(); -+ boolean flag2 = itemstack.isEmpty() || itemstack.getCount() <= org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack); // Leaves - stackable shulker boxes - if (flag || (flag1 && !ItemStack.matches(this.player.inventoryMenu.getSlot(packet.slotNum()).getItem(), packet.itemStack()))) { // Insist on valid slot - // CraftBukkit start - Call click event - InventoryView inventory = this.player.inventoryMenu.getBukkitView(); -diff --git a/src/main/java/net/minecraft/world/Container.java b/src/main/java/net/minecraft/world/Container.java -index 5db5ba026462ca642dcee718af732f80fadabef5..bca47bc78a444011b7e549aba949fea799e50c99 100644 ---- a/src/main/java/net/minecraft/world/Container.java -+++ b/src/main/java/net/minecraft/world/Container.java -@@ -35,6 +35,12 @@ public interface Container extends Clearable { - return Math.min(this.getMaxStackSize(), stack.getMaxStackSize()); - } - -+ // Leaves start - stackable shulker boxes -+ default int getMaxStackLeaves(ItemStack stack) { -+ return Math.min(this.getMaxStackSize(), org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(stack)); -+ } -+ // Leaves end - stackable shulker boxes -+ - void setChanged(); - - boolean stillValid(Player player); -diff --git a/src/main/java/net/minecraft/world/SimpleContainer.java b/src/main/java/net/minecraft/world/SimpleContainer.java -index 7ed52b887c4d766c23220a8809914d5d80f12ea4..8f8ae3f27be586de1413013db3ffca254f93f4eb 100644 ---- a/src/main/java/net/minecraft/world/SimpleContainer.java -+++ b/src/main/java/net/minecraft/world/SimpleContainer.java -@@ -222,7 +222,7 @@ public class SimpleContainer implements Container, StackedContentsCompatible { - @Override - public void setItem(int slot, ItemStack stack) { - this.items.set(slot, stack); -- stack.limitSize(this.getMaxStackSize(stack)); -+ stack.limitSize(this.getMaxStackLeaves(stack)); - this.setChanged(); - } - -@@ -318,7 +318,7 @@ public class SimpleContainer implements Container, StackedContentsCompatible { - } - - private void moveItemsBetweenStacks(ItemStack source, ItemStack target) { -- int i = this.getMaxStackSize(target); -+ int i = this.getMaxStackLeaves(target); // Leaves - stackable shulker boxes - int j = Math.min(source.getCount(), i - target.getCount()); - - if (j > 0) { -diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -index 0f086af57a5ff08c264dcbf89a8c3931ec73a609..8f2ca7211d4ebf494beae85caea5876fad4723eb 100644 ---- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -+++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -@@ -12,11 +12,13 @@ import net.minecraft.world.entity.Mob; - import net.minecraft.world.entity.MoverType; - import net.minecraft.world.entity.SlotAccess; - import net.minecraft.world.entity.TraceableEntity; -+import net.minecraft.world.item.BlockItem; - import net.minecraft.world.item.Item; - import net.minecraft.world.item.ItemStack; - import net.minecraft.world.level.Explosion; - import net.minecraft.world.level.GameRules; - import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.ShulkerBoxBlock; - import net.minecraft.world.level.gameevent.GameEvent; - import net.minecraft.world.level.portal.TeleportTransition; - import net.minecraft.world.phys.Vec3; -@@ -36,6 +38,7 @@ import net.minecraft.tags.ItemTags; - import net.minecraft.util.Mth; - import org.bukkit.craftbukkit.event.CraftEventFactory; - import org.bukkit.entity.Player; -+import org.bukkit.block.ShulkerBox; - import org.bukkit.event.entity.EntityPickupItemEvent; - import org.bukkit.event.entity.EntityRemoveEvent; - import org.bukkit.event.player.PlayerPickupItemEvent; -@@ -317,10 +320,49 @@ public class ItemEntity extends Entity implements TraceableEntity { - private boolean isMergable() { - ItemStack itemstack = this.getItem(); - -- return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < this.despawnRate && itemstack.getCount() < itemstack.getMaxStackSize(); // Paper - Alternative item-despawn-rate -+ return this.isAlive() && this.pickupDelay != 32767 && this.age != -32768 && this.age < this.despawnRate && itemstack.getCount() < org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack); // Paper - Alternative item-despawn-rate // Leaves - stackable shulker boxes - } - -+ // Leaves end - stackable shulker boxes -+ private boolean tryStackShulkerBoxes(ItemEntity other) { -+ ItemStack selfStack = this.getItem(); -+ if (org.leavesmc.leaves.LeavesConfig.modify.shulkerBoxStackSize == 1 || !(selfStack.getItem() instanceof BlockItem bi) || !(bi.getBlock() instanceof ShulkerBoxBlock)) { -+ return false; -+ } -+ -+ ItemStack otherStack = other.getItem(); -+ if (selfStack.getItem() == otherStack.getItem() -+ && org.leavesmc.leaves.util.ShulkerBoxUtils.shulkerBoxNoItem(selfStack) -+ && org.leavesmc.leaves.util.ShulkerBoxUtils.shulkerBoxNoItem(otherStack) -+ && Objects.equals(selfStack.getComponents(), otherStack.getComponents()) // empty block entity tags are cleaned up when spawning -+ && selfStack.getCount() != org.leavesmc.leaves.LeavesConfig.modify.shulkerBoxStackSize) { -+ int amount = Math.min(otherStack.getCount(), org.leavesmc.leaves.LeavesConfig.modify.shulkerBoxStackSize - selfStack.getCount()); -+ -+ selfStack.grow(amount); -+ this.setItem(selfStack); -+ -+ this.pickupDelay = Math.max(other.pickupDelay, this.pickupDelay); -+ this.age = Math.min(other.getAge(), this.age); -+ -+ otherStack.shrink(amount); -+ if (otherStack.isEmpty()) { -+ other.discard(); -+ } -+ else { -+ other.setItem(otherStack); -+ } -+ return true; -+ } -+ return false; -+ } -+ // Leaves end - stackable shulker boxes -+ - private void tryToMerge(ItemEntity other) { -+ // Leaves start - stackable shulker boxes -+ if (tryStackShulkerBoxes(other)) { -+ return; -+ } -+ // Leaves end - stackable shulker boxes - ItemStack itemstack = this.getItem(); - ItemStack itemstack1 = other.getItem(); - -diff --git a/src/main/java/net/minecraft/world/entity/player/Inventory.java b/src/main/java/net/minecraft/world/entity/player/Inventory.java -index d2bd4c5b2a7a9689680f4b021368f25356c58ac7..c07dae6cf6cd60a1f100415977dd9fe2ce639d72 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Inventory.java -+++ b/src/main/java/net/minecraft/world/entity/player/Inventory.java -@@ -110,7 +110,7 @@ public class Inventory implements Container, Nameable { - } - - private boolean hasRemainingSpaceForItem(ItemStack existingStack, ItemStack stack) { -- return !existingStack.isEmpty() && existingStack.isStackable() && existingStack.getCount() < this.getMaxStackSize(existingStack) && ItemStack.isSameItemSameComponents(existingStack, stack); // Paper - check if itemstack is stackable first -+ return !existingStack.isEmpty() && org.leavesmc.leaves.util.ShulkerBoxUtils.isStackable(existingStack) && existingStack.getCount() < org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(existingStack) && ItemStack.isSameItemSameComponents(existingStack, stack); // Paper - check if itemstack is stackable first - } - - // CraftBukkit start - Watch method above! :D -@@ -121,13 +121,13 @@ public class Inventory implements Container, Nameable { - if (itemstack1.isEmpty()) return itemstack.getCount(); - - if (this.hasRemainingSpaceForItem(itemstack1, itemstack)) { -- remains -= (itemstack1.getMaxStackSize() < this.getMaxStackSize() ? itemstack1.getMaxStackSize() : this.getMaxStackSize()) - itemstack1.getCount(); -+ remains -= (org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack1) < this.getMaxStackSize() ? org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack1) : this.getMaxStackSize()) - itemstack1.getCount(); // Leaves - } - if (remains <= 0) return itemstack.getCount(); - } - ItemStack offhandItemStack = this.getItem(this.items.size() + this.armor.size()); - if (this.hasRemainingSpaceForItem(offhandItemStack, itemstack)) { -- remains -= (offhandItemStack.getMaxStackSize() < this.getMaxStackSize() ? offhandItemStack.getMaxStackSize() : this.getMaxStackSize()) - offhandItemStack.getCount(); -+ remains -= (org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(offhandItemStack) < this.getMaxStackSize() ? org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(offhandItemStack) : this.getMaxStackSize()) - offhandItemStack.getCount(); // Leaves - } - if (remains <= 0) return itemstack.getCount(); - -@@ -273,7 +273,9 @@ public class Inventory implements Container, Nameable { - this.setItem(slot, itemstack1); - } - -- int k = this.getMaxStackSize(itemstack1) - itemstack1.getCount(); -+ // Leaves start - stackable shulker boxes -+ int k = org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack1) - itemstack1.getCount(); -+ // Leaves end - stackable shulker boxes - int l = Math.min(j, k); - - if (l == 0) { -@@ -388,7 +390,7 @@ public class Inventory implements Container, Nameable { - } - - if (i != -1) { -- int j = stack.getMaxStackSize() - this.getItem(i).getCount(); -+ int j = org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(stack) - this.getItem(i).getCount(); // Leaves - stackable shulker boxes - - if (!this.add(i, stack.split(j)) || !notifiesClient) { - continue; -diff --git a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java -index 45f6191cc8e2ecdacbc2df0ddb5ea7cc6a546812..02cfa01c85089c3aeee5288bff56cb4fa842dab6 100644 ---- a/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java -+++ b/src/main/java/net/minecraft/world/entity/vehicle/ContainerEntity.java -@@ -165,7 +165,7 @@ public interface ContainerEntity extends Container, MenuProvider { - default void setChestVehicleItem(int slot, ItemStack stack) { - this.unpackChestVehicleLootTable(null); - this.getItemStacks().set(slot, stack); -- stack.limitSize(this.getMaxStackSize(stack)); -+ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes - } - - default SlotAccess getChestVehicleSlot(int slot) { -diff --git a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -index a21c658343ab6e1eb3a98ff10369b490bd7d52da..8d9bcffdfe722dbe0fb4503cc2c2ead47d338acb 100644 ---- a/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/AbstractContainerMenu.java -@@ -493,7 +493,7 @@ public abstract class AbstractContainerMenu { - - if (slot1 != null && AbstractContainerMenu.canItemQuickReplace(slot1, itemstack2, true) && slot1.mayPlace(itemstack2) && (this.quickcraftType == 2 || itemstack2.getCount() >= this.quickcraftSlots.size()) && this.canDragTo(slot1)) { - int j1 = slot1.hasItem() ? slot1.getItem().getCount() : 0; -- int k1 = Math.min(itemstack1.getMaxStackSize(), slot1.getMaxStackSize(itemstack1)); -+ int k1 = Math.min(org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack1), slot1.getMaxStackSize(itemstack1)); // Leaves - stackable shulker boxes int l1 = Math.min(AbstractContainerMenu.getQuickCraftPlaceCount(this.quickcraftSlots, this.quickcraftType, itemstack1) + j1, k1); - int l1 = Math.min(AbstractContainerMenu.getQuickCraftPlaceCount(this.quickcraftSlots, this.quickcraftType, itemstack1) + j1, k1); - - l -= l1 - j1; -@@ -612,7 +612,7 @@ public abstract class AbstractContainerMenu { - slot.setByPlayer(itemstack3); - } - } else if (ItemStack.isSameItemSameComponents(itemstack, itemstack3)) { -- Optional optional1 = slot.tryRemove(itemstack.getCount(), itemstack3.getMaxStackSize() - itemstack3.getCount(), player); -+ Optional optional1 = slot.tryRemove(itemstack.getCount(), org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack3) - itemstack3.getCount(), player); - - optional1.ifPresent((itemstack4) -> { - itemstack3.grow(itemstack4.getCount()); -@@ -682,7 +682,7 @@ public abstract class AbstractContainerMenu { - slot2 = (Slot) this.slots.get(slotIndex); - if (slot2.hasItem()) { - itemstack1 = slot2.getItem(); -- this.setCarried(itemstack1.copyWithCount(itemstack1.getMaxStackSize())); -+ this.setCarried(itemstack1.copyWithCount(org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack1))); // Leaves - stackable shulker boxes - } - } else if (actionType == ClickType.THROW && this.getCarried().isEmpty() && slotIndex >= 0) { - slot2 = (Slot) this.slots.get(slotIndex); -@@ -713,13 +713,13 @@ public abstract class AbstractContainerMenu { - j2 = button == 0 ? 1 : -1; - - for (i2 = 0; i2 < 2; ++i2) { -- for (int k2 = l; k2 >= 0 && k2 < this.slots.size() && itemstack1.getCount() < itemstack1.getMaxStackSize(); k2 += j2) { -+ for (int k2 = l; k2 >= 0 && k2 < this.slots.size() && itemstack1.getCount() < org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack1); k2 += j2) { // Leaves - stackable shulker boxes - Slot slot3 = (Slot) this.slots.get(k2); - - if (slot3.hasItem() && AbstractContainerMenu.canItemQuickReplace(slot3, itemstack1, true) && slot3.mayPickup(player) && this.canTakeItemForPickAll(itemstack1, slot3)) { - ItemStack itemstack5 = slot3.getItem(); - -- if (i2 != 0 || itemstack5.getCount() != itemstack5.getMaxStackSize()) { -+ if (i2 != 0 || itemstack5.getCount() != org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack5)) { - ItemStack itemstack6 = slot3.safeTake(itemstack5.getCount(), itemstack1.getMaxStackSize() - itemstack1.getCount(), player); - - itemstack1.grow(itemstack6.getCount()); -@@ -868,7 +868,7 @@ public abstract class AbstractContainerMenu { - ItemStack itemstack1; - int l; - -- if (stack.isStackable()) { -+ if (org.leavesmc.leaves.util.ShulkerBoxUtils.isStackable(stack)) { // Leaves - stackable shulker boxes - while (!stack.isEmpty()) { - if (fromLast) { - if (k < startIndex) { -@@ -887,6 +887,7 @@ public abstract class AbstractContainerMenu { - // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - if (!itemstack1.isEmpty() && ItemStack.isSameItemSameComponents(stack, itemstack1)) { - l = itemstack1.getCount() + stack.getCount(); -+ // int i1 = org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack1); // Leaves - stackable shulker boxes // disabled temporarily - int i1 = slot.getMaxStackSize(itemstack1); - - if (l <= i1) { -@@ -938,6 +939,7 @@ public abstract class AbstractContainerMenu { - } - // Paper end - Add PlayerTradeEvent and PlayerPurchaseEvent - if (itemstack1.isEmpty() && slot.mayPlace(stack)) { -+ // l = org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(itemstack1); // Leaves - stackable shulker boxes // disabled temporarily - l = slot.getMaxStackSize(stack); - // Paper start - Add PlayerTradeEvent and PlayerPurchaseEvent - if (isCheck) { -diff --git a/src/main/java/net/minecraft/world/inventory/MerchantContainer.java b/src/main/java/net/minecraft/world/inventory/MerchantContainer.java -index 9140fab07aab32065f7a3b5d13dd17d61dc6d646..210b88b5d87fad641188cf5907c54824d21163ce 100644 ---- a/src/main/java/net/minecraft/world/inventory/MerchantContainer.java -+++ b/src/main/java/net/minecraft/world/inventory/MerchantContainer.java -@@ -130,7 +130,7 @@ public class MerchantContainer implements Container { - @Override - public void setItem(int slot, ItemStack stack) { - this.itemStacks.set(slot, stack); -- stack.limitSize(this.getMaxStackSize(stack)); -+ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes - if (this.isPaymentSlot(slot)) { - this.updateSellItem(); - } -diff --git a/src/main/java/net/minecraft/world/inventory/Slot.java b/src/main/java/net/minecraft/world/inventory/Slot.java -index 0bdbd192f9eb2d6d529dc5b2baee9b4f20885b9c..7851247575321784aa13b707a7d6b945b4f24587 100644 ---- a/src/main/java/net/minecraft/world/inventory/Slot.java -+++ b/src/main/java/net/minecraft/world/inventory/Slot.java -@@ -76,7 +76,7 @@ public class Slot { - } - - public int getMaxStackSize(ItemStack stack) { -- return Math.min(this.getMaxStackSize(), stack.getMaxStackSize()); -+ return Math.min(this.getMaxStackSize(), org.leavesmc.leaves.util.ShulkerBoxUtils.getItemStackMaxCount(stack)); // Leaves - stackable shulker boxes - } - - @Nullable -diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java -index 33e7d2884195677c4d6340d8b84c1dd85c636ec1..f38487562422ebaf59a679f493f956b90cc18ff1 100644 ---- a/src/main/java/net/minecraft/world/item/ItemStack.java -+++ b/src/main/java/net/minecraft/world/item/ItemStack.java -@@ -224,7 +224,7 @@ public final class ItemStack implements DataComponentHolder { - @Deprecated - @Nullable - private Item item; -- private PatchedDataComponentMap components; -+ public PatchedDataComponentMap components; // Leaves - stackable shulker boxes - @Nullable - private Entity entityRepresentation; - -diff --git a/src/main/java/net/minecraft/world/level/block/AbstractCauldronBlock.java b/src/main/java/net/minecraft/world/level/block/AbstractCauldronBlock.java -index e00ab1ed8088a1970249313ed63e09070fc6192d..89cfabda335d27d3657299c69837e29057617052 100644 ---- a/src/main/java/net/minecraft/world/level/block/AbstractCauldronBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/AbstractCauldronBlock.java -@@ -56,9 +56,27 @@ public abstract class AbstractCauldronBlock extends Block { - @Override - protected InteractionResult useItemOn(ItemStack stack, BlockState state, Level world, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { - CauldronInteraction cauldronInteraction = this.interactions.map().get(stack.getItem()); -- return cauldronInteraction.interact(state, world, pos, player, hand, stack, hit.getDirection()); // Paper - pass hit direction -+ return wrapInteractor(cauldronInteraction, state, world, pos, player, hand, stack, hit.getDirection()); // Paper - pass hit direction // Leaves - stackable shulker boxes - } - -+ // Leaves start - stackable shulker boxes -+ private InteractionResult wrapInteractor(CauldronInteraction cauldronBehavior, BlockState blockState, Level world, BlockPos blockPos, Player playerEntity, InteractionHand hand, ItemStack itemStack, net.minecraft.core.Direction hitDirection) { -+ int count = -1; -+ if (org.leavesmc.leaves.LeavesConfig.modify.shulkerBoxStackSize > 1 && itemStack.getItem() instanceof net.minecraft.world.item.BlockItem bi && -+ bi.getBlock() instanceof ShulkerBoxBlock) { -+ count = itemStack.getCount(); -+ } -+ InteractionResult result = cauldronBehavior.interact(blockState, world, blockPos, playerEntity, hand, itemStack, hitDirection); -+ if (count > 0 && result.consumesAction()) { -+ ItemStack current = playerEntity.getItemInHand(hand); -+ if (current.getItem() instanceof net.minecraft.world.item.BlockItem bi && bi.getBlock() instanceof ShulkerBoxBlock) { -+ current.setCount(count); -+ } -+ } -+ return result; -+ } -+ // Leaves end - stackable shulker boxes -+ - @Override - protected VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) { - return SHAPE; -diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -index 82e93a63ea64adbf648ea7b8a844a376bbe63597..46bdb85f18077c77c445ce0bc3cfe32468c75306 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -@@ -426,7 +426,7 @@ public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntit - boolean flag = !stack.isEmpty() && ItemStack.isSameItemSameComponents(itemstack1, stack); - - this.items.set(slot, stack); -- stack.limitSize(this.getMaxStackSize(stack)); -+ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes - if (slot == 0 && !flag) { - Level world = this.level; - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java -index 1f29b2419914ca9257db6553f01b7e7ec49bfc18..9d9a7c972119b224089f062d9c64c689e3d47bcc 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/BaseContainerBlockEntity.java -@@ -158,7 +158,7 @@ public abstract class BaseContainerBlockEntity extends BlockEntity implements Co - @Override - public void setItem(int slot, ItemStack stack) { - this.getItems().set(slot, stack); -- stack.limitSize(this.getMaxStackSize(stack)); -+ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes - this.setChanged(); - } - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -index 02bda85189fd57bd3f6d18cfd573553b2e607300..ad5a971a4de217aa93601db8364f2cad1aab4c38 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -@@ -125,7 +125,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - public void setItem(int slot, ItemStack stack) { - this.unpackLootTable((Player) null); - this.getItems().set(slot, stack); -- stack.limitSize(this.getMaxStackSize(stack)); -+ stack.limitSize(this.getMaxStackLeaves(stack)); // Leaves - stackable shulker boxes - } - - @Override -@@ -753,9 +753,9 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - if (itemstack1.isEmpty()) { - // Spigot start - SPIGOT-6693, InventorySubcontainer#setItem - ItemStack leftover = ItemStack.EMPTY; // Paper - Make hoppers respect inventory max stack size -- if (!stack.isEmpty() && stack.getCount() > to.getMaxStackSize()) { -+ if (!stack.isEmpty() && (stack.getCount() > to.getMaxStackSize() || stack.getCount() > stack.getMaxStackSize())) { // Leaves - stackable shulker boxes - leftover = stack; // Paper - Make hoppers respect inventory max stack size -- stack = stack.split(to.getMaxStackSize()); -+ stack = stack.split(Math.min(to.getMaxStackSize(), stack.getMaxStackSize())); // Leaves - stackable shulker boxes - } - // Spigot end - ignoreTileUpdates = true; // Paper - Perf: Optimize Hoppers -diff --git a/src/main/java/org/leavesmc/leaves/util/ShulkerBoxUtils.java b/src/main/java/org/leavesmc/leaves/util/ShulkerBoxUtils.java -new file mode 100644 -index 0000000000000000000000000000000000000000..07ed58fba9f8eb0f66c5aa6bd216fc02bbc5a5b3 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/ShulkerBoxUtils.java -@@ -0,0 +1,39 @@ -+package org.leavesmc.leaves.util; -+ -+import net.minecraft.core.component.DataComponents; -+import net.minecraft.world.item.BlockItem; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.component.ItemContainerContents; -+import net.minecraft.world.level.block.ShulkerBoxBlock; -+import org.bukkit.craftbukkit.inventory.CraftItemStack; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+ -+public class ShulkerBoxUtils { -+ -+ public static boolean shulkerBoxNoItem(@NotNull ItemStack stack) { -+ return stack.getComponents().getOrDefault(DataComponents.CONTAINER, ItemContainerContents.EMPTY).stream().findAny().isEmpty(); -+ } -+ -+ public static int getItemStackMaxCount(ItemStack stack) { -+ if (LeavesConfig.modify.shulkerBoxStackSize > 1 && stack.getItem() instanceof BlockItem bi && -+ bi.getBlock() instanceof ShulkerBoxBlock && shulkerBoxNoItem(stack)) { -+ return LeavesConfig.modify.shulkerBoxStackSize; -+ } -+ return stack.getMaxStackSize(); -+ } -+ -+ public static ItemStack correctItemStackMaxStackSize(ItemStack itemStack) { -+ int trulyMaxStackSize = getItemStackMaxCount(itemStack); -+ if (itemStack.getMaxStackSize() != trulyMaxStackSize) { -+ org.bukkit.inventory.ItemStack bkStack = CraftItemStack.asBukkitCopy(itemStack); -+ bkStack.editMeta(meta -> meta.setMaxStackSize(trulyMaxStackSize)); -+ itemStack = CraftItemStack.asNMSCopy(bkStack); -+ } -+ return itemStack; -+ } -+ -+ public static boolean isStackable(ItemStack itemStack) { -+ return getItemStackMaxCount(itemStack) > 1 && (!itemStack.isDamageableItem() || !itemStack.isDamaged()); -+ } -+} diff --git a/patches/server/0038-MC-Technical-Survival-Mode.patch b/patches/server/0038-MC-Technical-Survival-Mode.patch deleted file mode 100644 index dc13c7e9..00000000 --- a/patches/server/0038-MC-Technical-Survival-Mode.patch +++ /dev/null @@ -1,126 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 19 Jan 2023 23:38:50 +0800 -Subject: [PATCH] MC Technical Survival Mode - -Will automatically overwrite some configuration after startup - -diff --git a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java -index c5644d8d64f12073e39bc6ed79c8714f4560ff89..e2e5fdf539f69629d13e50ab08b3fc38474e3384 100644 ---- a/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java -+++ b/src/main/java/io/papermc/paper/configuration/PaperConfigurations.java -@@ -321,6 +321,7 @@ public class PaperConfigurations extends Configurations 0 && ++this.level().spigotConfig.currentPrimedTnt > this.level().spigotConfig.maxTntTicksPerTick) { return; } // Spigot -+ if (this.level().spigotConfig.maxTntTicksPerTick > 0 && ++this.level().spigotConfig.currentPrimedTnt > (org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode ? 2000 : this.level().spigotConfig.maxTntTicksPerTick)) { return; } // Spigot // Leaves - mc technical survival mode - this.handlePortal(); - this.applyGravity(); - this.move(MoverType.SELF, this.getDeltaMovement()); -diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -index c1b76a1ebc1eea7ab70cf61d8175a31794dd122a..f817fd922ffcf857e8a5fc803b10421f640a8cd1 100644 ---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java -+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -@@ -94,7 +94,7 @@ public final class NaturalSpawner { - - if (enumcreaturetype != MobCategory.MISC) { - // Paper start - Only count natural spawns -- if (!entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && -+ if (!org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode && !entity.level().paperConfig().entities.spawning.countAllMobsForSpawning && // Leaves - mc technical survival mode - !(entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.NATURAL || - entity.spawnReason == org.bukkit.event.entity.CreatureSpawnEvent.SpawnReason.CHUNK_GEN)) { - continue; -diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -index ad5a971a4de217aa93601db8364f2cad1aab4c38..7280d2216dcbe09278a2447eb994c4bd2aa98576 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -@@ -295,7 +295,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - origItemStack.setCount(originalItemCount); - } - } -- if (foundItem && level.paperConfig().hopper.cooldownWhenFull) { // Inventory was full - cooldown -+ if (foundItem && level.paperConfig().hopper.cooldownWhenFull && !org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode) { // Inventory was full - cooldown // Leaves - hopper.setCooldown(level.spigotConfig.hopperTransfer); - } - return false; -@@ -336,7 +336,7 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - } - origItemStack.setCount(originalItemCount); - -- if (level.paperConfig().hopper.cooldownWhenFull) { -+ if (level.paperConfig().hopper.cooldownWhenFull && !org.leavesmc.leaves.LeavesConfig.modify.mcTechnicalMode) { // Leaves - cooldownHopper(hopper); - } - -diff --git a/src/main/java/org/leavesmc/leaves/util/McTechnicalModeHelper.java b/src/main/java/org/leavesmc/leaves/util/McTechnicalModeHelper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0d8e5bf5f37357f2ed619e5574a8ce01589fe63e ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/McTechnicalModeHelper.java -@@ -0,0 +1,27 @@ -+package org.leavesmc.leaves.util; -+ -+import io.papermc.paper.configuration.GlobalConfiguration; -+import org.leavesmc.leaves.LeavesConfig; -+ -+import java.util.Map; -+ -+public class McTechnicalModeHelper { -+ -+ public static void doMcTechnicalModeIf() { -+ if (LeavesConfig.modify.mcTechnicalMode) { -+ doMcTechnicalMode(); -+ } -+ } -+ -+ public static void doMcTechnicalMode() { -+ GlobalConfiguration.get().unsupportedSettings.allowPistonDuplication = true; -+ GlobalConfiguration.get().unsupportedSettings.allowHeadlessPistons = true; -+ GlobalConfiguration.get().unsupportedSettings.allowPermanentBlockBreakExploits = true; -+ GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation = true; -+ GlobalConfiguration.get().packetLimiter.allPackets = new GlobalConfiguration.PacketLimiter.PacketLimit(GlobalConfiguration.get().packetLimiter.allPackets.interval(), -+ 5000.0, GlobalConfiguration.get().packetLimiter.allPackets.action()); -+ GlobalConfiguration.get().packetLimiter.overrides = Map.of(); -+ GlobalConfiguration.get().itemValidation.resolveSelectorsInBooks = true; -+ GlobalConfiguration.get().scoreboards.saveEmptyScoreboardTeams = true; -+ } -+} diff --git a/patches/server/0039-Return-nether-portal-fix.patch b/patches/server/0039-Return-nether-portal-fix.patch deleted file mode 100644 index 7d123197..00000000 --- a/patches/server/0039-Return-nether-portal-fix.patch +++ /dev/null @@ -1,203 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 24 Jan 2023 12:38:42 +0800 -Subject: [PATCH] Return nether portal fix - -This patch is powered by NetherPortalFix(https://github.com/TwelveIterationMods/NetherPortalFix) - -diff --git a/src/main/java/net/minecraft/server/level/ServerPlayer.java b/src/main/java/net/minecraft/server/level/ServerPlayer.java -index b0c1cc7196dae695b73eb766c5acbffeffd306b3..f80911edce05c7440ead01a2332452e20ed734c1 100644 ---- a/src/main/java/net/minecraft/server/level/ServerPlayer.java -+++ b/src/main/java/net/minecraft/server/level/ServerPlayer.java -@@ -1699,6 +1699,21 @@ public class ServerPlayer extends net.minecraft.world.entity.player.Player imple - PlayerChangedWorldEvent changeEvent = new PlayerChangedWorldEvent(this.getBukkitEntity(), worldserver1.getWorld()); - this.level().getCraftServer().getPluginManager().callEvent(changeEvent); - // CraftBukkit end -+ // Leaves start - nether portal fix -+ if (org.leavesmc.leaves.LeavesConfig.modify.netherPortalFix) { -+ final ResourceKey fromDim = worldserver1.dimension(); -+ final ResourceKey toDim = level().dimension(); -+ final ResourceKey OVERWORLD = Level.OVERWORLD; -+ final ResourceKey THE_NETHER = Level.NETHER; -+ if (!((fromDim != OVERWORLD || toDim != THE_NETHER) && (fromDim != THE_NETHER || toDim != OVERWORLD))) { -+ BlockPos fromPortal = org.leavesmc.leaves.util.ReturnPortalManager.findPortalAt(this, fromDim, lastPos); -+ BlockPos toPos = this.blockPosition(); -+ if (fromPortal != null) { -+ org.leavesmc.leaves.util.ReturnPortalManager.storeReturnPortal(this, toDim, toPos, fromPortal); -+ } -+ } -+ } -+ // Leaves end - nether portal fix - // Paper start - Reset shield blocking on dimension change - if (this.isBlocking()) { - this.stopUsingItem(); -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 7a741442ee2373f1a400214047bbf5844efecce6..eb43b1fcda50326f086999a6e134c9807b837045 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -909,6 +909,24 @@ public abstract class PlayerList { - if (fromWorld != worldserver) { - PlayerChangedWorldEvent event = new PlayerChangedWorldEvent(entityplayer.getBukkitEntity(), fromWorld.getWorld()); - this.server.server.getPluginManager().callEvent(event); -+ // Leaves start - nether portal fix -+ if (org.leavesmc.leaves.LeavesConfig.modify.netherPortalFix) { -+ final ResourceKey fromDim = entityplayer.level().dimension(); -+ final ResourceKey toDim = entityplayer1.level().dimension(); -+ final ResourceKey OVERWORLD = Level.OVERWORLD; -+ final ResourceKey THE_NETHER = Level.NETHER; -+ if (!((fromDim != OVERWORLD || toDim != THE_NETHER) && (fromDim != THE_NETHER || toDim != OVERWORLD))) { -+ BlockPos lastPos = entityplayer1.lastPos; -+ if (lastPos != null) { -+ net.minecraft.BlockUtil.FoundRectangle fromPortal = ReturnPortalManager.findPortalAt(entityplayer1, fromDim, lastPos); -+ BlockPos toPos = entityplayer1.blockPosition(); -+ if (fromPortal != null) { -+ ReturnPortalManager.storeReturnPortal(entityplayer1, toDim, toPos, fromPortal); -+ } -+ } -+ } -+ } -+ // Leaves end - nether portal fix - } - - // Save player file again if they were disconnected -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 3002879d96916ce24400159792b48e5f9b8e2e3e..bc0eeb8274a7248dca7f01ddd89c9a7e481da75b 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -272,7 +272,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - protected ItemStack useItem; - public int useItemRemaining; - protected int fallFlyTicks; -- private BlockPos lastPos; -+ public BlockPos lastPos; // Leaves - private -> public - private Optional lastClimbablePos; - @Nullable - private DamageSource lastDamageSource; -diff --git a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java -index 2b31bf586c1c0bd393d2aa8d0b6635dd9f22f21c..0a9ce12e958ac25a4d789254e1fec9ec2c2a33ca 100644 ---- a/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/NetherPortalBlock.java -@@ -180,7 +180,18 @@ public class NetherPortalBlock extends Block implements Portal { - - @Nullable - private TeleportTransition getExitPortal(ServerLevel worldserver, Entity entity, BlockPos blockposition, BlockPos blockposition1, boolean flag, WorldBorder worldborder, int searchRadius, boolean canCreatePortal, int createRadius) { -- Optional optional = worldserver.getPortalForcer().findClosestPortalPosition(blockposition1, worldborder, searchRadius); -+ // Leaves start - fix return portal -+ Optional optional = Optional.empty(); -+ if (org.leavesmc.leaves.LeavesConfig.modify.netherPortalFix && entity instanceof net.minecraft.server.level.ServerPlayer player) { -+ org.leavesmc.leaves.util.ReturnPortalManager.ReturnPortal portal = org.leavesmc.leaves.util.ReturnPortalManager.findReturnPortal(player, entity.level().dimension(), entity.blockPosition()); -+ if (portal != null && worldserver.getBlockState(portal.pos()).is(Blocks.NETHER_PORTAL)) { -+ optional = Optional.of(portal.pos()); -+ } -+ } -+ if (optional.isEmpty()) { -+ optional = worldserver.getPortalForcer().findClosestPortalPosition(blockposition1, worldborder, searchRadius); -+ } -+ // Leaves end - fix return portal - BlockUtil.FoundRectangle blockutil_rectangle; - TeleportTransition.PostTeleportTransition teleporttransition_a; - -diff --git a/src/main/java/org/leavesmc/leaves/util/ReturnPortalManager.java b/src/main/java/org/leavesmc/leaves/util/ReturnPortalManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..67eb48e5b30d1dfa93aacbeb31f1b650b5ecb763 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/ReturnPortalManager.java -@@ -0,0 +1,98 @@ -+package org.leavesmc.leaves.util; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.ListTag; -+import net.minecraft.nbt.Tag; -+import net.minecraft.resources.ResourceKey; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.portal.PortalForcer; -+import org.jetbrains.annotations.Nullable; -+ -+import java.util.UUID; -+ -+// Powered by NetherPortalFix(https://github.com/TwelveIterationMods/NetherPortalFix) -+public class ReturnPortalManager { -+ -+ private static final int MAX_PORTAL_DISTANCE_SQ = 16; -+ private static final String RETURN_PORTAL_LIST = "ReturnPortalList"; -+ private static final String RETURN_PORTAL_UID = "UID"; -+ private static final String FROM_DIM = "FromDim"; -+ private static final String FROM_POS = "FromPos"; -+ private static final String TO_POS = "ToPos"; -+ -+ public static BlockPos findPortalAt(Player player, ResourceKey dim, BlockPos pos) { -+ MinecraftServer server = player.level().getServer(); -+ if (server != null) { -+ ServerLevel fromWorld = server.getLevel(dim); -+ if (fromWorld != null) { -+ PortalForcer portalForcer = fromWorld.getPortalForcer(); -+ return portalForcer.findClosestPortalPosition(pos, false, fromWorld.getWorldBorder()).orElse(null); -+ } -+ } -+ -+ return null; -+ } -+ -+ public static ListTag getPlayerPortalList(Player player) { -+ CompoundTag data = player.getLeavesData(); -+ ListTag list = data.getList(RETURN_PORTAL_LIST, Tag.TAG_COMPOUND); -+ data.put(RETURN_PORTAL_LIST, list); -+ return list; -+ } -+ -+ @Nullable -+ public static ReturnPortal findReturnPortal(ServerPlayer player, ResourceKey fromDim, BlockPos fromPos) { -+ ListTag portalList = getPlayerPortalList(player); -+ for (Tag entry : portalList) { -+ CompoundTag portal = (CompoundTag) entry; -+ ResourceKey entryFromDim = ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(portal.getString(FROM_DIM))); -+ if (entryFromDim == fromDim) { -+ BlockPos portalTrigger = BlockPos.of(portal.getLong(FROM_POS)); -+ if (portalTrigger.distSqr(fromPos) <= MAX_PORTAL_DISTANCE_SQ) { -+ final var uid = portal.hasUUID(RETURN_PORTAL_UID) ? portal.getUUID(RETURN_PORTAL_UID) : UUID.randomUUID(); -+ final var pos = BlockPos.of(portal.getLong(TO_POS)); -+ return new ReturnPortal(uid, pos); -+ } -+ } -+ } -+ -+ return null; -+ } -+ -+ public static void storeReturnPortal(ServerPlayer player, ResourceKey fromDim, BlockPos fromPos, BlockPos toPos) { -+ ListTag portalList = getPlayerPortalList(player); -+ ReturnPortal returnPortal = findReturnPortal(player, fromDim, fromPos); -+ if (returnPortal != null) { -+ removeReturnPortal(player, returnPortal); -+ } -+ -+ CompoundTag portalCompound = new CompoundTag(); -+ portalCompound.putUUID(RETURN_PORTAL_UID, UUID.randomUUID()); -+ portalCompound.putString(FROM_DIM, String.valueOf(fromDim.location())); -+ portalCompound.putLong(FROM_POS, fromPos.asLong()); -+ portalCompound.putLong(TO_POS, toPos.asLong()); -+ portalList.add(portalCompound); -+ } -+ -+ public static void removeReturnPortal(ServerPlayer player, ReturnPortal portal) { -+ // This doesn't check if it's the right toDim, but it's probably so rare for positions to actually overlap that I don't care -+ ListTag portalList = getPlayerPortalList(player); -+ for (int i = 0; i < portalList.size(); i++) { -+ CompoundTag entry = (CompoundTag) portalList.get(i); -+ if (entry.hasUUID(RETURN_PORTAL_UID) && entry.getUUID(RETURN_PORTAL_UID).equals(portal.uid)) { -+ portalList.remove(i); -+ break; -+ } -+ } -+ } -+ -+ public record ReturnPortal(UUID uid, BlockPos pos) { -+ } -+} diff --git a/patches/server/0040-Appleskin-Protocol.patch b/patches/server/0040-Appleskin-Protocol.patch deleted file mode 100644 index b9f002ec..00000000 --- a/patches/server/0040-Appleskin-Protocol.patch +++ /dev/null @@ -1,139 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 25 Jan 2023 11:03:53 +0800 -Subject: [PATCH] Appleskin Protocol - - -diff --git a/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..dd8afa5a47e62829788c4d49d18e4436daa242dd ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java -@@ -0,0 +1,127 @@ -+package org.leavesmc.leaves.protocol; -+ -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.food.FoodData; -+import net.minecraft.world.level.GameRules; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+ -+@LeavesProtocol(namespace = "appleskin") -+public class AppleSkinProtocol { -+ -+ public static final String PROTOCOL_ID = "appleskin"; -+ -+ private static final ResourceLocation SATURATION_KEY = id("saturation"); -+ private static final ResourceLocation EXHAUSTION_KEY = id("exhaustion"); -+ private static final ResourceLocation NATURAL_REGENERATION_KEY = id("natural_regeneration"); -+ -+ private static final float MINIMUM_EXHAUSTION_CHANGE_THRESHOLD = 0.01F; -+ -+ private static final Map previousSaturationLevels = new HashMap<>(); -+ private static final Map previousExhaustionLevels = new HashMap<>(); -+ private static final Map previousNaturalRegeneration = new HashMap<>(); -+ -+ private static final Map> subscribedChannels = new HashMap<>(); -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation id(String path) { -+ return new ResourceLocation(PROTOCOL_ID, path); -+ } -+ -+ @ProtocolHandler.PlayerJoin -+ public static void onPlayerLoggedIn(@NotNull ServerPlayer player) { -+ if (LeavesConfig.protocol.appleskin.enable) { -+ resetPlayerData(player); -+ } -+ } -+ -+ @ProtocolHandler.PlayerLeave -+ public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { -+ if (LeavesConfig.protocol.appleskin.enable) { -+ subscribedChannels.remove(player); -+ resetPlayerData(player); -+ } -+ } -+ -+ @ProtocolHandler.MinecraftRegister(ignoreId = true) -+ public static void onPlayerSubscribed(@NotNull ServerPlayer player, String channel) { -+ if (LeavesConfig.protocol.appleskin.enable) { -+ subscribedChannels.computeIfAbsent(player, k -> new HashSet<>()).add(channel); -+ } -+ } -+ -+ @ProtocolHandler.Ticker -+ public static void tick() { -+ if (LeavesConfig.protocol.appleskin.enable) { -+ if (MinecraftServer.getServer().getTickCount() % LeavesConfig.protocol.appleskin.syncTickInterval != 0) { -+ return; -+ } -+ -+ for (Map.Entry> entry : subscribedChannels.entrySet()) { -+ ServerPlayer player = entry.getKey(); -+ FoodData data = player.getFoodData(); -+ -+ for (String channel : entry.getValue()) { -+ switch (channel) { -+ case "saturation" -> { -+ float saturation = data.getSaturationLevel(); -+ Float previousSaturation = previousSaturationLevels.get(player); -+ if (previousSaturation == null || saturation != previousSaturation) { -+ ProtocolUtils.sendPayloadPacket(player, SATURATION_KEY, buf -> buf.writeFloat(saturation)); -+ previousSaturationLevels.put(player, saturation); -+ } -+ } -+ -+ case "exhaustion" -> { -+ float exhaustion = data.exhaustionLevel; -+ Float previousExhaustion = previousExhaustionLevels.get(player); -+ if (previousExhaustion == null || Math.abs(exhaustion - previousExhaustion) >= MINIMUM_EXHAUSTION_CHANGE_THRESHOLD) { -+ ProtocolUtils.sendPayloadPacket(player, EXHAUSTION_KEY, buf -> buf.writeFloat(exhaustion)); -+ previousExhaustionLevels.put(player, exhaustion); -+ } -+ } -+ -+ case "natural_regeneration" -> { -+ boolean regeneration = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_NATURAL_REGENERATION); -+ Boolean previousRegeneration = previousNaturalRegeneration.get(player); -+ if (previousRegeneration == null || regeneration != previousRegeneration) { -+ ProtocolUtils.sendPayloadPacket(player, NATURAL_REGENERATION_KEY, buf -> buf.writeBoolean(regeneration)); -+ previousNaturalRegeneration.put(player, regeneration); -+ } -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ @ProtocolHandler.ReloadServer -+ public static void onServerReload() { -+ if (!LeavesConfig.protocol.appleskin.enable) { -+ disableAllPlayer(); -+ } -+ } -+ -+ public static void disableAllPlayer() { -+ for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { -+ onPlayerLoggedOut(player); -+ } -+ } -+ -+ private static void resetPlayerData(@NotNull ServerPlayer player) { -+ previousExhaustionLevels.remove(player); -+ previousSaturationLevels.remove(player); -+ previousNaturalRegeneration.remove(player); -+ } -+} diff --git a/patches/server/0041-Xaero-Map-Protocol.patch b/patches/server/0041-Xaero-Map-Protocol.patch deleted file mode 100644 index 64559bec..00000000 --- a/patches/server/0041-Xaero-Map-Protocol.patch +++ /dev/null @@ -1,66 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Fri, 27 Jan 2023 09:42:57 +0800 -Subject: [PATCH] Xaero Map Protocol - - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index eb43b1fcda50326f086999a6e134c9807b837045..7a80d3e7d46a9c2a729ca50e2bd265fe5711a23f 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -1256,6 +1256,7 @@ public abstract class PlayerList { - player.connection.send(new ClientboundInitializeBorderPacket(worldborder)); - player.connection.send(new ClientboundSetTimePacket(world.getGameTime(), world.getDayTime(), world.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT))); - player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(world.getSharedSpawnPos(), world.getSharedSpawnAngle())); -+ org.leavesmc.leaves.protocol.XaeroMapProtocol.onSendWorldInfo(player); // Leaves - xaero map protocol - if (world.isRaining()) { - // CraftBukkit start - handle player weather - // entityplayer.connection.send(new PacketPlayOutGameStateChange(PacketPlayOutGameStateChange.START_RAINING, 0.0F)); -diff --git a/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a94b5174282e0cb520c99a93cae7d3f439cd9cc8 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java -@@ -0,0 +1,42 @@ -+package org.leavesmc.leaves.protocol; -+ -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+@LeavesProtocol(namespace = {"xaerominimap", "xaeroworldmap"}) -+public class XaeroMapProtocol { -+ -+ public static final String PROTOCOL_ID_MINI = "xaerominimap"; -+ public static final String PROTOCOL_ID_WORLD = "xaeroworldmap"; -+ -+ private static final ResourceLocation MINIMAP_KEY = idMini("main"); -+ private static final ResourceLocation WORLDMAP_KEY = idWorld("main"); -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation idMini(String path) { -+ return new ResourceLocation(PROTOCOL_ID_MINI, path); -+ } -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation idWorld(String path) { -+ return new ResourceLocation(PROTOCOL_ID_WORLD, path); -+ } -+ -+ public static void onSendWorldInfo(@NotNull ServerPlayer player) { -+ if (LeavesConfig.protocol.xaeroMapProtocol) { -+ ProtocolUtils.sendPayloadPacket(player, MINIMAP_KEY, buf -> { -+ buf.writeByte(0); -+ buf.writeInt(LeavesConfig.protocol.xaeroMapServerID); -+ }); -+ ProtocolUtils.sendPayloadPacket(player, WORLDMAP_KEY, buf -> { -+ buf.writeByte(0); -+ buf.writeInt(LeavesConfig.protocol.xaeroMapServerID); -+ }); -+ } -+ } -+} diff --git a/patches/server/0042-Leaves-Extra-Yggdrasil-Service.patch b/patches/server/0042-Leaves-Extra-Yggdrasil-Service.patch deleted file mode 100644 index 81a44028..00000000 --- a/patches/server/0042-Leaves-Extra-Yggdrasil-Service.patch +++ /dev/null @@ -1,190 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 2 Feb 2023 16:01:18 +0800 -Subject: [PATCH] Leaves Extra Yggdrasil Service - - -diff --git a/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java b/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java -index 48e774677edf17d4a99ae9ed23d1b371dab41abb..21409ff86db65c00d92bff9eae8bdeb3a872a361 100644 ---- a/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java -+++ b/src/main/java/com/destroystokyo/paper/profile/PaperAuthenticationService.java -@@ -11,7 +11,7 @@ import java.net.Proxy; - - public class PaperAuthenticationService extends YggdrasilAuthenticationService { - -- private final Environment environment; -+ protected final Environment environment; // Leaves - private -> protected - - public PaperAuthenticationService(Proxy proxy) { - super(proxy); -diff --git a/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java -index 8c3151c25c172b846f0d028b5100718ada2d09d7..d59d05f8894481fb5ebe1aeb8ee7a162b3dc6f1c 100644 ---- a/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java -+++ b/src/main/java/com/mojang/authlib/yggdrasil/YggdrasilMinecraftSessionService.java -@@ -46,7 +46,7 @@ import java.util.stream.Collectors; - - public class YggdrasilMinecraftSessionService implements MinecraftSessionService { - private static final Logger LOGGER = LoggerFactory.getLogger(YggdrasilMinecraftSessionService.class); -- private final MinecraftClient client; -+ protected final MinecraftClient client; // Leaves - private -> protected - private final ServicesKeySet servicesKeySet; - private final String baseUrl; - private final URL joinUrl; -diff --git a/src/main/java/net/minecraft/server/Main.java b/src/main/java/net/minecraft/server/Main.java -index 90ca25c4aaf92a5639839a7cdaee2ffcdb75efa7..f0c265a171590bafaed7c87c4f2e64e8e631ea41 100644 ---- a/src/main/java/net/minecraft/server/Main.java -+++ b/src/main/java/net/minecraft/server/Main.java -@@ -191,7 +191,7 @@ public class Main { - file = new File(bukkitConfiguration.getString("settings.world-container", ".")); - } - // Paper end - fix SPIGOT-5824 -- Services services = Services.create(new com.destroystokyo.paper.profile.PaperAuthenticationService(Proxy.NO_PROXY), file, userCacheFile, optionset); // Paper - pass OptionSet to load paper config files; override authentication service; fix world-container -+ Services services = Services.create(new org.leavesmc.leaves.profile.LeavesAuthenticationService(Proxy.NO_PROXY), file, userCacheFile, optionset); // Paper - pass OptionSet to load paper config files; override authentication service; fix world-container // Leaves - extra-yggdrasil-service - // CraftBukkit start - String s = (String) Optional.ofNullable((String) optionset.valueOf("world")).orElse(dedicatedserversettings.getProperties().levelName); - LevelStorageSource convertable = LevelStorageSource.createDefault(file.toPath()); -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index ee43310ce1cd051e45486c4587c5e2aa1d000338..afe3cda698c415079d9f038b452cf594579b4eaf 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -272,7 +272,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop public - private long lastServerStatus; - public final Thread serverThread; - private long lastTickNanos; -diff --git a/src/main/java/org/leavesmc/leaves/profile/LeavesAuthenticationService.java b/src/main/java/org/leavesmc/leaves/profile/LeavesAuthenticationService.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6f14a66d78709fae9fecbefc80e3489b68e8d0c7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/profile/LeavesAuthenticationService.java -@@ -0,0 +1,18 @@ -+package org.leavesmc.leaves.profile; -+ -+import com.destroystokyo.paper.profile.PaperAuthenticationService; -+import com.mojang.authlib.minecraft.MinecraftSessionService; -+ -+import java.net.Proxy; -+ -+public class LeavesAuthenticationService extends PaperAuthenticationService { -+ -+ public LeavesAuthenticationService(Proxy proxy) { -+ super(proxy); -+ } -+ -+ @Override -+ public MinecraftSessionService createMinecraftSessionService() { -+ return new LeavesMinecraftSessionService(this.getServicesKeySet(), this.getProxy(), this.environment); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/profile/LeavesMinecraftSessionService.java b/src/main/java/org/leavesmc/leaves/profile/LeavesMinecraftSessionService.java -new file mode 100644 -index 0000000000000000000000000000000000000000..133c7619e3e7546a51879acbb0a29154e9bc9659 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/profile/LeavesMinecraftSessionService.java -@@ -0,0 +1,102 @@ -+package org.leavesmc.leaves.profile; -+ -+import com.destroystokyo.paper.profile.PaperMinecraftSessionService; -+import com.mojang.authlib.Environment; -+import com.mojang.authlib.GameProfile; -+import com.mojang.authlib.HttpAuthenticationService; -+import com.mojang.authlib.exceptions.AuthenticationUnavailableException; -+import com.mojang.authlib.exceptions.MinecraftClientException; -+import com.mojang.authlib.yggdrasil.ProfileActionType; -+import com.mojang.authlib.yggdrasil.ProfileResult; -+import com.mojang.authlib.yggdrasil.ServicesKeySet; -+import com.mojang.authlib.yggdrasil.response.HasJoinedMinecraftServerResponse; -+import com.mojang.authlib.yggdrasil.response.ProfileAction; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.bot.ServerBot; -+ -+import java.net.InetAddress; -+import java.net.Proxy; -+import java.net.URL; -+import java.util.ArrayList; -+import java.util.Collections; -+import java.util.HashMap; -+import java.util.List; -+import java.util.Map; -+import java.util.Set; -+import java.util.stream.Collectors; -+ -+public class LeavesMinecraftSessionService extends PaperMinecraftSessionService { -+ -+ protected LeavesMinecraftSessionService(ServicesKeySet keySet, Proxy authenticationService, Environment environment) { -+ super(keySet, authenticationService, environment); -+ } -+ -+ private static List extraYggdrasilList = List.of(); -+ -+ public static void initExtraYggdrasilList(List extraYggdrasilServiceList) { -+ List list = new ArrayList<>(); -+ for (String str : extraYggdrasilServiceList) { -+ list.add(HttpAuthenticationService.constantURL(str + "/sessionserver/session/minecraft/hasJoined")); -+ } -+ extraYggdrasilList = Collections.unmodifiableList(list); -+ } -+ -+ @Nullable -+ @Override -+ public ProfileResult hasJoinedServer(String profileName, String serverId, @Nullable InetAddress address) throws AuthenticationUnavailableException { -+ ProfileResult result = super.hasJoinedServer(profileName, serverId, address); // mojang -+ -+ ServerPlayer player = MinecraftServer.getServer().getPlayerList().getPlayerByName(profileName); -+ if (player != null && !(player instanceof ServerBot)) { -+ return null; // if it has same name, return null -+ } -+ -+ if (LeavesConfig.mics.yggdrasil.enable && result == null) { -+ final Map arguments = new HashMap<>(); -+ arguments.put("username", profileName); -+ arguments.put("serverId", serverId); -+ -+ if (address != null) { -+ arguments.put("ip", address.getHostAddress()); -+ } -+ -+ GameProfile cache = null; -+ if (LeavesConfig.mics.yggdrasil.loginProtect) { -+ cache = MinecraftServer.getServer().services.profileCache().getProfileIfCached(profileName); -+ } -+ -+ for (URL checkUrl : extraYggdrasilList) { -+ URL url = HttpAuthenticationService.concatenateURL(checkUrl, HttpAuthenticationService.buildQuery(arguments)); -+ try { -+ final HasJoinedMinecraftServerResponse response = client.get(url, HasJoinedMinecraftServerResponse.class); -+ if (response != null && response.id() != null) { -+ if (LeavesConfig.mics.yggdrasil.loginProtect && cache != null) { -+ if (response.id() != cache.getId()) { -+ continue; -+ } -+ } -+ -+ final GameProfile result1 = new GameProfile(response.id(), profileName); -+ -+ if (response.properties() != null) { -+ result1.getProperties().putAll(response.properties()); -+ } -+ -+ final Set profileActions = response.profileActions().stream() -+ .map(ProfileAction::type) -+ .collect(Collectors.toSet()); -+ return new ProfileResult(result1, profileActions); -+ } -+ } catch (final MinecraftClientException e) { -+ if (e.toAuthenticationException() instanceof final AuthenticationUnavailableException unavailable) { -+ throw unavailable; -+ } -+ } -+ } -+ } -+ return result; -+ } -+} diff --git a/patches/server/0043-Use-vanilla-random-config.patch b/patches/server/0043-Use-vanilla-random-config.patch deleted file mode 100644 index 1fa90581..00000000 --- a/patches/server/0043-Use-vanilla-random-config.patch +++ /dev/null @@ -1,77 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 16 Feb 2023 17:25:01 +0800 -Subject: [PATCH] Use vanilla random config - - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 7e61b008c9429f6ef60fcef4887bb50db4face91..b7d5a1f56067c6fec74d653b0da48e9610def283 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -578,7 +578,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.bb = Entity.INITIAL_AABB; - this.stuckSpeedMultiplier = Vec3.ZERO; - this.nextStep = 1.0F; -- this.random = SHARED_RANDOM; // Paper - Share random for entities to make them more random -+ this.random = org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom ? RandomSource.create() : SHARED_RANDOM; // Paper - Share random for entities to make them more random // Leaves - vanilla plz - this.remainingFireTicks = -this.getFireImmuneTicks(); - this.fluidHeight = new Object2DoubleArrayMap(2); - this.fluidOnEyes = new HashSet(); -diff --git a/src/main/java/net/minecraft/world/entity/animal/Bee.java b/src/main/java/net/minecraft/world/entity/animal/Bee.java -index 42276acfeadec6e7aa9a91d3f446f4fedb04829d..c2f178d74e61c6d169ccea15ce8d3d0568dbcbe8 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Bee.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Bee.java -@@ -1088,7 +1088,7 @@ public class Bee extends Animal implements NeutralMob, FlyingAnimal { - - BeeGoToHiveGoal() { - super(); -- this.travellingTicks = Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues -+ this.travellingTicks = org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom ? Bee.this.level().random.nextInt(10) : Bee.this.random.nextInt(10); // CraftBukkit - SPIGOT-7495: Give Bees another chance and let them use their own random, avoid concurrency issues // Leaves - why no vanilla - this.blacklistedTargets = Lists.newArrayList(); - this.setFlags(EnumSet.of(Goal.Flag.MOVE)); - } -diff --git a/src/main/java/net/minecraft/world/entity/animal/Squid.java b/src/main/java/net/minecraft/world/entity/animal/Squid.java -index f9fdc600dc680c55219fcbf9bc8f151a733a093c..ff9e8f501a537d005aba63737303177ca6882760 100644 ---- a/src/main/java/net/minecraft/world/entity/animal/Squid.java -+++ b/src/main/java/net/minecraft/world/entity/animal/Squid.java -@@ -46,7 +46,7 @@ public class Squid extends AgeableWaterCreature { - - public Squid(EntityType type, Level world) { - super(type, world); -- //this.random.setSeed((long)this.getId()); // Paper - Share random for entities to make them more random -+ if (org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom) this.random.setSeed(this.getId()); // Paper - Share random for entities to make them more random // Leaves - vanilla plz - this.tentacleSpeed = 1.0F / (this.random.nextFloat() + 1.0F) * 0.2F; - } - -diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -index 8f2ca7211d4ebf494beae85caea5876fad4723eb..9e8a40f51337822ac55b5020778076c7466f5ef3 100644 ---- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -+++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -@@ -79,7 +79,13 @@ public class ItemEntity extends Entity implements TraceableEntity { - // Paper start - Don't use level random in entity constructors (to make them thread-safe) - this(EntityType.ITEM, world); - this.setPos(x, y, z); -- this.setDeltaMovement(this.random.nextDouble() * 0.2D - 0.1D, 0.2D, this.random.nextDouble() * 0.2D - 0.1D); -+ // Leaves start - vanilla yes, paper no -+ if (org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom) { -+ this.setDeltaMovement(world.random.nextDouble() * 0.2D - 0.1D, 0.2D, world.random.nextDouble() * 0.2D - 0.1D); -+ } else { -+ this.setDeltaMovement(this.random.nextDouble() * 0.2D - 0.1D, 0.2D, this.random.nextDouble() * 0.2D - 0.1D); -+ } -+ // Leaves end - vanilla yes, paper no - this.setItem(stack); - // Paper end - Don't use level random in entity constructors - } -diff --git a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java -index 34b9384c0f852d28dc8b834a40785ede74b33cfd..aa4179781fc67e7e4194cfac795aa2a84abe616b 100644 ---- a/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java -+++ b/src/main/java/net/minecraft/world/entity/item/PrimedTnt.java -@@ -68,7 +68,7 @@ public class PrimedTnt extends Entity implements TraceableEntity { - public PrimedTnt(Level world, double x, double y, double z, @Nullable LivingEntity igniter) { - this(EntityType.TNT, world); - this.setPos(x, y, z); -- double d3 = this.random.nextDouble() * 6.2831854820251465D; // Paper - Don't use level random in entity constructors -+ double d3 = (org.leavesmc.leaves.LeavesConfig.modify.useVanillaRandom ? world.random.nextDouble() : this.random.nextDouble()) * 6.2831854820251465D; // Paper - Don't use level random in entity constructors // Leaves - why? - - this.setDeltaMovement(-Math.sin(d3) * 0.02D, 0.20000000298023224D, -Math.cos(d3) * 0.02D); - this.setFuse(80); diff --git a/patches/server/0044-Fix-update-suppression-crash.patch b/patches/server/0044-Fix-update-suppression-crash.patch deleted file mode 100644 index ed5b3c42..00000000 --- a/patches/server/0044-Fix-update-suppression-crash.patch +++ /dev/null @@ -1,123 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Fri, 17 Mar 2023 15:57:08 +0800 -Subject: [PATCH] Fix update suppression crash - - -diff --git a/src/main/java/net/minecraft/network/protocol/PacketUtils.java b/src/main/java/net/minecraft/network/protocol/PacketUtils.java -index 1f7f68aad97ee73763c042837f239bdc7167db55..1e8025ecb14acc7c24917793c97f54355b5a9346 100644 ---- a/src/main/java/net/minecraft/network/protocol/PacketUtils.java -+++ b/src/main/java/net/minecraft/network/protocol/PacketUtils.java -@@ -53,6 +53,10 @@ public class PacketUtils { - if (listener.shouldHandleMessage(packet)) { - try { - packet.handle(listener); -+ // Leaves start - update suppression crash fix -+ } catch (org.leavesmc.leaves.util.UpdateSuppressionException exception) { -+ org.leavesmc.leaves.LeavesLogger.LOGGER.info(exception.getMessage()); -+ // Leaves start - update suppression crash fix - } catch (Exception exception) { - if (exception instanceof ReportedException) { - ReportedException reportedexception = (ReportedException) exception; -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 4b69ccad6ae14c2c037fd324174bcb65e6308d84..6e75b3832a7986d7818868c422edd018f6fcc862 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1875,7 +1875,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop -Date: Wed, 8 May 2024 22:19:25 +0800 -Subject: [PATCH] Bedrock break list - - -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 6e75b3832a7986d7818868c422edd018f6fcc862..21a8c087dfe93ff6570d17e2757fe4c2250b3245 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1907,6 +1907,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> pistonCache = new HashMap<>(); -+ private static final List BBL = new ArrayList<>(); -+ private static final List MBB = new ArrayList<>(); -+ private static final List LBL = new ArrayList<>(); -+ -+ public static void endTick() { -+ for (var map : pistonCache.values()) { -+ if (!map.isEmpty()) { -+ map.clear(); -+ } -+ } -+ } -+ -+ public static void onPlayerPlacePiston(Level level, Player player, BlockPos pos) { -+ if (LeavesConfig.modify.bedrockBreakList) { -+ Direction pistonFacing = level.getBlockState(pos).getValue(DirectionalBlock.FACING); -+ BlockPos bedrockPos = pos.relative(pistonFacing); -+ if (level.getBlockState(bedrockPos).getBlock() == Blocks.BEDROCK) { -+ pistonCache.computeIfAbsent(level, k -> new HashMap<>()).put(bedrockPos, player); -+ } -+ } -+ } -+ -+ public static void onPistonBreakBedrock(Level level, BlockPos bedrock) { -+ if (LeavesConfig.modify.bedrockBreakList) { -+ Map map = pistonCache.get(level); -+ -+ boolean flag = map != null && map.get(bedrock) != null; -+ -+ if (flag) { -+ if (!BBL.isEmpty()) { -+ Player player = map.get(bedrock); -+ for (Objective objective : BBL) { -+ level.getScoreboard().getOrCreatePlayerScore(player, objective).increment(); -+ } -+ } -+ } else { -+ if (!MBB.isEmpty()) { -+ ScoreHolder world = ScoreHolder.forNameOnly("$" + level.dimension().location()); -+ for (Objective objective : MBB) { -+ level.getScoreboard().getOrCreatePlayerScore(world, objective).increment(); -+ level.getScoreboard().getOrCreatePlayerScore(ScoreHolder.forNameOnly("$total"), objective).increment(); -+ } -+ } -+ } -+ -+ if (!LBL.isEmpty() && !level.players().isEmpty()) { -+ Player closestPlayer = level.getNearestPlayer(bedrock.getX(), bedrock.getY(), bedrock.getZ(), 10.5, null); -+ if (closestPlayer != null) { -+ for (Objective objective : LBL) { -+ level.getScoreboard().getOrCreatePlayerScore(closestPlayer, objective).increment(); -+ } -+ } -+ } -+ } -+ } -+ -+ public static void onScoreboardAdd(@NotNull Objective objective) { -+ if (LeavesConfig.modify.bedrockBreakList) { -+ if (objective.getCriteria() == ObjectiveCriteria.DUMMY) { -+ String name = objective.getName(); -+ -+ int i = name.length() - 4; -+ if (i >= 0) { -+ String suffix = name.substring(i); -+ switch (suffix) { -+ case ".bbl" -> BBL.add(objective); -+ case ".mbb" -> MBB.add(objective); -+ case ".lbl" -> LBL.add(objective); -+ } -+ } -+ } -+ } -+ } -+ -+ public static void onScoreboardRemove(@NotNull Objective objective) { -+ if (LeavesConfig.modify.bedrockBreakList) { -+ if (objective.getCriteria() == ObjectiveCriteria.DUMMY) { -+ String name = objective.getName(); -+ -+ int i = name.length() - 4; -+ if (i >= 0) { -+ String suffix = name.substring(i); -+ switch (suffix) { -+ case ".bbl" -> BBL.remove(objective); -+ case ".mbb" -> MBB.remove(objective); -+ case ".lbl" -> LBL.remove(objective); -+ } -+ } -+ } -+ } -+ } -+} diff --git a/patches/server/0046-Fix-trapdoor-feature.patch b/patches/server/0046-Fix-trapdoor-feature.patch deleted file mode 100644 index 82882bca..00000000 --- a/patches/server/0046-Fix-trapdoor-feature.patch +++ /dev/null @@ -1,29 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sat, 20 May 2023 21:46:13 +0800 -Subject: [PATCH] Fix trapdoor feature - - -diff --git a/src/main/java/net/minecraft/world/level/block/TrapDoorBlock.java b/src/main/java/net/minecraft/world/level/block/TrapDoorBlock.java -index 872e52e13293a99d45f93d90d8fa7f6aa99d1f3a..2b57a82eeb98fffc47f37d073f7934e26a1a6221 100644 ---- a/src/main/java/net/minecraft/world/level/block/TrapDoorBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/TrapDoorBlock.java -@@ -157,6 +157,8 @@ public class TrapDoorBlock extends HorizontalDirectionalBlock implements SimpleW - flag1 = eventRedstone.getNewCurrent() > 0; - } - // CraftBukkit end -+ -+ /* Leaves - it is feature, not bug! - // Paper start - break redstone on trapdoors early - boolean open = (Boolean) state.getValue(TrapDoorBlock.OPEN) != flag1; - // note: this must run before any state for this block/its neighborus are written to the world -@@ -177,6 +179,9 @@ public class TrapDoorBlock extends HorizontalDirectionalBlock implements SimpleW - } - if (open) { - // Paper end - break redstone on trapdoors early -+ */ -+ -+ if ((Boolean) state.getValue(TrapDoorBlock.OPEN) != flag1) { - state = (BlockState) state.setValue(TrapDoorBlock.OPEN, flag1); - this.playSound((Player) null, world, pos, flag1); - } diff --git a/patches/server/0047-Disable-distance-check-for-UseItemOnPacket.patch b/patches/server/0047-Disable-distance-check-for-UseItemOnPacket.patch deleted file mode 100644 index 81acf322..00000000 --- a/patches/server/0047-Disable-distance-check-for-UseItemOnPacket.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 23 May 2023 17:10:00 +0800 -Subject: [PATCH] Disable distance check for UseItemOnPacket - - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 43cfe5e00285bf88d0724d27e55d28c2f825f741..c88cb9c1c7f588b2617a71ecd6acbcf98ade5687 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -1945,7 +1945,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - Vec3 vec3d1 = vec3d.subtract(Vec3.atCenterOf(blockposition)); - double d0 = 1.0000001D; - -- if (Math.abs(vec3d1.x()) < 1.0000001D && Math.abs(vec3d1.y()) < 1.0000001D && Math.abs(vec3d1.z()) < 1.0000001D) { -+ if (org.leavesmc.leaves.LeavesConfig.modify.disableDistanceCheckForUseItem || (Math.abs(vec3d1.x()) < 1.0000001D && Math.abs(vec3d1.y()) < 1.0000001D && Math.abs(vec3d1.z()) < 1.0000001D)) { - Direction enumdirection = movingobjectpositionblock.getDirection(); - - this.player.resetLastActionTime(); diff --git a/patches/server/0048-No-feather-falling-trample.patch b/patches/server/0048-No-feather-falling-trample.patch deleted file mode 100644 index 48708e8c..00000000 --- a/patches/server/0048-No-feather-falling-trample.patch +++ /dev/null @@ -1,24 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 25 May 2023 16:37:06 +0800 -Subject: [PATCH] No feather falling trample - - -diff --git a/src/main/java/net/minecraft/world/level/block/FarmBlock.java b/src/main/java/net/minecraft/world/level/block/FarmBlock.java -index c3dba0c2c94f3804338f86621dc42405e380a6b3..b3de7e10a5514100712998519a7e9303b347d6a8 100644 ---- a/src/main/java/net/minecraft/world/level/block/FarmBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/FarmBlock.java -@@ -113,6 +113,13 @@ public class FarmBlock extends Block { - super.fallOn(world, state, pos, entity, fallDistance); // CraftBukkit - moved here as game rules / events shouldn't affect fall damage. - if (world instanceof ServerLevel worldserver) { - if (world.random.nextFloat() < fallDistance - 0.5F && entity instanceof LivingEntity && (entity instanceof Player || worldserver.getGameRules().getBoolean(GameRules.RULE_MOBGRIEFING)) && entity.getBbWidth() * entity.getBbWidth() * entity.getBbHeight() > 0.512F) { -+ // Leaves start - noFeatherFallingTrample -+ if (org.leavesmc.leaves.LeavesConfig.modify.noFeatherFallingTrample) { -+ if (net.minecraft.world.item.enchantment.EnchantmentHelper.getEnchantmentLevel(world.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT).getOrThrow(net.minecraft.world.item.enchantment.Enchantments.FEATHER_FALLING), (LivingEntity) entity) > 0) { -+ return; -+ } -+ } -+ // Leaves end - noFeatherFallingTrample - // CraftBukkit start - Interact soil - org.bukkit.event.Cancellable cancellable; - if (entity instanceof Player) { diff --git a/patches/server/0049-Syncmatica-Protocol.patch b/patches/server/0049-Syncmatica-Protocol.patch deleted file mode 100644 index fd2b3d24..00000000 --- a/patches/server/0049-Syncmatica-Protocol.patch +++ /dev/null @@ -1,2062 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 18 May 2023 16:16:56 +0800 -Subject: [PATCH] Syncmatica Protocol - -This patch is Powered by Syncmatica(https://github.com/End-Tech/syncmatica) - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index c88cb9c1c7f588b2617a71ecd6acbcf98ade5687..58b260c37393eebc9fe838ab801287573c04cfde 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -324,6 +324,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - Objects.requireNonNull(server); - this.signedMessageDecoder = SignedMessageChain.Decoder.unsigned(uuid, server::enforceSecureProfile); - this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat -+ this.exchangeTarget = new org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget(this); // Leaves - Syncmatica Protocol - } - - // CraftBukkit start - add fields and methods -@@ -342,6 +343,8 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - private boolean justTeleported = false; - // CraftBukkit end - -+ public final org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget exchangeTarget; // Leaves - Syncmatica Protocol -+ - @Override - public void tick() { - if (this.ackBlockChangesUpTo > -1) { -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..365b07d8d9741d07bf4505d121394dd053825c76 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java -@@ -0,0 +1,396 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import com.mojang.authlib.GameProfile; -+import io.netty.buffer.Unpooled; -+import net.minecraft.core.BlockPos; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.chat.Component; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.level.block.Mirror; -+import net.minecraft.world.level.block.Rotation; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.syncmatica.exchange.DownloadExchange; -+import org.leavesmc.leaves.protocol.syncmatica.exchange.Exchange; -+import org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget; -+import org.leavesmc.leaves.protocol.syncmatica.exchange.ModifyExchangeServer; -+import org.leavesmc.leaves.protocol.syncmatica.exchange.UploadExchange; -+import org.leavesmc.leaves.protocol.syncmatica.exchange.VersionHandshakeServer; -+ -+import java.io.File; -+import java.io.FileNotFoundException; -+import java.io.IOException; -+import java.security.NoSuchAlgorithmException; -+import java.util.ArrayList; -+import java.util.Collection; -+import java.util.HashMap; -+import java.util.List; -+import java.util.Map; -+import java.util.UUID; -+ -+@LeavesProtocol(namespace = "syncmatica") -+public class CommunicationManager { -+ -+ private static final Map> downloadingFile = new HashMap<>(); -+ private static final Map playerMap = new HashMap<>(); -+ -+ protected static final Collection broadcastTargets = new ArrayList<>(); -+ -+ protected static final Map downloadState = new HashMap<>(); -+ protected static final Map modifyState = new HashMap<>(); -+ -+ protected static final Rotation[] rotOrdinals = Rotation.values(); -+ protected static final Mirror[] mirOrdinals = Mirror.values(); -+ -+ public CommunicationManager() { -+ } -+ -+ public static GameProfile getGameProfile(final ExchangeTarget exchangeTarget) { -+ return playerMap.get(exchangeTarget).getGameProfile(); -+ } -+ -+ public void sendMessage(final @NotNull ExchangeTarget client, final MessageType type, final String identifier) { -+ if (client.getFeatureSet().hasFeature(Feature.MESSAGE)) { -+ final FriendlyByteBuf newPacketBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ newPacketBuf.writeUtf(type.toString()); -+ newPacketBuf.writeUtf(identifier); -+ client.sendPacket(PacketType.MESSAGE.identifier, newPacketBuf); -+ } else if (playerMap.containsKey(client)) { -+ final ServerPlayer player = playerMap.get(client); -+ player.sendSystemMessage(Component.literal("Syncmatica " + type.toString() + " " + identifier)); -+ } -+ } -+ -+ @ProtocolHandler.PlayerJoin -+ public static void onPlayerJoin(ServerPlayer player) { -+ if (!LeavesConfig.protocol.syncmatica.enable) { -+ return; -+ } -+ final ExchangeTarget newPlayer = player.connection.exchangeTarget; -+ final VersionHandshakeServer hi = new VersionHandshakeServer(newPlayer); -+ playerMap.put(newPlayer, player); -+ final GameProfile profile = player.getGameProfile(); -+ SyncmaticaProtocol.getPlayerIdentifierProvider().updateName(profile.getId(), profile.getName()); -+ startExchangeUnchecked(hi); -+ } -+ -+ @ProtocolHandler.PlayerLeave -+ public static void onPlayerLeave(ServerPlayer player) { -+ if (!LeavesConfig.protocol.syncmatica.enable) { -+ return; -+ } -+ final ExchangeTarget oldPlayer = player.connection.exchangeTarget; -+ final Collection potentialMessageTarget = oldPlayer.getExchanges(); -+ if (potentialMessageTarget != null) { -+ for (final Exchange target : potentialMessageTarget) { -+ target.close(false); -+ handleExchange(target); -+ } -+ } -+ broadcastTargets.remove(oldPlayer); -+ playerMap.remove(oldPlayer); -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = SyncmaticaPayload.class, payloadId = "main") -+ public static void onPacketGet(ServerPlayer player, SyncmaticaPayload payload) { -+ if (!LeavesConfig.protocol.syncmatica.enable) { -+ return; -+ } -+ onPacket(player.connection.exchangeTarget, payload.packetType(), payload.data()); -+ } -+ -+ public static void onPacket(final @NotNull ExchangeTarget source, final ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ Exchange handler = null; -+ final Collection potentialMessageTarget = source.getExchanges(); -+ if (potentialMessageTarget != null) { -+ for (final Exchange target : potentialMessageTarget) { -+ if (target.checkPacket(id, packetBuf)) { -+ target.handle(id, packetBuf); -+ handler = target; -+ break; -+ } -+ } -+ } -+ if (handler == null) { -+ handle(source, id, packetBuf); -+ } else if (handler.isFinished()) { -+ notifyClose(handler); -+ } -+ } -+ -+ protected static void handle(ExchangeTarget source, @NotNull ResourceLocation id, FriendlyByteBuf packetBuf) { -+ if (id.equals(PacketType.REQUEST_LITEMATIC.identifier)) { -+ final UUID syncmaticaId = packetBuf.readUUID(); -+ final ServerPlacement placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(syncmaticaId); -+ if (placement == null) { -+ return; -+ } -+ final File toUpload = SyncmaticaProtocol.getFileStorage().getLocalLitematic(placement); -+ final UploadExchange upload; -+ try { -+ upload = new UploadExchange(placement, toUpload, source); -+ } catch (final FileNotFoundException e) { -+ e.printStackTrace(); -+ return; -+ } -+ startExchange(upload); -+ return; -+ } -+ if (id.equals(PacketType.REGISTER_METADATA.identifier)) { -+ final ServerPlacement placement = receiveMetaData(packetBuf, source); -+ if (SyncmaticaProtocol.getSyncmaticManager().getPlacement(placement.getId()) != null) { -+ cancelShare(source, placement); -+ return; -+ } -+ -+ final GameProfile profile = playerMap.get(source).getGameProfile(); -+ final PlayerIdentifier playerIdentifier = SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet(profile); -+ if (!placement.getOwner().equals(playerIdentifier)) { -+ placement.setOwner(playerIdentifier); -+ placement.setLastModifiedBy(playerIdentifier); -+ } -+ -+ if (!SyncmaticaProtocol.getFileStorage().getLocalState(placement).isLocalFileReady()) { -+ if (SyncmaticaProtocol.getFileStorage().getLocalState(placement) == LocalLitematicState.DOWNLOADING_LITEMATIC) { -+ downloadingFile.computeIfAbsent(placement.getHash(), key -> new ArrayList<>()).add(placement); -+ return; -+ } -+ try { -+ download(placement, source); -+ } catch (final Exception e) { -+ e.printStackTrace(); -+ } -+ return; -+ } -+ -+ addPlacement(source, placement); -+ return; -+ } -+ if (id.equals(PacketType.REMOVE_SYNCMATIC.identifier)) { -+ final UUID placementId = packetBuf.readUUID(); -+ final ServerPlacement placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(placementId); -+ if (placement != null) { -+ if (!getGameProfile(source).getId().equals(placement.getOwner().uuid)) { -+ return; -+ } -+ -+ final Exchange modifier = getModifier(placement); -+ if (modifier != null) { -+ modifier.close(true); -+ notifyClose(modifier); -+ } -+ SyncmaticaProtocol.getSyncmaticManager().removePlacement(placement); -+ for (final ExchangeTarget client : broadcastTargets) { -+ final FriendlyByteBuf newPacketBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ newPacketBuf.writeUUID(placement.getId()); -+ client.sendPacket(PacketType.REMOVE_SYNCMATIC.identifier, newPacketBuf); -+ } -+ } -+ } -+ if (id.equals(PacketType.MODIFY_REQUEST.identifier)) { -+ final UUID placementId = packetBuf.readUUID(); -+ final ModifyExchangeServer modifier = new ModifyExchangeServer(placementId, source); -+ startExchange(modifier); -+ } -+ } -+ -+ protected static void handleExchange(Exchange exchange) { -+ if (exchange instanceof DownloadExchange) { -+ final ServerPlacement p = ((DownloadExchange) exchange).getPlacement(); -+ -+ if (exchange.isSuccessful()) { -+ addPlacement(exchange.getPartner(), p); -+ if (downloadingFile.containsKey(p.getHash())) { -+ for (final ServerPlacement placement : downloadingFile.get(p.getHash())) { -+ addPlacement(exchange.getPartner(), placement); -+ } -+ } -+ } else { -+ cancelShare(exchange.getPartner(), p); -+ if (downloadingFile.containsKey(p.getHash())) { -+ for (final ServerPlacement placement : downloadingFile.get(p.getHash())) { -+ cancelShare(exchange.getPartner(), placement); -+ } -+ } -+ } -+ -+ downloadingFile.remove(p.getHash()); -+ return; -+ } -+ if (exchange instanceof VersionHandshakeServer && exchange.isSuccessful()) { -+ broadcastTargets.add(exchange.getPartner()); -+ } -+ if (exchange instanceof ModifyExchangeServer && exchange.isSuccessful()) { -+ final ServerPlacement placement = ((ModifyExchangeServer) exchange).getPlacement(); -+ for (final ExchangeTarget client : broadcastTargets) { -+ if (client.getFeatureSet().hasFeature(Feature.MODIFY)) { -+ final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); -+ buf.writeUUID(placement.getId()); -+ putPositionData(placement, buf, client); -+ if (client.getFeatureSet().hasFeature(Feature.CORE_EX)) { -+ buf.writeUUID(placement.getLastModifiedBy().uuid); -+ buf.writeUtf(placement.getLastModifiedBy().getName()); -+ } -+ client.sendPacket(PacketType.MODIFY.identifier, buf); -+ } else { -+ final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); -+ buf.writeUUID(placement.getId()); -+ client.sendPacket(PacketType.REMOVE_SYNCMATIC.identifier, buf); -+ sendMetaData(placement, client); -+ } -+ } -+ } -+ } -+ -+ private static void addPlacement(final ExchangeTarget t, final @NotNull ServerPlacement placement) { -+ if (SyncmaticaProtocol.getSyncmaticManager().getPlacement(placement.getId()) != null) { -+ cancelShare(t, placement); -+ return; -+ } -+ SyncmaticaProtocol.getSyncmaticManager().addPlacement(placement); -+ for (final ExchangeTarget target : broadcastTargets) { -+ sendMetaData(placement, target); -+ } -+ } -+ -+ private static void cancelShare(final @NotNull ExchangeTarget source, final @NotNull ServerPlacement placement) { -+ final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ FriendlyByteBuf.writeUUID(placement.getId()); -+ source.sendPacket(PacketType.CANCEL_SHARE.identifier, FriendlyByteBuf); -+ } -+ -+ public static void sendMetaData(final ServerPlacement metaData, final ExchangeTarget target) { -+ final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); -+ putMetaData(metaData, buf, target); -+ target.sendPacket(PacketType.REGISTER_METADATA.identifier, buf); -+ } -+ -+ public static void putMetaData(final @NotNull ServerPlacement metaData, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { -+ buf.writeUUID(metaData.getId()); -+ -+ buf.writeUtf(SyncmaticaProtocol.sanitizeFileName(metaData.getName())); -+ buf.writeUUID(metaData.getHash()); -+ -+ if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { -+ buf.writeUUID(metaData.getOwner().uuid); -+ buf.writeUtf(metaData.getOwner().getName()); -+ buf.writeUUID(metaData.getLastModifiedBy().uuid); -+ buf.writeUtf(metaData.getLastModifiedBy().getName()); -+ } -+ -+ putPositionData(metaData, buf, exchangeTarget); -+ } -+ -+ public static void putPositionData(final @NotNull ServerPlacement metaData, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { -+ buf.writeBlockPos(metaData.getPosition()); -+ buf.writeUtf(metaData.getDimension()); -+ buf.writeInt(metaData.getRotation().ordinal()); -+ buf.writeInt(metaData.getMirror().ordinal()); -+ -+ if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { -+ if (metaData.getSubRegionData().getModificationData() == null) { -+ buf.writeInt(0); -+ return; -+ } -+ -+ final Collection regionData = metaData.getSubRegionData().getModificationData().values(); -+ buf.writeInt(regionData.size()); -+ -+ for (final SubRegionPlacementModification subPlacement : regionData) { -+ buf.writeUtf(subPlacement.name); -+ buf.writeBlockPos(subPlacement.position); -+ buf.writeInt(subPlacement.rotation.ordinal()); -+ buf.writeInt(subPlacement.mirror.ordinal()); -+ } -+ } -+ } -+ -+ public static ServerPlacement receiveMetaData(final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { -+ final UUID id = buf.readUUID(); -+ -+ final String fileName = SyncmaticaProtocol.sanitizeFileName(buf.readUtf(32767)); -+ final UUID hash = buf.readUUID(); -+ -+ PlayerIdentifier owner = PlayerIdentifier.MISSING_PLAYER; -+ PlayerIdentifier lastModifiedBy = PlayerIdentifier.MISSING_PLAYER; -+ -+ if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { -+ final PlayerIdentifierProvider provider = SyncmaticaProtocol.getPlayerIdentifierProvider(); -+ owner = provider.createOrGet(buf.readUUID(), buf.readUtf(32767)); -+ lastModifiedBy = provider.createOrGet(buf.readUUID(), buf.readUtf(32767)); -+ } -+ -+ final ServerPlacement placement = new ServerPlacement(id, fileName, hash, owner); -+ placement.setLastModifiedBy(lastModifiedBy); -+ -+ receivePositionData(placement, buf, exchangeTarget); -+ -+ return placement; -+ } -+ -+ public static void receivePositionData(final @NotNull ServerPlacement placement, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { -+ final BlockPos pos = buf.readBlockPos(); -+ final String dimensionId = buf.readUtf(32767); -+ final Rotation rot = rotOrdinals[buf.readInt()]; -+ final Mirror mir = mirOrdinals[buf.readInt()]; -+ placement.move(dimensionId, pos, rot, mir); -+ -+ if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { -+ final SubRegionData subRegionData = placement.getSubRegionData(); -+ subRegionData.reset(); -+ final int limit = buf.readInt(); -+ for (int i = 0; i < limit; i++) { -+ subRegionData.modify(buf.readUtf(32767), buf.readBlockPos(), rotOrdinals[buf.readInt()], mirOrdinals[buf.readInt()]); -+ } -+ } -+ } -+ -+ public static void download(final ServerPlacement syncmatic, final ExchangeTarget source) throws NoSuchAlgorithmException, IOException { -+ if (!SyncmaticaProtocol.getFileStorage().getLocalState(syncmatic).isReadyForDownload()) { -+ throw new IllegalArgumentException(syncmatic.toString() + " is not ready for download local state is: " + SyncmaticaProtocol.getFileStorage().getLocalState(syncmatic).toString()); -+ } -+ final File toDownload = SyncmaticaProtocol.getFileStorage().createLocalLitematic(syncmatic); -+ final Exchange downloadExchange = new DownloadExchange(syncmatic, toDownload, source); -+ setDownloadState(syncmatic, true); -+ startExchange(downloadExchange); -+ } -+ -+ public static void setDownloadState(final @NotNull ServerPlacement syncmatic, final boolean b) { -+ downloadState.put(syncmatic.getHash(), b); -+ } -+ -+ public static boolean getDownloadState(final @NotNull ServerPlacement syncmatic) { -+ return downloadState.getOrDefault(syncmatic.getHash(), false); -+ } -+ -+ public static void setModifier(final @NotNull ServerPlacement syncmatic, final Exchange exchange) { -+ modifyState.put(syncmatic.getHash(), exchange); -+ } -+ -+ public static Exchange getModifier(final @NotNull ServerPlacement syncmatic) { -+ return modifyState.get(syncmatic.getHash()); -+ } -+ -+ public static void startExchange(final @NotNull Exchange newExchange) { -+ if (!broadcastTargets.contains(newExchange.getPartner())) { -+ throw new IllegalArgumentException(newExchange.getPartner().toString() + " is not a valid ExchangeTarget"); -+ } -+ startExchangeUnchecked(newExchange); -+ } -+ -+ protected static void startExchangeUnchecked(final @NotNull Exchange newExchange) { -+ newExchange.getPartner().getExchanges().add(newExchange); -+ newExchange.init(); -+ if (newExchange.isFinished()) { -+ notifyClose(newExchange); -+ } -+ } -+ -+ public static void notifyClose(final @NotNull Exchange e) { -+ e.getPartner().getExchanges().remove(e); -+ handleExchange(e); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7cb3465b88411c46e79ce661ac7a4bddcf5b33e2 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java -@@ -0,0 +1,23 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import org.jetbrains.annotations.Nullable; -+ -+public enum Feature { -+ CORE, -+ FEATURE, -+ MODIFY, -+ MESSAGE, -+ QUOTA, -+ DEBUG, -+ CORE_EX; -+ -+ @Nullable -+ public static Feature fromString(final String s) { -+ for (final Feature f : Feature.values()) { -+ if (f.toString().equals(s)) { -+ return f; -+ } -+ } -+ return null; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ddd0f498feb2ad62134ae15a3ddb21527f2f24bf ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java -@@ -0,0 +1,67 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+ -+import java.util.ArrayList; -+import java.util.Collection; -+import java.util.Collections; -+import java.util.HashMap; -+import java.util.Map; -+ -+public class FeatureSet { -+ -+ private static final Map versionFeatures; -+ private final Collection features; -+ -+ @Nullable -+ public static FeatureSet fromVersionString(@NotNull String version) { -+ if (version.matches("^\\d+(\\.\\d+){2,4}$")) { -+ final int minSize = version.indexOf("."); -+ while (version.length() > minSize) { -+ if (versionFeatures.containsKey(version)) { -+ return versionFeatures.get(version); -+ } -+ final int lastDot = version.lastIndexOf("."); -+ version = version.substring(0, lastDot); -+ } -+ } -+ return null; -+ } -+ -+ @NotNull -+ public static FeatureSet fromString(final @NotNull String features) { -+ final FeatureSet featureSet = new FeatureSet(new ArrayList<>()); -+ for (final String feature : features.split("\n")) { -+ final Feature f = Feature.fromString(feature); -+ if (f != null) { -+ featureSet.features.add(f); -+ } -+ } -+ return featureSet; -+ } -+ -+ @Override -+ public String toString() { -+ final StringBuilder output = new StringBuilder(); -+ boolean b = false; -+ for (final Feature feature : features) { -+ output.append(b ? "\n" + feature.toString() : feature.toString()); -+ b = true; -+ } -+ return output.toString(); -+ } -+ -+ public FeatureSet(final Collection features) { -+ this.features = features; -+ } -+ -+ public boolean hasFeature(final Feature f) { -+ return features.contains(f); -+ } -+ -+ static { -+ versionFeatures = new HashMap<>(); -+ versionFeatures.put("0.1", new FeatureSet(Collections.singletonList(Feature.CORE))); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9139394e87e23190fbfdd82295314b0d50f1acca ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java -@@ -0,0 +1,80 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+ -+import java.io.File; -+import java.io.FileInputStream; -+import java.io.IOException; -+import java.util.HashMap; -+import java.util.UUID; -+ -+public class FileStorage { -+ -+ private final HashMap buffer = new HashMap<>(); -+ -+ public LocalLitematicState getLocalState(final ServerPlacement placement) { -+ final File localFile = getSchematicPath(placement); -+ if (localFile.isFile()) { -+ if (isDownloading(placement)) { -+ return LocalLitematicState.DOWNLOADING_LITEMATIC; -+ } -+ if ((buffer.containsKey(placement) && buffer.get(placement) == localFile.lastModified()) || hashCompare(localFile, placement)) { -+ return LocalLitematicState.LOCAL_LITEMATIC_PRESENT; -+ } -+ return LocalLitematicState.LOCAL_LITEMATIC_DESYNC; -+ } -+ return LocalLitematicState.NO_LOCAL_LITEMATIC; -+ } -+ -+ private boolean isDownloading(final ServerPlacement placement) { -+ return SyncmaticaProtocol.getCommunicationManager().getDownloadState(placement); -+ } -+ -+ public File getLocalLitematic(final ServerPlacement placement) { -+ if (getLocalState(placement).isLocalFileReady()) { -+ return getSchematicPath(placement); -+ } else { -+ return null; -+ } -+ } -+ -+ public File createLocalLitematic(final ServerPlacement placement) { -+ if (getLocalState(placement).isLocalFileReady()) { -+ throw new IllegalArgumentException(""); -+ } -+ final File file = getSchematicPath(placement); -+ if (file.exists()) { -+ file.delete(); -+ } -+ try { -+ file.createNewFile(); -+ } catch (final IOException e) { -+ e.printStackTrace(); -+ } -+ return file; -+ } -+ -+ private boolean hashCompare(final File localFile, final ServerPlacement placement) { -+ UUID hash = null; -+ try { -+ hash = SyncmaticaProtocol.createChecksum(new FileInputStream(localFile)); -+ } catch (final Exception e) { -+ e.printStackTrace(); -+ } -+ -+ if (hash == null) { -+ return false; -+ } -+ if (hash.equals(placement.getHash())) { -+ buffer.put(placement, localFile.lastModified()); -+ return true; -+ } -+ return false; -+ } -+ -+ @Contract("_ -> new") -+ private @NotNull File getSchematicPath(final @NotNull ServerPlacement placement) { -+ return new File(SyncmaticaProtocol.getLitematicFolder(), placement.getHash().toString() + ".litematic"); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java -new file mode 100644 -index 0000000000000000000000000000000000000000..299c57397371b368461a532d2eab695cf4f01fff ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java -@@ -0,0 +1,24 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+public enum LocalLitematicState { -+ NO_LOCAL_LITEMATIC(true, false), -+ LOCAL_LITEMATIC_DESYNC(true, false), -+ DOWNLOADING_LITEMATIC(false, false), -+ LOCAL_LITEMATIC_PRESENT(false, true); -+ -+ private final boolean downloadReady; -+ private final boolean fileReady; -+ -+ LocalLitematicState(final boolean downloadReady, final boolean fileReady) { -+ this.downloadReady = downloadReady; -+ this.fileReady = fileReady; -+ } -+ -+ public boolean isReadyForDownload() { -+ return downloadReady; -+ } -+ -+ public boolean isLocalFileReady() { -+ return fileReady; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b56ca12c650edd13dd7ff52e13c9d3aa465c32ec ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java -@@ -0,0 +1,8 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+public enum MessageType { -+ SUCCESS, -+ INFO, -+ WARNING, -+ ERROR -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java -new file mode 100644 -index 0000000000000000000000000000000000000000..36c87c5cc586ad247e9aed26518c890f884010ae ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java -@@ -0,0 +1,30 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import net.minecraft.resources.ResourceLocation; -+ -+public enum PacketType { -+ REGISTER_METADATA("register_metadata"), -+ CANCEL_SHARE("cancel_share"), -+ REQUEST_LITEMATIC("request_download"), -+ SEND_LITEMATIC("send_litematic"), -+ RECEIVED_LITEMATIC("received_litematic"), -+ FINISHED_LITEMATIC("finished_litematic"), -+ CANCEL_LITEMATIC("cancel_litematic"), -+ REMOVE_SYNCMATIC("remove_syncmatic"), -+ REGISTER_VERSION("register_version"), -+ CONFIRM_USER("confirm_user"), -+ FEATURE_REQUEST("feature_request"), -+ FEATURE("feature"), -+ MODIFY("modify"), -+ MODIFY_REQUEST("modify_request"), -+ MODIFY_REQUEST_DENY("modify_request_deny"), -+ MODIFY_REQUEST_ACCEPT("modify_request_accept"), -+ MODIFY_FINISH("modify_finish"), -+ MESSAGE("mesage"); -+ -+ public final ResourceLocation identifier; -+ -+ PacketType(final String id) { -+ identifier = new ResourceLocation(SyncmaticaProtocol.PROTOCOL_ID, id); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b5891b0b49173acfb1a94051b98cb03b7a5ec9cd ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java -@@ -0,0 +1,37 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+ -+import java.util.UUID; -+ -+public class PlayerIdentifier { -+ -+ public static final UUID MISSING_PLAYER_UUID = UUID.fromString("4c1b738f-56fa-4011-8273-498c972424ea"); -+ public static final PlayerIdentifier MISSING_PLAYER = new PlayerIdentifier(MISSING_PLAYER_UUID, "No Player"); -+ -+ public final UUID uuid; -+ private String bufferedPlayerName; -+ -+ PlayerIdentifier(final UUID uuid, final String bufferedPlayerName) { -+ this.uuid = uuid; -+ this.bufferedPlayerName = bufferedPlayerName; -+ } -+ -+ public String getName() { -+ return bufferedPlayerName; -+ } -+ -+ public void updatePlayerName(final String name) { -+ bufferedPlayerName = name; -+ } -+ -+ public JsonObject toJson() { -+ final JsonObject jsonObject = new JsonObject(); -+ -+ jsonObject.add("uuid", new JsonPrimitive(uuid.toString())); -+ jsonObject.add("name", new JsonPrimitive(bufferedPlayerName)); -+ -+ return jsonObject; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java -new file mode 100644 -index 0000000000000000000000000000000000000000..df2254edf89e17eec73a692577e77c613cd4c8e4 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java -@@ -0,0 +1,46 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import com.google.gson.JsonObject; -+import com.mojang.authlib.GameProfile; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget; -+ -+import java.util.HashMap; -+import java.util.Map; -+import java.util.UUID; -+ -+public class PlayerIdentifierProvider { -+ -+ private final Map identifiers = new HashMap<>(); -+ -+ public PlayerIdentifierProvider() { -+ identifiers.put(PlayerIdentifier.MISSING_PLAYER_UUID, PlayerIdentifier.MISSING_PLAYER); -+ } -+ -+ public PlayerIdentifier createOrGet(final ExchangeTarget exchangeTarget) { -+ return createOrGet(SyncmaticaProtocol.getCommunicationManager().getGameProfile(exchangeTarget)); -+ } -+ -+ public PlayerIdentifier createOrGet(final @NotNull GameProfile gameProfile) { -+ return createOrGet(gameProfile.getId(), gameProfile.getName()); -+ } -+ -+ public PlayerIdentifier createOrGet(final UUID uuid, final String playerName) { -+ return identifiers.computeIfAbsent(uuid, id -> new PlayerIdentifier(uuid, playerName)); -+ } -+ -+ public void updateName(final UUID uuid, final String playerName) { -+ createOrGet(uuid, playerName).updatePlayerName(playerName); -+ } -+ -+ public PlayerIdentifier fromJson(final @NotNull JsonObject obj) { -+ if (!obj.has("uuid") || !obj.has("name")) { -+ return PlayerIdentifier.MISSING_PLAYER; -+ } -+ -+ final UUID jsonUUID = UUID.fromString(obj.get("uuid").getAsString()); -+ return identifiers.computeIfAbsent(jsonUUID, -+ key -> new PlayerIdentifier(jsonUUID, obj.get("name").getAsString()) -+ ); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java -new file mode 100644 -index 0000000000000000000000000000000000000000..70759c9d3c01959169230503954f1f48c5392075 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java -@@ -0,0 +1,166 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.level.block.Mirror; -+import net.minecraft.world.level.block.Rotation; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+ -+import java.util.UUID; -+ -+public class ServerPlacement { -+ -+ private final UUID id; -+ -+ private final String fileName; -+ private final UUID hashValue; -+ -+ private PlayerIdentifier owner; -+ private PlayerIdentifier lastModifiedBy; -+ -+ private ServerPosition origin; -+ private Rotation rotation; -+ private Mirror mirror; -+ -+ private SubRegionData subRegionData = new SubRegionData(); -+ -+ public ServerPlacement(final UUID id, final String fileName, final UUID hashValue, final PlayerIdentifier owner) { -+ this.id = id; -+ this.fileName = fileName; -+ this.hashValue = hashValue; -+ this.owner = owner; -+ lastModifiedBy = owner; -+ } -+ -+ public UUID getId() { -+ return id; -+ } -+ -+ public String getName() { -+ return fileName; -+ } -+ -+ public UUID getHash() { -+ return hashValue; -+ } -+ -+ public String getDimension() { -+ return origin.getDimensionId(); -+ } -+ -+ public BlockPos getPosition() { -+ return origin.getBlockPosition(); -+ } -+ -+ public ServerPosition getOrigin() { -+ return origin; -+ } -+ -+ public Rotation getRotation() { -+ return rotation; -+ } -+ -+ public Mirror getMirror() { -+ return mirror; -+ } -+ -+ public ServerPlacement move(final String dimensionId, final BlockPos origin, final Rotation rotation, final Mirror mirror) { -+ move(new ServerPosition(origin, dimensionId), rotation, mirror); -+ return this; -+ } -+ -+ public ServerPlacement move(final ServerPosition origin, final Rotation rotation, final Mirror mirror) { -+ this.origin = origin; -+ this.rotation = rotation; -+ this.mirror = mirror; -+ return this; -+ } -+ -+ public PlayerIdentifier getOwner() { -+ return owner; -+ } -+ -+ public void setOwner(final PlayerIdentifier playerIdentifier) { -+ owner = playerIdentifier; -+ } -+ -+ public PlayerIdentifier getLastModifiedBy() { -+ return lastModifiedBy; -+ } -+ -+ public void setLastModifiedBy(final PlayerIdentifier lastModifiedBy) { -+ this.lastModifiedBy = lastModifiedBy; -+ } -+ -+ public SubRegionData getSubRegionData() { -+ return subRegionData; -+ } -+ -+ public JsonObject toJson() { -+ final JsonObject obj = new JsonObject(); -+ obj.add("id", new JsonPrimitive(id.toString())); -+ -+ obj.add("file_name", new JsonPrimitive(fileName)); -+ obj.add("hash", new JsonPrimitive(hashValue.toString())); -+ -+ obj.add("origin", origin.toJson()); -+ obj.add("rotation", new JsonPrimitive(rotation.name())); -+ obj.add("mirror", new JsonPrimitive(mirror.name())); -+ -+ obj.add("owner", owner.toJson()); -+ if (!owner.equals(lastModifiedBy)) { -+ obj.add("lastModifiedBy", lastModifiedBy.toJson()); -+ } -+ -+ if (subRegionData.isModified()) { -+ obj.add("subregionData", subRegionData.toJson()); -+ } -+ -+ return obj; -+ } -+ -+ @Nullable -+ public static ServerPlacement fromJson(final @NotNull JsonObject obj) { -+ if (obj.has("id") -+ && obj.has("file_name") -+ && obj.has("hash") -+ && obj.has("origin") -+ && obj.has("rotation") -+ && obj.has("mirror")) { -+ final UUID id = UUID.fromString(obj.get("id").getAsString()); -+ final String name = obj.get("file_name").getAsString(); -+ final UUID hashValue = UUID.fromString(obj.get("hash").getAsString()); -+ -+ PlayerIdentifier owner = PlayerIdentifier.MISSING_PLAYER; -+ if (obj.has("owner")) { -+ owner = SyncmaticaProtocol.getPlayerIdentifierProvider().fromJson(obj.get("owner").getAsJsonObject()); -+ } -+ -+ final ServerPlacement newPlacement = new ServerPlacement(id, name, hashValue, owner); -+ final ServerPosition pos = ServerPosition.fromJson(obj.get("origin").getAsJsonObject()); -+ if (pos == null) { -+ return null; -+ } -+ newPlacement.origin = pos; -+ newPlacement.rotation = Rotation.valueOf(obj.get("rotation").getAsString()); -+ newPlacement.mirror = Mirror.valueOf(obj.get("mirror").getAsString()); -+ -+ if (obj.has("lastModifiedBy")) { -+ newPlacement.lastModifiedBy = SyncmaticaProtocol.getPlayerIdentifierProvider() -+ .fromJson(obj.get("lastModifiedBy").getAsJsonObject()); -+ } else { -+ newPlacement.lastModifiedBy = owner; -+ } -+ -+ if (obj.has("subregionData")) { -+ newPlacement.subRegionData = SubRegionData.fromJson(obj.get("subregionData")); -+ } -+ -+ return newPlacement; -+ } -+ -+ return null; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9775c6c42a253aaaf1ac7576dba3764c8593d7fe ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java -@@ -0,0 +1,51 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import com.google.gson.JsonArray; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+import net.minecraft.core.BlockPos; -+ -+public class ServerPosition { -+ -+ private final BlockPos position; -+ private final String dimensionId; -+ -+ public ServerPosition(final BlockPos pos, final String dim) { -+ position = pos; -+ dimensionId = dim; -+ } -+ -+ public BlockPos getBlockPosition() { -+ return position; -+ } -+ -+ public String getDimensionId() { -+ return dimensionId; -+ } -+ -+ public JsonObject toJson() { -+ final JsonObject obj = new JsonObject(); -+ final JsonArray arr = new JsonArray(); -+ arr.add(new JsonPrimitive(position.getX())); -+ arr.add(new JsonPrimitive(position.getY())); -+ arr.add(new JsonPrimitive(position.getZ())); -+ obj.add("position", arr); -+ obj.add("dimension", new JsonPrimitive(dimensionId)); -+ return obj; -+ } -+ -+ public static ServerPosition fromJson(final JsonObject obj) { -+ if (obj.has("position") && obj.has("dimension")) { -+ final int x; -+ final int y; -+ final int z; -+ final JsonArray arr = obj.get("position").getAsJsonArray(); -+ x = arr.get(0).getAsInt(); -+ y = arr.get(1).getAsInt(); -+ z = arr.get(2).getAsInt(); -+ final BlockPos pos = new BlockPos(x, y, z); -+ return new ServerPosition(pos, obj.get("dimension").getAsString()); -+ } -+ return null; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..22fdf92dd686a2dc573eb60cd4d9a08ba7faec5a ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java -@@ -0,0 +1,90 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.level.block.Mirror; -+import net.minecraft.world.level.block.Rotation; -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.HashMap; -+import java.util.Map; -+ -+public class SubRegionData { -+ -+ private boolean isModified; -+ private Map modificationData; // is null when isModified is false -+ -+ public SubRegionData() { -+ this(false, null); -+ } -+ -+ public SubRegionData(final boolean isModified, final Map modificationData) { -+ this.isModified = isModified; -+ this.modificationData = modificationData; -+ } -+ -+ public void reset() { -+ isModified = false; -+ modificationData = null; -+ } -+ -+ public void modify(final String name, final BlockPos position, final Rotation rotation, final Mirror mirror) { -+ modify(new SubRegionPlacementModification(name, position, rotation, mirror)); -+ } -+ -+ public void modify(final SubRegionPlacementModification subRegionPlacementModification) { -+ if (subRegionPlacementModification == null) { -+ return; -+ } -+ isModified = true; -+ if (modificationData == null) { -+ modificationData = new HashMap<>(); -+ } -+ modificationData.put(subRegionPlacementModification.name, subRegionPlacementModification); -+ } -+ -+ public boolean isModified() { -+ return isModified; -+ } -+ -+ public Map getModificationData() { -+ return modificationData; -+ } -+ -+ public JsonElement toJson() { -+ return modificationDataToJson(); -+ } -+ -+ @NotNull -+ private JsonElement modificationDataToJson() { -+ final JsonArray arr = new JsonArray(); -+ -+ for (final Map.Entry entry : modificationData.entrySet()) { -+ arr.add(entry.getValue().toJson()); -+ } -+ -+ return arr; -+ } -+ -+ @NotNull -+ public static SubRegionData fromJson(final @NotNull JsonElement obj) { -+ final SubRegionData newSubRegionData = new SubRegionData(); -+ -+ newSubRegionData.isModified = true; -+ -+ for (final JsonElement modification : obj.getAsJsonArray()) { -+ newSubRegionData.modify(SubRegionPlacementModification.fromJson(modification.getAsJsonObject())); -+ } -+ -+ return newSubRegionData; -+ } -+ -+ @Override -+ public String toString() { -+ if (!isModified) { -+ return "[]"; -+ } -+ return modificationData == null ? "[ERROR:null]" : modificationData.toString(); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a52e299be26d1ec13507dac8d68f7e5736117762 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java -@@ -0,0 +1,65 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import com.google.gson.JsonArray; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.level.block.Mirror; -+import net.minecraft.world.level.block.Rotation; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+ -+public class SubRegionPlacementModification { -+ -+ public final String name; -+ public final BlockPos position; -+ public final Rotation rotation; -+ public final Mirror mirror; -+ -+ SubRegionPlacementModification(final String name, final BlockPos position, final Rotation rotation, final Mirror mirror) { -+ this.name = name; -+ this.position = position; -+ this.rotation = rotation; -+ this.mirror = mirror; -+ } -+ -+ public JsonObject toJson() { -+ final JsonObject obj = new JsonObject(); -+ -+ final JsonArray arr = new JsonArray(); -+ arr.add(position.getX()); -+ arr.add(position.getY()); -+ arr.add(position.getZ()); -+ obj.add("position", arr); -+ -+ obj.add("name", new JsonPrimitive(name)); -+ obj.add("rotation", new JsonPrimitive(rotation.name())); -+ obj.add("mirror", new JsonPrimitive(mirror.name())); -+ -+ return obj; -+ } -+ -+ @Nullable -+ public static SubRegionPlacementModification fromJson(final @NotNull JsonObject obj) { -+ if (!obj.has("name") || !obj.has("position") || !obj.has("rotation") || !obj.has("mirror")) { -+ return null; -+ } -+ -+ final String name = obj.get("name").getAsString(); -+ final JsonArray arr = obj.get("position").getAsJsonArray(); -+ if (arr.size() != 3) { -+ return null; -+ } -+ -+ final BlockPos position = new BlockPos(arr.get(0).getAsInt(), arr.get(1).getAsInt(), arr.get(2).getAsInt()); -+ final Rotation rotation = Rotation.valueOf(obj.get("rotation").getAsString()); -+ final Mirror mirror = Mirror.valueOf(obj.get("mirror").getAsString()); -+ -+ return new SubRegionPlacementModification(name, position, rotation, mirror); -+ } -+ -+ @Override -+ public String toString() { -+ return String.format("[name=%s, position=%s, rotation=%s, mirror=%s]", name, position, rotation, mirror); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..27a056b306daa91400946a30e68ce01d47089ac8 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java -@@ -0,0 +1,108 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import com.google.gson.GsonBuilder; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonParser; -+import org.jetbrains.annotations.NotNull; -+ -+import java.io.File; -+import java.io.FileReader; -+import java.io.FileWriter; -+import java.io.IOException; -+import java.util.Collection; -+import java.util.HashMap; -+import java.util.Map; -+import java.util.UUID; -+ -+public class SyncmaticManager { -+ -+ public static final String PLACEMENTS_JSON_KEY = "placements"; -+ private final Map schematics = new HashMap<>(); -+ -+ public void addPlacement(final ServerPlacement placement) { -+ schematics.put(placement.getId(), placement); -+ updateServerPlacement(); -+ } -+ -+ public ServerPlacement getPlacement(final UUID id) { -+ return schematics.get(id); -+ } -+ -+ public Collection getAll() { -+ return schematics.values(); -+ } -+ -+ public void removePlacement(final @NotNull ServerPlacement placement) { -+ schematics.remove(placement.getId()); -+ updateServerPlacement(); -+ } -+ -+ public void updateServerPlacement() { -+ saveServer(); -+ } -+ -+ public void startup() { -+ loadServer(); -+ } -+ -+ private void saveServer() { -+ final JsonObject obj = new JsonObject(); -+ final JsonArray arr = new JsonArray(); -+ -+ for (final ServerPlacement p : getAll()) { -+ arr.add(p.toJson()); -+ } -+ -+ obj.add(PLACEMENTS_JSON_KEY, arr); -+ final File backup = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json.bak"); -+ final File incoming = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json.new"); -+ final File current = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json"); -+ -+ try (final FileWriter writer = new FileWriter(incoming)) { -+ writer.write(new GsonBuilder().setPrettyPrinting().create().toJson(obj)); -+ } catch (final IOException e) { -+ e.printStackTrace(); -+ return; -+ } -+ -+ SyncmaticaProtocol.backupAndReplace(backup.toPath(), current.toPath(), incoming.toPath()); -+ } -+ -+ private void loadServer() { -+ final File f = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json"); -+ if (f.exists() && f.isFile() && f.canRead()) { -+ JsonElement element = null; -+ try { -+ final JsonParser parser = new JsonParser(); -+ final FileReader reader = new FileReader(f); -+ -+ element = parser.parse(reader); -+ reader.close(); -+ -+ } catch (final Exception e) { -+ e.printStackTrace(); -+ } -+ if (element == null) { -+ return; -+ } -+ try { -+ final JsonObject obj = element.getAsJsonObject(); -+ if (obj == null || !obj.has(PLACEMENTS_JSON_KEY)) { -+ return; -+ } -+ final JsonArray arr = obj.getAsJsonArray(PLACEMENTS_JSON_KEY); -+ for (final JsonElement elem : arr) { -+ final ServerPlacement placement = ServerPlacement.fromJson(elem.getAsJsonObject()); -+ if (placement != null) { -+ schematics.put(placement.getId(), placement); -+ } -+ } -+ -+ } catch (final IllegalStateException | NullPointerException e) { -+ e.printStackTrace(); -+ } -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d7a3c85df0f5950f3f0c69c33fa5d809f69de4de ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java -@@ -0,0 +1,26 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+ -+public record SyncmaticaPayload(ResourceLocation packetType, FriendlyByteBuf data) implements LeavesCustomPayload { -+ -+ private static final ResourceLocation NETWORK_ID = new ResourceLocation(SyncmaticaProtocol.PROTOCOL_ID, "main"); -+ -+ @New -+ public static SyncmaticaPayload decode(ResourceLocation location, FriendlyByteBuf buf) { -+ return new SyncmaticaPayload(buf.readResourceLocation(), new FriendlyByteBuf(buf.readBytes(buf.readableBytes()))); -+ } -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ buf.writeResourceLocation(this.packetType); -+ buf.writeBytes(this.data.readBytes(this.data.readableBytes())); -+ } -+ -+ @Override -+ public ResourceLocation id() { -+ return NETWORK_ID; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..055da1928daddedaa72e8d805193f81e4aaafdea ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java -@@ -0,0 +1,127 @@ -+package org.leavesmc.leaves.protocol.syncmatica; -+ -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+ -+import java.io.File; -+import java.io.IOException; -+import java.io.InputStream; -+import java.nio.file.Files; -+import java.nio.file.Path; -+import java.security.MessageDigest; -+import java.security.NoSuchAlgorithmException; -+import java.util.Arrays; -+import java.util.UUID; -+ -+public class SyncmaticaProtocol { -+ -+ public static final String PROTOCOL_ID = "syncmatica"; -+ public static final String PROTOCOL_VERSION = "leaves-syncmatica-1.1.0"; -+ -+ private static boolean loaded = false; -+ private static final File litematicFolder = new File("." + File.separator + "syncmatics"); -+ private static final PlayerIdentifierProvider playerIdentifierProvider = new PlayerIdentifierProvider(); -+ private static final CommunicationManager communicationManager = new CommunicationManager(); -+ private static final FeatureSet featureSet = new FeatureSet(Arrays.asList(Feature.values())); -+ private static final SyncmaticManager syncmaticManager = new SyncmaticManager(); -+ private static final FileStorage fileStorage = new FileStorage(); -+ -+ public static File getLitematicFolder() { -+ return litematicFolder; -+ } -+ -+ public static PlayerIdentifierProvider getPlayerIdentifierProvider() { -+ return playerIdentifierProvider; -+ } -+ -+ public static CommunicationManager getCommunicationManager() { -+ return communicationManager; -+ } -+ -+ public static FeatureSet getFeatureSet() { -+ return featureSet; -+ } -+ -+ public static SyncmaticManager getSyncmaticManager() { -+ return syncmaticManager; -+ } -+ -+ public static FileStorage getFileStorage() { -+ return fileStorage; -+ } -+ -+ public static void init() { -+ if (!loaded) { -+ litematicFolder.mkdirs(); -+ syncmaticManager.startup(); -+ loaded = true; -+ } -+ } -+ -+ @NotNull -+ public static UUID createChecksum(final @NotNull InputStream fis) throws NoSuchAlgorithmException, IOException { -+ final byte[] buffer = new byte[4096]; -+ final MessageDigest messageDigest = MessageDigest.getInstance("MD5"); -+ int numRead; -+ -+ do { -+ numRead = fis.read(buffer); -+ if (numRead > 0) { -+ messageDigest.update(buffer, 0, numRead); -+ } -+ } while (numRead != -1); -+ -+ fis.close(); -+ return UUID.nameUUIDFromBytes(messageDigest.digest()); -+ } -+ -+ private static final int[] ILLEGAL_CHARS = {34, 60, 62, 124, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 58, 42, 63, 92, 47}; -+ private static final String ILLEGAL_PATTERNS = "(^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\\..*)?$)|(^\\.\\.*$)"; -+ -+ @NotNull -+ public static String sanitizeFileName(final @NotNull String badFileName) { -+ final StringBuilder sanitized = new StringBuilder(); -+ final int len = badFileName.codePointCount(0, badFileName.length()); -+ -+ for (int i = 0; i < len; i++) { -+ final int c = badFileName.codePointAt(i); -+ if (Arrays.binarySearch(ILLEGAL_CHARS, c) < 0) { -+ sanitized.appendCodePoint(c); -+ if (sanitized.length() == 255) { -+ break; -+ } -+ } -+ } -+ -+ return sanitized.toString().replaceAll(ILLEGAL_PATTERNS, "_"); -+ } -+ -+ public static boolean isOverQuota(int sent) { -+ return LeavesConfig.protocol.syncmatica.useQuota && sent > LeavesConfig.protocol.syncmatica.quotaLimit; -+ } -+ -+ public static void backupAndReplace(final Path backup, final Path current, final Path incoming) { -+ if (!Files.exists(incoming)) { -+ return; -+ } -+ if (overwrite(backup, current, 2) && !overwrite(current, incoming, 4)) { -+ overwrite(current, backup, 8); -+ } -+ } -+ -+ private static boolean overwrite(final Path backup, final Path current, final int tries) { -+ if (!Files.exists(current)) { -+ return true; -+ } -+ try { -+ Files.deleteIfExists(backup); -+ Files.move(current, backup); -+ } catch (final IOException exception) { -+ if (tries <= 0) { -+ return false; -+ } -+ return overwrite(backup, current, tries - 1); -+ } -+ return true; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b06ffeacf699b78f34253a26018ccdf723d5d0ce ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java -@@ -0,0 +1,66 @@ -+package org.leavesmc.leaves.protocol.syncmatica.exchange; -+ -+import net.minecraft.network.FriendlyByteBuf; -+import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; -+import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; -+ -+import java.util.UUID; -+ -+public abstract class AbstractExchange implements Exchange { -+ -+ private boolean success = false; -+ private boolean finished = false; -+ private final ExchangeTarget partner; -+ -+ protected AbstractExchange(final ExchangeTarget partner) { -+ this.partner = partner; -+ } -+ -+ @Override -+ public ExchangeTarget getPartner() { -+ return partner; -+ } -+ -+ @Override -+ public boolean isFinished() { -+ return finished; -+ } -+ -+ @Override -+ public boolean isSuccessful() { -+ return success; -+ } -+ -+ @Override -+ public void close(final boolean notifyPartner) { -+ finished = true; -+ success = false; -+ onClose(); -+ if (notifyPartner) { -+ sendCancelPacket(); -+ } -+ } -+ -+ public CommunicationManager getManager() { -+ return SyncmaticaProtocol.getCommunicationManager(); -+ } -+ -+ protected void sendCancelPacket() { -+ } -+ -+ protected void onClose() { -+ } -+ -+ protected void succeed() { -+ finished = true; -+ success = true; -+ onClose(); -+ } -+ -+ protected static boolean checkUUID(final FriendlyByteBuf sourceBuf, final UUID targetId) { -+ final int r = sourceBuf.readerIndex(); -+ final UUID sourceId = sourceBuf.readUUID(); -+ sourceBuf.readerIndex(r); -+ return sourceId.equals(targetId); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b0463dc8dc6f204cd48b73056dc4eb321e7ba602 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java -@@ -0,0 +1,125 @@ -+package org.leavesmc.leaves.protocol.syncmatica.exchange; -+ -+import io.netty.buffer.Unpooled; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.syncmatica.MessageType; -+import org.leavesmc.leaves.protocol.syncmatica.PacketType; -+import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; -+import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; -+ -+import java.io.File; -+import java.io.FileOutputStream; -+import java.io.IOException; -+import java.io.OutputStream; -+import java.security.DigestOutputStream; -+import java.security.MessageDigest; -+import java.security.NoSuchAlgorithmException; -+import java.util.UUID; -+ -+public class DownloadExchange extends AbstractExchange { -+ -+ private final ServerPlacement toDownload; -+ private final OutputStream outputStream; -+ private final MessageDigest md5; -+ private final File downloadFile; -+ private int bytesSent; -+ -+ public DownloadExchange(final ServerPlacement syncmatic, final File downloadFile, final ExchangeTarget partner) throws IOException, NoSuchAlgorithmException { -+ super(partner); -+ this.downloadFile = downloadFile; -+ final OutputStream os = new FileOutputStream(downloadFile); -+ toDownload = syncmatic; -+ md5 = MessageDigest.getInstance("MD5"); -+ outputStream = new DigestOutputStream(os, md5); -+ } -+ -+ @Override -+ public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ if (id.equals(PacketType.SEND_LITEMATIC.identifier) -+ || id.equals(PacketType.FINISHED_LITEMATIC.identifier) -+ || id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { -+ return checkUUID(packetBuf, toDownload.getId()); -+ } -+ return false; -+ } -+ -+ @Override -+ public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { -+ packetBuf.readUUID(); -+ if (id.equals(PacketType.SEND_LITEMATIC.identifier)) { -+ final int size = packetBuf.readInt(); -+ bytesSent += size; -+ if (SyncmaticaProtocol.isOverQuota(bytesSent)) { -+ close(true); -+ SyncmaticaProtocol.getCommunicationManager().sendMessage( -+ getPartner(), -+ MessageType.ERROR, -+ "syncmatica.error.cancelled_transmit_exceed_quota" -+ ); -+ } -+ try { -+ packetBuf.readBytes(outputStream, size); -+ } catch (final IOException e) { -+ close(true); -+ e.printStackTrace(); -+ return; -+ } -+ final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ FriendlyByteBuf.writeUUID(toDownload.getId()); -+ getPartner().sendPacket(PacketType.RECEIVED_LITEMATIC.identifier, FriendlyByteBuf); -+ return; -+ } -+ if (id.equals(PacketType.FINISHED_LITEMATIC.identifier)) { -+ try { -+ outputStream.flush(); -+ } catch (final IOException e) { -+ close(false); -+ e.printStackTrace(); -+ return; -+ } -+ final UUID downloadHash = UUID.nameUUIDFromBytes(md5.digest()); -+ if (downloadHash.equals(toDownload.getHash())) { -+ succeed(); -+ } else { -+ close(false); -+ } -+ return; -+ } -+ if (id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { -+ close(false); -+ } -+ } -+ -+ @Override -+ public void init() { -+ final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ FriendlyByteBuf.writeUUID(toDownload.getId()); -+ getPartner().sendPacket(PacketType.REQUEST_LITEMATIC.identifier, FriendlyByteBuf); -+ } -+ -+ @Override -+ protected void onClose() { -+ getManager().setDownloadState(toDownload, false); -+ try { -+ outputStream.close(); -+ } catch (final IOException e) { -+ e.printStackTrace(); -+ } -+ if (!isSuccessful() && downloadFile.exists()) { -+ downloadFile.delete(); -+ } -+ } -+ -+ @Override -+ protected void sendCancelPacket() { -+ final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ FriendlyByteBuf.writeUUID(toDownload.getId()); -+ getPartner().sendPacket(PacketType.CANCEL_LITEMATIC.identifier, FriendlyByteBuf); -+ } -+ -+ public ServerPlacement getPlacement() { -+ return toDownload; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0f45ef7f4abcd7cff627e5a3df2a9fca8d6e7585 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java -@@ -0,0 +1,20 @@ -+package org.leavesmc.leaves.protocol.syncmatica.exchange; -+ -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+ -+public interface Exchange { -+ ExchangeTarget getPartner(); -+ -+ boolean checkPacket(ResourceLocation id, FriendlyByteBuf packetBuf); -+ -+ void handle(ResourceLocation id, FriendlyByteBuf packetBuf); -+ -+ boolean isFinished(); -+ -+ boolean isSuccessful(); -+ -+ void close(boolean notifyPartner); -+ -+ void init(); -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b0da7abf0748d4bdb3141c28f201906157a0eaad ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java -@@ -0,0 +1,39 @@ -+package org.leavesmc.leaves.protocol.syncmatica.exchange; -+ -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.network.ServerGamePacketListenerImpl; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; -+import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaPayload; -+ -+import java.util.ArrayList; -+import java.util.Collection; -+import java.util.List; -+ -+public class ExchangeTarget { -+ -+ private final List ongoingExchanges = new ArrayList<>(); -+ private final ServerGamePacketListenerImpl client; -+ private FeatureSet features; -+ -+ public ExchangeTarget(final ServerGamePacketListenerImpl client) { -+ this.client = client; -+ } -+ -+ public void sendPacket(final ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ ProtocolUtils.sendPayloadPacket(client.player, new SyncmaticaPayload(id, packetBuf)); -+ } -+ -+ public FeatureSet getFeatureSet() { -+ return features; -+ } -+ -+ public void setFeatureSet(final FeatureSet f) { -+ features = f; -+ } -+ -+ public Collection getExchanges() { -+ return ongoingExchanges; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java -new file mode 100644 -index 0000000000000000000000000000000000000000..45fc01915b18a47bcdf7a7ce55161266e5cd1221 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java -@@ -0,0 +1,48 @@ -+package org.leavesmc.leaves.protocol.syncmatica.exchange; -+ -+import io.netty.buffer.Unpooled; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; -+import org.leavesmc.leaves.protocol.syncmatica.PacketType; -+import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; -+ -+public abstract class FeatureExchange extends AbstractExchange { -+ -+ protected FeatureExchange(final ExchangeTarget partner) { -+ super(partner); -+ } -+ -+ @Override -+ public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ return id.equals(PacketType.FEATURE_REQUEST.identifier) -+ || id.equals(PacketType.FEATURE.identifier); -+ } -+ -+ @Override -+ public void handle(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ if (id.equals(PacketType.FEATURE_REQUEST.identifier)) { -+ sendFeatures(); -+ } else if (id.equals(PacketType.FEATURE.identifier)) { -+ final FeatureSet fs = FeatureSet.fromString(packetBuf.readUtf(32767)); -+ getPartner().setFeatureSet(fs); -+ onFeatureSetReceive(); -+ } -+ } -+ -+ protected void onFeatureSetReceive() { -+ succeed(); -+ } -+ -+ public void requestFeatureSet() { -+ getPartner().sendPacket(PacketType.FEATURE_REQUEST.identifier, new FriendlyByteBuf(Unpooled.buffer())); -+ } -+ -+ private void sendFeatures() { -+ final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); -+ final FeatureSet fs = SyncmaticaProtocol.getFeatureSet(); -+ buf.writeUtf(fs.toString(), 32767); -+ getPartner().sendPacket(PacketType.FEATURE.identifier, buf); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c691201f0af82c4ac19df27639b32637b7f46caf ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java -@@ -0,0 +1,81 @@ -+package org.leavesmc.leaves.protocol.syncmatica.exchange; -+ -+import io.netty.buffer.Unpooled; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.syncmatica.PacketType; -+import org.leavesmc.leaves.protocol.syncmatica.PlayerIdentifier; -+import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; -+import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; -+ -+import java.util.UUID; -+ -+public class ModifyExchangeServer extends AbstractExchange { -+ -+ private final ServerPlacement placement; -+ UUID placementId; -+ -+ public ModifyExchangeServer(final UUID placeId, final ExchangeTarget partner) { -+ super(partner); -+ placementId = placeId; -+ placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(placementId); -+ } -+ -+ @Override -+ public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ return id.equals(PacketType.MODIFY_FINISH.identifier) && checkUUID(packetBuf, placement.getId()); -+ } -+ -+ @Override -+ public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { -+ packetBuf.readUUID(); -+ if (id.equals(PacketType.MODIFY_FINISH.identifier)) { -+ SyncmaticaProtocol.getCommunicationManager().receivePositionData(placement, packetBuf, getPartner()); -+ final PlayerIdentifier identifier = SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet( -+ getPartner() -+ ); -+ placement.setLastModifiedBy(identifier); -+ SyncmaticaProtocol.getSyncmaticManager().updateServerPlacement(); -+ succeed(); -+ } -+ } -+ -+ @Override -+ public void init() { -+ if (getPlacement() == null || SyncmaticaProtocol.getCommunicationManager().getModifier(placement) != null) { -+ close(true); -+ } else { -+ if (SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet(this.getPartner()).uuid.equals(placement.getOwner().uuid)) { -+ accept(); -+ } else { -+ close(true); -+ } -+ } -+ } -+ -+ private void accept() { -+ final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); -+ buf.writeUUID(placement.getId()); -+ getPartner().sendPacket(PacketType.MODIFY_REQUEST_ACCEPT.identifier, buf); -+ SyncmaticaProtocol.getCommunicationManager().setModifier(placement, this); -+ } -+ -+ @Override -+ protected void sendCancelPacket() { -+ final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); -+ buf.writeUUID(placementId); -+ getPartner().sendPacket(PacketType.MODIFY_REQUEST_DENY.identifier, buf); -+ } -+ -+ public ServerPlacement getPlacement() { -+ return placement; -+ } -+ -+ @Override -+ protected void onClose() { -+ if (SyncmaticaProtocol.getCommunicationManager().getModifier(placement) == this) { -+ SyncmaticaProtocol.getCommunicationManager().setModifier(placement, null); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java -new file mode 100644 -index 0000000000000000000000000000000000000000..065a39942603f7b884cca40d8d7b4b47b46d7985 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java -@@ -0,0 +1,101 @@ -+package org.leavesmc.leaves.protocol.syncmatica.exchange; -+ -+import io.netty.buffer.Unpooled; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.syncmatica.PacketType; -+import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; -+ -+import java.io.File; -+import java.io.FileInputStream; -+import java.io.FileNotFoundException; -+import java.io.IOException; -+import java.io.InputStream; -+ -+public class UploadExchange extends AbstractExchange { -+ -+ private static final int BUFFER_SIZE = 16384; -+ -+ private final ServerPlacement toUpload; -+ private final InputStream inputStream; -+ private final byte[] buffer = new byte[BUFFER_SIZE]; -+ -+ public UploadExchange(final ServerPlacement syncmatic, final File uploadFile, final ExchangeTarget partner) throws FileNotFoundException { -+ super(partner); -+ toUpload = syncmatic; -+ inputStream = new FileInputStream(uploadFile); -+ } -+ -+ @Override -+ public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ if (id.equals(PacketType.RECEIVED_LITEMATIC.identifier) -+ || id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { -+ return checkUUID(packetBuf, toUpload.getId()); -+ } -+ return false; -+ } -+ -+ @Override -+ public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { -+ packetBuf.readUUID(); -+ if (id.equals(PacketType.RECEIVED_LITEMATIC.identifier)) { -+ send(); -+ } -+ if (id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { -+ close(false); -+ } -+ } -+ -+ private void send() { -+ final int bytesRead; -+ try { -+ bytesRead = inputStream.read(buffer); -+ } catch (final IOException e) { -+ close(true); -+ e.printStackTrace(); -+ return; -+ } -+ if (bytesRead == -1) { -+ sendFinish(); -+ } else { -+ sendData(bytesRead); -+ } -+ } -+ -+ private void sendData(final int bytesRead) { -+ final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ FriendlyByteBuf.writeUUID(toUpload.getId()); -+ FriendlyByteBuf.writeInt(bytesRead); -+ FriendlyByteBuf.writeBytes(buffer, 0, bytesRead); -+ getPartner().sendPacket(PacketType.SEND_LITEMATIC.identifier, FriendlyByteBuf); -+ } -+ -+ private void sendFinish() { -+ final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ FriendlyByteBuf.writeUUID(toUpload.getId()); -+ getPartner().sendPacket(PacketType.FINISHED_LITEMATIC.identifier, FriendlyByteBuf); -+ succeed(); -+ } -+ -+ @Override -+ public void init() { -+ send(); -+ } -+ -+ @Override -+ protected void onClose() { -+ try { -+ inputStream.close(); -+ } catch (final IOException e) { -+ e.printStackTrace(); -+ } -+ } -+ -+ @Override -+ protected void sendCancelPacket() { -+ final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ FriendlyByteBuf.writeUUID(toUpload.getId()); -+ getPartner().sendPacket(PacketType.CANCEL_LITEMATIC.identifier, FriendlyByteBuf); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..cb7aed6c0aa31cd46c071da7b483c63e755a6bff ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java -@@ -0,0 +1,66 @@ -+package org.leavesmc.leaves.protocol.syncmatica.exchange; -+ -+import io.netty.buffer.Unpooled; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; -+import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; -+import org.leavesmc.leaves.protocol.syncmatica.PacketType; -+import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; -+import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; -+ -+import java.util.Collection; -+ -+public class VersionHandshakeServer extends FeatureExchange { -+ -+ public VersionHandshakeServer(final ExchangeTarget partner) { -+ super(partner); -+ } -+ -+ @Override -+ public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ return id.equals(PacketType.REGISTER_VERSION.identifier) -+ || super.checkPacket(id, packetBuf); -+ } -+ -+ @Override -+ public void handle(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { -+ if (id.equals(PacketType.REGISTER_VERSION.identifier)) { -+ String partnerVersion = packetBuf.readUtf(); -+ if (partnerVersion.equals("0.0.1")) { -+ close(false); -+ return; -+ } -+ final FeatureSet fs = FeatureSet.fromVersionString(partnerVersion); -+ if (fs == null) { -+ requestFeatureSet(); -+ } else { -+ getPartner().setFeatureSet(fs); -+ onFeatureSetReceive(); -+ } -+ } else { -+ super.handle(id, packetBuf); -+ } -+ -+ } -+ -+ @Override -+ public void onFeatureSetReceive() { -+ final FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ final Collection l = SyncmaticaProtocol.getSyncmaticManager().getAll(); -+ newBuf.writeInt(l.size()); -+ for (final ServerPlacement p : l) { -+ CommunicationManager.putMetaData(p, newBuf, getPartner()); -+ } -+ getPartner().sendPacket(PacketType.CONFIRM_USER.identifier, newBuf); -+ succeed(); -+ } -+ -+ @Override -+ public void init() { -+ final FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); -+ newBuf.writeUtf(SyncmaticaProtocol.PROTOCOL_VERSION); -+ getPartner().sendPacket(PacketType.REGISTER_VERSION.identifier, newBuf); -+ } -+} diff --git a/patches/server/0051-Redstone-wire-dont-connect-if-on-trapdoor.patch b/patches/server/0051-Redstone-wire-dont-connect-if-on-trapdoor.patch deleted file mode 100644 index 24f7d0fc..00000000 --- a/patches/server/0051-Redstone-wire-dont-connect-if-on-trapdoor.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 14 Jun 2023 12:07:07 +0800 -Subject: [PATCH] Redstone wire dont connect if on trapdoor - - -diff --git a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -index 09b8f5335cb7651d90f4d1ca61b2ec5aa324e443..22b4abfdda9df1003dc57696d0b7524db442de88 100644 ---- a/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/RedStoneWireBlock.java -@@ -204,7 +204,7 @@ public class RedStoneWireBlock extends Block { - RandomSource random - ) { - if (direction == Direction.DOWN) { -- return !this.canSurviveOn(world, neighborPos, neighborState) ? Blocks.AIR.defaultBlockState() : state; -+ return org.leavesmc.leaves.LeavesConfig.modify.oldMC.updater.redstoneDontCantOnTrapDoor ? state : !this.canSurviveOn(world, neighborPos, neighborState) ? Blocks.AIR.defaultBlockState() : state; // Leaves - behavior to 1.19 - } else if (direction == Direction.UP) { - return this.getConnectionState(world, state, pos); - } else { -@@ -263,7 +263,7 @@ public class RedStoneWireBlock extends Block { - BlockPos blockPos = pos.relative(direction); - BlockState blockState = world.getBlockState(blockPos); - if (bl) { -- boolean bl2 = blockState.getBlock() instanceof TrapDoorBlock || this.canSurviveOn(world, blockPos, blockState); -+ boolean bl2 = (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.updater.redstoneDontCantOnTrapDoor && blockState.getBlock() instanceof TrapDoorBlock) || this.canSurviveOn(world, blockPos, blockState); // Leaves - behavior to 1.19 - if (bl2 && shouldConnectTo(world.getBlockState(blockPos.above()))) { - if (blockState.isFaceSturdy(world, blockPos, direction.getOpposite())) { - return RedstoneSide.UP; diff --git a/patches/server/0054-Leaves-carpet-support.patch b/patches/server/0054-Leaves-carpet-support.patch deleted file mode 100644 index 0a5140e1..00000000 --- a/patches/server/0054-Leaves-carpet-support.patch +++ /dev/null @@ -1,132 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 27 Jun 2023 01:54:43 +0800 -Subject: [PATCH] Leaves carpet support - - -diff --git a/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b68179532ceace257968b8fd7776608ab0bb798d ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/CarpetServerProtocol.java -@@ -0,0 +1,120 @@ -+package org.leavesmc.leaves.protocol; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+import java.util.HashMap; -+import java.util.Locale; -+import java.util.Map; -+ -+@LeavesProtocol(namespace = "carpet") -+public class CarpetServerProtocol { -+ -+ public static final String PROTOCOL_ID = "carpet"; -+ public static final String VERSION = ProtocolUtils.buildProtocolVersion(PROTOCOL_ID); -+ -+ private static final ResourceLocation HELLO_ID = CarpetServerProtocol.id("hello"); -+ -+ private static final String HI = "69"; -+ private static final String HELLO = "420"; -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation id(String path) { -+ return new ResourceLocation(PROTOCOL_ID, path); -+ } -+ -+ @ProtocolHandler.PlayerJoin -+ public static void onPlayerJoin(ServerPlayer player) { -+ if (LeavesConfig.protocol.leavesCarpetSupport) { -+ CompoundTag data = new CompoundTag(); -+ data.putString(HI, VERSION); -+ ProtocolUtils.sendPayloadPacket(player, new CarpetPayload(data)); -+ } -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = CarpetPayload.class, payloadId = "hello") -+ private static void handleHello(@NotNull ServerPlayer player, @NotNull CarpetServerProtocol.CarpetPayload payload) { -+ if (LeavesConfig.protocol.leavesCarpetSupport) { -+ if (payload.nbt.contains(HELLO)) { -+ LeavesLogger.LOGGER.info("Player " + player.getScoreboardName() + " joined with carpet " + payload.nbt.getString(HELLO)); -+ CompoundTag data = new CompoundTag(); -+ CarpetRules.write(data); -+ ProtocolUtils.sendPayloadPacket(player, new CarpetPayload(data)); -+ } -+ } -+ } -+ -+ public static class CarpetRules { -+ -+ private static final Map rules = new HashMap<>(); -+ -+ public static void write(@NotNull CompoundTag tag) { -+ CompoundTag rulesNbt = new CompoundTag(); -+ rules.values().forEach(rule -> rule.writeNBT(rulesNbt)); -+ -+ tag.put("Rules", rulesNbt); -+ } -+ -+ public static void register(CarpetRule rule) { -+ rules.put(rule.name, rule); -+ } -+ } -+ -+ public record CarpetRule(String identifier, String name, String value) { -+ -+ @NotNull -+ @Contract("_, _, _ -> new") -+ public static CarpetRule of(String identifier, String name, Enum value) { -+ return new CarpetRule(identifier, name, value.name().toLowerCase(Locale.ROOT)); -+ } -+ -+ @NotNull -+ @Contract("_, _, _ -> new") -+ public static CarpetRule of(String identifier, String name, boolean value) { -+ return new CarpetRule(identifier, name, Boolean.toString(value)); -+ } -+ -+ public void writeNBT(@NotNull CompoundTag rules) { -+ CompoundTag rule = new CompoundTag(); -+ String key = name; -+ -+ while (rules.contains(key)) { -+ key = key + "2"; -+ } -+ -+ rule.putString("Value", value); -+ rule.putString("Manager", identifier); -+ rule.putString("Rule", name); -+ rules.put(key, rule); -+ } -+ } -+ -+ public record CarpetPayload(CompoundTag nbt) implements LeavesCustomPayload { -+ -+ @New -+ public CarpetPayload(ResourceLocation location, FriendlyByteBuf buf) { -+ this(buf.readNbt()); -+ } -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ buf.writeNbt(nbt); -+ } -+ -+ @Override -+ @NotNull -+ public ResourceLocation id() { -+ return HELLO_ID; -+ } -+ } -+} diff --git a/patches/server/0055-Creative-fly-no-clip.patch b/patches/server/0055-Creative-fly-no-clip.patch deleted file mode 100644 index 66b30f62..00000000 --- a/patches/server/0055-Creative-fly-no-clip.patch +++ /dev/null @@ -1,121 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 27 Jun 2023 09:26:58 +0800 -Subject: [PATCH] Creative fly no clip - - -diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index c7bc8f493305cca9bac60c79e4e35cf5e369d355..e4bb0b2eb8512f75042dab77bb79b4f51742ab4e 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Player.java -+++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -252,8 +252,8 @@ public abstract class Player extends LivingEntity { - - @Override - public void tick() { -- this.noPhysics = this.isSpectator(); -- if (this.isSpectator()) { -+ this.noPhysics = this.isCreativeFlyOrSpectator(); // Leaves - creative no clip -+ if (this.isCreativeFlyOrSpectator()) { // Leaves - creative no clip - this.setOnGround(false); - } - -@@ -476,7 +476,7 @@ public abstract class Player extends LivingEntity { - - Pose entitypose1; - -- if (!this.isSpectator() && !this.isPassenger() && !this.canPlayerFitWithinBlocksAndEntitiesWhen(entitypose)) { -+ if (!this.isCreativeFlyOrSpectator() && !this.isPassenger() && !this.canPlayerFitWithinBlocksAndEntitiesWhen(entitypose)) { // Leaves - creative no clip - if (this.canPlayerFitWithinBlocksAndEntitiesWhen(Pose.CROUCHING)) { - entitypose1 = Pose.CROUCHING; - } else { -@@ -612,7 +612,7 @@ public abstract class Player extends LivingEntity { - } - - this.bob += (f - this.bob) * 0.4F; -- if (this.getHealth() > 0.0F && !this.isSpectator()) { -+ if (this.getHealth() > 0.0F && !this.isCreativeFlyOrSpectator()) { // Leaves - creative no clip - AABB axisalignedbb; - - if (this.isPassenger() && !this.getVehicle().isRemoved()) { -@@ -2058,6 +2058,21 @@ public abstract class Player extends LivingEntity { - @Override - public abstract boolean isSpectator(); - -+ // Leaves start - creative no clip -+ public boolean isCreativeFlyOrSpectator() { -+ return isSpectator() || (org.leavesmc.leaves.LeavesConfig.modify.creativeNoClip && isCreative() && getAbilities().flying); -+ } -+ -+ public boolean canSpectatingPlace(Level world, BlockState state, BlockPos pos, net.minecraft.world.phys.shapes.CollisionContext context) { -+ if (this.isCreativeFlyOrSpectator()) { -+ net.minecraft.world.phys.shapes.VoxelShape voxelShape = state.getCollisionShape(world, pos, context); -+ return voxelShape.isEmpty() || world.isUnobstructed(this, voxelShape.move(pos.getX(), pos.getY(), pos.getZ())); -+ } else { -+ return world.isUnobstructed(state, pos, context); -+ } -+ } -+ // Leaves end - creative no clip -+ - @Override - public boolean canBeHitByProjectile() { - return !this.isSpectator() && super.canBeHitByProjectile(); -diff --git a/src/main/java/net/minecraft/world/item/BlockItem.java b/src/main/java/net/minecraft/world/item/BlockItem.java -index 77309808abd4ab476e815d60015ad828102a1f6b..b101113c67bee1fb1732c49a32a813736f4c7c18 100644 ---- a/src/main/java/net/minecraft/world/item/BlockItem.java -+++ b/src/main/java/net/minecraft/world/item/BlockItem.java -@@ -217,7 +217,7 @@ public class BlockItem extends Item { - CollisionContext voxelshapecollision = entityhuman == null ? CollisionContext.empty() : CollisionContext.of(entityhuman); - // CraftBukkit start - store default return - Level world = context.getLevel(); // Paper - Cancel hit for vanished players -- boolean defaultReturn = (!this.mustSurvive() || state.canSurvive(context.getLevel(), context.getClickedPos())) && world.checkEntityCollision(state, entityhuman, voxelshapecollision, context.getClickedPos(), true); // Paper - Cancel hit for vanished players -+ boolean defaultReturn = (!this.mustSurvive() || state.canSurvive(context.getLevel(), context.getClickedPos())) && (org.leavesmc.leaves.LeavesConfig.modify.creativeNoClip && context.getPlayer() != null ? context.getPlayer().canSpectatingPlace(world, state, context.getClickedPos(), voxelshapecollision) : world.checkEntityCollision(state, entityhuman, voxelshapecollision, context.getClickedPos(), true)); // Paper - Cancel hit for vanished players // Leaves - creative no clip - org.bukkit.entity.Player player = (context.getPlayer() instanceof ServerPlayer) ? (org.bukkit.entity.Player) context.getPlayer().getBukkitEntity() : null; - - BlockCanBuildEvent event = new BlockCanBuildEvent(CraftBlock.at(context.getLevel(), context.getClickedPos()), player, CraftBlockData.fromData(state), defaultReturn, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent -diff --git a/src/main/java/net/minecraft/world/item/StandingAndWallBlockItem.java b/src/main/java/net/minecraft/world/item/StandingAndWallBlockItem.java -index 4390adf843b395db688017eb9034b56a40971473..5527ebdffb09d2d75275c73481f2f2af702222f6 100644 ---- a/src/main/java/net/minecraft/world/item/StandingAndWallBlockItem.java -+++ b/src/main/java/net/minecraft/world/item/StandingAndWallBlockItem.java -@@ -56,7 +56,7 @@ public class StandingAndWallBlockItem extends BlockItem { - - // CraftBukkit start - if (iblockdata1 != null) { -- boolean defaultReturn = world.isUnobstructed(iblockdata1, blockposition, CollisionContext.empty()); -+ boolean defaultReturn = context.getPlayer() != null ? context.getPlayer().canSpectatingPlace(world, iblockdata1, blockposition, CollisionContext.empty()) : world.isUnobstructed(iblockdata1, blockposition, CollisionContext.empty()); // Leaves - creative no clip - org.bukkit.entity.Player player = (context.getPlayer() instanceof ServerPlayer) ? (org.bukkit.entity.Player) context.getPlayer().getBukkitEntity() : null; - - BlockCanBuildEvent event = new BlockCanBuildEvent(CraftBlock.at(world, blockposition), player, CraftBlockData.fromData(iblockdata1), defaultReturn, org.bukkit.craftbukkit.CraftEquipmentSlot.getHand(context.getHand())); // Paper - Expose hand in BlockCanBuildEvent -diff --git a/src/main/java/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java -index 3784e4c14bb59073f941b966f616ddac3f80a497..afd84141d1d6dcdba307cfd9663657c09c0728bd 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/ShulkerBoxBlockEntity.java -@@ -163,7 +163,7 @@ public class ShulkerBoxBlockEntity extends RandomizableContainerBlockEntity impl - while (iterator.hasNext()) { - Entity entity = (Entity) iterator.next(); - -- if (entity.getPistonPushReaction() != PushReaction.IGNORE) { -+ if (entity.getPistonPushReaction() != PushReaction.IGNORE && !(entity instanceof Player player && player.isCreativeFlyOrSpectator())) { // Leaves - creative no clip - entity.move(MoverType.SHULKER_BOX, new Vec3((axisalignedbb.getXsize() + 0.01D) * (double) enumdirection.getStepX(), (axisalignedbb.getYsize() + 0.01D) * (double) enumdirection.getStepY(), (axisalignedbb.getZsize() + 0.01D) * (double) enumdirection.getStepZ())); - } - } -diff --git a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -index e1c9a961064887070b29207efd7af47884f8dc29..13f0a27f493754e707e8008ebed5ca14c68e31fc 100644 ---- a/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/piston/PistonMovingBlockEntity.java -@@ -148,7 +148,7 @@ public class PistonMovingBlockEntity extends BlockEntity { - h = (double)direction.getStepZ(); - } - -- entity.setDeltaMovement(e, g, h); -+ if (!(entity instanceof net.minecraft.world.entity.player.Player player) || !player.isCreativeFlyOrSpectator()) entity.setDeltaMovement(e, g, h); // Leaves - creative no clip - // Paper - EAR items stuck in in slime pushed by a piston - entity.activatedTick = Math.max(entity.activatedTick, net.minecraft.server.MinecraftServer.currentTick + 10); - entity.activatedImmunityTick = Math.max(entity.activatedImmunityTick, net.minecraft.server.MinecraftServer.currentTick + 10); -@@ -184,6 +184,7 @@ public class PistonMovingBlockEntity extends BlockEntity { - } - - private static void moveEntityByPiston(Direction direction, Entity entity, double distance, Direction movementDirection) { -+ if (entity instanceof net.minecraft.world.entity.player.Player player && player.isCreativeFlyOrSpectator()) return; // Leaves - creative no clip - NOCLIP.set(direction); - entity.move( - MoverType.PISTON, diff --git a/patches/server/0057-Shave-snow-layers.patch b/patches/server/0057-Shave-snow-layers.patch deleted file mode 100644 index 3cba8c18..00000000 --- a/patches/server/0057-Shave-snow-layers.patch +++ /dev/null @@ -1,53 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 27 Jun 2023 14:07:00 +0800 -Subject: [PATCH] Shave snow layers - - -diff --git a/src/main/java/net/minecraft/world/item/ShovelItem.java b/src/main/java/net/minecraft/world/item/ShovelItem.java -index 55c18f182166f4905d623d6f5e909eefd5ed2483..80de1b720abc98d8abdda4d6ef356e7cc3dda631 100644 ---- a/src/main/java/net/minecraft/world/item/ShovelItem.java -+++ b/src/main/java/net/minecraft/world/item/ShovelItem.java -@@ -44,6 +44,26 @@ public class ShovelItem extends DiggerItem { - return InteractionResult.PASS; - } else { - Player player = context.getPlayer(); -+ // Leaves start - shaveSnowLayers -+ if (org.leavesmc.leaves.LeavesConfig.modify.shaveSnowLayers && blockState.is(Blocks.SNOW)) { -+ int layers = blockState.getValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS); -+ ItemStack tool = context.getItemInHand(); -+ boolean hasSilkTouch = net.minecraft.world.item.enchantment.EnchantmentHelper.getItemEnchantmentLevel(level.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.ENCHANTMENT).getOrThrow(net.minecraft.world.item.enchantment.Enchantments.SILK_TOUCH), tool) > 0; -+ BlockState shavedBlockState = layers > 1 ? blockState.setValue(net.minecraft.world.level.block.SnowLayerBlock.LAYERS, layers - 1) : Blocks.AIR.defaultBlockState(); -+ -+ level.setBlock(blockPos, shavedBlockState, Block.UPDATE_ALL_IMMEDIATE); -+ level.gameEvent(GameEvent.BLOCK_CHANGE, blockPos, GameEvent.Context.of(player, shavedBlockState)); -+ -+ Block.popResource(level, blockPos, new ItemStack(hasSilkTouch ? Items.SNOW : Items.SNOWBALL)); -+ level.playSound(player, blockPos, SoundEvents.SNOW_BREAK, SoundSource.BLOCKS, 1.0F, 1.0F); -+ -+ if (player != null) { -+ tool.hurtAndBreak(1, player, LivingEntity.getSlotForHand(context.getHand())); -+ } -+ -+ return InteractionResult.SUCCESS; -+ } -+ // Leaves end - shaveSnowLayers - BlockState blockState2 = FLATTENABLES.get(blockState.getBlock()); - BlockState blockState3 = null; - Runnable afterAction = null; // Paper -@@ -52,11 +72,11 @@ public class ShovelItem extends DiggerItem { - blockState3 = blockState2; - } else if (blockState.getBlock() instanceof CampfireBlock && blockState.getValue(CampfireBlock.LIT)) { - afterAction = () -> { // Paper -- if (!level.isClientSide()) { -- level.levelEvent(null, 1009, blockPos, 0); -- } -+ if (!level.isClientSide()) { -+ level.levelEvent(null, 1009, blockPos, 0); -+ } - -- CampfireBlock.dowse(context.getPlayer(), level, blockPos, blockState); -+ CampfireBlock.dowse(context.getPlayer(), level, blockPos, blockState); - }; // Paper - blockState3 = blockState.setValue(CampfireBlock.LIT, Boolean.valueOf(false)); - } diff --git a/patches/server/0058-Elytra-aeronautics-no-chunk-load.patch b/patches/server/0058-Elytra-aeronautics-no-chunk-load.patch deleted file mode 100644 index d238311d..00000000 --- a/patches/server/0058-Elytra-aeronautics-no-chunk-load.patch +++ /dev/null @@ -1,126 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sun, 2 Jul 2023 09:25:00 +0800 -Subject: [PATCH] Elytra aeronautics no chunk load - - -diff --git a/src/main/java/net/minecraft/server/level/ChunkMap.java b/src/main/java/net/minecraft/server/level/ChunkMap.java -index 9a226396b4c38768574d5785744f22e61d6913f3..9a5488df906a0a3dd07f94942750fe9b646a96d9 100644 ---- a/src/main/java/net/minecraft/server/level/ChunkMap.java -+++ b/src/main/java/net/minecraft/server/level/ChunkMap.java -@@ -791,7 +791,8 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - private boolean skipPlayer(ServerPlayer player) { -- return player.isSpectator() && !this.level.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS); -+ return (player.isSpectator() && !this.level.getGameRules().getBoolean(GameRules.RULE_SPECTATORSGENERATECHUNKS)) -+ || (org.leavesmc.leaves.LeavesConfig.modify.elytraAeronautics.noChunk && player.elytraAeronauticsNoChunk); // Leaves - Elytra aeronautics - } - - void updatePlayerStatus(ServerPlayer player, boolean added) { -@@ -829,6 +830,7 @@ public class ChunkMap extends ChunkStorage implements ChunkHolder.PlayerProvider - } - - public void move(ServerPlayer player) { -+ if (player.elytraAeronauticsNoChunk) return; // Leaves - no chunk - // Paper - optimise entity tracker - - SectionPos sectionposition = player.getLastSectionPos(); -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index c5427e40ff55f90aca61866837311d30a74f0f08..4c3a4d6021fe3d02a5c8283eb86293a64b762da5 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -564,7 +564,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - speed *= 2f; // TODO: Get the speed of the vehicle instead of the player - - // Paper start - Prevent moving into unloaded chunks -- if (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && ( -+ if (this.player.level().paperConfig().chunks.preventMovingIntoUnloadedChunks && !player.elytraAeronauticsNoChunk && ( // Leaves - no chunk load - !worldserver.areChunksLoadedForMove(this.player.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(this.player.position()))) || - !worldserver.areChunksLoadedForMove(entity.getBoundingBox().expandTowards(new Vec3(toX, toY, toZ).subtract(entity.position()))) - )) { -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index b7d5a1f56067c6fec74d653b0da48e9610def283..13956aa8210d0234176a9d7450afdf42a2311f11 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -1186,7 +1186,13 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return; - } - } -- -+ // Leaves start - elytra aeronautics -+ if (org.leavesmc.leaves.LeavesConfig.modify.elytraAeronautics.noChunk && this instanceof Player player) { -+ if (type == MoverType.PLAYER && player.isFallFlying()) { -+ org.leavesmc.leaves.util.ElytraAeronauticsHelper.flightBehaviour(player, movement); -+ } -+ } -+ // Leaves end - elytra aeronautics - ProfilerFiller gameprofilerfiller = Profiler.get(); - - gameprofilerfiller.push("move"); -@@ -2184,6 +2190,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - this.yo = y; - this.zo = d4; - this.setPos(d3, y, d4); -+ if (this instanceof Player player && player.elytraAeronauticsNoChunk) return; // Leaves - elytra aeronautics - if (this.valid) this.level.getChunk((int) Math.floor(this.getX()) >> 4, (int) Math.floor(this.getZ()) >> 4); // CraftBukkit - } - -diff --git a/src/main/java/net/minecraft/world/entity/player/Player.java b/src/main/java/net/minecraft/world/entity/player/Player.java -index e4bb0b2eb8512f75042dab77bb79b4f51742ab4e..b36d67774cf34cf95bcfbaa2fc8cb4f56b1e7557 100644 ---- a/src/main/java/net/minecraft/world/entity/player/Player.java -+++ b/src/main/java/net/minecraft/world/entity/player/Player.java -@@ -197,6 +197,7 @@ public abstract class Player extends LivingEntity { - private int currentImpulseContextResetGraceTime; - public boolean affectsSpawning = true; // Paper - Affects Spawning API - public net.kyori.adventure.util.TriState flyingFallDamage = net.kyori.adventure.util.TriState.NOT_SET; // Paper - flying fall damage -+ public boolean elytraAeronauticsNoChunk = false; // Leaves - Elytra aeronautics - - // CraftBukkit start - public boolean fauxSleeping; -diff --git a/src/main/java/org/leavesmc/leaves/util/ElytraAeronauticsHelper.java b/src/main/java/org/leavesmc/leaves/util/ElytraAeronauticsHelper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..96282f07cbb4609741e66b53ec639de0cc33f6b6 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/ElytraAeronauticsHelper.java -@@ -0,0 +1,40 @@ -+package org.leavesmc.leaves.util; -+ -+import ca.spottedleaf.moonrise.common.util.ChunkSystem; -+import net.minecraft.core.SectionPos; -+import net.minecraft.network.chat.Component; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.entity.player.Player; -+import net.minecraft.world.phys.Vec3; -+import org.leavesmc.leaves.LeavesConfig; -+ -+public class ElytraAeronauticsHelper { -+ -+ public static void flightBehaviour(Player player, Vec3 velocity) { -+ if (LeavesConfig.modify.elytraAeronautics.noChunk) { -+ if ((LeavesConfig.modify.elytraAeronautics.noChunkSpeed <= 0.0D || velocity.horizontalDistanceSqr() >= LeavesConfig.modify.elytraAeronautics.noChunkSpeed) -+ && (LeavesConfig.modify.elytraAeronautics.noChunkHeight <= 0.0D || player.getY() >= LeavesConfig.modify.elytraAeronautics.noChunkHeight)) { -+ if (!player.elytraAeronauticsNoChunk) { -+ player.elytraAeronauticsNoChunk = true; -+ ServerPlayer serverPlayer = (ServerPlayer) player; -+ if (LeavesConfig.modify.elytraAeronautics.noChunkMes) { -+ serverPlayer.sendSystemMessage(Component.literal(LeavesConfig.modify.elytraAeronautics.noChunkStartMes), true); -+ } -+ ChunkSystem.removePlayerFromDistanceMaps((ServerLevel) serverPlayer.level(), serverPlayer); -+ ((ServerLevel) serverPlayer.level()).chunkSource.chunkMap.getDistanceManager().removePlayer(serverPlayer.getLastSectionPos(), serverPlayer); -+ } -+ } else { -+ if (player.elytraAeronauticsNoChunk) { -+ player.elytraAeronauticsNoChunk = false; -+ ServerPlayer serverPlayer = (ServerPlayer) player; -+ if (LeavesConfig.modify.elytraAeronautics.noChunkMes) { -+ serverPlayer.sendSystemMessage(Component.literal(LeavesConfig.modify.elytraAeronautics.noChunkEndMes), true); -+ } -+ ChunkSystem.addPlayerToDistanceMaps((ServerLevel) serverPlayer.level(), serverPlayer); -+ ((ServerLevel) serverPlayer.level()).chunkSource.chunkMap.getDistanceManager().addPlayer(SectionPos.of(serverPlayer), serverPlayer); -+ } -+ } -+ } -+ } -+} diff --git a/patches/server/0059-Cache-ignite-odds.patch b/patches/server/0059-Cache-ignite-odds.patch deleted file mode 100644 index cb3135fc..00000000 --- a/patches/server/0059-Cache-ignite-odds.patch +++ /dev/null @@ -1,64 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 5 Jul 2023 17:42:24 +0800 -Subject: [PATCH] Cache ignite odds - - -diff --git a/src/main/java/net/minecraft/world/level/block/FireBlock.java b/src/main/java/net/minecraft/world/level/block/FireBlock.java -index 0e5a47ab235d99e6cb1468905f791c2c59ac0082..f2b5ae54fbe5a98bf464d85a7dafcef6d6cacd42 100644 ---- a/src/main/java/net/minecraft/world/level/block/FireBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/FireBlock.java -@@ -220,6 +220,7 @@ public class FireBlock extends BaseFireBlock { - this.trySpread(world, pos.south(), 300 + k, random, i, pos); - // CraftBukkit end - BlockPos.MutableBlockPos blockposition_mutableblockposition = new BlockPos.MutableBlockPos(); -+ Object2IntOpenHashMap blockPositionIgniteCache = new Object2IntOpenHashMap<>(); // Leaves - cache ignite odds - - for (int l = -1; l <= 1; ++l) { - for (int i1 = -1; i1 <= 1; ++i1) { -@@ -232,7 +233,7 @@ public class FireBlock extends BaseFireBlock { - } - - blockposition_mutableblockposition.setWithOffset(pos, l, j1, i1); -- int l1 = this.getIgniteOdds(world, blockposition_mutableblockposition); -+ int l1 = this.getIgniteOdds(world, blockposition_mutableblockposition, org.leavesmc.leaves.LeavesConfig.performance.cacheIgniteOdds ? blockPositionIgniteCache : null); // Leaves - cache ignite odds - - if (l1 > 0) { - int i2 = (l1 + 40 + world.getDifficulty().getId() * 7) / (i + 30); -@@ -344,6 +345,11 @@ public class FireBlock extends BaseFireBlock { - } - - private int getIgniteOdds(LevelReader world, BlockPos pos) { -+ // Leaves start - cache ignite odds -+ return getIgniteOdds(world, pos, null); -+ } -+ -+ private int getIgniteOdds(LevelReader world, BlockPos pos, Object2IntOpenHashMap cache) { - if (!world.isEmptyBlock(pos)) { - return 0; - } else { -@@ -352,10 +358,20 @@ public class FireBlock extends BaseFireBlock { - int j = aenumdirection.length; - - for (int k = 0; k < j; ++k) { -- Direction enumdirection = aenumdirection[k]; -- BlockState iblockdata = world.getBlockState(pos.relative(enumdirection)); -- -- i = Math.max(this.getIgniteOdds(iblockdata), i); -+ if (cache != null) { -+ int finalK = k; -+ i = Math.max(cache.computeIfAbsent(pos, key -> { -+ Direction enumdirection = aenumdirection[finalK]; -+ BlockState iblockdata = world.getBlockState(pos.relative(enumdirection)); -+ return this.getIgniteOdds(iblockdata); -+ }), i); -+ } else { -+ Direction enumdirection = aenumdirection[k]; -+ BlockState iblockdata = world.getBlockState(pos.relative(enumdirection)); -+ -+ i = Math.max(this.getIgniteOdds(iblockdata), i); -+ } -+ // Leaves end - cache ignite odds - } - - return i; diff --git a/patches/server/0060-Lava-riptide.patch b/patches/server/0060-Lava-riptide.patch deleted file mode 100644 index 9d3e4b54..00000000 --- a/patches/server/0060-Lava-riptide.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Fri, 7 Jul 2023 16:53:32 +0800 -Subject: [PATCH] Lava riptide - - -diff --git a/src/main/java/net/minecraft/world/item/TridentItem.java b/src/main/java/net/minecraft/world/item/TridentItem.java -index 8b9a93ef71164cce8a616735b71d96d37e83b1a8..98b10cca7e583da08f43e312d387e37a95fa7686 100644 ---- a/src/main/java/net/minecraft/world/item/TridentItem.java -+++ b/src/main/java/net/minecraft/world/item/TridentItem.java -@@ -75,7 +75,7 @@ public class TridentItem extends Item implements ProjectileItem { - } else { - float f = EnchantmentHelper.getTridentSpinAttackStrength(stack, entityhuman); - -- if (f > 0.0F && !entityhuman.isInWaterOrRain()) { -+ if (f > 0.0F && !entityhuman.isInWaterOrRain() || (org.leavesmc.leaves.LeavesConfig.modify.lavaRiptide && entityhuman.isInLava())) { // Leaves - lava riptide - return false; - } else if (stack.nextDamageWillBreak()) { - return false; -@@ -158,7 +158,7 @@ public class TridentItem extends Item implements ProjectileItem { - - if (itemstack.nextDamageWillBreak()) { - return InteractionResult.FAIL; -- } else if (EnchantmentHelper.getTridentSpinAttackStrength(itemstack, user) > 0.0F && !user.isInWaterOrRain()) { -+ } else if (EnchantmentHelper.getTridentSpinAttackStrength(itemstack, user) > 0.0F && !user.isInWaterOrRain() && !(org.leavesmc.leaves.LeavesConfig.modify.lavaRiptide && user.isInLava())) { // Leaves - lava riptide - return InteractionResult.FAIL; - } else { - user.startUsingItem(hand); diff --git a/patches/server/0061-No-block-update-command.patch b/patches/server/0061-No-block-update-command.patch deleted file mode 100644 index 0709a1ea..00000000 --- a/patches/server/0061-No-block-update-command.patch +++ /dev/null @@ -1,151 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Fri, 7 Jul 2023 21:27:24 +0800 -Subject: [PATCH] No block update command - - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 67f67d185c7578a5f25e077467eff2873e0ed174..cd6d1e625d5b22acf279be22da158d827686a922 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2461,6 +2461,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - - @Override - public void blockUpdated(BlockPos pos, Block block) { -+ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update - if (!this.isDebug()) { - // CraftBukkit start - if (this.populating) { -diff --git a/src/main/java/net/minecraft/world/item/ItemStack.java b/src/main/java/net/minecraft/world/item/ItemStack.java -index f38487562422ebaf59a679f493f956b90cc18ff1..b6b3189f15c9e56f104a788716bb134b352cc83c 100644 ---- a/src/main/java/net/minecraft/world/item/ItemStack.java -+++ b/src/main/java/net/minecraft/world/item/ItemStack.java -@@ -533,7 +533,7 @@ public final class ItemStack implements DataComponentHolder { - net.minecraft.world.level.block.state.BlockState block = world.getBlockState(newblockposition); - - if (!(block.getBlock() instanceof BaseEntityBlock)) { // Containers get placed automatically -- block.onPlace(world, newblockposition, oldBlock, true, context); -+ if (!org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) block.onPlace(world, newblockposition, oldBlock, true, context); // Leaves - no block update - } - - world.notifyAndUpdatePhysics(newblockposition, null, oldBlock, block, world.getBlockState(newblockposition), updateFlag, 512); // send null chunk as chunk.k() returns false by this point -diff --git a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -index 2fea5a46bba27578366c36f594472c3e38395bee..80c71d08ea7588ba95416296531c5629097d9257 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -+++ b/src/main/java/net/minecraft/world/level/chunk/LevelChunk.java -@@ -427,7 +427,7 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p - } else { - // CraftBukkit - Don't place while processing the BlockPlaceEvent, unless it's a BlockContainer. Prevents blocks such as TNT from activating when cancelled. - if (!this.level.isClientSide && doPlace && (!this.level.captureBlockStates || block instanceof net.minecraft.world.level.block.BaseEntityBlock)) { -- iblockdata.onPlace(this.level, blockposition, iblockdata1, flag); -+ if (!org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) iblockdata.onPlace(this.level, blockposition, iblockdata1, flag); // Leaves - no block update - } - - if (iblockdata.hasBlockEntity()) { -diff --git a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java -index f4fbcbb8ff6d2677af1a02a0801a323c06dce9b1..dd9e7b030b64b10d61481e744fe3e53ff80087f8 100644 ---- a/src/main/java/net/minecraft/world/level/material/FlowingFluid.java -+++ b/src/main/java/net/minecraft/world/level/material/FlowingFluid.java -@@ -516,6 +516,7 @@ public abstract class FlowingFluid extends Fluid { - - @Override - public void tick(ServerLevel world, BlockPos pos, BlockState blockState, FluidState fluidState) { -+ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update - if (!fluidState.isSource()) { - FluidState fluid1 = this.getNewLiquid(world, pos, world.getBlockState(pos)); - int i = this.getSpreadDelay(world, pos, fluidState, fluid1); -diff --git a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java -index 3a95e3236eafd14baed035e53503b58c2e21b68a..717d9042a9a67aea771b0b544776a9956eb52eab 100644 ---- a/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java -+++ b/src/main/java/net/minecraft/world/level/redstone/CollectingNeighborUpdater.java -@@ -49,6 +49,7 @@ public class CollectingNeighborUpdater implements NeighborUpdater { - } - - private void addAndRun(BlockPos pos, CollectingNeighborUpdater.NeighborUpdates entry) { -+ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update - boolean bl = this.count > 0; - boolean bl2 = this.maxChainedNeighborUpdates >= 0 && this.count >= this.maxChainedNeighborUpdates; - this.count++; -diff --git a/src/main/java/net/minecraft/world/level/redstone/InstantNeighborUpdater.java b/src/main/java/net/minecraft/world/level/redstone/InstantNeighborUpdater.java -index e1c3dbd74a009ca8adc6269fd22f9547ee03f347..efb9cc019e804aa4f88d77cbee6d05cb9f65de29 100644 ---- a/src/main/java/net/minecraft/world/level/redstone/InstantNeighborUpdater.java -+++ b/src/main/java/net/minecraft/world/level/redstone/InstantNeighborUpdater.java -@@ -16,17 +16,20 @@ public class InstantNeighborUpdater implements NeighborUpdater { - - @Override - public void shapeUpdate(Direction direction, BlockState neighborState, BlockPos pos, BlockPos neighborPos, int flags, int maxUpdateDepth) { -+ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update - NeighborUpdater.executeShapeUpdate(this.level, direction, pos, neighborPos, neighborState, flags, maxUpdateDepth - 1); - } - - @Override - public void neighborChanged(BlockPos pos, Block sourceBlock, @Nullable Orientation orientation) { -+ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update - BlockState blockState = this.level.getBlockState(pos); - this.neighborChanged(blockState, pos, sourceBlock, orientation, false); - } - - @Override - public void neighborChanged(BlockState state, BlockPos pos, Block sourceBlock, @Nullable Orientation orientation, boolean notify) { -+ if (org.leavesmc.leaves.command.NoBlockUpdateCommand.isNoBlockUpdate()) return; // Leaves - no block update - NeighborUpdater.executeUpdate(this.level, state, pos, sourceBlock, orientation, notify); - } - } -diff --git a/src/main/java/org/leavesmc/leaves/command/NoBlockUpdateCommand.java b/src/main/java/org/leavesmc/leaves/command/NoBlockUpdateCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8f25ce0145341d363bb42183a9e64622159663ff ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/NoBlockUpdateCommand.java -@@ -0,0 +1,52 @@ -+package org.leavesmc.leaves.command; -+ -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.JoinConfiguration; -+import net.kyori.adventure.text.format.NamedTextColor; -+import org.bukkit.Bukkit; -+import org.bukkit.command.Command; -+import org.bukkit.command.CommandSender; -+import org.bukkit.permissions.Permission; -+import org.bukkit.permissions.PermissionDefault; -+import org.bukkit.plugin.PluginManager; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+ -+import java.util.List; -+ -+public class NoBlockUpdateCommand extends Command { -+ -+ private static boolean noBlockUpdate = false; -+ -+ public NoBlockUpdateCommand(@NotNull String name) { -+ super(name); -+ this.description = "No Block Update Command"; -+ this.usageMessage = "/blockupdate"; -+ this.setPermission("bukkit.command.blockupdate"); -+ final PluginManager pluginManager = Bukkit.getServer().getPluginManager(); -+ if (pluginManager.getPermission("bukkit.command.blockupdate") == null) { -+ pluginManager.addPermission(new Permission("bukkit.command.blockupdate", PermissionDefault.OP)); -+ } -+ } -+ -+ @Override -+ public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { -+ return List.of(); -+ } -+ -+ @Override -+ public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { -+ if (!testPermission(sender)) return true; -+ noBlockUpdate = !noBlockUpdate; -+ Bukkit.broadcast(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Block update status: ", NamedTextColor.GRAY), -+ Component.text(!noBlockUpdate, noBlockUpdate ? NamedTextColor.AQUA : NamedTextColor.GRAY) -+ ), "bukkit.command.blockupdate"); -+ -+ return true; -+ } -+ -+ public static boolean isNoBlockUpdate() { -+ return LeavesConfig.modify.noBlockUpdateCommand && noBlockUpdate; -+ } -+} diff --git a/patches/server/0062-Raider-die-skip-self-raid-check.patch b/patches/server/0062-Raider-die-skip-self-raid-check.patch deleted file mode 100644 index 986edc9e..00000000 --- a/patches/server/0062-Raider-die-skip-self-raid-check.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sun, 9 Jul 2023 16:51:47 +0800 -Subject: [PATCH] Raider die skip self raid check - - -diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java -index ab132041982df2a701e4baea8195873f31b4a5fb..9b911254b24bc77930c518a9c61916983ba72e3c 100644 ---- a/src/main/java/net/minecraft/world/entity/raid/Raider.java -+++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java -@@ -124,7 +124,7 @@ public abstract class Raider extends PatrollingMonster { - Raid raid = this.getCurrentRaid(); - - if (raid != null) { -- if (this.isPatrolLeader()) { -+ if (!org.leavesmc.leaves.LeavesConfig.modify.skipSelfRaidCheck && this.isPatrolLeader()) { // Leaves - skip self raid check - raid.removeLeader(this.getWave()); - } - diff --git a/patches/server/0063-Container-open-passthrough.patch b/patches/server/0063-Container-open-passthrough.patch deleted file mode 100644 index a84e8fe0..00000000 --- a/patches/server/0063-Container-open-passthrough.patch +++ /dev/null @@ -1,55 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 17 Jul 2023 11:41:50 +0800 -Subject: [PATCH] Container open passthrough - - -diff --git a/src/main/java/net/minecraft/world/level/block/SignBlock.java b/src/main/java/net/minecraft/world/level/block/SignBlock.java -index b212fe323f048dab40c0ccbb9327c9fc73b9e03a..ccfa890b1c83fe9492cb4f5863911008536b46f0 100644 ---- a/src/main/java/net/minecraft/world/level/block/SignBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/SignBlock.java -@@ -111,6 +111,18 @@ public abstract class SignBlock extends BaseEntityBlock implements SimpleWaterlo - } else { - return InteractionResult.TRY_WITH_EMPTY_HAND; - } -+ // Leaves start - signContainerPassthrough -+ } else if (org.leavesmc.leaves.LeavesConfig.modify.containerPassthrough) { -+ BlockPos pos1 = pos.relative(hit.getDirection().getOpposite()); -+ if (this instanceof WallSignBlock || this instanceof WallHangingSignBlock) { -+ pos1 = pos.relative(state.getValue(HorizontalDirectionalBlock.FACING).getOpposite()); -+ } -+ if (world.getBlockEntity(pos1) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity) { -+ BlockState state1 = world.getBlockState(pos1); -+ return state1.useItemOn(stack, world, player, hand, hit.withPosition(pos1)); -+ } -+ return InteractionResult.PASS; -+ // Leaves end - signContainerPassthrough - } else { - return InteractionResult.TRY_WITH_EMPTY_HAND; - } -@@ -139,6 +151,25 @@ public abstract class SignBlock extends BaseEntityBlock implements SimpleWaterlo - return InteractionResult.SUCCESS_SERVER; - } else if (flag1) { - return InteractionResult.SUCCESS_SERVER; -+ // Leaves start - signContainerPassthrough -+ } else if (org.leavesmc.leaves.LeavesConfig.modify.containerPassthrough) { -+ if (player.isShiftKeyDown()) { -+ if (!this.otherPlayerIsEditingSign(player, tileentitysign) && player.mayBuild() && this.hasEditableText(player, tileentitysign, flag1)) { -+ this.openTextEdit(player, tileentitysign, flag1); -+ return InteractionResult.SUCCESS; -+ } -+ } -+ -+ BlockPos pos1 = pos.relative(hit.getDirection().getOpposite()); -+ if (this instanceof WallSignBlock || this instanceof WallHangingSignBlock) { -+ pos1 = pos.relative(state.getValue(HorizontalDirectionalBlock.FACING).getOpposite()); -+ } -+ if (world.getBlockEntity(pos1) instanceof net.minecraft.world.level.block.entity.RandomizableContainerBlockEntity) { -+ BlockState state1 = world.getBlockState(pos1); -+ return state1.useWithoutItem(world, player, hit.withPosition(pos1)); -+ } -+ return InteractionResult.PASS; -+ // Leaves end - signContainerPassthrough - } else if (!this.otherPlayerIsEditingSign(player, tileentitysign) && player.mayBuild() && this.hasEditableText(player, tileentitysign, flag)) { - this.openTextEdit(player, tileentitysign, flag, io.papermc.paper.event.player.PlayerOpenSignEvent.Cause.INTERACT); // Paper - Add PlayerOpenSignEvent - return InteractionResult.SUCCESS_SERVER; diff --git a/patches/server/0064-SIMD-support.patch b/patches/server/0064-SIMD-support.patch deleted file mode 100644 index d8227b35..00000000 --- a/patches/server/0064-SIMD-support.patch +++ /dev/null @@ -1,27 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 17 Jul 2023 22:45:48 +0800 -Subject: [PATCH] SIMD support - - -diff --git a/build.gradle.kts b/build.gradle.kts -index b8132401ddaedf13bc9ddc74524166d0e0dcf419..0add94373b7b38f271893b1c44a448051b845e8b 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -92,6 +92,7 @@ tasks.withType { - compilerArgs.add("-Xlint:-module") - compilerArgs.add("-Xlint:-removal") - compilerArgs.add("-Xlint:-dep-ann") -+ compilerArgs.add("--add-modules=jdk.incubator.vector") // Leaves - SIMD support - } - // Leaves end - hide irrelevant compilation warnings - -@@ -210,6 +211,8 @@ fun TaskContainer.registerRunTask( - minHeapSize = "${memoryGb}G" - maxHeapSize = "${memoryGb}G" - -+ jvmArgs("--add-modules=jdk.incubator.vector") // Leaves - SIMD support -+ - doFirst { - workingDir.mkdirs() - } diff --git a/patches/server/0065-Dont-respond-ping-before-start-fully.patch b/patches/server/0065-Dont-respond-ping-before-start-fully.patch deleted file mode 100644 index f5ba8181..00000000 --- a/patches/server/0065-Dont-respond-ping-before-start-fully.patch +++ /dev/null @@ -1,24 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 17 Jul 2023 23:16:58 +0800 -Subject: [PATCH] Dont respond ping before start fully - -This patch is Powered by Gale(https://github.com/GaleMC/Gale) - -diff --git a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java -index 532f09089b8d6798999cf3f83e852df7479e450e..e4751b27607f33b25623e2c593840323c4f0242c 100644 ---- a/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerStatusPacketListenerImpl.java -@@ -154,6 +154,12 @@ public class ServerStatusPacketListenerImpl implements ServerStatusPacketListene - this.connection.send(new ClientboundStatusResponsePacket(ping)); - // CraftBukkit end - */ -+ // Leaves start - dont respond it before start full -+ var status = MinecraftServer.getServer().getStatus(); -+ if (org.leavesmc.leaves.LeavesConfig.mics.dontRespondPingBeforeStart && (status == null || status.version() == null || status.version().isEmpty())) { -+ return; -+ } -+ // Leaves end - dont respond it before start full - com.destroystokyo.paper.network.StandardPaperServerListPingEventImpl.processRequest(MinecraftServer.getServer(), this.connection); - // Paper end - } diff --git a/patches/server/0066-Faster-chunk-serialization.patch b/patches/server/0066-Faster-chunk-serialization.patch deleted file mode 100644 index a7989861..00000000 --- a/patches/server/0066-Faster-chunk-serialization.patch +++ /dev/null @@ -1,501 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 18 Jul 2023 13:14:15 +0800 -Subject: [PATCH] Faster chunk serialization - -This patch is Powered by Gale(https://github.com/GaleMC/Gale) - -diff --git a/src/main/java/net/minecraft/util/BitStorage.java b/src/main/java/net/minecraft/util/BitStorage.java -index e4e153cb8899e70273aa150b8ea26907cf68b15c..cf5adffc17da35067c75e6432eae038762bd36f1 100644 ---- a/src/main/java/net/minecraft/util/BitStorage.java -+++ b/src/main/java/net/minecraft/util/BitStorage.java -@@ -1,5 +1,7 @@ - package net.minecraft.util; - -+import net.minecraft.world.level.chunk.Palette; -+ - import java.util.function.IntConsumer; - - public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counting.BlockCountingBitStorage { // Paper - block counting -@@ -21,6 +23,8 @@ public interface BitStorage extends ca.spottedleaf.moonrise.patches.block_counti - - BitStorage copy(); - -+ void compact(Palette srcPalette, Palette dstPalette, short[] out); // Leaves - faster chunk serialization -+ - // Paper start - block counting - // provide default impl in case mods implement this... - @Override -diff --git a/src/main/java/net/minecraft/util/SimpleBitStorage.java b/src/main/java/net/minecraft/util/SimpleBitStorage.java -index d99ec470b4653beab630999a5b2c1a6428b20c38..404d1cc8467505cf1ca81c3d845b880033e03dbf 100644 ---- a/src/main/java/net/minecraft/util/SimpleBitStorage.java -+++ b/src/main/java/net/minecraft/util/SimpleBitStorage.java -@@ -3,6 +3,7 @@ package net.minecraft.util; - import java.util.function.IntConsumer; - import javax.annotation.Nullable; - import org.apache.commons.lang3.Validate; -+import net.minecraft.world.level.chunk.Palette; - - public class SimpleBitStorage implements BitStorage { - private static final int[] MAGIC = new int[]{ -@@ -472,4 +473,44 @@ public class SimpleBitStorage implements BitStorage { - super(message); - } - } -+ -+ // Leaves start - faster chunk serialization -+ @Override -+ public void compact(Palette srcPalette, Palette dstPalette, short[] out) { -+ if (this.size >= Short.MAX_VALUE) { -+ throw new IllegalStateException("Array too large"); -+ } -+ -+ if (this.size != out.length) { -+ throw new IllegalStateException("Array size mismatch"); -+ } -+ -+ short[] mappings = new short[(int) (this.mask + 1)]; -+ -+ int idx = 0; -+ -+ for (long word : this.data) { -+ long bits = word; -+ -+ for (int elementIdx = 0; elementIdx < this.valuesPerLong; ++elementIdx) { -+ int value = (int) (bits & this.mask); -+ int remappedId = mappings[value]; -+ -+ if (remappedId == 0) { -+ remappedId = dstPalette.idFor(srcPalette.valueFor(value)) + 1; -+ mappings[value] = (short) remappedId; -+ } -+ -+ out[idx] = (short) (remappedId - 1); -+ bits >>= this.bits; -+ -+ ++idx; -+ -+ if (idx >= this.size) { -+ return; -+ } -+ } -+ } -+ } -+ // Leaves end - faster chunk serialization - } -diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java -index 1f9c436a632e4f110be61cf76fcfc3b7eb80334e..87393a181c64af6a02ebddfccae674c4fbcb3903 100644 ---- a/src/main/java/net/minecraft/util/ZeroBitStorage.java -+++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java -@@ -3,6 +3,7 @@ package net.minecraft.util; - import java.util.Arrays; - import java.util.function.IntConsumer; - import org.apache.commons.lang3.Validate; -+import net.minecraft.world.level.chunk.Palette; - - public class ZeroBitStorage implements BitStorage { - public static final long[] RAW = new long[0]; -@@ -63,6 +64,8 @@ public class ZeroBitStorage implements BitStorage { - return this; - } - -+ @Override public void compact(Palette srcPalette, Palette dstPalette, short[] out) {} // Leaves - faster chunk serialization -+ - // Paper start - block counting - @Override - public final it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap moonrise$countEntries() { -diff --git a/src/main/java/net/minecraft/world/level/chunk/PaletteResize.java b/src/main/java/net/minecraft/world/level/chunk/PaletteResize.java -index acae3eb30e0689048937f479dc3070f0688abdad..029b62acf1d9f8479ab64a55c12f00ba467249e3 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/PaletteResize.java -+++ b/src/main/java/net/minecraft/world/level/chunk/PaletteResize.java -@@ -1,5 +1,5 @@ - package net.minecraft.world.level.chunk; - --interface PaletteResize { -+public interface PaletteResize { // Leaves - package -> public - int onResize(int newBits, T object); - } -diff --git a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -index 8b84bf2272556ac3321cbf16361d7f48a1cc6873..404cb8e85c1983346c9e4de5b5d5a0c040915eff 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -+++ b/src/main/java/net/minecraft/world/level/chunk/PalettedContainer.java -@@ -23,8 +23,25 @@ import net.minecraft.util.Mth; - import net.minecraft.util.SimpleBitStorage; - import net.minecraft.util.ThreadingDetector; - import net.minecraft.util.ZeroBitStorage; -+import org.leavesmc.leaves.lithium.common.world.chunk.LithiumHashPalette; - - public class PalettedContainer implements PaletteResize, PalettedContainerRO { -+ -+ // Leaves start - faster chunk serialization -+ private static final ThreadLocal CACHED_ARRAY_4096 = ThreadLocal.withInitial(() -> new short[4096]); -+ private static final ThreadLocal CACHED_ARRAY_64 = ThreadLocal.withInitial(() -> new short[64]); -+ private Optional asOptional(long[] data) { -+ return Optional.of(Arrays.stream(data)); -+ } -+ private short[] getOrCreate(int size) { -+ return switch (size) { -+ case 64 -> CACHED_ARRAY_64.get(); -+ case 4096 -> CACHED_ARRAY_4096.get(); -+ default -> new short[size]; -+ }; -+ } -+ // Leaves end - faster chunk serialization -+ - private static final int MIN_PALETTE_BITS = 0; - private final PaletteResize dummyPaletteResize = (newSize, added) -> 0; - public final IdMap registry; -@@ -348,28 +365,78 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - public synchronized PalettedContainerRO.PackedData pack(IdMap idList, PalettedContainer.Strategy paletteProvider) { // Paper - synchronize - this.acquire(); - -- PalettedContainerRO.PackedData var12; -- try { -- HashMapPalette hashMapPalette = new HashMapPalette<>(idList, this.data.storage.getBits(), this.dummyPaletteResize); -- int i = paletteProvider.size(); -- int[] is = new int[i]; -- this.data.storage.unpack(is); -- swapPalette(is, id -> hashMapPalette.idFor(this.data.palette.valueFor(id))); -- int j = paletteProvider.calculateBitsForSerialization(idList, hashMapPalette.getSize()); -- Optional optional; -- if (j != 0) { -- SimpleBitStorage simpleBitStorage = new SimpleBitStorage(j, i, is); -- optional = Optional.of(Arrays.stream(simpleBitStorage.getRaw())); -- } else { -- optional = Optional.empty(); -+ // Leaves start - faster chunk serialization -+ if (!org.leavesmc.leaves.LeavesConfig.performance.fasterChunkSerialization) { -+ PalettedContainerRO.PackedData var12; -+ try { -+ HashMapPalette hashMapPalette = new HashMapPalette<>(idList, this.data.storage.getBits(), this.dummyPaletteResize); -+ int i = paletteProvider.size(); -+ int[] is = new int[i]; -+ this.data.storage.unpack(is); -+ swapPalette(is, (id) -> { -+ return hashMapPalette.idFor(this.data.palette.valueFor(id)); -+ }); -+ int j = paletteProvider.calculateBitsForSerialization(idList, hashMapPalette.getSize()); -+ Optional optional; -+ if (j != 0) { -+ SimpleBitStorage simpleBitStorage = new SimpleBitStorage(j, i, is); -+ optional = Optional.of(Arrays.stream(simpleBitStorage.getRaw())); -+ } else { -+ optional = Optional.empty(); -+ } -+ -+ var12 = new PalettedContainerRO.PackedData<>(hashMapPalette.getEntries(), optional); -+ } finally { -+ this.release(); - } - -- var12 = new PalettedContainerRO.PackedData<>(hashMapPalette.getEntries(), optional); -- } finally { -- this.release(); -- } -+ return var12; -+ } else { -+ Optional data = Optional.empty(); -+ List elements = null; -+ try { -+ // The palette that will be serialized -+ LithiumHashPalette hashPalette = null; -+ -+ final Palette palette = this.data.palette(); -+ final BitStorage storage = this.data.storage(); -+ if (storage instanceof ZeroBitStorage || palette.getSize() == 1) { -+ // If the palette only contains one entry, don't attempt to repack it. -+ elements = List.of(palette.valueFor(0)); -+ } else if (palette instanceof LithiumHashPalette lithiumHashPalette) { -+ hashPalette = lithiumHashPalette; -+ } - -- return var12; -+ if (elements == null) { -+ LithiumHashPalette compactedPalette = new LithiumHashPalette<>(idList, storage.getBits(), this.dummyPaletteResize); -+ short[] array = this.getOrCreate(paletteProvider.size()); -+ -+ storage.compact(this.data.palette(), compactedPalette, array); -+ -+ // If the palette didn't change during compaction, do a simple copy of the data array -+ if (hashPalette != null && hashPalette.getSize() == compactedPalette.getSize() && storage.getBits() == paletteProvider.calculateBitsForSerialization(idList, hashPalette.getSize())) { // paletteSize can de-sync from palette - see https://github.com/CaffeineMC/lithium-fabric/issues/279 -+ data = this.asOptional(storage.getRaw().clone()); -+ elements = hashPalette.getElements(); -+ } else { -+ int bits = paletteProvider.calculateBitsForSerialization(idList, compactedPalette.getSize()); -+ if (bits != 0) { -+ // Re-pack the integer array as the palette has changed size -+ SimpleBitStorage copy = new SimpleBitStorage(bits, array.length); -+ for (int i = 0; i < array.length; ++i) { -+ copy.set(i, array[i]); -+ } -+ // We don't need to clone the data array as we are the sole owner of it -+ data = this.asOptional(copy.getRaw()); -+ } -+ } -+ elements = compactedPalette.getElements(); -+ } -+ } finally { -+ this.release(); -+ } -+ return new PalettedContainerRO.PackedData<>(elements, data); -+ } -+ // Leaves end - faster chunk serialization - } - - private static void swapPalette(int[] is, IntUnaryOperator applier) { -@@ -409,13 +476,47 @@ public class PalettedContainer implements PaletteResize, PalettedContainer - - @Override - public void count(PalettedContainer.CountConsumer counter) { -- if (this.data.palette.getSize() == 1) { -- counter.accept(this.data.palette.valueFor(0), this.data.storage.getSize()); -+ // Leaves start - faster chunk serialization -+ if (!org.leavesmc.leaves.LeavesConfig.performance.fasterChunkSerialization) { -+ if (this.data.palette.getSize() == 1) { -+ counter.accept(this.data.palette.valueFor(0), this.data.storage.getSize()); -+ } else { -+ Int2IntOpenHashMap int2IntOpenHashMap = new Int2IntOpenHashMap(); -+ this.data.storage.getAll((key) -> { -+ int2IntOpenHashMap.addTo(key, 1); -+ }); -+ int2IntOpenHashMap.int2IntEntrySet().forEach((entry) -> { -+ counter.accept(this.data.palette.valueFor(entry.getIntKey()), entry.getIntValue()); -+ }); -+ } - } else { -- Int2IntOpenHashMap int2IntOpenHashMap = new Int2IntOpenHashMap(); -- this.data.storage.getAll(key -> int2IntOpenHashMap.addTo(key, 1)); -- int2IntOpenHashMap.int2IntEntrySet().forEach(entry -> counter.accept(this.data.palette.valueFor(entry.getIntKey()), entry.getIntValue())); -+ int len = this.data.palette().getSize(); -+ -+ // Do not allocate huge arrays if we're using a large palette -+ if (len > 4096) { -+ if (this.data.palette.getSize() == 1) { -+ counter.accept(this.data.palette.valueFor(0), this.data.storage.getSize()); -+ } else { -+ Int2IntOpenHashMap int2IntOpenHashMap = new Int2IntOpenHashMap(); -+ this.data.storage.getAll((key) -> { -+ int2IntOpenHashMap.addTo(key, 1); -+ }); -+ int2IntOpenHashMap.int2IntEntrySet().forEach((entry) -> { -+ counter.accept(this.data.palette.valueFor(entry.getIntKey()), entry.getIntValue()); -+ }); -+ } -+ } -+ -+ short[] counts = new short[len]; -+ this.data.storage().getAll(i -> counts[i]++); -+ for (int i = 0; i < counts.length; i++) { -+ T obj = this.data.palette().valueFor(i); -+ if (obj != null) { -+ counter.accept(obj, counts[i]); -+ } -+ } - } -+ // Leaves end - faster chunk serialization - } - - static record Configuration(Palette.Factory factory, int bits) { -diff --git a/src/main/java/org/leavesmc/leaves/lithium/common/world/chunk/LithiumHashPalette.java b/src/main/java/org/leavesmc/leaves/lithium/common/world/chunk/LithiumHashPalette.java -new file mode 100644 -index 0000000000000000000000000000000000000000..da32fdd8bbb9a966bd17c7457d94bb676f28c60d ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/lithium/common/world/chunk/LithiumHashPalette.java -@@ -0,0 +1,197 @@ -+package org.leavesmc.leaves.lithium.common.world.chunk; -+ -+import com.google.common.collect.ImmutableList; -+import it.unimi.dsi.fastutil.HashCommon; -+import it.unimi.dsi.fastutil.objects.Reference2IntMap; -+import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; -+ -+import java.util.Arrays; -+import java.util.List; -+import java.util.function.Predicate; -+ -+import net.minecraft.core.IdMap; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.VarInt; -+import net.minecraft.world.level.chunk.Palette; -+import net.minecraft.world.level.chunk.PaletteResize; -+import org.jetbrains.annotations.NotNull; -+ -+import static it.unimi.dsi.fastutil.Hash.FAST_LOAD_FACTOR; -+ -+// Powered by Gale(https://github.com/GaleMC/Gale) -+ -+/** -+ * Generally provides better performance over the vanilla {@link net.minecraft.world.level.chunk.HashMapPalette} when calling -+ * {@link LithiumHashPalette#idFor(Object)} through using a faster backing map and reducing pointer chasing. -+ */ -+public class LithiumHashPalette implements Palette { -+ private static final int ABSENT_VALUE = -1; -+ -+ private final IdMap idList; -+ private final PaletteResize resizeHandler; -+ private final int indexBits; -+ -+ private final Reference2IntMap table; -+ private T[] entries; -+ private int size = 0; -+ -+ public LithiumHashPalette(IdMap idList, PaletteResize resizeHandler, int indexBits, T[] entries, Reference2IntMap table, int size) { -+ this.idList = idList; -+ this.resizeHandler = resizeHandler; -+ this.indexBits = indexBits; -+ this.entries = entries; -+ this.table = table; -+ this.size = size; -+ } -+ -+ public LithiumHashPalette(IdMap idList, int bits, PaletteResize resizeHandler, @NotNull List list) { -+ this(idList, bits, resizeHandler); -+ -+ for (T t : list) { -+ this.addEntry(t); -+ } -+ } -+ -+ @SuppressWarnings("unchecked") -+ public LithiumHashPalette(IdMap idList, int bits, PaletteResize resizeHandler) { -+ this.idList = idList; -+ this.indexBits = bits; -+ this.resizeHandler = resizeHandler; -+ -+ int capacity = 1 << bits; -+ -+ this.entries = (T[]) new Object[capacity]; -+ this.table = new Reference2IntOpenHashMap<>(capacity, FAST_LOAD_FACTOR); -+ this.table.defaultReturnValue(ABSENT_VALUE); -+ } -+ -+ @Override -+ public int idFor(@NotNull T obj) { -+ int id = this.table.getInt(obj); -+ -+ if (id == ABSENT_VALUE) { -+ id = this.computeEntry(obj); -+ } -+ -+ return id; -+ } -+ -+ @Override -+ public boolean maybeHas(@NotNull Predicate predicate) { -+ for (int i = 0; i < this.size; ++i) { -+ if (predicate.test(this.entries[i])) { -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ -+ private int computeEntry(T obj) { -+ int id = this.addEntry(obj); -+ -+ if (id >= 1 << this.indexBits) { -+ if (this.resizeHandler == null) { -+ throw new IllegalStateException("Cannot grow"); -+ } else { -+ id = this.resizeHandler.onResize(this.indexBits + 1, obj); -+ } -+ } -+ -+ return id; -+ } -+ -+ private int addEntry(T obj) { -+ int nextId = this.size; -+ -+ if (nextId >= this.entries.length) { -+ this.resize(this.size); -+ } -+ -+ this.table.put(obj, nextId); -+ this.entries[nextId] = obj; -+ -+ this.size++; -+ -+ return nextId; -+ } -+ -+ private void resize(int neededCapacity) { -+ this.entries = Arrays.copyOf(this.entries, HashCommon.nextPowerOfTwo(neededCapacity + 1)); -+ } -+ -+ @Override -+ public T valueFor(int id) { -+ T[] entries = this.entries; -+ -+ if (id >= 0 && id < entries.length) { -+ return entries[id]; -+ } -+ -+ return null; -+ } -+ -+ @Override -+ public void read(FriendlyByteBuf buf) { -+ this.clear(); -+ -+ int entryCount = buf.readVarInt(); -+ -+ for (int i = 0; i < entryCount; ++i) { -+ this.addEntry(this.idList.byId(buf.readVarInt())); -+ } -+ } -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ int size = this.size; -+ buf.writeVarInt(size); -+ -+ for (int i = 0; i < size; ++i) { -+ buf.writeVarInt(this.idList.getId(this.valueFor(i))); -+ } -+ } -+ -+ @Override -+ public int getSerializedSize() { -+ int size = VarInt.getByteSize(this.size); -+ -+ for (int i = 0; i < this.size; ++i) { -+ size += VarInt.getByteSize(this.idList.getId(this.valueFor(i))); -+ } -+ -+ return size; -+ } -+ -+ @Override -+ public int getSize() { -+ return this.size; -+ } -+ -+ @NotNull -+ @Override -+ public Palette copy(@NotNull PaletteResize resizeListener) { -+ return new LithiumHashPalette<>(this.idList, resizeHandler, this.indexBits, this.entries.clone(), new Reference2IntOpenHashMap<>(this.table), this.size); -+ } -+ -+ private void clear() { -+ Arrays.fill(this.entries, null); -+ this.table.clear(); -+ this.size = 0; -+ } -+ -+ public List getElements() { -+ ImmutableList.Builder builder = new ImmutableList.Builder<>(); -+ for (T entry : this.entries) { -+ if (entry != null) { -+ builder.add(entry); -+ } -+ } -+ return builder.build(); -+ } -+ -+ public static Palette create(int bits, IdMap idList, PaletteResize listener, List list) { -+ return new LithiumHashPalette<>(idList, bits, listener, list); -+ } -+} -+ diff --git a/patches/server/0067-Bladeren-Protocol.patch b/patches/server/0067-Bladeren-Protocol.patch deleted file mode 100644 index f3da2991..00000000 --- a/patches/server/0067-Bladeren-Protocol.patch +++ /dev/null @@ -1,162 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Fri, 7 Jul 2023 16:50:06 +0800 -Subject: [PATCH] Bladeren Protocol - - -diff --git a/src/main/java/org/leavesmc/leaves/protocol/bladeren/BladerenProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/bladeren/BladerenProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6aeab5b37f142fd84c9e290f08078180d7e327b2 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/bladeren/BladerenProtocol.java -@@ -0,0 +1,150 @@ -+package org.leavesmc.leaves.protocol.bladeren; -+ -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+import java.util.HashMap; -+import java.util.Map; -+import java.util.function.BiConsumer; -+ -+@LeavesProtocol(namespace = "bladeren") -+public class BladerenProtocol { -+ -+ public static final String PROTOCOL_ID = "bladeren"; -+ public static final String PROTOCOL_VERSION = ProtocolUtils.buildProtocolVersion(PROTOCOL_ID); -+ -+ private static final ResourceLocation HELLO_ID = id("hello"); -+ private static final ResourceLocation FEATURE_MODIFY_ID = id("feature_modify"); -+ -+ private static final Map> registeredFeatures = new HashMap<>(); -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation id(String path) { -+ return new ResourceLocation(PROTOCOL_ID, path); -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = BladerenHelloPayload.class, payloadId = "hello") -+ private static void handleHello(@NotNull ServerPlayer player, @NotNull BladerenHelloPayload payload) { -+ if (LeavesConfig.protocol.bladeren.enable) { -+ String clientVersion = payload.version; -+ CompoundTag tag = payload.nbt; -+ -+ LeavesLogger.LOGGER.info("Player " + player.getScoreboardName() + " joined with bladeren " + clientVersion); -+ -+ if (tag != null) { -+ CompoundTag featureNbt = tag.getCompound("Features"); -+ for (String name : featureNbt.getAllKeys()) { -+ if (registeredFeatures.containsKey(name)) { -+ registeredFeatures.get(name).accept(player, featureNbt.getCompound(name)); -+ } -+ } -+ } -+ } -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = BladerenFeatureModifyPayload.class, payloadId = "feature_modify") -+ private static void handleModify(@NotNull ServerPlayer player, @NotNull BladerenFeatureModifyPayload payload) { -+ if (LeavesConfig.protocol.bladeren.enable) { -+ String name = payload.name; -+ CompoundTag tag = payload.nbt; -+ -+ if (registeredFeatures.containsKey(name)) { -+ registeredFeatures.get(name).accept(player, tag); -+ } -+ } -+ } -+ -+ @ProtocolHandler.PlayerJoin -+ public static void onPlayerJoin(@NotNull ServerPlayer player) { -+ if (LeavesConfig.protocol.bladeren.enable) { -+ CompoundTag tag = new CompoundTag(); -+ LeavesFeatureSet.writeNBT(tag); -+ ProtocolUtils.sendPayloadPacket(player, new BladerenHelloPayload(PROTOCOL_VERSION, tag)); -+ } -+ } -+ -+ public static void registerFeature(String name, BiConsumer consumer) { -+ registeredFeatures.put(name, consumer); -+ } -+ -+ public static class LeavesFeatureSet { -+ -+ private static final Map features = new HashMap<>(); -+ -+ public static void writeNBT(@NotNull CompoundTag tag) { -+ CompoundTag featureNbt = new CompoundTag(); -+ features.values().forEach(feature -> feature.writeNBT(featureNbt)); -+ tag.put("Features", featureNbt); -+ } -+ -+ public static void register(LeavesFeature feature) { -+ features.put(feature.name, feature); -+ } -+ } -+ -+ public record LeavesFeature(String name, String value) { -+ -+ @NotNull -+ @Contract("_, _ -> new") -+ public static LeavesFeature of(String name, boolean value) { -+ return new LeavesFeature(name, Boolean.toString(value)); -+ } -+ -+ public void writeNBT(@NotNull CompoundTag rules) { -+ CompoundTag rule = new CompoundTag(); -+ rule.putString("Feature", name); -+ rule.putString("Value", value); -+ rules.put(name, rule); -+ } -+ } -+ -+ public record BladerenFeatureModifyPayload(String name, CompoundTag nbt) implements LeavesCustomPayload { -+ -+ @New -+ public BladerenFeatureModifyPayload(ResourceLocation location, FriendlyByteBuf buf) { -+ this(buf.readUtf(), buf.readNbt()); -+ } -+ -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ buf.writeUtf(name); -+ buf.writeNbt(nbt); -+ } -+ -+ @Override -+ @NotNull -+ public ResourceLocation id() { -+ return FEATURE_MODIFY_ID; -+ } -+ } -+ -+ public record BladerenHelloPayload(String version, CompoundTag nbt) implements LeavesCustomPayload { -+ -+ @New -+ public BladerenHelloPayload(ResourceLocation location, @NotNull FriendlyByteBuf buf) { -+ this(buf.readUtf(64), buf.readNbt()); -+ } -+ -+ @Override -+ public void write(@NotNull FriendlyByteBuf buf) { -+ buf.writeUtf(version); -+ buf.writeNbt(nbt); -+ } -+ -+ @Override -+ @NotNull -+ public ResourceLocation id() { -+ return HELLO_ID; -+ } -+ } -+} diff --git a/patches/server/0069-Bladeren-mspt-sync-protocol.patch b/patches/server/0069-Bladeren-mspt-sync-protocol.patch deleted file mode 100644 index 90c97f05..00000000 --- a/patches/server/0069-Bladeren-mspt-sync-protocol.patch +++ /dev/null @@ -1,89 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 3 Jul 2023 22:12:16 +0800 -Subject: [PATCH] Bladeren mspt sync protocol - - -diff --git a/src/main/java/org/leavesmc/leaves/protocol/bladeren/MsptSyncProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/bladeren/MsptSyncProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6e4ea71f6d1a6e784248ce2eacd270e2d410da66 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/bladeren/MsptSyncProtocol.java -@@ -0,0 +1,77 @@ -+package org.leavesmc.leaves.protocol.bladeren; -+ -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerPlayer; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.List; -+import java.util.OptionalDouble; -+ -+@LeavesProtocol(namespace = "bladeren") -+public class MsptSyncProtocol { -+ -+ public static final String PROTOCOL_ID = "bladeren"; -+ -+ private static final ResourceLocation MSPT_SYNC = id("mspt_sync"); -+ -+ private static final List players = new ArrayList<>(); -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation id(String path) { -+ return new ResourceLocation(PROTOCOL_ID, path); -+ } -+ -+ @ProtocolHandler.Init -+ public static void init() { -+ BladerenProtocol.registerFeature("mspt_sync", (player, compoundTag) -> { -+ if (compoundTag.getString("Value").equals("true")) { -+ onPlayerSubmit(player); -+ } else { -+ onPlayerLoggedOut(player); -+ } -+ }); -+ } -+ -+ @ProtocolHandler.PlayerLeave -+ public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { -+ if (LeavesConfig.protocol.bladeren.msptSyncProtocol) { -+ players.remove(player); -+ } -+ } -+ -+ @ProtocolHandler.Ticker -+ public static void tick() { -+ if (LeavesConfig.protocol.bladeren.msptSyncProtocol) { -+ if (players.isEmpty()) { -+ return; -+ } -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ if (server.getTickCount() % LeavesConfig.protocol.bladeren.msptSyncTickInterval == 0) { -+ OptionalDouble msptArr = Arrays.stream(server.getTickTimesNanos()).average(); -+ if (msptArr.isPresent()) { -+ double mspt = msptArr.getAsDouble() * 1.0E-6D; -+ double tps = 1000.0D / Math.max(mspt, 50); -+ players.forEach(player -> ProtocolUtils.sendPayloadPacket(player, MSPT_SYNC, buf -> { -+ buf.writeDouble(mspt); -+ buf.writeDouble(tps); -+ })); -+ } -+ } -+ } -+ } -+ -+ public static void onPlayerSubmit(@NotNull ServerPlayer player) { -+ if (LeavesConfig.protocol.bladeren.msptSyncProtocol) { -+ players.add(player); -+ } -+ } -+} diff --git a/patches/server/0071-Optimize-noise-generation.patch b/patches/server/0071-Optimize-noise-generation.patch deleted file mode 100644 index 9304759b..00000000 --- a/patches/server/0071-Optimize-noise-generation.patch +++ /dev/null @@ -1,281 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 18 Jul 2023 15:27:19 +0800 -Subject: [PATCH] Optimize noise generation - -This patch is Powered by Gale(https://github.com/GaleMC/Gale) - -diff --git a/src/main/java/net/minecraft/world/level/levelgen/synth/ImprovedNoise.java b/src/main/java/net/minecraft/world/level/levelgen/synth/ImprovedNoise.java -index 9a97e5cd23d839183ac4d243d28df92af3119fe7..344a0873334d2626a7c745c4dd2547f54c581806 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/synth/ImprovedNoise.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/synth/ImprovedNoise.java -@@ -11,6 +11,27 @@ public final class ImprovedNoise { - public final double yo; - public final double zo; - -+ // Leaves start - optimize noise generation -+ private static final double[] FLAT_SIMPLEX_GRAD = new double[]{ -+ 1, 1, 0, 0, -+ -1, 1, 0, 0, -+ 1, -1, 0, 0, -+ -1, -1, 0, 0, -+ 1, 0, 1, 0, -+ -1, 0, 1, 0, -+ 1, 0, -1, 0, -+ -1, 0, -1, 0, -+ 0, 1, 1, 0, -+ 0, -1, 1, 0, -+ 0, 1, -1, 0, -+ 0, -1, -1, 0, -+ 1, 1, 0, 0, -+ 0, -1, 1, 0, -+ -1, 1, 0, 0, -+ 0, -1, -1, 0, -+ }; -+ // Leaves end - optimize noise generation -+ - public ImprovedNoise(RandomSource random) { - this.xo = random.nextDouble() * 256.0; - this.yo = random.nextDouble() * 256.0; -@@ -41,9 +62,20 @@ public final class ImprovedNoise { - int i = Mth.floor(d); - int j = Mth.floor(e); - int k = Mth.floor(f); -- double g = d - (double)i; -- double h = e - (double)j; -- double l = f - (double)k; -+ // Leaves start - optimize noise generation -+ double g; -+ double h; -+ double l; -+ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { -+ g = d - (double)i; -+ h = e - (double)j; -+ l = f - (double)k; -+ } else { -+ g = d - i; -+ h = e - j; -+ l = f - k; -+ } -+ // Leaves end - optimize noise generation - double o; - if (yScale != 0.0) { - double m; -@@ -53,12 +85,24 @@ public final class ImprovedNoise { - m = h; - } - -- o = (double)Mth.floor(m / yScale + 1.0E-7F) * yScale; -+ // Leaves start - optimize noise generation -+ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { -+ o = (double)Mth.floor(m / yScale + 1.0E-7F) * yScale; -+ } else { -+ o = Math.floor(m / yScale + (double)1.0E-7F) * yScale; -+ } -+ // Leaves end - optimize noise generation - } else { - o = 0.0; - } - -- return this.sampleAndLerp(i, j, k, g, h - o, l, h); -+ // Leaves start - optimize noise generation -+ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { -+ return this.sampleAndLerp(i, j, k, g, h - o, l, h); -+ } else { -+ return this.sampleAndLerp((int) i, (int) j, (int) k, g, h - o, l, h); -+ } -+ // Leaves end - optimize noise generation - } - - public double noiseWithDerivative(double x, double y, double z, double[] ds) { -@@ -68,10 +112,19 @@ public final class ImprovedNoise { - int i = Mth.floor(d); - int j = Mth.floor(e); - int k = Mth.floor(f); -- double g = d - (double)i; -- double h = e - (double)j; -- double l = f - (double)k; -- return this.sampleWithDerivative(i, j, k, g, h, l, ds); -+ // Leaves start - optimize noise generation -+ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { -+ double g = d - (double)i; -+ double h = e - (double)j; -+ double l = f - (double)k; -+ return this.sampleWithDerivative(i, j, k, g, h, l, ds); -+ } else { -+ double g = d - i; -+ double h = e - j; -+ double l = f - k; -+ return this.sampleWithDerivative((int) i, (int) j, (int) k, g, h, l, ds); -+ } -+ // Leaves end - optimize noise generation - } - - private static double gradDot(int hash, double x, double y, double z) { -@@ -83,24 +136,90 @@ public final class ImprovedNoise { - } - - private double sampleAndLerp(int sectionX, int sectionY, int sectionZ, double localX, double localY, double localZ, double fadeLocalY) { -- int i = this.p(sectionX); -- int j = this.p(sectionX + 1); -- int k = this.p(i + sectionY); -- int l = this.p(i + sectionY + 1); -- int m = this.p(j + sectionY); -- int n = this.p(j + sectionY + 1); -- double d = gradDot(this.p(k + sectionZ), localX, localY, localZ); -- double e = gradDot(this.p(m + sectionZ), localX - 1.0, localY, localZ); -- double f = gradDot(this.p(l + sectionZ), localX, localY - 1.0, localZ); -- double g = gradDot(this.p(n + sectionZ), localX - 1.0, localY - 1.0, localZ); -- double h = gradDot(this.p(k + sectionZ + 1), localX, localY, localZ - 1.0); -- double o = gradDot(this.p(m + sectionZ + 1), localX - 1.0, localY, localZ - 1.0); -- double p = gradDot(this.p(l + sectionZ + 1), localX, localY - 1.0, localZ - 1.0); -- double q = gradDot(this.p(n + sectionZ + 1), localX - 1.0, localY - 1.0, localZ - 1.0); -- double r = Mth.smoothstep(localX); -- double s = Mth.smoothstep(fadeLocalY); -- double t = Mth.smoothstep(localZ); -- return Mth.lerp3(r, s, t, d, e, f, g, h, o, p, q); -+ // Leaves start - optimize noise generation -+ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { -+ int i = this.p(sectionX); -+ int j = this.p(sectionX + 1); -+ int k = this.p(i + sectionY); -+ int l = this.p(i + sectionY + 1); -+ int m = this.p(j + sectionY); -+ int n = this.p(j + sectionY + 1); -+ double d = gradDot(this.p(k + sectionZ), localX, localY, localZ); -+ double e = gradDot(this.p(m + sectionZ), localX - 1.0, localY, localZ); -+ double f = gradDot(this.p(l + sectionZ), localX, localY - 1.0, localZ); -+ double g = gradDot(this.p(n + sectionZ), localX - 1.0, localY - 1.0, localZ); -+ double h = gradDot(this.p(k + sectionZ + 1), localX, localY, localZ - 1.0); -+ double o = gradDot(this.p(m + sectionZ + 1), localX - 1.0, localY, localZ - 1.0); -+ double p = gradDot(this.p(l + sectionZ + 1), localX, localY - 1.0, localZ - 1.0); -+ double q = gradDot(this.p(n + sectionZ + 1), localX - 1.0, localY - 1.0, localZ - 1.0); -+ double r = Mth.smoothstep(localX); -+ double s = Mth.smoothstep(fadeLocalY); -+ double t = Mth.smoothstep(localZ); -+ return Mth.lerp3(r, s, t, d, e, f, g, h, o, p, q); -+ } else { -+ final int var0 = sectionX & 0xFF; -+ final int var1 = (sectionX + 1) & 0xFF; -+ final int var2 = this.p[var0] & 0xFF; -+ final int var3 = this.p[var1] & 0xFF; -+ final int var4 = (var2 + sectionY) & 0xFF; -+ final int var5 = (var3 + sectionY) & 0xFF; -+ final int var6 = (var2 + sectionY + 1) & 0xFF; -+ final int var7 = (var3 + sectionY + 1) & 0xFF; -+ final int var8 = this.p[var4] & 0xFF; -+ final int var9 = this.p[var5] & 0xFF; -+ final int var10 = this.p[var6] & 0xFF; -+ final int var11 = this.p[var7] & 0xFF; -+ -+ final int var12 = (var8 + sectionZ) & 0xFF; -+ final int var13 = (var9 + sectionZ) & 0xFF; -+ final int var14 = (var10 + sectionZ) & 0xFF; -+ final int var15 = (var11 + sectionZ) & 0xFF; -+ final int var16 = (var8 + sectionZ + 1) & 0xFF; -+ final int var17 = (var9 + sectionZ + 1) & 0xFF; -+ final int var18 = (var10 + sectionZ + 1) & 0xFF; -+ final int var19 = (var11 + sectionZ + 1) & 0xFF; -+ final int var20 = (this.p[var12] & 15) << 2; -+ final int var21 = (this.p[var13] & 15) << 2; -+ final int var22 = (this.p[var14] & 15) << 2; -+ final int var23 = (this.p[var15] & 15) << 2; -+ final int var24 = (this.p[var16] & 15) << 2; -+ final int var25 = (this.p[var17] & 15) << 2; -+ final int var26 = (this.p[var18] & 15) << 2; -+ final int var27 = (this.p[var19] & 15) << 2; -+ final double var60 = localX - 1.0; -+ final double var61 = localY - 1.0; -+ final double var62 = localZ - 1.0; -+ final double var87 = FLAT_SIMPLEX_GRAD[(var20) | 0] * localX + FLAT_SIMPLEX_GRAD[(var20) | 1] * localY + FLAT_SIMPLEX_GRAD[(var20) | 2] * localZ; -+ final double var88 = FLAT_SIMPLEX_GRAD[(var21) | 0] * var60 + FLAT_SIMPLEX_GRAD[(var21) | 1] * localY + FLAT_SIMPLEX_GRAD[(var21) | 2] * localZ; -+ final double var89 = FLAT_SIMPLEX_GRAD[(var22) | 0] * localX + FLAT_SIMPLEX_GRAD[(var22) | 1] * var61 + FLAT_SIMPLEX_GRAD[(var22) | 2] * localZ; -+ final double var90 = FLAT_SIMPLEX_GRAD[(var23) | 0] * var60 + FLAT_SIMPLEX_GRAD[(var23) | 1] * var61 + FLAT_SIMPLEX_GRAD[(var23) | 2] * localZ; -+ final double var91 = FLAT_SIMPLEX_GRAD[(var24) | 0] * localX + FLAT_SIMPLEX_GRAD[(var24) | 1] * localY + FLAT_SIMPLEX_GRAD[(var24) | 2] * var62; -+ final double var92 = FLAT_SIMPLEX_GRAD[(var25) | 0] * var60 + FLAT_SIMPLEX_GRAD[(var25) | 1] * localY + FLAT_SIMPLEX_GRAD[(var25) | 2] * var62; -+ final double var93 = FLAT_SIMPLEX_GRAD[(var26) | 0] * localX + FLAT_SIMPLEX_GRAD[(var26) | 1] * var61 + FLAT_SIMPLEX_GRAD[(var26) | 2] * var62; -+ final double var94 = FLAT_SIMPLEX_GRAD[(var27) | 0] * var60 + FLAT_SIMPLEX_GRAD[(var27) | 1] * var61 + FLAT_SIMPLEX_GRAD[(var27) | 2] * var62; -+ -+ final double var95 = localX * 6.0 - 15.0; -+ final double var96 = fadeLocalY * 6.0 - 15.0; -+ final double var97 = localZ * 6.0 - 15.0; -+ final double var98 = localX * var95 + 10.0; -+ final double var99 = fadeLocalY * var96 + 10.0; -+ final double var100 = localZ * var97 + 10.0; -+ final double var101 = localX * localX * localX * var98; -+ final double var102 = fadeLocalY * fadeLocalY * fadeLocalY * var99; -+ final double var103 = localZ * localZ * localZ * var100; -+ -+ final double var113 = var87 + var101 * (var88 - var87); -+ final double var114 = var93 + var101 * (var94 - var93); -+ final double var115 = var91 + var101 * (var92 - var91); -+ final double var116 = var89 + var101 * (var90 - var89); -+ final double var117 = var114 - var115; -+ final double var118 = var102 * (var116 - var113); -+ final double var119 = var102 * var117; -+ final double var120 = var113 + var118; -+ final double var121 = var115 + var119; -+ return var120 + (var103 * (var121 - var120)); -+ } -+ // Leaves end - optimize noise generation - } - - private double sampleWithDerivative(int sectionX, int sectionY, int sectionZ, double localX, double localY, double localZ, double[] ds) { -diff --git a/src/main/java/net/minecraft/world/level/levelgen/synth/PerlinNoise.java b/src/main/java/net/minecraft/world/level/levelgen/synth/PerlinNoise.java -index 35820670837376bcad8891241724d5b946fbd31f..317a3cc6d01309513a1177a19b6c9380d5792fc5 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/synth/PerlinNoise.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/synth/PerlinNoise.java -@@ -26,6 +26,10 @@ public class PerlinNoise { - private final double lowestFreqValueFactor; - private final double lowestFreqInputFactor; - private final double maxValue; -+ // Leaves start - optimize noise generation -+ private final int octaveSamplersCount; -+ private final double [] amplitudesArray; -+ // Leaves end - optimize noise generation - - @Deprecated - public static PerlinNoise createLegacyForBlendedNoise(RandomSource random, IntStream octaves) { -@@ -127,6 +131,10 @@ public class PerlinNoise { - this.lowestFreqInputFactor = Math.pow(2.0, (double)(-j)); - this.lowestFreqValueFactor = Math.pow(2.0, (double)(i - 1)) / (Math.pow(2.0, (double)i) - 1.0); - this.maxValue = this.edgeValue(2.0); -+ // Leaves start - optimize noise generation -+ this.octaveSamplersCount = this.noiseLevels.length; -+ this.amplitudesArray = this.amplitudes.toDoubleArray(); -+ // Leaves end - optimize noise generation - } - - protected double maxValue() { -@@ -138,7 +146,30 @@ public class PerlinNoise { - } - - public double getValue(double x, double y, double z) { -- return this.getValue(x, y, z, 0.0, 0.0, false); -+ // Leaves start - optimize noise generation -+ if (!org.leavesmc.leaves.LeavesConfig.performance.optimizeNoiseGeneration) { -+ return this.getValue(x, y, z, 0.0, 0.0, false); -+ } else { -+ double d = 0.0; -+ double e = this.lowestFreqInputFactor; -+ double f = this.lowestFreqValueFactor; -+ -+ for(int i = 0; i < this.octaveSamplersCount; ++i) { -+ ImprovedNoise perlinNoiseSampler = this.noiseLevels[i]; -+ if (perlinNoiseSampler != null) { -+ @SuppressWarnings("deprecation") -+ double g = perlinNoiseSampler.noise( -+ wrap(x * e), wrap(y * e), wrap(z * e), 0.0, 0.0 -+ ); -+ d += this.amplitudesArray[i] * g * f; -+ } -+ -+ e *= 2.0; -+ f /= 2.0; -+ } -+ return d; -+ } -+ // Leaves end - optimize noise generation - } - - @Deprecated diff --git a/patches/server/0073-Reduce-array-allocations.patch b/patches/server/0073-Reduce-array-allocations.patch deleted file mode 100644 index 8dada6f8..00000000 --- a/patches/server/0073-Reduce-array-allocations.patch +++ /dev/null @@ -1,445 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 20 Jul 2023 15:03:28 +0800 -Subject: [PATCH] Reduce array allocations - -This patch is Powered by Gale(https://github.com/GaleMC/Gale) - -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -index b3c993a790fc3fab6a408c731deb297f74c959ce..f85775a40eb72f207ac8b8f8169b20b584b19737 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/level/entity/ChunkEntitySlices.java -@@ -29,6 +29,7 @@ import java.util.Arrays; - import java.util.Iterator; - import java.util.List; - import java.util.function.Predicate; -+import org.leavesmc.leaves.util.ArrayConstants; - - public final class ChunkEntitySlices { - -@@ -387,7 +388,7 @@ public final class ChunkEntitySlices { - - private static final class BasicEntityList { - -- private static final Entity[] EMPTY = new Entity[0]; -+ // protected static final Entity[] EMPTY = new Entity[0]; // Leaves - reduce array allocations - private static final int DEFAULT_CAPACITY = 4; - - private E[] storage; -@@ -398,7 +399,7 @@ public final class ChunkEntitySlices { - } - - public BasicEntityList(final int cap) { -- this.storage = (E[])(cap <= 0 ? EMPTY : new Entity[cap]); -+ this.storage = (E[])(cap <= 0 ? ArrayConstants.emptyEntityArray : new Entity[cap]); // Leaves - reduce array allocations - } - - public boolean isEmpty() { -@@ -410,7 +411,7 @@ public final class ChunkEntitySlices { - } - - private void resize() { -- if (this.storage == EMPTY) { -+ if (this.storage == ArrayConstants.emptyEntityArray) { // Leaves - reduce array allocations - this.storage = (E[])new Entity[DEFAULT_CAPACITY]; - } else { - this.storage = Arrays.copyOf(this.storage, this.storage.length * 2); -diff --git a/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java b/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java -index ae60bd96b5284d54676d8e7e4dd5d170b526ec1e..0c474b1eb4dbef547890b7db5fcf9c13c86092a2 100644 ---- a/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java -+++ b/src/main/java/io/papermc/paper/command/subcommands/VersionCommand.java -@@ -7,6 +7,7 @@ import org.bukkit.command.CommandSender; - import org.checkerframework.checker.nullness.qual.NonNull; - import org.checkerframework.checker.nullness.qual.Nullable; - import org.checkerframework.framework.qual.DefaultQualifier; -+import org.leavesmc.leaves.util.ArrayConstants; - - @DefaultQualifier(NonNull.class) - public final class VersionCommand implements PaperSubcommand { -@@ -14,7 +15,7 @@ public final class VersionCommand implements PaperSubcommand { - public boolean execute(final CommandSender sender, final String subCommand, final String[] args) { - final @Nullable Command ver = MinecraftServer.getServer().server.getCommandMap().getCommand("version"); - if (ver != null) { -- ver.execute(sender, "paper", new String[0]); -+ ver.execute(sender, "paper", ArrayConstants.emptyStringArray); // Leaves - reduce array allocations - } - return true; - } -diff --git a/src/main/java/net/minecraft/nbt/ByteArrayTag.java b/src/main/java/net/minecraft/nbt/ByteArrayTag.java -index 06648f9751fd8a322d0809ffebf6a544596ee1a4..d6761bdb37619b91f147ff7a9197b730b90bd6cb 100644 ---- a/src/main/java/net/minecraft/nbt/ByteArrayTag.java -+++ b/src/main/java/net/minecraft/nbt/ByteArrayTag.java -@@ -7,6 +7,7 @@ import java.io.IOException; - import java.util.Arrays; - import java.util.List; - import org.apache.commons.lang3.ArrayUtils; -+import org.leavesmc.leaves.util.ArrayConstants; - - public class ByteArrayTag extends CollectionTag { - -@@ -175,7 +176,7 @@ public class ByteArrayTag extends CollectionTag { - } - - public void clear() { -- this.data = new byte[0]; -+ this.data = ArrayConstants.emptyByteArray; // Leaves - reduce array allocations - } - - @Override -diff --git a/src/main/java/net/minecraft/nbt/CompoundTag.java b/src/main/java/net/minecraft/nbt/CompoundTag.java -index d7bb00a946346dff0b0269cbd65276e146a63fb0..3f6545f3d2e421b9d2778adb113781b1d516fa0c 100644 ---- a/src/main/java/net/minecraft/nbt/CompoundTag.java -+++ b/src/main/java/net/minecraft/nbt/CompoundTag.java -@@ -18,6 +18,7 @@ import javax.annotation.Nullable; - import net.minecraft.CrashReport; - import net.minecraft.CrashReportCategory; - import net.minecraft.ReportedException; -+import org.leavesmc.leaves.util.ArrayConstants; - - public class CompoundTag implements Tag { - public static final Codec CODEC = Codec.PASSTHROUGH -@@ -409,7 +410,7 @@ public class CompoundTag implements Tag { - throw new ReportedException(this.createReport(key, ByteArrayTag.TYPE, var3)); - } - -- return new byte[0]; -+ return ArrayConstants.emptyByteArray; // Leaves - reduce array allocations - } - - public int[] getIntArray(String key) { -@@ -421,7 +422,7 @@ public class CompoundTag implements Tag { - throw new ReportedException(this.createReport(key, IntArrayTag.TYPE, var3)); - } - -- return new int[0]; -+ return ArrayConstants.emptyIntArray; // Leaves - reduce array allocations - } - - public long[] getLongArray(String key) { -@@ -433,7 +434,7 @@ public class CompoundTag implements Tag { - throw new ReportedException(this.createReport(key, LongArrayTag.TYPE, var3)); - } - -- return new long[0]; -+ return ArrayConstants.emptyLongArray; // Leaves - reduce array allocations - } - - public CompoundTag getCompound(String key) { -diff --git a/src/main/java/net/minecraft/nbt/IntArrayTag.java b/src/main/java/net/minecraft/nbt/IntArrayTag.java -index ff13d67151c50ea11a45117e524c7524e2b1a202..8c3ee4c9aa22bcc46f2dc3a5bc35bdde5fae3e64 100644 ---- a/src/main/java/net/minecraft/nbt/IntArrayTag.java -+++ b/src/main/java/net/minecraft/nbt/IntArrayTag.java -@@ -7,6 +7,7 @@ import java.io.IOException; - import java.util.Arrays; - import java.util.List; - import org.apache.commons.lang3.ArrayUtils; -+import org.leavesmc.leaves.util.ArrayConstants; - - public class IntArrayTag extends CollectionTag { - -@@ -186,7 +187,7 @@ public class IntArrayTag extends CollectionTag { - } - - public void clear() { -- this.data = new int[0]; -+ this.data = ArrayConstants.emptyIntArray; // Leaves - reduce array allocations - } - - @Override -diff --git a/src/main/java/net/minecraft/network/Connection.java b/src/main/java/net/minecraft/network/Connection.java -index 49e03ba7c04381e263aaee5cda9ed6c042bf6c0e..42e7da8be33fc4f85bf2d57b6f895762772a547c 100644 ---- a/src/main/java/net/minecraft/network/Connection.java -+++ b/src/main/java/net/minecraft/network/Connection.java -@@ -66,6 +66,7 @@ import org.apache.commons.lang3.Validate; - import org.slf4j.Logger; - import org.slf4j.Marker; - import org.slf4j.MarkerFactory; -+import org.leavesmc.leaves.util.ArrayConstants; - - public class Connection extends SimpleChannelInboundHandler> { - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index cd6d1e625d5b22acf279be22da158d827686a922..f1a570b1eb25f2cbe83ce31a59ebfc26887add76 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -184,6 +184,7 @@ import org.bukkit.event.server.MapInitializeEvent; - import org.bukkit.event.weather.LightningStrikeEvent; - import org.bukkit.event.world.TimeSkipEvent; - // CraftBukkit end -+import org.leavesmc.leaves.util.ArrayConstants; - - public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevelReader, ca.spottedleaf.moonrise.patches.chunk_tick_iteration.ChunkTickServerLevel { // Paper - rewrite chunk system // Paper - chunk tick iteration - -diff --git a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 4c3a4d6021fe3d02a5c8283eb86293a64b762da5..2bd29d075eafd72a146406836a933841a23cde76 100644 ---- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java -@@ -260,6 +260,7 @@ import org.bukkit.inventory.EquipmentSlot; - import org.bukkit.inventory.InventoryView; - import org.bukkit.inventory.SmithingInventory; - // CraftBukkit end -+import org.leavesmc.leaves.util.ArrayConstants; - - public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl implements ServerGamePacketListener, ServerPlayerConnection, TickablePacketListener { - -@@ -818,7 +819,7 @@ public class ServerGamePacketListenerImpl extends ServerCommonPacketListenerImpl - // Paper start - final int index; - if (packet.getCommand().length() > 64 && ((index = packet.getCommand().indexOf(' ')) == -1 || index >= 64)) { -- this.disconnectAsync(Component.translatable("disconnect.spam"), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - add proper async disconnect -+ this.disconnectAsync(Component.translatable("disconnect.spam", ArrayConstants.emptyObjectArray), org.bukkit.event.player.PlayerKickEvent.Cause.SPAM); // Paper - add proper async disconnect // Leaves - reduce array allocations - return; - } - // Paper end -diff --git a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -index 033755682c61c889723c3669b5cff4de147f637e..f4261a204577f349354b5ebca2d2070203597d63 100644 ---- a/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -+++ b/src/main/java/net/minecraft/server/network/ServerLoginPacketListenerImpl.java -@@ -51,6 +51,7 @@ import org.bukkit.craftbukkit.entity.CraftPlayer; - import org.bukkit.craftbukkit.util.Waitable; - import org.bukkit.event.player.AsyncPlayerPreLoginEvent; - import org.bukkit.event.player.PlayerPreLoginEvent; -+import org.leavesmc.leaves.util.ArrayConstants; - - public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, TickablePacketListener, CraftPlayer.TransferCookieConnection { - -@@ -172,12 +173,12 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, - - @Override - public void handleHello(ServerboundHelloPacket packet) { -- Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet", new Object[0]); -+ Validate.validState(this.state == ServerLoginPacketListenerImpl.State.HELLO, "Unexpected hello packet", ArrayConstants.emptyObjectArray); // Leaves - reduce array allocations - // Paper start - Validate usernames - if (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", new Object[0]); -+ Validate.validState(StringUtil.isReasonablePlayerName(packet.name()), "Invalid characters in username", ArrayConstants.emptyObjectArray); // Leaves - reduce array allocations - } - // Paper end - Validate usernames - this.requestedUsername = packet.name(); -@@ -275,7 +276,7 @@ public class ServerLoginPacketListenerImpl implements ServerLoginPacketListener, - - @Override - public void handleKey(ServerboundKeyPacket packet) { -- Validate.validState(this.state == ServerLoginPacketListenerImpl.State.KEY, "Unexpected key packet", new Object[0]); -+ Validate.validState(this.state == ServerLoginPacketListenerImpl.State.KEY, "Unexpected key packet", ArrayConstants.emptyObjectArray); // Leaves - reduce array allocations - - final String s; - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 74fe50e7b95d3e993a6af18bdea3939ceaa40c0d..0d64e38f37f471de62d093de092ec5c0975f10ee 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -916,13 +916,9 @@ public abstract class PlayerList { - final ResourceKey OVERWORLD = Level.OVERWORLD; - final ResourceKey THE_NETHER = Level.NETHER; - if (!((fromDim != OVERWORLD || toDim != THE_NETHER) && (fromDim != THE_NETHER || toDim != OVERWORLD))) { -- BlockPos lastPos = entityplayer1.lastPos; -- if (lastPos != null) { -- net.minecraft.BlockUtil.FoundRectangle fromPortal = ReturnPortalManager.findPortalAt(entityplayer1, fromDim, lastPos); -- BlockPos toPos = entityplayer1.blockPosition(); -- if (fromPortal != null) { -- ReturnPortalManager.storeReturnPortal(entityplayer1, toDim, toPos, fromPortal); -- } -+ BlockPos fromPortal = org.leavesmc.leaves.util.ReturnPortalManager.findPortalAt(entityplayer1, fromDim, entityplayer1.lastPos); -+ if (fromPortal != null) { -+ org.leavesmc.leaves.util.ReturnPortalManager.storeReturnPortal(entityplayer1, toDim, entityplayer1.blockPosition(), fromPortal); - } - } - } -diff --git a/src/main/java/net/minecraft/server/players/StoredUserList.java b/src/main/java/net/minecraft/server/players/StoredUserList.java -index c038da20b76c0b7b1c18471b20be01e849d29f3a..603007a376dc76c46d34f265283dda693cbf6c88 100644 ---- a/src/main/java/net/minecraft/server/players/StoredUserList.java -+++ b/src/main/java/net/minecraft/server/players/StoredUserList.java -@@ -24,6 +24,7 @@ import javax.annotation.Nullable; - import net.minecraft.Util; - import net.minecraft.util.GsonHelper; - import org.slf4j.Logger; -+import org.leavesmc.leaves.util.ArrayConstants; - - public abstract class StoredUserList> { - -@@ -76,7 +77,7 @@ public abstract class StoredUserList> { - } - - public String[] getUserList() { -- return (String[]) this.map.keySet().toArray(new String[0]); -+ return (String[]) this.map.keySet().toArray(ArrayConstants.emptyStringArray); // Leaves - reduce array allocations - } - - public boolean isEmpty() { -diff --git a/src/main/java/net/minecraft/util/ZeroBitStorage.java b/src/main/java/net/minecraft/util/ZeroBitStorage.java -index 87393a181c64af6a02ebddfccae674c4fbcb3903..09a402e95cdb8c75881fbeff66ca853709b62a33 100644 ---- a/src/main/java/net/minecraft/util/ZeroBitStorage.java -+++ b/src/main/java/net/minecraft/util/ZeroBitStorage.java -@@ -4,9 +4,10 @@ import java.util.Arrays; - import java.util.function.IntConsumer; - import org.apache.commons.lang3.Validate; - import net.minecraft.world.level.chunk.Palette; -+import org.leavesmc.leaves.util.ArrayConstants; - - public class ZeroBitStorage implements BitStorage { -- public static final long[] RAW = new long[0]; -+ public static final long[] RAW = ArrayConstants.emptyLongArray; // Leaves - reduce array allocations - private final int size; - - public ZeroBitStorage(int size) { -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index d50f928b70eb0b77088e77b76788c6d878b722ee..16cec435f86390ef57388ae83825dec397777b1c 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -3439,7 +3439,7 @@ public abstract class LivingEntity extends Entity implements Attackable { - throw new MatchException((String) null, (Throwable) null); - } - -- ItemStack itemstack2 = itemstack1; final ItemStack oldEquipment = itemstack2; // Paper - PlayerArmorChangeEvent - obfhelper -+ ItemStack itemstack2 = itemstack1; final ItemStack oldEquipment = itemstack2; // Paper - PlayerArmorChangeEvent - obfhelper - - itemstack = this.getItemBySlot(enumitemslot); final ItemStack newEquipment = itemstack;// Paper - PlayerArmorChangeEvent - obfhelper - if (this.equipmentHasChanged(itemstack2, itemstack)) { -diff --git a/src/main/java/net/minecraft/world/item/crafting/ShapedRecipe.java b/src/main/java/net/minecraft/world/item/crafting/ShapedRecipe.java -index 980fea65899ef5f37808506b822fd3de5830d2c7..11a89f7ce8096b375e6fb4786ecca53675943026 100644 ---- a/src/main/java/net/minecraft/world/item/crafting/ShapedRecipe.java -+++ b/src/main/java/net/minecraft/world/item/crafting/ShapedRecipe.java -@@ -23,6 +23,7 @@ import org.bukkit.craftbukkit.inventory.CraftRecipe; - import org.bukkit.craftbukkit.inventory.CraftShapedRecipe; - import org.bukkit.inventory.RecipeChoice; - // CraftBukkit end -+import org.leavesmc.leaves.util.ArrayConstants; - - public class ShapedRecipe implements CraftingRecipe { - -diff --git a/src/main/java/net/minecraft/world/level/Level.java b/src/main/java/net/minecraft/world/level/Level.java -index 83cfa4fb4fd85bda767de6f3c5e440a0a5229b59..bb5a19253fdb46fabb0da2faa2a7d608fa79e279 100644 ---- a/src/main/java/net/minecraft/world/level/Level.java -+++ b/src/main/java/net/minecraft/world/level/Level.java -@@ -104,6 +104,7 @@ import org.bukkit.craftbukkit.util.CraftSpawnCategory; - import org.bukkit.entity.SpawnCategory; - import org.bukkit.event.block.BlockPhysicsEvent; - // CraftBukkit end -+import org.leavesmc.leaves.util.ArrayConstants; - - public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemLevel, ca.spottedleaf.moonrise.patches.chunk_system.world.ChunkSystemEntityGetter { // Paper - rewrite chunk system // Paper - optimise collisions - -diff --git a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java -index db837b250fc35af5b528bf973b3b07f63e79bc46..4c55f5c1abbbe7ea55ee60e65a11b0d274fa746e 100644 ---- a/src/main/java/net/minecraft/world/level/block/ComposterBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/ComposterBlock.java -@@ -45,6 +45,7 @@ import net.minecraft.world.phys.shapes.VoxelShape; - import org.bukkit.craftbukkit.inventory.CraftBlockInventoryHolder; - import org.bukkit.craftbukkit.util.DummyGeneratorAccess; - // CraftBukkit end -+import org.leavesmc.leaves.util.ArrayConstants; - - public class ComposterBlock extends Block implements WorldlyContainerHolder { - -@@ -434,7 +435,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { - - @Override - public int[] getSlotsForFace(Direction side) { -- return side == Direction.DOWN ? new int[]{0} : new int[0]; -+ return side == Direction.DOWN ? ArrayConstants.zeroSingletonIntArray : ArrayConstants.emptyIntArray; // Leaves - reduce array allocations - } - - @Override -@@ -483,7 +484,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { - - @Override - public int[] getSlotsForFace(Direction side) { -- return side == Direction.UP ? new int[]{0} : new int[0]; -+ return side == Direction.UP ? ArrayConstants.zeroSingletonIntArray : ArrayConstants.emptyIntArray; // Leaves - reduce array allocations - } - - @Override -@@ -525,7 +526,7 @@ public class ComposterBlock extends Block implements WorldlyContainerHolder { - - @Override - public int[] getSlotsForFace(Direction side) { -- return new int[0]; -+ return ArrayConstants.emptyIntArray; // Leaves - reduce array allocations - } - - @Override -diff --git a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -index 46bdb85f18077c77c445ce0bc3cfe32468c75306..8a17b6a2e8fcd5d0bc071e94a123a0da264ffe60 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity.java -@@ -54,6 +54,7 @@ import org.bukkit.event.inventory.FurnaceSmeltEvent; - import org.bukkit.event.inventory.FurnaceStartSmeltEvent; - import org.bukkit.inventory.CookingRecipe; - // CraftBukkit end -+import org.leavesmc.leaves.util.ArrayConstants; - - public abstract class AbstractFurnaceBlockEntity extends BaseContainerBlockEntity implements WorldlyContainer, RecipeCraftingHolder, StackedContentsCompatible { - -@@ -61,7 +62,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 = ArrayConstants.zeroSingletonIntArray; // Leaves - 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/src/main/java/org/bukkit/craftbukkit/inventory/CraftEntityEquipment.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftEntityEquipment.java -index fdcc414f4fa246082ad0732133c870d915ae3084..556247696cde0d31cbb70907648d2970acf81153 100644 ---- a/src/main/java/org/bukkit/craftbukkit/inventory/CraftEntityEquipment.java -+++ b/src/main/java/org/bukkit/craftbukkit/inventory/CraftEntityEquipment.java -@@ -165,7 +165,7 @@ public class CraftEntityEquipment implements EntityEquipment { - - @Override - public void clear() { -- for (net.minecraft.world.entity.EquipmentSlot slot : net.minecraft.world.entity.EquipmentSlot.values()) { -+ for (net.minecraft.world.entity.EquipmentSlot slot : net.minecraft.world.entity.EquipmentSlot.VALUES) { // Leaves - reduce array allocations - this.setEquipment(slot, null, false); - } - } -diff --git a/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java b/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java -index b25dc23b81687dd4d4e70b3615ffb91f8c03c68b..8fdadf58054b2475f2023f76824af7bbe1303383 100644 ---- a/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java -+++ b/src/main/java/org/bukkit/craftbukkit/util/WeakCollection.java -@@ -6,6 +6,7 @@ import java.util.ArrayList; - import java.util.Collection; - import java.util.Iterator; - import java.util.NoSuchElementException; -+import org.leavesmc.leaves.util.ArrayConstants; - - public final class WeakCollection implements Collection { - static final Object NO_VALUE = new Object(); -@@ -164,7 +165,7 @@ public final class WeakCollection implements Collection { - - @Override - public Object[] toArray() { -- return this.toArray(new Object[0]); -+ return this.toArray(ArrayConstants.emptyObjectArray); // Leaves - reduce array allocations - } - - @Override -diff --git a/src/main/java/org/leavesmc/leaves/util/ArrayConstants.java b/src/main/java/org/leavesmc/leaves/util/ArrayConstants.java -new file mode 100644 -index 0000000000000000000000000000000000000000..99d6484ca682d3edb6461157dc57322a0ff8584a ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/ArrayConstants.java -@@ -0,0 +1,21 @@ -+package org.leavesmc.leaves.util; -+ -+import net.minecraft.server.level.ServerLevel; -+ -+// Powered by Gale(https://github.com/GaleMC/Gale) -+ -+public class ArrayConstants { -+ -+ private ArrayConstants() {} -+ -+ public static final Object[] emptyObjectArray = new Object[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 ServerLevel[] emptyServerLevelArray = new ServerLevel[0]; -+ -+} diff --git a/patches/server/0078-Check-frozen-ticks-before-landing-block.patch b/patches/server/0078-Check-frozen-ticks-before-landing-block.patch deleted file mode 100644 index f33f88ac..00000000 --- a/patches/server/0078-Check-frozen-ticks-before-landing-block.patch +++ /dev/null @@ -1,25 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 20 Jul 2023 20:33:52 +0800 -Subject: [PATCH] Check frozen ticks before landing block - -This patch is Powered by Gale(https://github.com/GaleMC/Gale) - -diff --git a/src/main/java/net/minecraft/world/entity/LivingEntity.java b/src/main/java/net/minecraft/world/entity/LivingEntity.java -index 16cec435f86390ef57388ae83825dec397777b1c..b5a3606b0810290fbbd624a401977e24d5a6a178 100644 ---- a/src/main/java/net/minecraft/world/entity/LivingEntity.java -+++ b/src/main/java/net/minecraft/world/entity/LivingEntity.java -@@ -581,11 +581,11 @@ public abstract class LivingEntity extends Entity implements Attackable { - } - - protected void tryAddFrost() { -- if (!this.getBlockStateOnLegacy().isAir()) { -+ if (org.leavesmc.leaves.LeavesConfig.performance.checkFrozenTicksBeforeLandingBlock || !this.getBlockStateOnLegacy().isAir()) { // Leaves - check frozen ticks before landing block - int i = this.getTicksFrozen(); - - if (i > 0) { -- AttributeInstance attributemodifiable = this.getAttribute(Attributes.MOVEMENT_SPEED); -+ AttributeInstance attributemodifiable = !org.leavesmc.leaves.LeavesConfig.performance.checkFrozenTicksBeforeLandingBlock || !this.getBlockStateOnLegacy().isAir() ? this.getAttribute(Attributes.MOVEMENT_SPEED) : null; // Leaves - check frozen ticks before landing block - - if (attributemodifiable == null) { - return; diff --git a/patches/server/0081-Fix-villagers-dont-release-memory.patch b/patches/server/0081-Fix-villagers-dont-release-memory.patch deleted file mode 100644 index 2b5e309c..00000000 --- a/patches/server/0081-Fix-villagers-dont-release-memory.patch +++ /dev/null @@ -1,43 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sat, 22 Jul 2023 12:00:59 +0800 -Subject: [PATCH] Fix villagers dont release memory - - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index bab140f5489ab13bb67a3372cb1429e5ead1d93d..c317035db4f7456988dd02b72729ec6ad7703a7c 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -4021,7 +4021,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - return this; - } - -- private Entity teleportCrossDimension(ServerLevel world, TeleportTransition teleportTarget) { -+ protected Entity teleportCrossDimension(ServerLevel world, TeleportTransition teleportTarget) { // Leaves - private -> protected - List list = this.getPassengers(); - List list1 = new ArrayList(list.size()); - -diff --git a/src/main/java/net/minecraft/world/entity/npc/Villager.java b/src/main/java/net/minecraft/world/entity/npc/Villager.java -index b7a34f1c4d7b5ef3f7a843d152e33c839dcdedd5..8d3959e86ef1357e49aac6ef3c8285c770f2856a 100644 ---- a/src/main/java/net/minecraft/world/entity/npc/Villager.java -+++ b/src/main/java/net/minecraft/world/entity/npc/Villager.java -@@ -1045,4 +1045,19 @@ public class Villager extends AbstractVillager implements ReputationEventHandler - return worldTime - olong < 24000L; - }).isPresent(); - } -+ -+ // Leaves start - fixes a memory leak when villagers get moved to another world -+ @Override -+ public Entity teleportCrossDimension(ServerLevel world, net.minecraft.world.level.portal.TeleportTransition transition) { -+ if (org.leavesmc.leaves.LeavesConfig.performance.villagersDontReleaseMemoryFix) { -+ this.releaseAllPois(); -+ this.getBrain().eraseMemory(MemoryModuleType.HOME); -+ this.getBrain().eraseMemory(MemoryModuleType.JOB_SITE); -+ this.getBrain().eraseMemory(MemoryModuleType.POTENTIAL_JOB_SITE); -+ this.getBrain().eraseMemory(MemoryModuleType.MEETING_POINT); -+ this.refreshBrain(transition.newLevel()); -+ } -+ return super.teleportCrossDimension(world, transition); -+ } -+ // Leaves end - fixes a memory leak when villagers get moved to another world - } diff --git a/patches/server/0084-Zero-tick-plants.patch b/patches/server/0084-Zero-tick-plants.patch deleted file mode 100644 index 81510761..00000000 --- a/patches/server/0084-Zero-tick-plants.patch +++ /dev/null @@ -1,94 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sun, 30 Jul 2023 12:20:16 +0800 -Subject: [PATCH] Zero tick plants - - -diff --git a/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java b/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java -index 5e88bd02f5c53124f1aeec3eae727a1f83cc8238..6243ca580b976375084732e468adf83ff2f9cd1c 100644 ---- a/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/BambooStalkBlock.java -@@ -122,8 +122,11 @@ public class BambooStalkBlock extends Block implements BonemealableBlock { - protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { - if (!state.canSurvive(world, pos)) { - world.destroyBlock(pos, true); -+ // Leaves start - zero tick plants -+ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { -+ this.randomTick(state, world, pos, random); -+ // Leaves end - zero tick plants - } -- - } - - @Override -diff --git a/src/main/java/net/minecraft/world/level/block/CactusBlock.java b/src/main/java/net/minecraft/world/level/block/CactusBlock.java -index c045b1cccf0047dbef8c04d5a28d31d53389054f..5154d910639b431a9b9be6dc009b0be66280e5e3 100644 ---- a/src/main/java/net/minecraft/world/level/block/CactusBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/CactusBlock.java -@@ -47,8 +47,11 @@ public class CactusBlock extends Block { - protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { - if (!state.canSurvive(world, pos)) { - world.destroyBlock(pos, true); -+ // Leaves start - zero tick plants -+ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { -+ this.randomTick(state, world, pos, random); -+ // Leaves end - zero tick plants - } -- - } - - @Override -diff --git a/src/main/java/net/minecraft/world/level/block/ChorusFlowerBlock.java b/src/main/java/net/minecraft/world/level/block/ChorusFlowerBlock.java -index 6d0d13e70a82c4db7848e1007f5b6d670dd5acad..2325219113f47424425a7896e8ccdca0e754ab4e 100644 ---- a/src/main/java/net/minecraft/world/level/block/ChorusFlowerBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/ChorusFlowerBlock.java -@@ -52,8 +52,11 @@ public class ChorusFlowerBlock extends Block { - protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { - if (!state.canSurvive(world, pos)) { - world.destroyBlock(pos, true); -+ // Leaves start - zero tick plants -+ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { -+ this.randomTick(state, world, pos, random); -+ // Leaves end - zero tick plants - } -- - } - - @Override -diff --git a/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java b/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java -index 9b424d7661fedf8ee1eb9f3167c62e563f04d4d1..f385600aae4075a7776ef1517cbc0dcfe39b5718 100644 ---- a/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/GrowingPlantHeadBlock.java -@@ -142,4 +142,15 @@ public abstract class GrowingPlantHeadBlock extends GrowingPlantBlock implements - protected GrowingPlantHeadBlock getHeadBlock() { - return this; - } -+ -+ // Leaves start - zero tick plants -+ @Override -+ public void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { -+ if (!state.canSurvive(world, pos)) { -+ world.destroyBlock(pos, true); -+ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { -+ this.randomTick(state, world, pos, random); -+ } -+ } -+ // Leaves end - zero tick plants - } -diff --git a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java -index 547ea09ed84595286c97c128b3b96f6d387ae25f..2c0b84fe8d3d891a32b1a50f265f7cffd8b9371f 100644 ---- a/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/SugarCaneBlock.java -@@ -46,8 +46,11 @@ public class SugarCaneBlock extends Block { - protected void tick(BlockState state, ServerLevel world, BlockPos pos, RandomSource random) { - if (!state.canSurvive(world, pos)) { - world.destroyBlock(pos, true); -+ // Leaves start - zero tick plants -+ } else if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.zeroTickPlants) { -+ this.randomTick(state, world, pos, random); -+ // Leaves end - zero tick plants - } -- - } - - @Override diff --git a/patches/server/0085-Leaves-Updater.patch b/patches/server/0085-Leaves-Updater.patch deleted file mode 100644 index ad53c227..00000000 --- a/patches/server/0085-Leaves-Updater.patch +++ /dev/null @@ -1,303 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 31 Jul 2023 17:42:25 +0800 -Subject: [PATCH] Leaves Updater - ---------- - -Co-authored-by: MC_XiaoHei - -diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -index 393163c80b9e2ce04b089e90d20eefd7b7ad8366..41f4245f558f1c41ea84891b920f1d03dcb07066 100644 ---- a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -@@ -32,6 +32,7 @@ public final class LeavesCommand extends Command { - private static final Map SUBCOMMANDS = Util.make(() -> { - final Map, LeavesSubcommand> commands = new HashMap<>(); - commands.put(Set.of("config"), new ConfigCommand()); -+ commands.put(Set.of("update"), new UpdateCommand()); - - return commands.entrySet().stream() - .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) -diff --git a/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java b/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..7f94df607e8ffd48ab2cb7c90d520c2b4a906cdc ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/subcommands/UpdateCommand.java -@@ -0,0 +1,21 @@ -+package org.leavesmc.leaves.command.subcommands; -+ -+import org.bukkit.ChatColor; -+import org.bukkit.command.CommandSender; -+import org.leavesmc.leaves.command.LeavesSubcommand; -+import org.leavesmc.leaves.util.LeavesUpdateHelper; -+ -+public class UpdateCommand implements LeavesSubcommand { -+ -+ @Override -+ public boolean execute(CommandSender sender, String subCommand, String[] args) { -+ sender.sendMessage(ChatColor.GRAY + "Trying to update Leaves, see the console for more info."); -+ LeavesUpdateHelper.tryUpdateLeaves(); -+ return true; -+ } -+ -+ @Override -+ public boolean tabCompletes() { -+ return false; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java b/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0b48f91f0390092f08c157a822acf7af3eb96588 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/LeavesUpdateHelper.java -@@ -0,0 +1,249 @@ -+package org.leavesmc.leaves.util; -+ -+import com.google.common.base.Charsets; -+import com.google.gson.Gson; -+import com.google.gson.JsonArray; -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonSyntaxException; -+import io.papermc.paper.ServerBuildInfo; -+import net.minecraft.Util; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+ -+import java.io.BufferedReader; -+import java.io.BufferedWriter; -+import java.io.File; -+import java.io.FileInputStream; -+import java.io.FileWriter; -+import java.io.IOException; -+import java.io.InputStreamReader; -+import java.net.HttpURLConnection; -+import java.net.URI; -+import java.net.URISyntaxException; -+import java.net.URL; -+import java.nio.channels.Channels; -+import java.nio.channels.FileChannel; -+import java.nio.channels.ReadableByteChannel; -+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.Map; -+import java.util.concurrent.Executors; -+import java.util.concurrent.ScheduledExecutorService; -+import java.util.concurrent.TimeUnit; -+import java.util.concurrent.locks.ReentrantLock; -+ -+import static java.nio.file.StandardOpenOption.CREATE; -+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING; -+import static java.nio.file.StandardOpenOption.WRITE; -+ -+public class LeavesUpdateHelper { -+ -+ private final static String autoUpdateDir = "auto_update"; -+ private final static String corePathFileName = autoUpdateDir + File.separator + "core.path"; -+ -+ private final static ReentrantLock updateLock = new ReentrantLock(); -+ private static boolean updateTaskStarted = false; -+ -+ private static final ScheduledExecutorService autoUpdateExecutor = Executors.newScheduledThreadPool(1); -+ -+ public static void init() { -+ File workingDirFile = new File(autoUpdateDir); -+ if (!workingDirFile.exists()) { -+ if (!workingDirFile.mkdir()) { -+ LeavesLogger.LOGGER.warning("Failed to create working directory: " + autoUpdateDir); -+ } -+ } -+ -+ File corePathFile = new File(corePathFileName); -+ if (!corePathFile.exists()) { -+ try { -+ if (!corePathFile.createNewFile()) { -+ throw new IOException(); -+ } -+ } catch (IOException e) { -+ LeavesLogger.LOGGER.severe("Failed to create core path file: " + corePathFileName, e); -+ } -+ } -+ -+ File leavesUpdateDir = new File(autoUpdateDir + File.separator + "leaves"); -+ if (!leavesUpdateDir.exists()) { -+ if (!leavesUpdateDir.mkdir()) { -+ LeavesLogger.LOGGER.warning("Failed to create leaves update directory: " + leavesUpdateDir); -+ } -+ } -+ -+ if (LeavesConfig.mics.autoUpdate.enable) { -+ LocalTime currentTime = LocalTime.now(); -+ long dailyTaskPeriod = 24 * 60 * 60 * 1000; -+ -+ for (String time : LeavesConfig.mics.autoUpdate.updateTime) { -+ try { -+ LocalTime taskTime = LocalTime.of(Integer.parseInt(time.split(":")[0]), Integer.parseInt(time.split(":")[1])); -+ Duration task = Duration.between(currentTime, taskTime); -+ if (task.isNegative()) { -+ task = task.plusDays(1); -+ } -+ autoUpdateExecutor.scheduleAtFixedRate(LeavesUpdateHelper::tryUpdateLeaves, task.toMillis(), dailyTaskPeriod, TimeUnit.MILLISECONDS); -+ } catch (Exception ignored) { -+ LeavesLogger.LOGGER.warning("Illegal auto-update time ignored: " + time); -+ } -+ } -+ } -+ } -+ -+ public static void tryUpdateLeaves() { -+ updateLock.lock(); -+ try { -+ if (!updateTaskStarted) { -+ updateTaskStarted = true; -+ new Thread(LeavesUpdateHelper::downloadLeaves).start(); -+ } -+ } finally { -+ updateLock.unlock(); -+ } -+ } -+ -+ private static void downloadLeaves() { -+ ServerBuildInfo version = ServerBuildInfo.buildInfo(); -+ if (version.gitCommit().isEmpty() || version.buildNumber().isEmpty()) { -+ LeavesLogger.LOGGER.info("IDE, custom build? Can not update!"); -+ updateTaskStarted = false; -+ return; -+ } -+ -+ LeavesLogger.LOGGER.info("Now gitHash: " + version.gitCommit().get()); -+ LeavesLogger.LOGGER.info("Trying to get latest build info."); -+ LeavesBuildInfo buildInfo = getLatestBuildInfo(version.minecraftVersionId(), version.gitCommit().get()); -+ -+ if (buildInfo != LeavesBuildInfo.ERROR) { -+ if (!buildInfo.needUpdate) { -+ LeavesLogger.LOGGER.warning("You are running the latest version, stopping update."); -+ updateTaskStarted = false; -+ return; -+ } -+ -+ LeavesLogger.LOGGER.info("Got build info, trying to download " + buildInfo.fileName); -+ try { -+ Path outFile = Path.of(autoUpdateDir, "leaves", buildInfo.fileName + ".cache"); -+ Files.deleteIfExists(outFile); -+ -+ try ( -+ final ReadableByteChannel source = Channels.newChannel(new URI( -+ buildInfo.url + LeavesConfig.mics.autoUpdate.source).toURL().openStream() -+ ); -+ final FileChannel fileChannel = FileChannel.open(outFile, CREATE, WRITE, TRUNCATE_EXISTING) -+ ) { -+ fileChannel.transferFrom(source, 0, Long.MAX_VALUE); -+ LeavesLogger.LOGGER.info("Download " + buildInfo.fileName + " completed."); -+ } catch (final IOException e) { -+ LeavesLogger.LOGGER.warning("Download " + buildInfo.fileName + " failed.", e); -+ Files.deleteIfExists(outFile); -+ updateTaskStarted = false; -+ return; -+ } -+ -+ if (!isFileValid(outFile, buildInfo.sha256)) { -+ LeavesLogger.LOGGER.warning("Hash check failed for downloaded file " + buildInfo.fileName); -+ Files.deleteIfExists(outFile); -+ updateTaskStarted = false; -+ return; -+ } -+ -+ File nowServerCore = new File(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName); -+ File backupServerCore = new File(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName + ".old"); -+ Util.safeReplaceFile(nowServerCore.toPath(), outFile, backupServerCore.toPath()); -+ -+ try (BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(corePathFileName))) { -+ bufferedWriter.write(autoUpdateDir + File.separator + "leaves" + File.separator + buildInfo.fileName); -+ } catch (IOException e) { -+ LeavesLogger.LOGGER.warning("Fail to download leaves core", e); -+ updateTaskStarted = false; -+ return; -+ } -+ -+ LeavesLogger.LOGGER.info("Leaves update completed, please restart your server."); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.severe("Leaves update failed", e); -+ } -+ } else { -+ LeavesLogger.LOGGER.warning("Stopping update."); -+ } -+ updateTaskStarted = false; -+ } -+ -+ private static boolean isFileValid(Path file, String hash) { -+ try (FileInputStream inputStream = new FileInputStream(file.toFile())) { -+ byte[] buffer = new byte[1024]; -+ MessageDigest md5 = MessageDigest.getInstance("SHA-256"); -+ -+ for (int numRead; (numRead = inputStream.read(buffer)) > 0; ) { -+ md5.update(buffer, 0, numRead); -+ } -+ -+ return toHexString(md5.digest()).equals(hash); -+ } catch (Exception e) { -+ LeavesLogger.LOGGER.warning("Fail to validate file " + file, e); -+ } -+ return false; -+ } -+ -+ @NotNull -+ private static String toHexString(byte @NotNull [] bytes) { -+ StringBuilder builder = new StringBuilder(); -+ for (byte b : bytes) { -+ builder.append(String.format("%02x", b)); -+ } -+ return builder.toString(); -+ } -+ -+ private static LeavesBuildInfo getLatestBuildInfo(String mcVersion, String gitHash) { -+ try { -+ HttpURLConnection connection = (HttpURLConnection) new URI( -+ "https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/latest" -+ ).toURL().openConnection(); -+ connection.connect(); -+ if (connection.getResponseCode() == HttpURLConnection.HTTP_NOT_FOUND) { -+ return LeavesBuildInfo.ERROR; -+ } -+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), Charsets.UTF_8))) { -+ JsonObject obj = new Gson().fromJson(reader, JsonObject.class); -+ String channel = obj.get("channel").getAsString(); -+ if ("experimental".equals(channel) && !LeavesConfig.mics.autoUpdate.allowExperimental) { -+ LeavesLogger.LOGGER.warning("Experimental version is not allowed to update for default, if you really want to update, please set misc.auto-update.allow-experimental to true in leaves.yml"); -+ return LeavesBuildInfo.ERROR; -+ } -+ int build = obj.get("build").getAsInt(); -+ -+ JsonArray changes = obj.get("changes").getAsJsonArray(); -+ boolean needUpdate = true; -+ for (JsonElement change : changes) { -+ if (change.getAsJsonObject().get("commit").getAsString().startsWith(gitHash)) { -+ needUpdate = false; -+ break; -+ } -+ } -+ -+ JsonObject downloadInfo = obj.get("downloads").getAsJsonObject().get("application").getAsJsonObject(); -+ String fileName = downloadInfo.get("name").getAsString(); -+ String sha256 = downloadInfo.get("sha256").getAsString(); -+ String url = "https://api.leavesmc.org/v2/projects/leaves/versions/" + mcVersion + "/builds/" + build + "/downloads/"; -+ return new LeavesBuildInfo(build, fileName, sha256, needUpdate, url); -+ } catch (JsonSyntaxException | NumberFormatException e) { -+ LeavesLogger.LOGGER.warning("Fail to get latest build info", e); -+ return LeavesBuildInfo.ERROR; -+ } -+ } catch (IOException | URISyntaxException e) { -+ LeavesLogger.LOGGER.warning("Fail to get latest build info", e); -+ return LeavesBuildInfo.ERROR; -+ } -+ } -+ -+ private record LeavesBuildInfo(int build, String fileName, String sha256, boolean needUpdate, String url) { -+ public static LeavesBuildInfo ERROR = null; -+ } -+} diff --git a/patches/server/0086-Force-peaceful-mode-switch.patch b/patches/server/0086-Force-peaceful-mode-switch.patch deleted file mode 100644 index c12afb0f..00000000 --- a/patches/server/0086-Force-peaceful-mode-switch.patch +++ /dev/null @@ -1,227 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 3 Aug 2023 14:21:47 +0800 -Subject: [PATCH] Force peaceful mode switch - - -diff --git a/src/main/java/net/minecraft/server/level/ServerChunkCache.java b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -index d021cd5b6136f0125076513977f430c6d4dd4f9f..f2b46b264762f9f58eebc37ab3c217d949625506 100644 ---- a/src/main/java/net/minecraft/server/level/ServerChunkCache.java -+++ b/src/main/java/net/minecraft/server/level/ServerChunkCache.java -@@ -176,6 +176,12 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - // Paper end - chunk tick iteration optimisations - - -+ // Leaves start - peaceful mode switch -+ public int peacefulModeSwitchTick = org.leavesmc.leaves.LeavesConfig.modify.forcePeacefulMode; -+ public int peacefulModeSwitchCount = -1; -+ private final List> peacefulModeSwitchEntityTypes = List.of(net.minecraft.world.entity.boss.wither.WitherBoss.class, net.minecraft.world.entity.monster.Shulker.class, net.minecraft.world.entity.monster.warden.Warden.class); -+ // Leaves end - peaceful mode switch -+ - public ServerChunkCache(ServerLevel world, LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, StructureTemplateManager structureTemplateManager, Executor workerExecutor, ChunkGenerator chunkGenerator, int viewDistance, int simulationDistance, boolean dsync, ChunkProgressListener worldGenerationProgressListener, ChunkStatusUpdateListener chunkStatusChangeListener, Supplier persistentStateManagerFactory) { - this.level = world; - this.mainThreadProcessor = new ServerChunkCache.MainThreadExecutor(world); -@@ -482,6 +488,21 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - if (!this.level.isDebug()) { - ProfilerFiller gameprofilerfiller = Profiler.get(); - -+ // Leaves start - peaceful mode switch -+ if (peacefulModeSwitchTick > 0) { -+ if (this.level.getLevelData().getGameTime() % peacefulModeSwitchTick == 0) { -+ peacefulModeSwitchCount = 0; -+ this.level.getAllEntities().forEach(entity -> { -+ if (peacefulModeSwitchEntityTypes.contains(entity.getClass())) { -+ peacefulModeSwitchCount++; -+ } -+ }); -+ } -+ } else { -+ peacefulModeSwitchCount = -1; -+ } -+ // Leaves end - peaceful mode switch -+ - gameprofilerfiller.push("pollingChunks"); - if (this.level.tickRateManager().runsNormally()) { - List list = this.tickingChunks; -@@ -579,6 +600,15 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - int k = this.level.getGameRules().getInt(GameRules.RULE_RANDOMTICKING); - List list1; - -+ // Leaves start - peaceful mode switch -+ boolean peacefulModeSwitch = false; -+ if (lastSpawnState != null && peacefulModeSwitchCount != -1) { -+ if (peacefulModeSwitchCount >= NaturalSpawner.globalLimitForCategory(level, net.minecraft.world.entity.MobCategory.MONSTER, lastSpawnState.getSpawnableChunkCount())) { -+ peacefulModeSwitch = true; -+ } -+ } -+ // Leaves end - peaceful mode switch -+ - if (flag && (this.spawnEnemies || this.spawnFriendlies)) { - // Paper start - PlayerNaturallySpawnCreaturesEvent - for (ServerPlayer entityPlayer : this.level.players()) { -@@ -590,7 +620,7 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon - // Paper end - PlayerNaturallySpawnCreaturesEvent - boolean flag1 = this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) != 0L && this.level.getLevelData().getGameTime() % this.level.ticksPerSpawnCategory.getLong(org.bukkit.entity.SpawnCategory.ANIMAL) == 0L; // CraftBukkit - -- list1 = NaturalSpawner.getFilteredSpawningCategories(spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1, this.level); // CraftBukkit -+ list1 = NaturalSpawner.getFilteredSpawningCategories(spawnercreature_d, this.spawnFriendlies, this.spawnEnemies, flag1, this.level, peacefulModeSwitch); // CraftBukkit Leaves start - peaceful mode switch - } else { - list1 = List.of(); - } -diff --git a/src/main/java/net/minecraft/world/level/NaturalSpawner.java b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -index f817fd922ffcf857e8a5fc803b10421f640a8cd1..d5ece1268e7cab245a2ef2885bb83323d961bbd7 100644 ---- a/src/main/java/net/minecraft/world/level/NaturalSpawner.java -+++ b/src/main/java/net/minecraft/world/level/NaturalSpawner.java -@@ -131,7 +131,12 @@ public final class NaturalSpawner { - } - - // CraftBukkit start - add server -+ // Leaves start - peaceful mode switch - public static List getFilteredSpawningCategories(NaturalSpawner.SpawnState spawnercreature_d, boolean flag, boolean flag1, boolean flag2, ServerLevel worldserver) { -+ return getFilteredSpawningCategories(spawnercreature_d, flag, flag1, flag2, worldserver, false); -+ } -+ public static List getFilteredSpawningCategories(NaturalSpawner.SpawnState spawnercreature_d, boolean flag, boolean flag1, boolean flag2, ServerLevel worldserver, boolean peacefulModeSwitch) { -+ // Leaves end - peaceful mode switch - LevelData worlddata = worldserver.getLevelData(); // CraftBukkit - Other mob type spawn tick rate - // CraftBukkit end - List list = new ArrayList(NaturalSpawner.SPAWNING_CATEGORIES.length); -@@ -140,6 +145,11 @@ public final class NaturalSpawner { - - for (int j = 0; j < i; ++j) { - MobCategory enumcreaturetype = aenumcreaturetype[j]; -+ // Leaves start - peaceful mode switch -+ if (enumcreaturetype == MobCategory.MONSTER && peacefulModeSwitch) { -+ continue; -+ } -+ // Leaves end - peaceful mode switch - // CraftBukkit start - Use per-world spawn limits - boolean spawnThisTick = true; - int limit = enumcreaturetype.getMaxInstancesPerChunk(); -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -index d47bab785d7be71155c964ef13e839768797c9cb..469a019a4fe850a3a20a1232592e51b588387d6f 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftWorld.java -@@ -2415,6 +2415,18 @@ public class CraftWorld extends CraftRegionAccessor implements World { - - // Paper - replace feature flag API - -+ // Leaves start - unsupported settings -+ @Override -+ public void setPeacefulModeSwitchTick(int tick) { -+ this.getHandle().chunkSource.peacefulModeSwitchTick = tick; -+ } -+ -+ @Override -+ public int getPeacefulModeSwitchTick() { -+ return this.getHandle().chunkSource.peacefulModeSwitchTick; -+ } -+ // Leaves end - unsupported settings -+ - public void storeBukkitValues(CompoundTag c) { - if (!this.persistentDataContainer.isEmpty()) { - c.put("BukkitValues", this.persistentDataContainer.toTagCompound()); -diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -index 41f4245f558f1c41ea84891b920f1d03dcb07066..92a8c3e4fc400988b3d984e7632a8149a2ce152e 100644 ---- a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -@@ -33,6 +33,7 @@ public final class LeavesCommand extends Command { - final Map, LeavesSubcommand> commands = new HashMap<>(); - commands.put(Set.of("config"), new ConfigCommand()); - commands.put(Set.of("update"), new UpdateCommand()); -+ commands.put(Set.of("peaceful"), new PeacefulModeSwitchCommand()); - - return commands.entrySet().stream() - .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) -diff --git a/src/main/java/org/leavesmc/leaves/command/subcommands/PeacefulModeSwitchCommand.java b/src/main/java/org/leavesmc/leaves/command/subcommands/PeacefulModeSwitchCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1fe1d08cfb6caefc67cea27ac8fc188b8d6675c5 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/subcommands/PeacefulModeSwitchCommand.java -@@ -0,0 +1,87 @@ -+package org.leavesmc.leaves.command.subcommands; -+ -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.JoinConfiguration; -+import net.kyori.adventure.text.format.NamedTextColor; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.world.entity.MobCategory; -+import net.minecraft.world.level.NaturalSpawner; -+import org.bukkit.Bukkit; -+import org.bukkit.World; -+import org.bukkit.command.CommandSender; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.bukkit.entity.Player; -+import org.leavesmc.leaves.command.LeavesCommandUtil; -+import org.leavesmc.leaves.command.LeavesSubcommand; -+ -+import java.util.ArrayList; -+import java.util.Collections; -+import java.util.List; -+ -+public class PeacefulModeSwitchCommand implements LeavesSubcommand { -+ -+ @Override -+ public boolean execute(CommandSender sender, String subCommand, String[] args) { -+ World world; -+ if (args.length == 0) { -+ if (sender instanceof Player player) { -+ world = player.getWorld(); -+ } else { -+ sender.sendMessage(Component.text("Must specify a world! ex: '/leaves peaceful world'", NamedTextColor.RED)); -+ return true; -+ } -+ } else { -+ final String input = args[0]; -+ final World inputWorld = Bukkit.getWorld(input); -+ if (inputWorld == null) { -+ sender.sendMessage(Component.text("'" + input + "' is not a valid world!", NamedTextColor.RED)); -+ return true; -+ } else { -+ world = inputWorld; -+ } -+ } -+ -+ final ServerLevel level = ((CraftWorld) world).getHandle(); -+ int chunks = 0; -+ if (level.chunkSource.getLastSpawnState() != null) { -+ chunks = level.chunkSource.getLastSpawnState().getSpawnableChunkCount(); -+ } -+ -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Peaceful Mode Switch for world: "), -+ Component.text(world.getName(), NamedTextColor.AQUA) -+ )); -+ -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Refuses per "), -+ Component.text(level.chunkSource.peacefulModeSwitchTick, level.chunkSource.peacefulModeSwitchTick > 0 ? NamedTextColor.AQUA : NamedTextColor.GRAY), -+ Component.text(" tick") -+ )); -+ -+ int count = level.chunkSource.peacefulModeSwitchCount; -+ int limit = NaturalSpawner.globalLimitForCategory(level, MobCategory.MONSTER, chunks); -+ NamedTextColor color = count >= limit ? NamedTextColor.AQUA : NamedTextColor.GRAY; -+ -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Now count "), -+ Component.text(count, color), -+ Component.text("/", color), -+ Component.text(limit, color) -+ )); -+ -+ return true; -+ } -+ -+ @Override -+ public List tabComplete(final CommandSender sender, final String subCommand, final String[] args) { -+ return LeavesCommandUtil.getListMatchingLast(sender, args, this.suggestPeacefulModeSwitch(args)); -+ } -+ -+ private List suggestPeacefulModeSwitch(final String[] args) { -+ if (args.length == 1) { -+ return new ArrayList<>(Bukkit.getWorlds().stream().map(World::getName).toList()); -+ } -+ -+ return Collections.emptyList(); -+ } -+} diff --git a/patches/server/0087-Replay-Mod-API.patch b/patches/server/0087-Replay-Mod-API.patch deleted file mode 100644 index b9ff14ec..00000000 --- a/patches/server/0087-Replay-Mod-API.patch +++ /dev/null @@ -1,1599 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 3 Aug 2023 20:36:38 +0800 -Subject: [PATCH] Replay Mod API - -This patch is Powered by ReplayMod(https://github.com/ReplayMod) - -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 0724bd95143cb5dc69b5f1eb2e67ecd679e09a99..e1d66ac593c6da954ef02def16601dc401421b9f 100644 ---- a/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java -+++ b/src/main/java/io/papermc/paper/plugin/manager/PaperEventManager.java -@@ -42,7 +42,7 @@ class PaperEventManager { - } - - // Leaves start - skip bot -- if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Bot) { -+ if (event instanceof org.bukkit.event.player.PlayerEvent playerEvent && (playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Bot || playerEvent.getPlayer() instanceof org.leavesmc.leaves.entity.Photographer)) { // Leaves - and photographer - return; - } - // Leaves end - skip bot -diff --git a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -index 716d37ea7c398c4f15f362c7759daca9d3fe32cb..2b586b222bdeddfb5a6a746fa4e60501fb73e847 100644 ---- a/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -+++ b/src/main/java/net/minecraft/commands/arguments/EntityArgument.java -@@ -154,6 +154,7 @@ public class EntityArgument implements ArgumentType { - if (icompletionprovider instanceof CommandSourceStack commandSourceStack && commandSourceStack.getEntity() instanceof ServerPlayer sourcePlayer) { - collection = new java.util.ArrayList<>(); - for (final ServerPlayer player : commandSourceStack.getServer().getPlayerList().getPlayers()) { -+ if (player instanceof org.leavesmc.leaves.replay.ServerPhotographer) continue; // Leaves - skip photographer - if (sourcePlayer.getBukkitEntity().canSee(player.getBukkitEntity())) { - collection.add(player.getGameProfile().getName()); - } -diff --git a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java -index c8d39e6e1c570c9219f6066da273dc0130920519..96a074281d16a7f64058619da4b102f387c85b28 100644 ---- a/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java -+++ b/src/main/java/net/minecraft/commands/arguments/selector/EntitySelector.java -@@ -117,6 +117,7 @@ public class EntitySelector { - return this.findPlayers(source); - } else if (this.playerName != null) { - ServerPlayer entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName); -+ entityplayer = entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer - - return entityplayer == null ? List.of() : List.of(entityplayer); - } else if (this.entityUUID != null) { -@@ -126,7 +127,7 @@ public class EntitySelector { - ServerLevel worldserver = (ServerLevel) iterator.next(); - Entity entity = worldserver.getEntity(this.entityUUID); - -- if (entity != null) { -+ if (entity != null && !(entity instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { - if (entity.getType().isEnabled(source.enabledFeatures())) { - return List.of(entity); - } -@@ -142,7 +143,7 @@ public class EntitySelector { - - if (this.currentEntity) { - predicate = this.getPredicate(vec3d, axisalignedbb, (FeatureFlagSet) null); -- return source.getEntity() != null && predicate.test(source.getEntity()) ? List.of(source.getEntity()) : List.of(); -+ return source.getEntity() != null && !(source.getEntity() instanceof org.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(source.getEntity()) ? List.of(source.getEntity()) : List.of(); // Leaves - skip photographer - } else { - predicate = this.getPredicate(vec3d, axisalignedbb, source.enabledFeatures()); - List list = new ObjectArrayList(); -@@ -158,6 +159,7 @@ public class EntitySelector { - this.addEntities(list, worldserver1, axisalignedbb, predicate); - } - } -+ list.removeIf(entity -> entity instanceof org.leavesmc.leaves.replay.ServerPhotographer); // Leaves - skip photographer - - return this.sortAndLimit(vec3d, list); - } -@@ -198,9 +200,11 @@ public class EntitySelector { - - if (this.playerName != null) { - entityplayer = source.getServer().getPlayerList().getPlayerByName(this.playerName); -+ entityplayer = entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer - return entityplayer == null ? List.of() : List.of(entityplayer); - } else if (this.entityUUID != null) { - entityplayer = source.getServer().getPlayerList().getPlayer(this.entityUUID); -+ entityplayer = entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer ? null : entityplayer; // Leaves - skip photographer - return entityplayer == null ? List.of() : List.of(entityplayer); - } else { - Vec3 vec3d = (Vec3) this.position.apply(source.getPosition()); -@@ -213,7 +217,7 @@ public class EntitySelector { - if (entity instanceof ServerPlayer) { - ServerPlayer entityplayer1 = (ServerPlayer) entity; - -- if (predicate.test(entityplayer1)) { -+ if (predicate.test(entityplayer1) && !(entityplayer1 instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // Leaves - skip photographer - return List.of(entityplayer1); - } - } -@@ -224,7 +228,7 @@ public class EntitySelector { - Object object; - - if (this.isWorldLimited()) { -- object = source.getLevel().getPlayers(predicate, i); -+ object = source.getLevel().getPlayers((entityplayer3 -> !(entityplayer3 instanceof org.leavesmc.leaves.replay.ServerPhotographer) && predicate.test(entityplayer3)), i); // Leaves - skip photographer - } else { - object = new ObjectArrayList(); - Iterator iterator = source.getServer().getPlayerList().getPlayers().iterator(); -@@ -232,7 +236,7 @@ public class EntitySelector { - while (iterator.hasNext()) { - ServerPlayer entityplayer2 = (ServerPlayer) iterator.next(); - -- if (predicate.test(entityplayer2)) { -+ if (predicate.test(entityplayer2) && !(entityplayer2 instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // Leaves - skip photographer - ((List) object).add(entityplayer2); - if (((List) object).size() >= i) { - return (List) object; -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 21a8c087dfe93ff6570d17e2757fe4c2250b3245..94c239ccf8d5ebca84810509abd13db1badfe008 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -1776,7 +1776,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop list = this.playerList.getPlayers(); -+ List list = this.playerList.realPlayers; // Leaves - only real player - int i = this.getMaxPlayers(); - - if (this.hidesOnlinePlayers()) { -diff --git a/src/main/java/net/minecraft/server/PlayerAdvancements.java b/src/main/java/net/minecraft/server/PlayerAdvancements.java -index 7f8eaf4590a29b147aa8c05cec919fd7744e74ba..4365a3d4b7fa0903ca6ff0581f72b57cdee762d4 100644 ---- a/src/main/java/net/minecraft/server/PlayerAdvancements.java -+++ b/src/main/java/net/minecraft/server/PlayerAdvancements.java -@@ -227,7 +227,7 @@ public class PlayerAdvancements { - } - // Leaves end - spectator don't get advancement - // Leaves start - bot can't get advancement -- if (player instanceof org.leavesmc.leaves.bot.ServerBot) { -+ if (player instanceof org.leavesmc.leaves.bot.ServerBot || player instanceof org.leavesmc.leaves.replay.ServerPhotographer) { // Leaves - and photographer - return false; - } - // Leaves end - bot can't get advancement -diff --git a/src/main/java/net/minecraft/server/commands/OpCommand.java b/src/main/java/net/minecraft/server/commands/OpCommand.java -index e7b444a10b244828827b3c66c53465206ea8e0ec..030601fdfde2232a933b2ad7022e9909db3a3783 100644 ---- a/src/main/java/net/minecraft/server/commands/OpCommand.java -+++ b/src/main/java/net/minecraft/server/commands/OpCommand.java -@@ -25,7 +25,7 @@ public class OpCommand { - (context, builder) -> { - PlayerList playerList = context.getSource().getServer().getPlayerList(); - return SharedSuggestionProvider.suggest( -- playerList.getPlayers() -+ playerList.realPlayers // Leaves - only real player - .stream() - .filter(player -> !playerList.isOp(player.getGameProfile())) - .map(player -> player.getGameProfile().getName()), -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index f1a570b1eb25f2cbe83ce31a59ebfc26887add76..4ff490f7fcf574d0c42a2e1c2773ccf85a80fdb9 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2720,7 +2720,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - if (entity instanceof ServerPlayer entityplayer) { - ServerLevel.this.players.add(entityplayer); - // Leaves start - skip -- if (!(entityplayer instanceof org.leavesmc.leaves.bot.ServerBot)) { -+ if (!(entityplayer instanceof org.leavesmc.leaves.bot.ServerBot) && !(entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // and photographer - ServerLevel.this.realPlayers.add(entityplayer); - } - // Leaves end - skip -@@ -2802,7 +2802,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - if (entity instanceof ServerPlayer entityplayer) { - ServerLevel.this.players.remove(entityplayer); - // Leaves start - skip -- if (!(entityplayer instanceof org.leavesmc.leaves.bot.ServerBot)) { -+ if (!(entityplayer instanceof org.leavesmc.leaves.bot.ServerBot) && !(entityplayer instanceof org.leavesmc.leaves.replay.ServerPhotographer)) { // and photographer - ServerLevel.this.realPlayers.remove(entityplayer); - } - // Leaves end - skip -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 0d64e38f37f471de62d093de092ec5c0975f10ee..13dea31c8232cdb08aff09627e84711d2ef0aa6b 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -125,6 +125,7 @@ import org.bukkit.event.player.PlayerSpawnChangeEvent; - // CraftBukkit end - - import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.replay.ServerPhotographer; - - public abstract class PlayerList { - -@@ -157,6 +158,7 @@ public abstract class PlayerList { - private boolean allowCommandsForAllPlayers; - private static final boolean ALLOW_LOGOUTIVATOR = false; - private int sendAllPlayerInfoIn; -+ public final List realPlayers = new java.util.concurrent.CopyOnWriteArrayList(); // Leaves - replay api - - // CraftBukkit start - private CraftServer cserver; -@@ -183,6 +185,119 @@ public abstract class PlayerList { - } - abstract public void loadAndSaveFiles(); // Paper - fix converting txt to json file; moved from DedicatedPlayerList constructor - -+ // Leaves start - replay api -+ public void placeNewPhotographer(Connection connection, ServerPhotographer player, ServerLevel worldserver, Location location) { -+ player.isRealPlayer = true; // Paper -+ player.loginTime = System.currentTimeMillis(); // Paper -+ -+ ServerLevel worldserver1 = worldserver; -+ -+ player.setServerLevel(worldserver1); -+ player.spawnIn(worldserver1); -+ player.gameMode.setLevel((ServerLevel) player.level()); -+ -+ LevelData worlddata = worldserver1.getLevelData(); -+ -+ player.loadGameTypes(null); -+ ServerGamePacketListenerImpl playerconnection = new ServerGamePacketListenerImpl(this.server, connection, player, CommonListenerCookie.createInitial(player.gameProfile, false)); -+ GameRules gamerules = worldserver1.getGameRules(); -+ boolean flag = gamerules.getBoolean(GameRules.RULE_DO_IMMEDIATE_RESPAWN); -+ boolean flag1 = gamerules.getBoolean(GameRules.RULE_REDUCEDDEBUGINFO); -+ boolean flag2 = gamerules.getBoolean(GameRules.RULE_LIMITED_CRAFTING); -+ -+ playerconnection.send(new ClientboundLoginPacket(player.getId(), worlddata.isHardcore(), this.server.levelKeys(), this.getMaxPlayers(), worldserver1.getWorld().getSendViewDistance(), worldserver1.getWorld().getSimulationDistance(), flag1, !flag, flag2, player.createCommonSpawnInfo(worldserver1), this.server.enforceSecureProfile())); // Paper - replace old player chunk management -+ player.getBukkitEntity().sendSupportedChannels(); // CraftBukkit -+ playerconnection.send(new ClientboundChangeDifficultyPacket(worlddata.getDifficulty(), worlddata.isDifficultyLocked())); -+ playerconnection.send(new ClientboundPlayerAbilitiesPacket(player.getAbilities())); -+ playerconnection.send(new ClientboundSetHeldSlotPacket(player.getInventory().selected)); -+ RecipeManager craftingmanager = this.server.getRecipeManager(); -+ playerconnection.send(new ClientboundUpdateRecipesPacket(craftingmanager.getSynchronizedItemProperties(), craftingmanager.getSynchronizedStonecutterRecipes())); -+ -+ this.sendPlayerPermissionLevel(player); -+ player.getStats().markAllDirty(); -+ player.getRecipeBook().sendInitialRecipeBook(player); -+ this.updateEntireScoreboard(worldserver1.getScoreboard(), player); -+ this.server.invalidateStatus(); -+ -+ playerconnection.teleport(player.getX(), player.getY(), player.getZ(), player.getYRot(), player.getXRot()); -+ ServerStatus serverping = this.server.getStatus(); -+ -+ if (serverping != null) { -+ player.sendServerStatus(serverping); -+ } -+ -+ this.players.add(player); -+ this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot -+ this.playersByUUID.put(player.getUUID(), player); -+ -+ player.supressTrackerForLogin = true; -+ worldserver1.addNewPlayer(player); -+ this.server.getCustomBossEvents().onPlayerConnect(player); -+ CraftPlayer bukkitPlayer = player.getBukkitEntity(); -+ -+ player.containerMenu.transferTo(player.containerMenu, bukkitPlayer); -+ if (!player.connection.isAcceptingMessages()) { -+ return; -+ } -+ -+ // org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol -+ -+ // Leaves start - bot support -+ if (org.leavesmc.leaves.LeavesConfig.modify.fakeplayer.enable) { -+ ServerBot bot = this.server.getBotList().getBotByName(player.getScoreboardName()); -+ if (bot != null) { -+ this.server.getBotList().removeBot(bot, org.leavesmc.leaves.event.bot.BotRemoveEvent.RemoveReason.INTERNAL, player.getBukkitEntity(), false); -+ } -+ this.server.getBotList().bots.forEach(bot1 -> { -+ bot1.sendPlayerInfo(player); -+ bot1.sendFakeDataIfNeed(player, true); -+ }); // Leaves - render bot -+ } -+ // Leaves end - bot support -+ -+ final List onlinePlayers = Lists.newArrayListWithExpectedSize(this.players.size() - 1); -+ for (int i = 0; i < this.players.size(); ++i) { -+ ServerPlayer entityplayer1 = this.players.get(i); -+ -+ if (entityplayer1 == player || !bukkitPlayer.canSee(entityplayer1.getBukkitEntity())) { -+ continue; -+ } -+ -+ onlinePlayers.add(entityplayer1); -+ } -+ if (!onlinePlayers.isEmpty()) { -+ player.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(onlinePlayers, player)); -+ } -+ -+ player.sentListPacket = true; -+ player.supressTrackerForLogin = false; -+ ((ServerLevel)player.level()).getChunkSource().chunkMap.addEntity(player); -+ -+ this.sendLevelInfo(player, worldserver1); -+ -+ if (player.level() == worldserver1 && !worldserver1.players().contains(player)) { -+ worldserver1.addNewPlayer(player); -+ this.server.getCustomBossEvents().onPlayerConnect(player); -+ } -+ -+ worldserver1 = player.serverLevel(); -+ Iterator iterator = player.getActiveEffects().iterator(); -+ while (iterator.hasNext()) { -+ MobEffectInstance mobeffect = iterator.next(); -+ playerconnection.send(new ClientboundUpdateMobEffectPacket(player.getId(), mobeffect, false)); -+ } -+ -+ if (player.isDeadOrDying()) { -+ net.minecraft.core.Holder plains = worldserver1.registryAccess().lookupOrThrow(net.minecraft.core.registries.Registries.BIOME) -+ .getOrThrow(net.minecraft.world.level.biome.Biomes.PLAINS); -+ player.connection.send(new net.minecraft.network.protocol.game.ClientboundLevelChunkWithLightPacket( -+ new net.minecraft.world.level.chunk.EmptyLevelChunk(worldserver1, player.chunkPosition(), plains), -+ worldserver1.getLightEngine(), null, null, false) -+ ); -+ } -+ } -+ // Leaves end - replay api -+ - public void placeNewPlayer(Connection connection, ServerPlayer player, CommonListenerCookie clientData) { - player.isRealPlayer = true; // Paper - player.loginTime = System.currentTimeMillis(); // Paper - Replace OfflinePlayer#getLastPlayed -@@ -331,6 +446,7 @@ public abstract class PlayerList { - - // entityplayer.connection.send(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(this.players)); // CraftBukkit - replaced with loop below - this.players.add(player); -+ this.realPlayers.add(player); // Leaves - replay api - this.playersByName.put(player.getScoreboardName().toLowerCase(java.util.Locale.ROOT), player); // Spigot - this.playersByUUID.put(player.getUUID(), player); - // this.broadcastAll(ClientboundPlayerInfoUpdatePacket.createPlayerInitializing(List.of(entityplayer))); // CraftBukkit - replaced with loop below -@@ -401,6 +517,12 @@ public abstract class PlayerList { - continue; - } - -+ // Leaves start - skip photographer -+ if (entityplayer1 instanceof ServerPhotographer) { -+ continue; -+ } -+ // Leaves end - skip photographer -+ - onlinePlayers.add(entityplayer1); // Paper - Use single player info update packet on join - } - // Paper start - Use single player info update packet on join -@@ -551,6 +673,43 @@ public abstract class PlayerList { - - } - -+ // Leaevs start - replay mod api -+ public void removePhotographer(ServerPhotographer entityplayer) { -+ ServerLevel worldserver = entityplayer.serverLevel(); -+ -+ entityplayer.awardStat(Stats.LEAVE_GAME); -+ -+ if (entityplayer.containerMenu != entityplayer.inventoryMenu) { -+ entityplayer.closeContainer(org.bukkit.event.inventory.InventoryCloseEvent.Reason.DISCONNECT); -+ } -+ -+ if (server.isSameThread()) entityplayer.doTick(); -+ -+ if (this.collideRuleTeamName != null) { -+ final net.minecraft.world.scores.Scoreboard scoreBoard = this.server.getLevel(Level.OVERWORLD).getScoreboard(); -+ final PlayerTeam team = scoreBoard.getPlayersTeam(this.collideRuleTeamName); -+ if (entityplayer.getTeam() == team && team != null) { -+ scoreBoard.removePlayerFromTeam(entityplayer.getScoreboardName(), team); -+ } -+ } -+ -+ worldserver.removePlayerImmediately(entityplayer, Entity.RemovalReason.UNLOADED_WITH_PLAYER); -+ entityplayer.retireScheduler(); -+ entityplayer.getAdvancements().stopListening(); -+ this.players.remove(entityplayer); -+ this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); -+ this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer); -+ UUID uuid = entityplayer.getUUID(); -+ ServerPlayer entityplayer1 = this.playersByUUID.get(uuid); -+ -+ if (entityplayer1 == entityplayer) { -+ this.playersByUUID.remove(uuid); -+ } -+ -+ this.cserver.getScoreboardManager().removePlayer(entityplayer.getBukkitEntity()); -+ } -+ // Leaves stop - replay mod api -+ - public net.kyori.adventure.text.Component remove(ServerPlayer entityplayer) { // CraftBukkit - return string // Paper - return Component - // Paper start - Fix kick event leave message not being sent - return this.remove(entityplayer, net.kyori.adventure.text.Component.translatable("multiplayer.player.left", net.kyori.adventure.text.format.NamedTextColor.YELLOW, io.papermc.paper.configuration.GlobalConfiguration.get().messages.useDisplayNameInQuitMessage ? entityplayer.getBukkitEntity().displayName() : io.papermc.paper.adventure.PaperAdventure.asAdventure(entityplayer.getDisplayName()))); -@@ -633,6 +792,7 @@ public abstract class PlayerList { - entityplayer.retireScheduler(); // Paper - Folia schedulers - entityplayer.getAdvancements().stopListening(); - this.players.remove(entityplayer); -+ this.realPlayers.remove(entityplayer); // Leaves - replay api - this.playersByName.remove(entityplayer.getScoreboardName().toLowerCase(java.util.Locale.ROOT)); // Spigot - this.server.getCustomBossEvents().onPlayerDisconnect(entityplayer); - UUID uuid = entityplayer.getUUID(); -@@ -727,7 +887,7 @@ public abstract class PlayerList { - event.disallow(PlayerLoginEvent.Result.KICK_BANNED, io.papermc.paper.adventure.PaperAdventure.asAdventure(ichatmutablecomponent)); // Paper - Adventure - } else { - // return this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile) ? IChatBaseComponent.translatable("multiplayer.disconnect.server_full") : null; -- if (this.players.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { -+ if (this.realPlayers.size() >= this.maxPlayers && !this.canBypassPlayerLimit(gameprofile)) { // Leaves - only real player - event.disallow(PlayerLoginEvent.Result.KICK_FULL, net.kyori.adventure.text.serializer.legacy.LegacyComponentSerializer.legacySection().deserialize(org.spigotmc.SpigotConfig.serverFullMessage)); // Spigot // Paper - Adventure - } - } -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index 26ef09c86315c1125167af044323dbd3dbcfc6f0..ea4def367b645a442e7ee4fd8b1b2075b8604ccb 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -314,6 +314,7 @@ public final class CraftServer implements Server { - private final io.papermc.paper.potion.PaperPotionBrewer potionBrewer; // Paper - Custom Potion Mixes - public final io.papermc.paper.SparksFly spark; // Paper - spark - private final org.leavesmc.leaves.entity.CraftBotManager botManager; // Leaves -+ private final org.leavesmc.leaves.entity.CraftPhotographerManager photographerManager = new org.leavesmc.leaves.entity.CraftPhotographerManager(); // Leaves - - // Paper start - Folia region threading API - private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); -@@ -408,7 +409,7 @@ public final class CraftServer implements Server { - public CraftServer(DedicatedServer console, PlayerList playerList) { - this.console = console; - this.playerList = (DedicatedPlayerList) playerList; -- this.playerView = Collections.unmodifiableList(Lists.transform(playerList.players, new Function() { -+ this.playerView = Collections.unmodifiableList(Lists.transform(playerList.realPlayers, new Function() { // Leaves - replay api - @Override - public CraftPlayer apply(ServerPlayer player) { - return player.getBukkitEntity(); -@@ -3290,4 +3291,11 @@ public final class CraftServer implements Server { - return botManager; - } - // Leaves end - Bot API -+ -+ // Leaves start - replay mod api -+ @Override -+ public org.leavesmc.leaves.entity.CraftPhotographerManager getPhotographerManager() { -+ return photographerManager; -+ } -+ // Leaves end - replay mod api - } -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -index dea387f418cd173980be2e6e24797b55f9f58409..3e230983586f044c3a5e021fc8e27f6b88978bf6 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftEntity.java -@@ -96,6 +96,7 @@ public abstract class CraftEntity implements org.bukkit.entity.Entity { - } - - if (entity instanceof org.leavesmc.leaves.bot.ServerBot bot) { return new org.leavesmc.leaves.entity.CraftBot(server, bot); } -+ if (entity instanceof org.leavesmc.leaves.replay.ServerPhotographer photographer) { return new org.leavesmc.leaves.entity.CraftPhotographer(server, photographer); } - - // 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 d4e497961578bb693275cdf95915b60b2cc76eb7..d0132751fb057dc29e13ae3489beedb580225fa7 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -2259,7 +2259,7 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - - @Override - public boolean canSee(Player player) { -- return this.canSee((org.bukkit.entity.Entity) player); -+ return !(player instanceof org.leavesmc.leaves.entity.Photographer) && this.canSee((org.bukkit.entity.Entity) player); // Leaves - skip photographer - } - - @Override -diff --git a/src/main/java/org/leavesmc/leaves/entity/CraftPhotographer.java b/src/main/java/org/leavesmc/leaves/entity/CraftPhotographer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fed2005cb711d0d15d5c87e5f0f7939c7a6a8ffa ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/entity/CraftPhotographer.java -@@ -0,0 +1,73 @@ -+package org.leavesmc.leaves.entity; -+ -+import net.minecraft.server.level.ServerPlayer; -+import org.bukkit.craftbukkit.CraftServer; -+import org.bukkit.craftbukkit.entity.CraftPlayer; -+import org.bukkit.entity.Player; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.replay.ServerPhotographer; -+ -+import java.io.File; -+ -+public class CraftPhotographer extends CraftPlayer implements Photographer { -+ -+ public CraftPhotographer(CraftServer server, ServerPhotographer entity) { -+ super(server, entity); -+ } -+ -+ @Override -+ public void stopRecording() { -+ this.stopRecording(true); -+ } -+ -+ @Override -+ public void stopRecording(boolean async) { -+ this.stopRecording(async, true); -+ } -+ -+ @Override -+ public void stopRecording(boolean async, boolean save) { -+ this.getHandle().remove(async, save); -+ } -+ -+ @Override -+ public void pauseRecording() { -+ this.getHandle().pauseRecording(); -+ } -+ -+ @Override -+ public void resumeRecording() { -+ this.getHandle().resumeRecording(); -+ } -+ -+ @Override -+ public void setRecordFile(@NotNull File file) { -+ this.getHandle().setSaveFile(file); -+ } -+ -+ @Override -+ public void setFollowPlayer(@Nullable Player player) { -+ ServerPlayer serverPlayer = player != null ? ((CraftPlayer) player).getHandle() : null; -+ this.getHandle().setFollowPlayer(serverPlayer); -+ } -+ -+ @Override -+ public @NotNull String getId() { -+ return this.getHandle().createState.id; -+ } -+ -+ @Override -+ public ServerPhotographer getHandle() { -+ return (ServerPhotographer) entity; -+ } -+ -+ public void setHandle(final ServerPhotographer entity) { -+ super.setHandle(entity); -+ } -+ -+ @Override -+ public String toString() { -+ return "CraftPhotographer{" + "name=" + getName() + '}'; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/entity/CraftPhotographerManager.java b/src/main/java/org/leavesmc/leaves/entity/CraftPhotographerManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..202e1694123ddfdf716b25d4eaef88d00ed2e3c7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/entity/CraftPhotographerManager.java -@@ -0,0 +1,84 @@ -+package org.leavesmc.leaves.entity; -+ -+import com.google.common.collect.Lists; -+import org.bukkit.Location; -+import org.bukkit.util.Consumer; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.bot.ServerBot; -+import org.leavesmc.leaves.replay.BukkitRecorderOption; -+import org.leavesmc.leaves.replay.RecorderOption; -+import org.leavesmc.leaves.replay.ServerPhotographer; -+ -+import java.util.Collection; -+import java.util.Collections; -+import java.util.UUID; -+ -+public class CraftPhotographerManager implements PhotographerManager { -+ -+ private final Collection photographerViews = Collections.unmodifiableList(Lists.transform(ServerPhotographer.getPhotographers(), ServerPhotographer::getBukkitPlayer)); -+ -+ @Override -+ public @Nullable Photographer getPhotographer(@NotNull UUID uuid) { -+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid); -+ if (photographer != null) { -+ return photographer.getBukkitPlayer(); -+ } -+ return null; -+ } -+ -+ @Override -+ public @Nullable Photographer getPhotographer(@NotNull String id) { -+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(id); -+ if (photographer != null) { -+ return photographer.getBukkitPlayer(); -+ } -+ return null; -+ } -+ -+ @Override -+ public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location) { -+ ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createDefaultOption()).createSync(); -+ if (photographer != null) { -+ return photographer.getBukkitPlayer(); -+ } -+ return null; -+ } -+ -+ @Override -+ public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption) { -+ ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createFromBukkit(recorderOption)).createSync(); -+ if (photographer != null) { -+ return photographer.getBukkitPlayer(); -+ } -+ return null; -+ } -+ -+ @Override -+ public void removePhotographer(@NotNull String id) { -+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(id); -+ if (photographer != null) { -+ photographer.remove(true); -+ } -+ } -+ -+ @Override -+ public void removePhotographer(@NotNull UUID uuid) { -+ ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid); -+ if (photographer != null) { -+ photographer.remove(true); -+ } -+ } -+ -+ @Override -+ public void removeAllPhotographers() { -+ for (ServerPhotographer photographer : ServerPhotographer.getPhotographers()) { -+ photographer.remove(true); -+ } -+ } -+ -+ @Override -+ public Collection getPhotographers() { -+ return photographerViews; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/replay/DigestOutputStream.java b/src/main/java/org/leavesmc/leaves/replay/DigestOutputStream.java -new file mode 100644 -index 0000000000000000000000000000000000000000..e67ff063b7f50b4bfdaaaeb88f225eb768d89623 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/replay/DigestOutputStream.java -@@ -0,0 +1,46 @@ -+package org.leavesmc.leaves.replay; -+ -+import org.jetbrains.annotations.NotNull; -+ -+import java.io.IOException; -+import java.io.OutputStream; -+import java.util.zip.Checksum; -+ -+public class DigestOutputStream extends OutputStream { -+ -+ private final Checksum sum; -+ private final OutputStream out; -+ -+ public DigestOutputStream(OutputStream out, Checksum sum) { -+ this.out = out; -+ this.sum = sum; -+ } -+ -+ @Override -+ public void close() throws IOException { -+ out.close(); -+ } -+ -+ @Override -+ public void flush() throws IOException { -+ out.flush(); -+ } -+ -+ @Override -+ public void write(int b) throws IOException { -+ sum.update(b); -+ out.write(b); -+ } -+ -+ @Override -+ public void write(byte @NotNull [] b) throws IOException { -+ sum.update(b); -+ out.write(b); -+ } -+ -+ @Override -+ public void write(byte @NotNull [] b, int off, int len) throws IOException { -+ sum.update(b, off, len); -+ out.write(b, off, len); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/replay/RecordMetaData.java b/src/main/java/org/leavesmc/leaves/replay/RecordMetaData.java -new file mode 100644 -index 0000000000000000000000000000000000000000..0e8cd7e878ec1294d6cb830a004eeefd8b82c415 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/replay/RecordMetaData.java -@@ -0,0 +1,23 @@ -+package org.leavesmc.leaves.replay; -+ -+import java.util.HashSet; -+import java.util.Set; -+import java.util.UUID; -+ -+public class RecordMetaData { -+ -+ public static final int CURRENT_FILE_FORMAT_VERSION = 14; -+ -+ public boolean singleplayer = false; -+ public String serverName = "Leaves"; -+ public int duration = 0; -+ public long date; -+ public String mcversion; -+ public String fileFormat = "MCPR"; -+ public int fileFormatVersion; -+ public int protocol; -+ public String generator; -+ public int selfId = -1; -+ -+ public Set players = new HashSet<>(); -+} -diff --git a/src/main/java/org/leavesmc/leaves/replay/Recorder.java b/src/main/java/org/leavesmc/leaves/replay/Recorder.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d1fb2f08f2d357c6551de7832eb3cf6980d44fb5 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/replay/Recorder.java -@@ -0,0 +1,285 @@ -+package org.leavesmc.leaves.replay; -+ -+import com.mojang.serialization.DynamicOps; -+import io.netty.channel.local.LocalChannel; -+import net.minecraft.SharedConstants; -+import net.minecraft.core.LayeredRegistryAccess; -+import net.minecraft.core.RegistrySynchronization; -+import net.minecraft.nbt.NbtOps; -+import net.minecraft.nbt.Tag; -+import net.minecraft.network.Connection; -+import net.minecraft.network.ConnectionProtocol; -+import net.minecraft.network.PacketSendListener; -+import net.minecraft.network.protocol.BundlePacket; -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.PacketFlow; -+import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; -+import net.minecraft.network.protocol.common.ClientboundDisconnectPacket; -+import net.minecraft.network.protocol.common.ClientboundResourcePackPushPacket; -+import net.minecraft.network.protocol.common.ClientboundServerLinksPacket; -+import net.minecraft.network.protocol.common.ClientboundUpdateTagsPacket; -+import net.minecraft.network.protocol.common.custom.BrandPayload; -+import net.minecraft.network.protocol.configuration.ClientboundFinishConfigurationPacket; -+import net.minecraft.network.protocol.configuration.ClientboundRegistryDataPacket; -+import net.minecraft.network.protocol.configuration.ClientboundSelectKnownPacks; -+import net.minecraft.network.protocol.configuration.ClientboundUpdateEnabledFeaturesPacket; -+import net.minecraft.network.protocol.game.ClientboundAddEntityPacket; -+import net.minecraft.network.protocol.game.ClientboundGameEventPacket; -+import net.minecraft.network.protocol.game.ClientboundPlayerChatPacket; -+import net.minecraft.network.protocol.game.ClientboundSetTimePacket; -+import net.minecraft.network.protocol.game.ClientboundSystemChatPacket; -+import net.minecraft.network.protocol.login.ClientboundLoginFinishedPacket; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.RegistryLayer; -+import net.minecraft.server.packs.repository.KnownPack; -+import net.minecraft.tags.TagNetworkSerialization; -+import net.minecraft.world.entity.EntityType; -+import net.minecraft.world.flag.FeatureFlags; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesLogger; -+ -+import java.io.File; -+import java.io.IOException; -+import java.util.List; -+import java.util.Optional; -+import java.util.Set; -+import java.util.concurrent.CompletableFuture; -+import java.util.concurrent.CompletionException; -+import java.util.concurrent.ExecutorService; -+import java.util.concurrent.Executors; -+import java.util.concurrent.TimeUnit; -+ -+public class Recorder extends Connection { -+ -+ private static final LeavesLogger LOGGER = LeavesLogger.LOGGER; -+ -+ private final ReplayFile replayFile; -+ private final ServerPhotographer photographer; -+ private final RecorderOption recorderOption; -+ private final RecordMetaData metaData; -+ -+ private final ExecutorService saveService = Executors.newSingleThreadExecutor(); -+ -+ private boolean stopped = false; -+ private boolean paused = false; -+ private boolean resumeOnNextPacket = true; -+ -+ private long startTime; -+ private long lastPacket; -+ private long timeShift = 0; -+ -+ private boolean isSaved; -+ private boolean isSaving; -+ private ConnectionProtocol state = ConnectionProtocol.LOGIN; -+ -+ public Recorder(ServerPhotographer photographer, RecorderOption recorderOption, File replayFile) throws IOException { -+ super(PacketFlow.CLIENTBOUND); -+ -+ this.photographer = photographer; -+ this.recorderOption = recorderOption; -+ this.metaData = new RecordMetaData(); -+ this.replayFile = new ReplayFile(replayFile); -+ this.channel = new LocalChannel(); -+ } -+ -+ public void start() { -+ startTime = System.currentTimeMillis(); -+ -+ metaData.singleplayer = false; -+ metaData.serverName = recorderOption.serverName; -+ metaData.date = startTime; -+ metaData.mcversion = SharedConstants.getCurrentVersion().getName(); -+ -+ // TODO start event -+ this.savePacket(new ClientboundLoginFinishedPacket(photographer.getGameProfile()), ConnectionProtocol.LOGIN); -+ this.startConfiguration(); -+ -+ if (recorderOption.forceWeather != null) { -+ setWeather(recorderOption.forceWeather); -+ } -+ } -+ -+ public void startConfiguration() { -+ this.state = ConnectionProtocol.CONFIGURATION; -+ MinecraftServer server = MinecraftServer.getServer(); -+ -+ this.savePacket(new ClientboundCustomPayloadPacket(new BrandPayload(server.getServerModName())), ConnectionProtocol.CONFIGURATION); -+ this.savePacket(new ClientboundServerLinksPacket(server.serverLinks().untrust()), ConnectionProtocol.CONFIGURATION); -+ this.savePacket(new ClientboundUpdateEnabledFeaturesPacket(FeatureFlags.REGISTRY.toNames(server.getWorldData().enabledFeatures())), ConnectionProtocol.CONFIGURATION); -+ -+ List knownPackslist = server.getResourceManager().listPacks().flatMap((iresourcepack) -> iresourcepack.location().knownPackInfo().stream()).toList(); -+ this.savePacket(new ClientboundSelectKnownPacks(knownPackslist), ConnectionProtocol.CONFIGURATION); -+ -+ server.getServerResourcePack().ifPresent((info) -> this.savePacket(new ClientboundResourcePackPushPacket( -+ info.id(), info.url(), info.hash(), info.isRequired(), Optional.ofNullable(info.prompt()) -+ ))); -+ -+ LayeredRegistryAccess layeredregistryaccess = server.registries(); -+ DynamicOps dynamicOps = layeredregistryaccess.compositeAccess().createSerializationContext(NbtOps.INSTANCE); -+ RegistrySynchronization.packRegistries(dynamicOps, layeredregistryaccess.getAccessFrom(RegistryLayer.WORLDGEN), Set.copyOf(knownPackslist), -+ (key, entries) -> -+ this.savePacket(new ClientboundRegistryDataPacket(key, entries), ConnectionProtocol.CONFIGURATION) -+ ); -+ this.savePacket(new ClientboundUpdateTagsPacket(TagNetworkSerialization.serializeTagsToNetwork(layeredregistryaccess)), ConnectionProtocol.CONFIGURATION); -+ -+ this.savePacket(ClientboundFinishConfigurationPacket.INSTANCE, ConnectionProtocol.CONFIGURATION); -+ state = ConnectionProtocol.PLAY; -+ } -+ -+ @Override -+ public void flushChannel() { -+ } -+ -+ public void stop() { -+ stopped = true; -+ } -+ -+ public void pauseRecording() { -+ resumeOnNextPacket = false; -+ paused = true; -+ } -+ -+ public void resumeRecording() { -+ resumeOnNextPacket = true; -+ } -+ -+ public void setWeather(RecorderOption.RecordWeather weather) { -+ weather.getPackets().forEach(this::savePacket); -+ } -+ -+ public long getRecordedTime() { -+ final long base = System.currentTimeMillis() - startTime; -+ return base - timeShift; -+ } -+ -+ private synchronized long getCurrentTimeAndUpdate() { -+ long now = getRecordedTime(); -+ if (paused) { -+ if (resumeOnNextPacket) { -+ paused = false; -+ } -+ timeShift += now - lastPacket; -+ return lastPacket; -+ } -+ return lastPacket = now; -+ } -+ -+ @Override -+ public boolean isConnected() { -+ return true; -+ } -+ -+ @Override -+ public void send(@NotNull Packet packet, @Nullable PacketSendListener callbacks, boolean flush) { -+ if (!stopped) { -+ if (packet instanceof BundlePacket packet1) { -+ packet1.subPackets().forEach(subPacket -> send(subPacket, null)); -+ return; -+ } -+ -+ if (packet instanceof ClientboundAddEntityPacket packet1) { -+ if (packet1.getType() == EntityType.PLAYER) { -+ metaData.players.add(packet1.getUUID()); -+ saveMetadata(); -+ } -+ } -+ -+ if (packet instanceof ClientboundDisconnectPacket) { -+ return; -+ } -+ -+ if (recorderOption.forceDayTime != -1 && packet instanceof ClientboundSetTimePacket packet1) { -+ packet = new ClientboundSetTimePacket(packet1.dayTime(), recorderOption.forceDayTime, false); -+ } -+ -+ if (recorderOption.forceWeather != null && packet instanceof ClientboundGameEventPacket packet1) { -+ ClientboundGameEventPacket.Type type = packet1.getEvent(); -+ if (type == ClientboundGameEventPacket.START_RAINING || type == ClientboundGameEventPacket.STOP_RAINING || type == ClientboundGameEventPacket.RAIN_LEVEL_CHANGE || type == ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE) { -+ return; -+ } -+ } -+ -+ if (recorderOption.ignoreChat && (packet instanceof ClientboundSystemChatPacket || packet instanceof ClientboundPlayerChatPacket)) { -+ return; -+ } -+ -+ savePacket(packet); -+ } -+ } -+ -+ private void saveMetadata() { -+ saveService.submit(() -> { -+ try { -+ replayFile.saveMetaData(metaData); -+ } catch (IOException e) { -+ e.printStackTrace(); -+ } -+ }); -+ } -+ -+ private void savePacket(Packet packet) { -+ this.savePacket(packet, state); -+ } -+ -+ private void savePacket(Packet packet, final ConnectionProtocol protocol) { -+ try { -+ final long timestamp = getCurrentTimeAndUpdate(); -+ saveService.submit(() -> { -+ try { -+ replayFile.savePacket(timestamp, packet, protocol); -+ } catch (Exception e) { -+ LOGGER.severe("Error saving packet"); -+ e.printStackTrace(); -+ } -+ }); -+ } catch (Exception e) { -+ LOGGER.severe("Error saving packet"); -+ e.printStackTrace(); -+ } -+ } -+ -+ public boolean isSaved() { -+ return isSaved; -+ } -+ -+ public CompletableFuture saveRecording(File dest, boolean save) { -+ isSaved = true; -+ if (!isSaving) { -+ isSaving = true; -+ metaData.duration = (int) lastPacket; -+ return CompletableFuture.runAsync(() -> { -+ saveMetadata(); -+ saveService.shutdown(); -+ boolean interrupted = false; -+ try { -+ saveService.awaitTermination(10, TimeUnit.SECONDS); -+ } catch (InterruptedException e) { -+ interrupted = true; -+ } -+ try { -+ if (save) { -+ replayFile.closeAndSave(dest); -+ } else { -+ replayFile.closeNotSave(); -+ } -+ } catch (IOException e) { -+ e.printStackTrace(); -+ throw new CompletionException(e); -+ } finally { -+ if (interrupted) { -+ Thread.currentThread().interrupt(); -+ } -+ } -+ }, runnable -> { -+ final Thread thread = new Thread(runnable, "Recording file save thread"); -+ thread.start(); -+ }); -+ } else { -+ LOGGER.warning("saveRecording() called twice"); -+ return CompletableFuture.supplyAsync(() -> { -+ throw new IllegalStateException("saveRecording() called twice"); -+ }); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/replay/RecorderOption.java b/src/main/java/org/leavesmc/leaves/replay/RecorderOption.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8978fe0c7ed092334618e27892f940ee8c302fc7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/replay/RecorderOption.java -@@ -0,0 +1,57 @@ -+package org.leavesmc.leaves.replay; -+ -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.game.ClientboundGameEventPacket; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+ -+import java.util.List; -+ -+public class RecorderOption { -+ -+ public int recordDistance = -1; -+ public String serverName = "Leaves"; -+ public RecordWeather forceWeather = null; -+ public int forceDayTime = -1; -+ public boolean ignoreChat = false; -+ public boolean ignoreItem = false; -+ -+ @NotNull -+ @Contract(" -> new") -+ public static RecorderOption createDefaultOption() { -+ return new RecorderOption(); -+ } -+ -+ @NotNull -+ public static RecorderOption createFromBukkit(@NotNull BukkitRecorderOption bukkitRecorderOption) { -+ RecorderOption recorderOption = new RecorderOption(); -+ // recorderOption.recordDistance = bukkitRecorderOption.recordDistance; -+ // recorderOption.ignoreItem = bukkitRecorderOption.ignoreItem; -+ recorderOption.serverName = bukkitRecorderOption.serverName; -+ recorderOption.ignoreChat = bukkitRecorderOption.ignoreChat; -+ recorderOption.forceDayTime = bukkitRecorderOption.forceDayTime; -+ recorderOption.forceWeather = switch (bukkitRecorderOption.forceWeather) { -+ case RAIN -> RecordWeather.RAIN; -+ case CLEAR -> RecordWeather.CLEAR; -+ case THUNDER -> RecordWeather.THUNDER; -+ case NULL -> null; -+ }; -+ return recorderOption; -+ } -+ -+ public enum RecordWeather { -+ CLEAR(new ClientboundGameEventPacket(ClientboundGameEventPacket.STOP_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)), -+ RAIN(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 0)), -+ THUNDER(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0), new ClientboundGameEventPacket(ClientboundGameEventPacket.RAIN_LEVEL_CHANGE, 1), new ClientboundGameEventPacket(ClientboundGameEventPacket.THUNDER_LEVEL_CHANGE, 1)); -+ -+ private final List> packets; -+ -+ private RecordWeather(Packet... packets) { -+ this.packets = List.of(packets); -+ } -+ -+ public List> getPackets() { -+ return packets; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java b/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8d96445fa45db5c1976c4f4d6811184810951be0 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/replay/ReplayFile.java -@@ -0,0 +1,198 @@ -+package org.leavesmc.leaves.replay; -+ -+import com.google.gson.Gson; -+import com.google.gson.GsonBuilder; -+import io.netty.buffer.ByteBuf; -+import io.netty.buffer.Unpooled; -+import net.minecraft.SharedConstants; -+import net.minecraft.network.ConnectionProtocol; -+import net.minecraft.network.ProtocolInfo; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.protocol.Packet; -+import net.minecraft.network.protocol.configuration.ConfigurationProtocols; -+import net.minecraft.network.protocol.game.GameProtocols; -+import net.minecraft.network.protocol.login.LoginProtocols; -+import net.minecraft.network.protocol.status.StatusProtocols; -+import net.minecraft.server.MinecraftServer; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+import org.leavesmc.leaves.util.UUIDSerializer; -+ -+import java.io.BufferedOutputStream; -+import java.io.DataOutputStream; -+import java.io.File; -+import java.io.FileInputStream; -+import java.io.FileOutputStream; -+import java.io.IOException; -+import java.io.InputStream; -+import java.io.OutputStream; -+import java.io.OutputStreamWriter; -+import java.io.Writer; -+import java.nio.charset.StandardCharsets; -+import java.nio.file.Files; -+import java.util.List; -+import java.util.Map; -+import java.util.UUID; -+import java.util.zip.CRC32; -+import java.util.zip.ZipEntry; -+import java.util.zip.ZipOutputStream; -+ -+public class ReplayFile { -+ -+ private static final String RECORDING_FILE = "recording.tmcpr"; -+ private static final String RECORDING_FILE_CRC32 = "recording.tmcpr.crc32"; -+ private static final String MARKER_FILE = "markers.json"; -+ private static final String META_FILE = "metaData.json"; -+ -+ private static final Gson MARKER_GSON = new GsonBuilder().registerTypeAdapter(ReplayMarker.class, new ReplayMarker.Serializer()).create(); -+ private static final Gson META_GSON = new GsonBuilder().registerTypeAdapter(UUID.class, new UUIDSerializer()).create(); -+ -+ private final File tmpDir; -+ private final DataOutputStream packetStream; -+ private final CRC32 crc32 = new CRC32(); -+ -+ private final File markerFile; -+ private final File metaFile; -+ -+ private final Map> protocols; -+ -+ public ReplayFile(@NotNull File name) throws IOException { -+ this.tmpDir = new File(name.getParentFile(), name.getName() + ".tmp"); -+ if (tmpDir.exists()) { -+ if (!ReplayFile.deleteDir(tmpDir)) { -+ throw new IOException("Recording file " + name + " already exists!"); -+ } -+ } -+ -+ if (!tmpDir.mkdirs()) { -+ throw new IOException("Failed to create temp directory for recording " + tmpDir); -+ } -+ -+ File packetFile = new File(tmpDir, RECORDING_FILE); -+ this.metaFile = new File(tmpDir, META_FILE); -+ this.markerFile = new File(tmpDir, MARKER_FILE); -+ -+ this.packetStream = new DataOutputStream(new DigestOutputStream(new BufferedOutputStream(new FileOutputStream(packetFile)), crc32)); -+ -+ this.protocols = Map.of( -+ ConnectionProtocol.STATUS, StatusProtocols.CLIENTBOUND, -+ ConnectionProtocol.LOGIN, LoginProtocols.CLIENTBOUND, -+ ConnectionProtocol.CONFIGURATION, ConfigurationProtocols.CLIENTBOUND, -+ ConnectionProtocol.PLAY, GameProtocols.CLIENTBOUND_TEMPLATE.bind(RegistryFriendlyByteBuf.decorator(MinecraftServer.getServer().registryAccess())) -+ ); -+ } -+ -+ @SuppressWarnings({"rawtypes", "unchecked"}) -+ private byte @NotNull [] getPacketBytes(Packet packet, ConnectionProtocol state) { -+ ProtocolInfo protocol = this.protocols.get(state); -+ if (protocol == null) { -+ throw new IllegalArgumentException("Unknown protocol state " + state); -+ } -+ -+ ByteBuf buf = Unpooled.buffer(); -+ protocol.codec().encode(buf, packet); -+ -+ buf.readerIndex(0); -+ byte[] ret = new byte[buf.readableBytes()]; -+ buf.readBytes(ret); -+ buf.release(); -+ return ret; -+ } -+ -+ public void saveMarkers(List markers) throws IOException { -+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(markerFile), StandardCharsets.UTF_8)) { -+ writer.write(MARKER_GSON.toJson(markers)); -+ } -+ } -+ -+ public void saveMetaData(@NotNull RecordMetaData data) throws IOException { -+ data.fileFormat = "MCPR"; -+ data.fileFormatVersion = RecordMetaData.CURRENT_FILE_FORMAT_VERSION; -+ data.protocol = SharedConstants.getCurrentVersion().getProtocolVersion(); -+ data.generator = ProtocolUtils.buildProtocolVersion("replay"); -+ -+ try (Writer writer = new OutputStreamWriter(new FileOutputStream(metaFile), StandardCharsets.UTF_8)) { -+ writer.write(META_GSON.toJson(data)); -+ } -+ } -+ -+ public void savePacket(long timestamp, Packet packet, ConnectionProtocol protocol) throws Exception { -+ byte[] data = getPacketBytes(packet, protocol); -+ packetStream.writeInt((int) timestamp); -+ packetStream.writeInt(data.length); -+ packetStream.write(data); -+ } -+ -+ public synchronized void closeAndSave(File file) throws IOException { -+ packetStream.close(); -+ -+ String[] files = tmpDir.list(); -+ if (files == null) { -+ return; -+ } -+ -+ try (ZipOutputStream os = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(file)))) { -+ for (String fileName : files) { -+ os.putNextEntry(new ZipEntry(fileName)); -+ File f = new File(tmpDir, fileName); -+ copy(new FileInputStream(f), os); -+ } -+ -+ os.putNextEntry(new ZipEntry(RECORDING_FILE_CRC32)); -+ Writer writer = new OutputStreamWriter(os); -+ writer.write(Long.toString(crc32.getValue())); -+ writer.flush(); -+ } -+ -+ for (String fileName : files) { -+ File f = new File(tmpDir, fileName); -+ Files.delete(f.toPath()); -+ } -+ Files.delete(tmpDir.toPath()); -+ } -+ -+ public synchronized void closeNotSave() throws IOException { -+ packetStream.close(); -+ -+ String[] files = tmpDir.list(); -+ if (files == null) { -+ return; -+ } -+ -+ for (String fileName : files) { -+ File f = new File(tmpDir, fileName); -+ Files.delete(f.toPath()); -+ } -+ Files.delete(tmpDir.toPath()); -+ } -+ -+ private void copy(@NotNull InputStream in, OutputStream out) throws IOException { -+ byte[] buffer = new byte[8192]; -+ int len; -+ while ((len = in.read(buffer)) > -1) { -+ out.write(buffer, 0, len); -+ } -+ in.close(); -+ } -+ -+ private static boolean deleteDir(File dir) { -+ if (dir == null || !dir.exists()) { -+ return false; -+ } -+ -+ File[] files = dir.listFiles(); -+ if (files != null) { -+ for (File file : files) { -+ if (file.isDirectory()) { -+ deleteDir(file); -+ } else { -+ if (!file.delete()) { -+ return false; -+ } -+ } -+ } -+ } -+ -+ return dir.delete(); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/replay/ReplayMarker.java b/src/main/java/org/leavesmc/leaves/replay/ReplayMarker.java -new file mode 100644 -index 0000000000000000000000000000000000000000..1568f6928d5d4f38ca1919c6de6ec9bb9deb20b2 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/replay/ReplayMarker.java -@@ -0,0 +1,43 @@ -+package org.leavesmc.leaves.replay; -+ -+import com.google.gson.JsonElement; -+import com.google.gson.JsonObject; -+import com.google.gson.JsonPrimitive; -+import com.google.gson.JsonSerializationContext; -+import com.google.gson.JsonSerializer; -+ -+import java.lang.reflect.Type; -+ -+public class ReplayMarker { -+ -+ public int time; -+ public String name; -+ public double x = 0; -+ public double y = 0; -+ public double z = 0; -+ public float phi = 0; -+ public float theta = 0; -+ public float varphi = 0; -+ -+ public static class Serializer implements JsonSerializer { -+ @Override -+ public JsonElement serialize(ReplayMarker src, Type typeOfSrc, JsonSerializationContext context) { -+ JsonObject ret = new JsonObject(); -+ JsonObject value = new JsonObject(); -+ JsonObject position = new JsonObject(); -+ ret.add("realTimestamp", new JsonPrimitive(src.time)); -+ ret.add("value", value); -+ -+ value.add("name", new JsonPrimitive(src.name)); -+ value.add("position", position); -+ -+ position.add("x", new JsonPrimitive(src.x)); -+ position.add("y", new JsonPrimitive(src.y)); -+ position.add("z", new JsonPrimitive(src.z)); -+ position.add("yaw", new JsonPrimitive(src.phi)); -+ position.add("pitch", new JsonPrimitive(src.theta)); -+ position.add("roll", new JsonPrimitive(src.varphi)); -+ return ret; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java b/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..d8f8f071c67cd0e29411d3f0f3b5e931abe86f65 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/replay/ServerPhotographer.java -@@ -0,0 +1,222 @@ -+package org.leavesmc.leaves.replay; -+ -+import com.mojang.authlib.GameProfile; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ClientInformation; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.stats.ServerStatsCounter; -+import net.minecraft.world.damagesource.DamageSource; -+import net.minecraft.world.phys.Vec3; -+import org.bukkit.Bukkit; -+import org.bukkit.Location; -+import org.bukkit.craftbukkit.CraftWorld; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.bot.BotStatsCounter; -+import org.leavesmc.leaves.entity.CraftPhotographer; -+import org.leavesmc.leaves.entity.Photographer; -+ -+import java.io.File; -+import java.io.IOException; -+import java.util.List; -+import java.util.UUID; -+import java.util.concurrent.CompletableFuture; -+import java.util.concurrent.CopyOnWriteArrayList; -+ -+public class ServerPhotographer extends ServerPlayer { -+ -+ private static final List photographers = new CopyOnWriteArrayList<>(); -+ -+ public PhotographerCreateState createState; -+ private ServerPlayer followPlayer; -+ private Recorder recorder; -+ private File saveFile; -+ private Vec3 lastPos; -+ -+ private final ServerStatsCounter stats; -+ -+ private ServerPhotographer(MinecraftServer server, ServerLevel world, GameProfile profile) { -+ super(server, world, profile, ClientInformation.createDefault()); -+ this.gameMode = new ServerPhotographerGameMode(this); -+ this.followPlayer = null; -+ this.stats = new BotStatsCounter(server); -+ this.lastPos = this.position(); -+ } -+ -+ public static ServerPhotographer createPhotographer(@NotNull PhotographerCreateState state) throws IOException { -+ if (!isCreateLegal(state.id)) { -+ throw new IllegalArgumentException(state.id + " is a invalid photographer id"); -+ } -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ -+ ServerLevel world = ((CraftWorld) state.loc.getWorld()).getHandle(); -+ GameProfile profile = new GameProfile(UUID.randomUUID(), state.id); -+ -+ ServerPhotographer photographer = new ServerPhotographer(server, world, profile); -+ photographer.recorder = new Recorder(photographer, state.option, new File("replay", state.id)); -+ photographer.saveFile = new File("replay", state.id + ".mcpr"); -+ photographer.createState = state; -+ -+ photographer.recorder.start(); -+ MinecraftServer.getServer().getPlayerList().placeNewPhotographer(photographer.recorder, photographer, world, state.loc); -+ photographer.serverLevel().chunkSource.move(photographer); -+ photographer.setInvisible(true); -+ photographers.add(photographer); -+ -+ LeavesLogger.LOGGER.info("Photographer " + state.id + " created"); -+ -+ // TODO record distance -+ -+ return photographer; -+ } -+ -+ @Override -+ public void tick() { -+ super.tick(); -+ super.doTick(); -+ -+ if (this.server.getTickCount() % 10 == 0) { -+ connection.resetPosition(); -+ this.serverLevel().chunkSource.move(this); -+ } -+ -+ if (this.followPlayer != null) { -+ if (this.getCamera() == this || this.getCamera().level() != this.level()) { -+ this.getBukkitPlayer().teleport(this.getCamera().getBukkitEntity().getLocation()); -+ this.setCamera(followPlayer); -+ } -+ if (lastPos.distanceToSqr(this.position()) > 1024D) { -+ this.getBukkitPlayer().teleport(this.getCamera().getBukkitEntity().getLocation()); -+ } -+ } -+ -+ lastPos = this.position(); -+ } -+ -+ @Override -+ public void die(@NotNull DamageSource damageSource) { -+ super.die(damageSource); -+ remove(true); -+ } -+ -+ @Override -+ public boolean isInvulnerableTo(@NotNull ServerLevel world, @NotNull DamageSource damageSource) { -+ return true; -+ } -+ -+ @Override -+ public boolean hurtServer(@NotNull ServerLevel world, @NotNull DamageSource source, float amount) { -+ return false; -+ } -+ -+ @Override -+ public void setHealth(float health) { -+ } -+ -+ @NotNull -+ @Override -+ public ServerStatsCounter getStats() { -+ return stats; -+ } -+ -+ public void remove(boolean async) { -+ this.remove(async, true); -+ } -+ -+ public void remove(boolean async, boolean save) { -+ super.remove(RemovalReason.KILLED); -+ photographers.remove(this); -+ this.recorder.stop(); -+ this.server.getPlayerList().removePhotographer(this); -+ -+ LeavesLogger.LOGGER.info("Photographer " + createState.id + " removed"); -+ -+ if (!recorder.isSaved()) { -+ CompletableFuture future = recorder.saveRecording(saveFile, save); -+ if (!async) { -+ future.join(); -+ } -+ } -+ } -+ -+ public void setFollowPlayer(ServerPlayer followPlayer) { -+ this.setCamera(followPlayer); -+ this.followPlayer = followPlayer; -+ } -+ -+ public void setSaveFile(File saveFile) { -+ this.saveFile = saveFile; -+ } -+ -+ public void pauseRecording() { -+ this.recorder.pauseRecording(); -+ } -+ -+ public void resumeRecording() { -+ this.recorder.resumeRecording(); -+ } -+ -+ public static ServerPhotographer getPhotographer(String id) { -+ for (ServerPhotographer photographer : photographers) { -+ if (photographer.createState.id.equals(id)) { -+ return photographer; -+ } -+ } -+ return null; -+ } -+ -+ public static ServerPhotographer getPhotographer(UUID uuid) { -+ for (ServerPhotographer photographer : photographers) { -+ if (photographer.getUUID().equals(uuid)) { -+ return photographer; -+ } -+ } -+ return null; -+ } -+ -+ public static List getPhotographers() { -+ return photographers; -+ } -+ -+ public Photographer getBukkitPlayer() { -+ return getBukkitEntity(); -+ } -+ -+ @Override -+ @NotNull -+ public CraftPhotographer getBukkitEntity() { -+ return (CraftPhotographer) super.getBukkitEntity(); -+ } -+ -+ public static boolean isCreateLegal(@NotNull String name) { -+ if (!name.matches("^[a-zA-Z0-9_]{4,16}$")) { -+ return false; -+ } -+ -+ return Bukkit.getPlayerExact(name) == null && ServerPhotographer.getPhotographer(name) == null; -+ } -+ -+ public static class PhotographerCreateState { -+ -+ public RecorderOption option; -+ public Location loc; -+ public final String id; -+ -+ public PhotographerCreateState(Location loc, String id, RecorderOption option) { -+ this.loc = loc; -+ this.id = id; -+ this.option = option; -+ } -+ -+ public ServerPhotographer createSync() { -+ try { -+ return createPhotographer(this); -+ } catch (IOException e) { -+ e.printStackTrace(); -+ } -+ return null; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/replay/ServerPhotographerGameMode.java b/src/main/java/org/leavesmc/leaves/replay/ServerPhotographerGameMode.java -new file mode 100644 -index 0000000000000000000000000000000000000000..c612215b0f1e8c3fae641e7a23c7cf7d165eca87 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/replay/ServerPhotographerGameMode.java -@@ -0,0 +1,35 @@ -+package org.leavesmc.leaves.replay; -+ -+import net.kyori.adventure.text.Component; -+import net.minecraft.server.level.ServerPlayerGameMode; -+import net.minecraft.world.level.GameType; -+import org.bukkit.event.player.PlayerGameModeChangeEvent; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+ -+public class ServerPhotographerGameMode extends ServerPlayerGameMode { -+ -+ public ServerPhotographerGameMode(ServerPhotographer photographer) { -+ super(photographer); -+ super.setGameModeForPlayer(GameType.SPECTATOR, null); -+ } -+ -+ @Override -+ public boolean changeGameModeForPlayer(@NotNull GameType gameMode) { -+ return false; -+ } -+ -+ @Nullable -+ @Override -+ public PlayerGameModeChangeEvent changeGameModeForPlayer(@NotNull GameType gameMode, PlayerGameModeChangeEvent.@NotNull Cause cause, @Nullable Component cancelMessage) { -+ return null; -+ } -+ -+ @Override -+ protected void setGameModeForPlayer(@NotNull GameType gameMode, @Nullable GameType previousGameMode) { -+ } -+ -+ @Override -+ public void tick() { -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/util/UUIDSerializer.java b/src/main/java/org/leavesmc/leaves/util/UUIDSerializer.java -new file mode 100644 -index 0000000000000000000000000000000000000000..b0834f4b569b3e28ec7e026b3ff4236219498011 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/UUIDSerializer.java -@@ -0,0 +1,17 @@ -+package org.leavesmc.leaves.util; -+ -+import com.google.gson.JsonElement; -+import com.google.gson.JsonPrimitive; -+import com.google.gson.JsonSerializationContext; -+import com.google.gson.JsonSerializer; -+import org.jetbrains.annotations.NotNull; -+ -+import java.lang.reflect.Type; -+import java.util.UUID; -+ -+public class UUIDSerializer implements JsonSerializer { -+ @Override -+ public JsonElement serialize(@NotNull UUID src, Type typeOfSrc, JsonSerializationContext context) { -+ return new JsonPrimitive(src.toString()); -+ } -+} diff --git a/patches/server/0088-Leaves-I18n.patch b/patches/server/0088-Leaves-I18n.patch deleted file mode 100644 index 65a81166..00000000 --- a/patches/server/0088-Leaves-I18n.patch +++ /dev/null @@ -1,6288 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 16 Aug 2023 12:08:10 +0800 -Subject: [PATCH] Leaves I18n - - -diff --git a/src/main/java/net/minecraft/locale/Language.java b/src/main/java/net/minecraft/locale/Language.java -index 93bf8a8adbf9082982fa01fc999d14d11afe0168..f2ed44f4cf7ac490424ae84d21b3a4644f52acfd 100644 ---- a/src/main/java/net/minecraft/locale/Language.java -+++ b/src/main/java/net/minecraft/locale/Language.java -@@ -31,6 +31,42 @@ public abstract class Language { - public static final String DEFAULT = "en_us"; - private static volatile Language instance = loadDefault(); - -+ // Leaves start - i18n -+ public static void loadI18N(String lang) { -+ DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource(); -+ Map map = new HashMap<>(); -+ BiConsumer biConsumer = map::put; -+ parseTranslations(biConsumer, "/assets/minecraft/lang/" + lang + ".json"); -+ deprecatedTranslationsInfo.applyToMap(map); -+ final Map map2 = Map.copyOf(map); -+ Language language = new Language() { -+ @Override -+ public String getOrDefault(String key, String fallback) { -+ return map2.getOrDefault(key, fallback); -+ } -+ -+ @Override -+ public boolean has(String key) { -+ return map2.containsKey(key); -+ } -+ -+ @Override -+ public boolean isDefaultRightToLeft() { -+ return false; -+ } -+ -+ @Override -+ public FormattedCharSequence getVisualOrder(FormattedText text) { -+ return visitor -> text.visit( -+ (style, string) -> StringDecomposer.iterateFormatted(string, style, visitor) ? Optional.empty() : FormattedText.STOP_ITERATION, -+ Style.EMPTY -+ ).isPresent(); -+ } -+ }; -+ inject(language); -+ } -+ // Leaves end - i18n -+ - private static Language loadDefault() { - DeprecatedTranslationsInfo deprecatedTranslationsInfo = DeprecatedTranslationsInfo.loadFromDefaultResource(); - Map map = new HashMap<>(); -diff --git a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -index 679a40aeedc7e259401a6c244ed54136d6c75a40..f6c081b3362d9b84d16874e8af30e2f865b3ef7c 100644 ---- a/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -+++ b/src/main/java/net/minecraft/server/dedicated/DedicatedServer.java -@@ -239,6 +239,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - - org.leavesmc.leaves.LeavesConfig.init((java.io.File) options.valueOf("leaves-settings")); // Leaves - Server Config - System.setProperty("spark.serverconfigs.extra", "leaves.yml"); // Leaves - spark config -+ net.minecraft.locale.Language.loadI18N(org.leavesmc.leaves.LeavesConfig.mics.serverLang); // Leaves - i18n - - com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics // Leaves - down - -diff --git a/src/main/resources/assets/minecraft/lang/zh_cn.json b/src/main/resources/assets/minecraft/lang/zh_cn.json -new file mode 100644 -index 0000000000000000000000000000000000000000..59289b3d0a66917b2ba4ada454dea66bd5377521 ---- /dev/null -+++ b/src/main/resources/assets/minecraft/lang/zh_cn.json -@@ -0,0 +1,6217 @@ -+{ -+ "accessibility.onboarding.screen.narrator": "按下Enter键启用复述功能", -+ "accessibility.onboarding.screen.title": "欢迎进入Minecraft!\n\n需要启用复述功能或访问辅助功能设置吗?", -+ "addServer.add": "完成", -+ "addServer.enterIp": "服务器地址", -+ "addServer.enterName": "服务器名称", -+ "addServer.hideAddress": "隐藏地址", -+ "addServer.resourcePack": "服务器资源包", -+ "addServer.resourcePack.disabled": "禁用", -+ "addServer.resourcePack.enabled": "启用", -+ "addServer.resourcePack.prompt": "询问", -+ "addServer.title": "编辑服务器信息", -+ "advMode.allEntities": "用“@e”来代表全部实体", -+ "advMode.allPlayers": "用“@a”来代表全部玩家", -+ "advMode.command": "控制台命令", -+ "advMode.mode": "模式", -+ "advMode.mode.auto": "循环", -+ "advMode.mode.autoexec.bat": "保持开启", -+ "advMode.mode.conditional": "条件制约", -+ "advMode.mode.redstone": "脉冲", -+ "advMode.mode.redstoneTriggered": "红石控制", -+ "advMode.mode.sequence": "连锁", -+ "advMode.mode.unconditional": "不受制约", -+ "advMode.nearestPlayer": "用“@p”来代表最近的玩家", -+ "advMode.notAllowed": "必须是处于创造模式的管理员", -+ "advMode.notEnabled": "命令方块没有在此服务器上启用", -+ "advMode.previousOutput": "上一个输出", -+ "advMode.randomPlayer": "用“@r”来代表随机玩家", -+ "advMode.self": "用“@s”来代表执行实体", -+ "advMode.setCommand": "设置此方块的控制台命令", -+ "advMode.setCommand.success": "成功设置:%s", -+ "advMode.trackOutput": "记录输出", -+ "advMode.triggering": "触发方式", -+ "advMode.type": "类型", -+ "advancement.advancementNotFound": "未知的进度:%s", -+ "advancements.adventure.adventuring_time.description": "发现所有的生物群系", -+ "advancements.adventure.adventuring_time.title": "探索的时光", -+ "advancements.adventure.arbalistic.description": "用弩一发击杀五只不同的生物", -+ "advancements.adventure.arbalistic.title": "劲弩手", -+ "advancements.adventure.avoid_vibration.description": "在幽匿感测体或监守者周围潜行以防被它们探测到", -+ "advancements.adventure.avoid_vibration.title": "潜行100级", -+ "advancements.adventure.bullseye.description": "从至少30米外射中标靶的靶心", -+ "advancements.adventure.bullseye.title": "正中靶心", -+ "advancements.adventure.craft_decorated_pot_using_only_sherds.description": "用4个陶片制作饰纹陶罐", -+ "advancements.adventure.craft_decorated_pot_using_only_sherds.title": "精修细补", -+ "advancements.adventure.fall_from_world_height.description": "从世界顶部(建筑高度限制处)自由落体,坠至世界底部并存活下来", -+ "advancements.adventure.fall_from_world_height.title": "上天入地", -+ "advancements.adventure.hero_of_the_village.description": "成功在袭击中保卫村庄", -+ "advancements.adventure.hero_of_the_village.title": "村庄英雄", -+ "advancements.adventure.honey_block_slide.description": "跳入蜂蜜块以缓冲摔落", -+ "advancements.adventure.honey_block_slide.title": "胶着状态", -+ "advancements.adventure.kill_a_mob.description": "杀死任意敌对性怪物", -+ "advancements.adventure.kill_a_mob.title": "怪物猎人", -+ "advancements.adventure.kill_all_mobs.description": "杀死每一种敌对性怪物", -+ "advancements.adventure.kill_all_mobs.title": "资深怪物猎人", -+ "advancements.adventure.kill_mob_near_sculk_catalyst.description": "在幽匿催发体附近杀死生物", -+ "advancements.adventure.kill_mob_near_sculk_catalyst.title": "它蔓延了", -+ "advancements.adventure.lightning_rod_with_villager_no_fire.description": "在不引发火灾的前提下保护村民免受雷击", -+ "advancements.adventure.lightning_rod_with_villager_no_fire.title": "电涌保护器", -+ "advancements.adventure.ol_betsy.description": "用弩进行一次射击", -+ "advancements.adventure.ol_betsy.title": "扣下悬刀", -+ "advancements.adventure.play_jukebox_in_meadows.description": "用唱片机的音乐声为草甸增添生机", -+ "advancements.adventure.play_jukebox_in_meadows.title": "音乐之声", -+ "advancements.adventure.read_power_from_chiseled_bookshelf.description": "使用红石比较器获取雕纹书架的信号强度", -+ "advancements.adventure.read_power_from_chiseled_bookshelf.title": "知识就是力量", -+ "advancements.adventure.root.description": "冒险、探索与战斗", -+ "advancements.adventure.root.title": "冒险", -+ "advancements.adventure.salvage_sherd.description": "刷扫可疑的方块来获得陶片", -+ "advancements.adventure.salvage_sherd.title": "探古寻源", -+ "advancements.adventure.shoot_arrow.description": "用弓箭射点什么", -+ "advancements.adventure.shoot_arrow.title": "瞄准目标", -+ "advancements.adventure.sleep_in_bed.description": "在床上睡觉以改变你的重生点", -+ "advancements.adventure.sleep_in_bed.title": "甜蜜的梦", -+ "advancements.adventure.sniper_duel.description": "从至少50米外射杀一只骷髅", -+ "advancements.adventure.sniper_duel.title": "狙击手的对决", -+ "advancements.adventure.spyglass_at_dragon.description": "透过望远镜观察末影龙", -+ "advancements.adventure.spyglass_at_dragon.title": "那是飞机吗?", -+ "advancements.adventure.spyglass_at_ghast.description": "透过望远镜观察恶魂", -+ "advancements.adventure.spyglass_at_ghast.title": "那是气球吗?", -+ "advancements.adventure.spyglass_at_parrot.description": "透过望远镜观察鹦鹉", -+ "advancements.adventure.spyglass_at_parrot.title": "那是鸟吗?", -+ "advancements.adventure.summon_iron_golem.description": "召唤一只铁傀儡来帮忙守卫村庄", -+ "advancements.adventure.summon_iron_golem.title": "招募援兵", -+ "advancements.adventure.throw_trident.description": "往什么东西扔出三叉戟。\n注:别把你唯一的武器也抖掉了。", -+ "advancements.adventure.throw_trident.title": "抖包袱", -+ "advancements.adventure.totem_of_undying.description": "利用不死图腾逃离死神", -+ "advancements.adventure.totem_of_undying.title": "超越生死", -+ "advancements.adventure.trade.description": "成功与一名村民进行交易", -+ "advancements.adventure.trade.title": "成交!", -+ "advancements.adventure.trade_at_world_height.description": "在建筑高度限制处与村民交易", -+ "advancements.adventure.trade_at_world_height.title": "星际商人", -+ "advancements.adventure.trim_with_all_exclusive_armor_patterns.description": "将下列锻造模板都至少使用一次:尖塔、猪鼻、肋骨、监守、幽静、恼鬼、潮汐、向导", -+ "advancements.adventure.trim_with_all_exclusive_armor_patterns.title": "匠心独具", -+ "advancements.adventure.trim_with_any_armor_pattern.description": "在锻造台中合成带有纹饰的盔甲", -+ "advancements.adventure.trim_with_any_armor_pattern.title": "旧貌锻新颜", -+ "advancements.adventure.two_birds_one_arrow.description": "用一支穿透箭射杀两只幻翼", -+ "advancements.adventure.two_birds_one_arrow.title": "一箭双雕", -+ "advancements.adventure.very_very_frightening.description": "雷击一名村民", -+ "advancements.adventure.very_very_frightening.title": "魔女审判", -+ "advancements.adventure.voluntary_exile.description": "杀死一名袭击队长。\n或许该考虑暂时远离村庄……", -+ "advancements.adventure.voluntary_exile.title": "自我放逐", -+ "advancements.adventure.walk_on_powder_snow_with_leather_boots.description": "在细雪上行走……并且不陷进去", -+ "advancements.adventure.walk_on_powder_snow_with_leather_boots.title": "轻功雪上飘", -+ "advancements.adventure.whos_the_pillager_now.description": "让掠夺者也尝尝弩的滋味", -+ "advancements.adventure.whos_the_pillager_now.title": "现在谁才是掠夺者?", -+ "advancements.empty": "这里好像什么都没有……", -+ "advancements.end.dragon_breath.description": "用玻璃瓶收集一些龙息", -+ "advancements.end.dragon_breath.title": "你需要来点薄荷糖", -+ "advancements.end.dragon_egg.description": "获得龙蛋", -+ "advancements.end.dragon_egg.title": "下一世代", -+ "advancements.end.elytra.description": "找到鞘翅", -+ "advancements.end.elytra.title": "天空即为极限", -+ "advancements.end.enter_end_gateway.description": "逃离这座岛屿", -+ "advancements.end.enter_end_gateway.title": "远程折跃", -+ "advancements.end.find_end_city.description": "进去吧,又能发生什么呢?", -+ "advancements.end.find_end_city.title": "在游戏尽头的城市", -+ "advancements.end.kill_dragon.description": "祝君好运", -+ "advancements.end.kill_dragon.title": "解放末地", -+ "advancements.end.levitate.description": "利用潜影贝的攻击向上漂浮50个方块", -+ "advancements.end.levitate.title": "这上面的风景不错", -+ "advancements.end.respawn_dragon.description": "复活末影龙", -+ "advancements.end.respawn_dragon.title": "结束了…再一次…", -+ "advancements.end.root.description": "抑或是起点?", -+ "advancements.end.root.title": "末地", -+ "advancements.husbandry.allay_deliver_cake_to_note_block.description": "让悦灵向音符盒投掷一块蛋糕", -+ "advancements.husbandry.allay_deliver_cake_to_note_block.title": "生日快乐歌", -+ "advancements.husbandry.allay_deliver_item_to_player.description": "让悦灵向你投掷物品", -+ "advancements.husbandry.allay_deliver_item_to_player.title": "找到一个好朋友", -+ "advancements.husbandry.axolotl_in_a_bucket.description": "用铁桶捕获一只美西螈", -+ "advancements.husbandry.axolotl_in_a_bucket.title": "最萌捕食者", -+ "advancements.husbandry.balanced_diet.description": "尝遍天下食材,即便是对身体不好的", -+ "advancements.husbandry.balanced_diet.title": "均衡饮食", -+ "advancements.husbandry.breed_all_animals.description": "繁殖每种动物!", -+ "advancements.husbandry.breed_all_animals.title": "成双成对", -+ "advancements.husbandry.breed_an_animal.description": "繁殖一对动物", -+ "advancements.husbandry.breed_an_animal.title": "我从哪儿来?", -+ "advancements.husbandry.complete_catalogue.description": "驯服所有种类的猫!", -+ "advancements.husbandry.complete_catalogue.title": "百猫全书", -+ "advancements.husbandry.feed_snifflet.description": "喂食一只幼年嗅探兽", -+ "advancements.husbandry.feed_snifflet.title": "小小嗅探兽", -+ "advancements.husbandry.fishy_business.description": "钓到一条鱼", -+ "advancements.husbandry.fishy_business.title": "腥味十足的生意", -+ "advancements.husbandry.froglights.description": "在你的物品栏中集齐所有种类的蛙明灯", -+ "advancements.husbandry.froglights.title": "相映生辉!", -+ "advancements.husbandry.kill_axolotl_target.description": "与美西螈并肩作战并赢得胜利", -+ "advancements.husbandry.kill_axolotl_target.title": "友谊的治愈力!", -+ "advancements.husbandry.leash_all_frog_variants.description": "用拴绳拴住所有种类的青蛙", -+ "advancements.husbandry.leash_all_frog_variants.title": "呱呱队出动", -+ "advancements.husbandry.make_a_sign_glow.description": "让任意种类告示牌上的文本发光", -+ "advancements.husbandry.make_a_sign_glow.title": "眼前一亮!", -+ "advancements.husbandry.netherite_hoe.description": "用下界合金锭升级一把锄,然后重新考虑你的人生抉择", -+ "advancements.husbandry.netherite_hoe.title": "终极奉献", -+ "advancements.husbandry.obtain_sniffer_egg.description": "获得嗅探兽蛋", -+ "advancements.husbandry.obtain_sniffer_egg.title": "怪味蛋", -+ "advancements.husbandry.plant_any_sniffer_seed.description": "种植任意嗅探兽种子", -+ "advancements.husbandry.plant_any_sniffer_seed.title": "播种往事", -+ "advancements.husbandry.plant_seed.description": "种下种子,见证它的成长", -+ "advancements.husbandry.plant_seed.title": "开荒垦地", -+ "advancements.husbandry.ride_a_boat_with_a_goat.description": "与山羊同船共渡", -+ "advancements.husbandry.ride_a_boat_with_a_goat.title": "羊帆起航!", -+ "advancements.husbandry.root.description": "世界无处没有朋友与美食", -+ "advancements.husbandry.root.title": "农牧业", -+ "advancements.husbandry.safely_harvest_honey.description": "利用营火在不惊动蜜蜂的情况下从蜂巢收集蜂蜜", -+ "advancements.husbandry.safely_harvest_honey.title": "与蜂共舞", -+ "advancements.husbandry.silk_touch_nest.description": "用精准采集移动住着3只蜜蜂的蜂巢", -+ "advancements.husbandry.silk_touch_nest.title": "举巢搬迁", -+ "advancements.husbandry.tactical_fishing.description": "不用钓鱼竿抓住一条鱼!", -+ "advancements.husbandry.tactical_fishing.title": "战术性钓鱼", -+ "advancements.husbandry.tadpole_in_a_bucket.description": "用铁桶捕获一只蝌蚪", -+ "advancements.husbandry.tadpole_in_a_bucket.title": "蚪到桶里来", -+ "advancements.husbandry.tame_an_animal.description": "驯服一只动物", -+ "advancements.husbandry.tame_an_animal.title": "永恒的伙伴", -+ "advancements.husbandry.wax_off.description": "给铜块脱蜡!", -+ "advancements.husbandry.wax_off.title": "脱蜡", -+ "advancements.husbandry.wax_on.description": "将蜜脾涂到铜块上!", -+ "advancements.husbandry.wax_on.title": "涂蜡", -+ "advancements.nether.all_effects.description": "同时拥有所有状态效果", -+ "advancements.nether.all_effects.title": "为什么会变成这样呢?", -+ "advancements.nether.all_potions.description": "同时拥有所有药水效果", -+ "advancements.nether.all_potions.title": "狂乱的鸡尾酒", -+ "advancements.nether.brew_potion.description": "酿造一瓶药水", -+ "advancements.nether.brew_potion.title": "本地酿造厂", -+ "advancements.nether.charge_respawn_anchor.description": "为重生锚充满能量", -+ "advancements.nether.charge_respawn_anchor.title": "锚没有九条命", -+ "advancements.nether.create_beacon.description": "建造并放置一个信标", -+ "advancements.nether.create_beacon.title": "带信标回家", -+ "advancements.nether.create_full_beacon.description": "让一座信标发挥最大功效", -+ "advancements.nether.create_full_beacon.title": "信标工程师", -+ "advancements.nether.distract_piglin.description": "用金质物品让猪灵分神", -+ "advancements.nether.distract_piglin.title": "金光闪闪", -+ "advancements.nether.explore_nether.description": "探索所有下界生物群系", -+ "advancements.nether.explore_nether.title": "热门景点", -+ "advancements.nether.fast_travel.description": "利用下界移动对应主世界7千米的距离", -+ "advancements.nether.fast_travel.title": "曲速泡", -+ "advancements.nether.find_bastion.description": "进入堡垒遗迹", -+ "advancements.nether.find_bastion.title": "光辉岁月", -+ "advancements.nether.find_fortress.description": "用你的方式进入下界要塞", -+ "advancements.nether.find_fortress.title": "阴森的要塞", -+ "advancements.nether.get_wither_skull.description": "获得凋灵骷髅的头颅", -+ "advancements.nether.get_wither_skull.title": "惊悚恐怖骷髅头", -+ "advancements.nether.loot_bastion.description": "掠夺堡垒遗迹里的箱子", -+ "advancements.nether.loot_bastion.title": "战猪", -+ "advancements.nether.netherite_armor.description": "获得一整套下界合金盔甲", -+ "advancements.nether.netherite_armor.title": "残骸裹身", -+ "advancements.nether.obtain_ancient_debris.description": "获得远古残骸", -+ "advancements.nether.obtain_ancient_debris.title": "深藏不露", -+ "advancements.nether.obtain_blaze_rod.description": "让烈焰人从烈焰棒中解放吧", -+ "advancements.nether.obtain_blaze_rod.title": "与火共舞", -+ "advancements.nether.obtain_crying_obsidian.description": "获得哭泣的黑曜石", -+ "advancements.nether.obtain_crying_obsidian.title": "谁在切洋葱?", -+ "advancements.nether.return_to_sender.description": "用一团火球干掉一只恶魂", -+ "advancements.nether.return_to_sender.title": "见鬼去吧", -+ "advancements.nether.ride_strider.description": "手持诡异菌钓竿骑乘炽足兽", -+ "advancements.nether.ride_strider.title": "画船添足", -+ "advancements.nether.ride_strider_in_overworld_lava.description": "带炽足兽在主世界的熔岩湖上来一场长——途旅行", -+ "advancements.nether.ride_strider_in_overworld_lava.title": "温暖如家", -+ "advancements.nether.root.description": "记得带夏装", -+ "advancements.nether.root.title": "下界", -+ "advancements.nether.summon_wither.description": "召唤凋灵", -+ "advancements.nether.summon_wither.title": "凋零山庄", -+ "advancements.nether.uneasy_alliance.description": "从下界救出一只恶魂,将其安全地带到主世界……然后干掉它", -+ "advancements.nether.uneasy_alliance.title": "脆弱的同盟", -+ "advancements.nether.use_lodestone.description": "对着磁石使用指南针", -+ "advancements.nether.use_lodestone.title": "天涯共此石", -+ "advancements.sad_label": ":(", -+ "advancements.story.cure_zombie_villager.description": "弱化并治疗一名僵尸村民", -+ "advancements.story.cure_zombie_villager.title": "僵尸科医生", -+ "advancements.story.deflect_arrow.description": "用盾牌反弹一个弹射物", -+ "advancements.story.deflect_arrow.title": "不吃这套,谢谢", -+ "advancements.story.enchant_item.description": "在附魔台里附魔一样物品", -+ "advancements.story.enchant_item.title": "附魔师", -+ "advancements.story.enter_the_end.description": "进入末地传送门", -+ "advancements.story.enter_the_end.title": "结束了?", -+ "advancements.story.enter_the_nether.description": "建造、激活并进入一座下界传送门", -+ "advancements.story.enter_the_nether.title": "勇往直下", -+ "advancements.story.follow_ender_eye.description": "跟随末影之眼", -+ "advancements.story.follow_ender_eye.title": "隔墙有眼", -+ "advancements.story.form_obsidian.description": "获得一块黑曜石", -+ "advancements.story.form_obsidian.title": "冰桶挑战", -+ "advancements.story.iron_tools.description": "升级你的镐", -+ "advancements.story.iron_tools.title": "这不是铁镐么", -+ "advancements.story.lava_bucket.description": "用铁桶装点熔岩", -+ "advancements.story.lava_bucket.title": "热腾腾的", -+ "advancements.story.mine_diamond.description": "获得钻石", -+ "advancements.story.mine_diamond.title": "钻石!", -+ "advancements.story.mine_stone.description": "用你的新镐挖掘石头", -+ "advancements.story.mine_stone.title": "石器时代", -+ "advancements.story.obtain_armor.description": "用铁盔甲来保护你自己", -+ "advancements.story.obtain_armor.title": "整装上阵", -+ "advancements.story.root.description": "游戏的核心与故事", -+ "advancements.story.root.title": "Minecraft", -+ "advancements.story.shiny_gear.description": "钻石盔甲能救人", -+ "advancements.story.shiny_gear.title": "钻石护体", -+ "advancements.story.smelt_iron.description": "冶炼出一块铁锭", -+ "advancements.story.smelt_iron.title": "来硬的", -+ "advancements.story.upgrade_tools.description": "制作一把更好的镐", -+ "advancements.story.upgrade_tools.title": "获得升级", -+ "advancements.toast.challenge": "挑战已完成!", -+ "advancements.toast.goal": "目标已达成!", -+ "advancements.toast.task": "进度已达成!", -+ "argument.anchor.invalid": "无效的实体锚点%s", -+ "argument.angle.incomplete": "不完整(应有1个角度)", -+ "argument.angle.invalid": "无效的角度", -+ "argument.block.id.invalid": "未知的方块类型“%s”", -+ "argument.block.property.duplicate": "“%s”属性只能给%s设置一次", -+ "argument.block.property.invalid": "%1$s的%3$s属性不能被设为“%2$s”", -+ "argument.block.property.novalue": "%s上必须要有“%s”属性", -+ "argument.block.property.unclosed": "方块属性应以]结束", -+ "argument.block.property.unknown": "方块%s没有属性“%s”", -+ "argument.block.tag.disallowed": "无法在此使用标签,只允许使用实际的方块", -+ "argument.color.invalid": "未知的颜色“%s”", -+ "argument.component.invalid": "无效的聊天组件: %s", -+ "argument.criteria.invalid": "未知的准则:“%s”", -+ "argument.dimension.invalid": "未知的维度“%s”", -+ "argument.double.big": "双精度浮点型数据不能大于%s,但发现了%s", -+ "argument.double.low": "双精度浮点型数据不能小于%s,但发现了%s", -+ "argument.entity.invalid": "无效的名称或UUID", -+ "argument.entity.notfound.entity": "未找到实体", -+ "argument.entity.notfound.player": "未找到玩家", -+ "argument.entity.options.advancements.description": "玩家拥有的进度", -+ "argument.entity.options.distance.description": "与实体间的距离", -+ "argument.entity.options.distance.negative": "距离不能为负", -+ "argument.entity.options.dx.description": "位于x与x+dx之间的实体", -+ "argument.entity.options.dy.description": "位于y与y+dy之间的实体", -+ "argument.entity.options.dz.description": "位于z与z+dz之间的实体", -+ "argument.entity.options.gamemode.description": "玩家的游戏模式", -+ "argument.entity.options.inapplicable": "“%s”选项不适用于这里", -+ "argument.entity.options.level.description": "经验等级", -+ "argument.entity.options.level.negative": "等级不应该为负数", -+ "argument.entity.options.limit.description": "最大返回实体数", -+ "argument.entity.options.limit.toosmall": "限制必须至少为1", -+ "argument.entity.options.mode.invalid": "无效或未知的游戏模式“%s”", -+ "argument.entity.options.name.description": "实体名称", -+ "argument.entity.options.nbt.description": "实体所带的NBT", -+ "argument.entity.options.predicate.description": "自定义谓词", -+ "argument.entity.options.scores.description": "实体的分数", -+ "argument.entity.options.sort.description": "对实体排序", -+ "argument.entity.options.sort.irreversible": "无效或未知的排序类型“%s”", -+ "argument.entity.options.tag.description": "实体所带的标签", -+ "argument.entity.options.team.description": "实体所在的队伍", -+ "argument.entity.options.type.description": "实体类型", -+ "argument.entity.options.type.invalid": "无效或未知的实体类型“%s”", -+ "argument.entity.options.unknown": "未知的选项“%s”", -+ "argument.entity.options.unterminated": "选项的方括号不成对", -+ "argument.entity.options.valueless": "选项“%s”应有值", -+ "argument.entity.options.x.description": "X轴位置", -+ "argument.entity.options.x_rotation.description": "实体的X轴旋转角度", -+ "argument.entity.options.y.description": "Y轴位置", -+ "argument.entity.options.y_rotation.description": "实体的Y轴旋转角度", -+ "argument.entity.options.z.description": "Z轴位置", -+ "argument.entity.selector.allEntities": "所有实体", -+ "argument.entity.selector.allPlayers": "所有玩家", -+ "argument.entity.selector.missing": "缺少选择器类型", -+ "argument.entity.selector.nearestPlayer": "距离最近的玩家", -+ "argument.entity.selector.not_allowed": "不能使用选择器", -+ "argument.entity.selector.randomPlayer": "随机玩家", -+ "argument.entity.selector.self": "当前实体", -+ "argument.entity.selector.unknown": "未知的选择器类型“%s”", -+ "argument.entity.toomany": "只允许一个实体,但提供的选择器允许多个实体", -+ "argument.enum.invalid": "无效的值“%s”", -+ "argument.float.big": "浮点型数据不能大于%s,但发现了%s", -+ "argument.float.low": "浮点型数据不能小于%s,但发现了%s", -+ "argument.gamemode.invalid": "未知的游戏模式:%s", -+ "argument.id.invalid": "无效的ID", -+ "argument.id.unknown": "未知的ID:%s", -+ "argument.integer.big": "整型数据不能大于%s,但发现了%s", -+ "argument.integer.low": "整型数据不能小于%s,但发现了%s", -+ "argument.item.id.invalid": "未知的物品“%s”", -+ "argument.item.tag.disallowed": "无法在此使用标签,只允许使用实际的物品", -+ "argument.literal.incorrect": "应为字面量%s", -+ "argument.long.big": "长整型数据不能大于%s,但发现了%s", -+ "argument.long.low": "长整型数据不能小于%s,但发现了%s", -+ "argument.nbt.array.invalid": "无效的数组类型 “%s”", -+ "argument.nbt.array.mixed": "无法将%s插入%s", -+ "argument.nbt.expected.key": "应为键", -+ "argument.nbt.expected.value": "应为值", -+ "argument.nbt.list.mixed": "无法将%s插入%s的列表", -+ "argument.nbt.trailing": "多余的尾随数据", -+ "argument.player.entities": "只有玩家会受此命令的影响,但提供的选择器包括其它实体", -+ "argument.player.toomany": "只允许一名玩家,但提供的选择器允许多名玩家", -+ "argument.player.unknown": "该玩家不存在", -+ "argument.pos.missing.double": "应为一个坐标", -+ "argument.pos.missing.int": "应为一个方块的位置", -+ "argument.pos.mixed": "不能混用世界与局部坐标(必须全部用^或都不使用)", -+ "argument.pos.outofbounds": "该位置超出了允许的范围。", -+ "argument.pos.outofworld": "该位置已超出此世界!", -+ "argument.pos.unloaded": "该位置尚未被加载", -+ "argument.pos2d.incomplete": "不完整(应有2个坐标)", -+ "argument.pos3d.incomplete": "不完整(应有3个坐标)", -+ "argument.range.empty": "应为值或取值范围", -+ "argument.range.ints": "只允许整数,不允许小数", -+ "argument.range.swapped": "最小值不能大于最大值", -+ "argument.resource.invalid_type": "元素“%s”的类型“%s”错误(应为“%s”)", -+ "argument.resource.not_found": "无法找到类型为“%2$s”的元素“%1$s”", -+ "argument.resource_tag.invalid_type": "标签“%s”的类型“%s”错误(应为“%s”)", -+ "argument.resource_tag.not_found": "无法找到类型为“%2$s”的标签“%1$s”", -+ "argument.rotation.incomplete": "不完整(应有2个坐标)", -+ "argument.scoreHolder.empty": "找不到与分数关联的持有者", -+ "argument.scoreboardDisplaySlot.invalid": "未知的显示位置“%s”", -+ "argument.time.invalid_tick_count": "刻的计数必须为非负数", -+ "argument.time.invalid_unit": "无效的单位", -+ "argument.time.tick_count_too_low": "刻数不能小于%s,却发现了%s", -+ "argument.uuid.invalid": "无效的UUID", -+ "arguments.block.tag.unknown": "未知的方块标签“%s”", -+ "arguments.function.tag.unknown": "未知的函数标签“%s”", -+ "arguments.function.unknown": "未知的函数%s", -+ "arguments.item.overstacked": "%s只可以堆叠到%s", -+ "arguments.item.tag.unknown": "未知的物品标签“%s”", -+ "arguments.nbtpath.node.invalid": "无效的NBT路径元素", -+ "arguments.nbtpath.nothing_found": "没有与%s相匹配的元素", -+ "arguments.nbtpath.too_deep": "生成的NBT嵌套过深", -+ "arguments.nbtpath.too_large": "生成的NBT过大", -+ "arguments.objective.notFound": "未知的记分项“%s”", -+ "arguments.objective.readonly": "记分项“%s”为只读类型", -+ "arguments.operation.div0": "不能除以零", -+ "arguments.operation.invalid": "无效的操作", -+ "arguments.swizzle.invalid": "无效的坐标组合,需要'x'、'y'和'z'的组合", -+ "attribute.modifier.equals.0": "%s %s", -+ "attribute.modifier.equals.1": "%s%% %s", -+ "attribute.modifier.equals.2": "%s%% %s", -+ "attribute.modifier.plus.0": "+%s %s", -+ "attribute.modifier.plus.1": "+%s%% %s", -+ "attribute.modifier.plus.2": "+%s%% %s", -+ "attribute.modifier.take.0": "-%s %s", -+ "attribute.modifier.take.1": "-%s%% %s", -+ "attribute.modifier.take.2": "-%s%% %s", -+ "attribute.name.generic.armor": "护甲值", -+ "attribute.name.generic.armor_toughness": "盔甲韧性", -+ "attribute.name.generic.attack_damage": "攻击伤害", -+ "attribute.name.generic.attack_knockback": "击退", -+ "attribute.name.generic.attack_speed": "攻击速度", -+ "attribute.name.generic.flying_speed": "飞行速度", -+ "attribute.name.generic.follow_range": "生物跟随距离", -+ "attribute.name.generic.knockback_resistance": "击退抗性", -+ "attribute.name.generic.luck": "幸运值", -+ "attribute.name.generic.max_health": "最大生命值", -+ "attribute.name.generic.movement_speed": "速度", -+ "attribute.name.horse.jump_strength": "马匹跳跃能力", -+ "attribute.name.zombie.spawn_reinforcements": "僵尸增援", -+ "biome.minecraft.badlands": "恶地", -+ "biome.minecraft.bamboo_jungle": "竹林", -+ "biome.minecraft.basalt_deltas": "玄武岩三角洲", -+ "biome.minecraft.beach": "沙滩", -+ "biome.minecraft.birch_forest": "桦木森林", -+ "biome.minecraft.cherry_grove": "樱花树林", -+ "biome.minecraft.cold_ocean": "冷水海洋", -+ "biome.minecraft.crimson_forest": "绯红森林", -+ "biome.minecraft.dark_forest": "黑森林", -+ "biome.minecraft.deep_cold_ocean": "冷水深海", -+ "biome.minecraft.deep_dark": "深暗之域", -+ "biome.minecraft.deep_frozen_ocean": "冰冻深海", -+ "biome.minecraft.deep_lukewarm_ocean": "温水深海", -+ "biome.minecraft.deep_ocean": "深海", -+ "biome.minecraft.desert": "沙漠", -+ "biome.minecraft.dripstone_caves": "溶洞", -+ "biome.minecraft.end_barrens": "末地荒地", -+ "biome.minecraft.end_highlands": "末地高地", -+ "biome.minecraft.end_midlands": "末地内陆", -+ "biome.minecraft.eroded_badlands": "风蚀恶地", -+ "biome.minecraft.flower_forest": "繁花森林", -+ "biome.minecraft.forest": "森林", -+ "biome.minecraft.frozen_ocean": "冻洋", -+ "biome.minecraft.frozen_peaks": "冰封山峰", -+ "biome.minecraft.frozen_river": "冻河", -+ "biome.minecraft.grove": "雪林", -+ "biome.minecraft.ice_spikes": "冰刺之地", -+ "biome.minecraft.jagged_peaks": "尖峭山峰", -+ "biome.minecraft.jungle": "丛林", -+ "biome.minecraft.lukewarm_ocean": "温水海洋", -+ "biome.minecraft.lush_caves": "繁茂洞穴", -+ "biome.minecraft.mangrove_swamp": "红树林沼泽", -+ "biome.minecraft.meadow": "草甸", -+ "biome.minecraft.mushroom_fields": "蘑菇岛", -+ "biome.minecraft.nether_wastes": "下界荒地", -+ "biome.minecraft.ocean": "海洋", -+ "biome.minecraft.old_growth_birch_forest": "原始桦木森林", -+ "biome.minecraft.old_growth_pine_taiga": "原始松木针叶林", -+ "biome.minecraft.old_growth_spruce_taiga": "原始云杉针叶林", -+ "biome.minecraft.plains": "平原", -+ "biome.minecraft.river": "河流", -+ "biome.minecraft.savanna": "热带草原", -+ "biome.minecraft.savanna_plateau": "热带高原", -+ "biome.minecraft.small_end_islands": "末地小型岛屿", -+ "biome.minecraft.snowy_beach": "积雪沙滩", -+ "biome.minecraft.snowy_plains": "雪原", -+ "biome.minecraft.snowy_slopes": "积雪山坡", -+ "biome.minecraft.snowy_taiga": "积雪针叶林", -+ "biome.minecraft.soul_sand_valley": "灵魂沙峡谷", -+ "biome.minecraft.sparse_jungle": "稀疏丛林", -+ "biome.minecraft.stony_peaks": "裸岩山峰", -+ "biome.minecraft.stony_shore": "石岸", -+ "biome.minecraft.sunflower_plains": "向日葵平原", -+ "biome.minecraft.swamp": "沼泽", -+ "biome.minecraft.taiga": "针叶林", -+ "biome.minecraft.the_end": "末地", -+ "biome.minecraft.the_void": "虚空", -+ "biome.minecraft.warm_ocean": "暖水海洋", -+ "biome.minecraft.warped_forest": "诡异森林", -+ "biome.minecraft.windswept_forest": "风袭森林", -+ "biome.minecraft.windswept_gravelly_hills": "风袭沙砾丘陵", -+ "biome.minecraft.windswept_hills": "风袭丘陵", -+ "biome.minecraft.windswept_savanna": "风袭热带草原", -+ "biome.minecraft.wooded_badlands": "疏林恶地", -+ "block.minecraft.acacia_button": "金合欢木按钮", -+ "block.minecraft.acacia_door": "金合欢木门", -+ "block.minecraft.acacia_fence": "金合欢木栅栏", -+ "block.minecraft.acacia_fence_gate": "金合欢木栅栏门", -+ "block.minecraft.acacia_hanging_sign": "悬挂式金合欢木告示牌", -+ "block.minecraft.acacia_leaves": "金合欢树叶", -+ "block.minecraft.acacia_log": "金合欢原木", -+ "block.minecraft.acacia_planks": "金合欢木板", -+ "block.minecraft.acacia_pressure_plate": "金合欢木压力板", -+ "block.minecraft.acacia_sapling": "金合欢树苗", -+ "block.minecraft.acacia_sign": "金合欢木告示牌", -+ "block.minecraft.acacia_slab": "金合欢木台阶", -+ "block.minecraft.acacia_stairs": "金合欢木楼梯", -+ "block.minecraft.acacia_trapdoor": "金合欢木活板门", -+ "block.minecraft.acacia_wall_hanging_sign": "墙上的悬挂式金合欢木告示牌", -+ "block.minecraft.acacia_wall_sign": "墙上的金合欢木告示牌", -+ "block.minecraft.acacia_wood": "金合欢木", -+ "block.minecraft.activator_rail": "激活铁轨", -+ "block.minecraft.air": "空气", -+ "block.minecraft.allium": "绒球葱", -+ "block.minecraft.amethyst_block": "紫水晶块", -+ "block.minecraft.amethyst_cluster": "紫水晶簇", -+ "block.minecraft.ancient_debris": "远古残骸", -+ "block.minecraft.andesite": "安山岩", -+ "block.minecraft.andesite_slab": "安山岩台阶", -+ "block.minecraft.andesite_stairs": "安山岩楼梯", -+ "block.minecraft.andesite_wall": "安山岩墙", -+ "block.minecraft.anvil": "铁砧", -+ "block.minecraft.attached_melon_stem": "结果的西瓜茎", -+ "block.minecraft.attached_pumpkin_stem": "结果的南瓜茎", -+ "block.minecraft.azalea": "杜鹃花丛", -+ "block.minecraft.azalea_leaves": "杜鹃树叶", -+ "block.minecraft.azure_bluet": "蓝花美耳草", -+ "block.minecraft.bamboo": "竹子", -+ "block.minecraft.bamboo_block": "竹块", -+ "block.minecraft.bamboo_button": "竹按钮", -+ "block.minecraft.bamboo_door": "竹门", -+ "block.minecraft.bamboo_fence": "竹栅栏", -+ "block.minecraft.bamboo_fence_gate": "竹栅栏门", -+ "block.minecraft.bamboo_hanging_sign": "悬挂式竹告示牌", -+ "block.minecraft.bamboo_mosaic": "竹马赛克", -+ "block.minecraft.bamboo_mosaic_slab": "竹马赛克台阶", -+ "block.minecraft.bamboo_mosaic_stairs": "竹马赛克楼梯", -+ "block.minecraft.bamboo_planks": "竹板", -+ "block.minecraft.bamboo_pressure_plate": "竹压力板", -+ "block.minecraft.bamboo_sapling": "竹笋", -+ "block.minecraft.bamboo_sign": "竹告示牌", -+ "block.minecraft.bamboo_slab": "竹台阶", -+ "block.minecraft.bamboo_stairs": "竹楼梯", -+ "block.minecraft.bamboo_trapdoor": "竹活板门", -+ "block.minecraft.bamboo_wall_hanging_sign": "墙上的悬挂式竹告示牌", -+ "block.minecraft.bamboo_wall_sign": "墙上的竹告示牌", -+ "block.minecraft.banner.base.black": "黑底", -+ "block.minecraft.banner.base.blue": "蓝底", -+ "block.minecraft.banner.base.brown": "棕底", -+ "block.minecraft.banner.base.cyan": "青底", -+ "block.minecraft.banner.base.gray": "灰底", -+ "block.minecraft.banner.base.green": "绿底", -+ "block.minecraft.banner.base.light_blue": "淡蓝底", -+ "block.minecraft.banner.base.light_gray": "淡灰底", -+ "block.minecraft.banner.base.lime": "黄绿底", -+ "block.minecraft.banner.base.magenta": "品红底", -+ "block.minecraft.banner.base.orange": "橙底", -+ "block.minecraft.banner.base.pink": "粉红底", -+ "block.minecraft.banner.base.purple": "紫底", -+ "block.minecraft.banner.base.red": "红底", -+ "block.minecraft.banner.base.white": "白底", -+ "block.minecraft.banner.base.yellow": "黄底", -+ "block.minecraft.banner.border.black": "黑色方框边", -+ "block.minecraft.banner.border.blue": "蓝色方框边", -+ "block.minecraft.banner.border.brown": "棕色方框边", -+ "block.minecraft.banner.border.cyan": "青色方框边", -+ "block.minecraft.banner.border.gray": "灰色方框边", -+ "block.minecraft.banner.border.green": "绿色方框边", -+ "block.minecraft.banner.border.light_blue": "淡蓝色方框边", -+ "block.minecraft.banner.border.light_gray": "淡灰色方框边", -+ "block.minecraft.banner.border.lime": "黄绿色方框边", -+ "block.minecraft.banner.border.magenta": "品红色方框边", -+ "block.minecraft.banner.border.orange": "橙色方框边", -+ "block.minecraft.banner.border.pink": "粉红色方框边", -+ "block.minecraft.banner.border.purple": "紫色方框边", -+ "block.minecraft.banner.border.red": "红色方框边", -+ "block.minecraft.banner.border.white": "白色方框边", -+ "block.minecraft.banner.border.yellow": "黄色方框边", -+ "block.minecraft.banner.bricks.black": "黑色砖纹", -+ "block.minecraft.banner.bricks.blue": "蓝色砖纹", -+ "block.minecraft.banner.bricks.brown": "棕色砖纹", -+ "block.minecraft.banner.bricks.cyan": "青色砖纹", -+ "block.minecraft.banner.bricks.gray": "灰色砖纹", -+ "block.minecraft.banner.bricks.green": "绿色砖纹", -+ "block.minecraft.banner.bricks.light_blue": "淡蓝色砖纹", -+ "block.minecraft.banner.bricks.light_gray": "淡灰色砖纹", -+ "block.minecraft.banner.bricks.lime": "黄绿色砖纹", -+ "block.minecraft.banner.bricks.magenta": "品红色砖纹", -+ "block.minecraft.banner.bricks.orange": "橙色砖纹", -+ "block.minecraft.banner.bricks.pink": "粉红色砖纹", -+ "block.minecraft.banner.bricks.purple": "紫色砖纹", -+ "block.minecraft.banner.bricks.red": "红色砖纹", -+ "block.minecraft.banner.bricks.white": "白色砖纹", -+ "block.minecraft.banner.bricks.yellow": "黄色砖纹", -+ "block.minecraft.banner.circle.black": "黑色圆形", -+ "block.minecraft.banner.circle.blue": "蓝色圆形", -+ "block.minecraft.banner.circle.brown": "棕色圆形", -+ "block.minecraft.banner.circle.cyan": "青色圆形", -+ "block.minecraft.banner.circle.gray": "灰色圆形", -+ "block.minecraft.banner.circle.green": "绿色圆形", -+ "block.minecraft.banner.circle.light_blue": "淡蓝色圆形", -+ "block.minecraft.banner.circle.light_gray": "淡灰色圆形", -+ "block.minecraft.banner.circle.lime": "黄绿色圆形", -+ "block.minecraft.banner.circle.magenta": "品红色圆形", -+ "block.minecraft.banner.circle.orange": "橙色圆形", -+ "block.minecraft.banner.circle.pink": "粉红色圆形", -+ "block.minecraft.banner.circle.purple": "紫色圆形", -+ "block.minecraft.banner.circle.red": "红色圆形", -+ "block.minecraft.banner.circle.white": "白色圆形", -+ "block.minecraft.banner.circle.yellow": "黄色圆形", -+ "block.minecraft.banner.creeper.black": "黑色苦力怕盾徽", -+ "block.minecraft.banner.creeper.blue": "蓝色苦力怕盾徽", -+ "block.minecraft.banner.creeper.brown": "棕色苦力怕盾徽", -+ "block.minecraft.banner.creeper.cyan": "青色苦力怕盾徽", -+ "block.minecraft.banner.creeper.gray": "灰色苦力怕盾徽", -+ "block.minecraft.banner.creeper.green": "绿色苦力怕盾徽", -+ "block.minecraft.banner.creeper.light_blue": "淡蓝色苦力怕盾徽", -+ "block.minecraft.banner.creeper.light_gray": "淡灰色苦力怕盾徽", -+ "block.minecraft.banner.creeper.lime": "黄绿色苦力怕盾徽", -+ "block.minecraft.banner.creeper.magenta": "品红色苦力怕盾徽", -+ "block.minecraft.banner.creeper.orange": "橙色苦力怕盾徽", -+ "block.minecraft.banner.creeper.pink": "粉红色苦力怕盾徽", -+ "block.minecraft.banner.creeper.purple": "紫色苦力怕盾徽", -+ "block.minecraft.banner.creeper.red": "红色苦力怕盾徽", -+ "block.minecraft.banner.creeper.white": "白色苦力怕盾徽", -+ "block.minecraft.banner.creeper.yellow": "黄色苦力怕盾徽", -+ "block.minecraft.banner.cross.black": "黑斜十字", -+ "block.minecraft.banner.cross.blue": "蓝斜十字", -+ "block.minecraft.banner.cross.brown": "棕斜十字", -+ "block.minecraft.banner.cross.cyan": "青斜十字", -+ "block.minecraft.banner.cross.gray": "灰斜十字", -+ "block.minecraft.banner.cross.green": "绿斜十字", -+ "block.minecraft.banner.cross.light_blue": "淡蓝斜十字", -+ "block.minecraft.banner.cross.light_gray": "淡灰斜十字", -+ "block.minecraft.banner.cross.lime": "黄绿斜十字", -+ "block.minecraft.banner.cross.magenta": "品红斜十字", -+ "block.minecraft.banner.cross.orange": "橙斜十字", -+ "block.minecraft.banner.cross.pink": "粉红斜十字", -+ "block.minecraft.banner.cross.purple": "紫斜十字", -+ "block.minecraft.banner.cross.red": "红斜十字", -+ "block.minecraft.banner.cross.white": "白斜十字", -+ "block.minecraft.banner.cross.yellow": "黄斜十字", -+ "block.minecraft.banner.curly_border.black": "黑色波纹边", -+ "block.minecraft.banner.curly_border.blue": "蓝色波纹边", -+ "block.minecraft.banner.curly_border.brown": "棕色波纹边", -+ "block.minecraft.banner.curly_border.cyan": "青色波纹边", -+ "block.minecraft.banner.curly_border.gray": "灰色波纹边", -+ "block.minecraft.banner.curly_border.green": "绿色波纹边", -+ "block.minecraft.banner.curly_border.light_blue": "淡蓝色波纹边", -+ "block.minecraft.banner.curly_border.light_gray": "淡灰色波纹边", -+ "block.minecraft.banner.curly_border.lime": "黄绿色波纹边", -+ "block.minecraft.banner.curly_border.magenta": "品红色波纹边", -+ "block.minecraft.banner.curly_border.orange": "橙色波纹边", -+ "block.minecraft.banner.curly_border.pink": "粉红色波纹边", -+ "block.minecraft.banner.curly_border.purple": "紫色波纹边", -+ "block.minecraft.banner.curly_border.red": "红色波纹边", -+ "block.minecraft.banner.curly_border.white": "白色波纹边", -+ "block.minecraft.banner.curly_border.yellow": "黄色波纹边", -+ "block.minecraft.banner.diagonal_left.black": "黑色右上三角", -+ "block.minecraft.banner.diagonal_left.blue": "蓝色右上三角", -+ "block.minecraft.banner.diagonal_left.brown": "棕色右上三角", -+ "block.minecraft.banner.diagonal_left.cyan": "青色右上三角", -+ "block.minecraft.banner.diagonal_left.gray": "灰色右上三角", -+ "block.minecraft.banner.diagonal_left.green": "绿色右上三角", -+ "block.minecraft.banner.diagonal_left.light_blue": "淡蓝色右上三角", -+ "block.minecraft.banner.diagonal_left.light_gray": "淡灰色右上三角", -+ "block.minecraft.banner.diagonal_left.lime": "黄绿色右上三角", -+ "block.minecraft.banner.diagonal_left.magenta": "品红色右上三角", -+ "block.minecraft.banner.diagonal_left.orange": "橙色右上三角", -+ "block.minecraft.banner.diagonal_left.pink": "粉红色右上三角", -+ "block.minecraft.banner.diagonal_left.purple": "紫色右上三角", -+ "block.minecraft.banner.diagonal_left.red": "红色右上三角", -+ "block.minecraft.banner.diagonal_left.white": "白色右上三角", -+ "block.minecraft.banner.diagonal_left.yellow": "黄色右上三角", -+ "block.minecraft.banner.diagonal_right.black": "黑色左上三角", -+ "block.minecraft.banner.diagonal_right.blue": "蓝色左上三角", -+ "block.minecraft.banner.diagonal_right.brown": "棕色左上三角", -+ "block.minecraft.banner.diagonal_right.cyan": "青色左上三角", -+ "block.minecraft.banner.diagonal_right.gray": "灰色左上三角", -+ "block.minecraft.banner.diagonal_right.green": "绿色左上三角", -+ "block.minecraft.banner.diagonal_right.light_blue": "淡蓝色左上三角", -+ "block.minecraft.banner.diagonal_right.light_gray": "淡灰色左上三角", -+ "block.minecraft.banner.diagonal_right.lime": "黄绿色左上三角", -+ "block.minecraft.banner.diagonal_right.magenta": "品红色左上三角", -+ "block.minecraft.banner.diagonal_right.orange": "橙色左上三角", -+ "block.minecraft.banner.diagonal_right.pink": "粉红色左上三角", -+ "block.minecraft.banner.diagonal_right.purple": "紫色左上三角", -+ "block.minecraft.banner.diagonal_right.red": "红色左上三角", -+ "block.minecraft.banner.diagonal_right.white": "白色左上三角", -+ "block.minecraft.banner.diagonal_right.yellow": "黄色左上三角", -+ "block.minecraft.banner.diagonal_up_left.black": "黑色右下三角", -+ "block.minecraft.banner.diagonal_up_left.blue": "蓝色右下三角", -+ "block.minecraft.banner.diagonal_up_left.brown": "棕色右下三角", -+ "block.minecraft.banner.diagonal_up_left.cyan": "青色右下三角", -+ "block.minecraft.banner.diagonal_up_left.gray": "灰色右下三角", -+ "block.minecraft.banner.diagonal_up_left.green": "绿色右下三角", -+ "block.minecraft.banner.diagonal_up_left.light_blue": "淡蓝色右下三角", -+ "block.minecraft.banner.diagonal_up_left.light_gray": "淡灰色右下三角", -+ "block.minecraft.banner.diagonal_up_left.lime": "黄绿色右下三角", -+ "block.minecraft.banner.diagonal_up_left.magenta": "品红色右下三角", -+ "block.minecraft.banner.diagonal_up_left.orange": "橙色右下三角", -+ "block.minecraft.banner.diagonal_up_left.pink": "粉红色右下三角", -+ "block.minecraft.banner.diagonal_up_left.purple": "紫色右下三角", -+ "block.minecraft.banner.diagonal_up_left.red": "红色右下三角", -+ "block.minecraft.banner.diagonal_up_left.white": "白色右下三角", -+ "block.minecraft.banner.diagonal_up_left.yellow": "黄色右下三角", -+ "block.minecraft.banner.diagonal_up_right.black": "黑色左下三角", -+ "block.minecraft.banner.diagonal_up_right.blue": "蓝色左下三角", -+ "block.minecraft.banner.diagonal_up_right.brown": "棕色左下三角", -+ "block.minecraft.banner.diagonal_up_right.cyan": "青色左下三角", -+ "block.minecraft.banner.diagonal_up_right.gray": "灰色左下三角", -+ "block.minecraft.banner.diagonal_up_right.green": "绿色左下三角", -+ "block.minecraft.banner.diagonal_up_right.light_blue": "淡蓝色左下三角", -+ "block.minecraft.banner.diagonal_up_right.light_gray": "淡灰色左下三角", -+ "block.minecraft.banner.diagonal_up_right.lime": "黄绿色左下三角", -+ "block.minecraft.banner.diagonal_up_right.magenta": "品红色左下三角", -+ "block.minecraft.banner.diagonal_up_right.orange": "橙色左下三角", -+ "block.minecraft.banner.diagonal_up_right.pink": "粉红色左下三角", -+ "block.minecraft.banner.diagonal_up_right.purple": "紫色左下三角", -+ "block.minecraft.banner.diagonal_up_right.red": "红色左下三角", -+ "block.minecraft.banner.diagonal_up_right.white": "白色左下三角", -+ "block.minecraft.banner.diagonal_up_right.yellow": "黄色左下三角", -+ "block.minecraft.banner.flower.black": "黑色花朵盾徽", -+ "block.minecraft.banner.flower.blue": "蓝色花朵盾徽", -+ "block.minecraft.banner.flower.brown": "棕色花朵盾徽", -+ "block.minecraft.banner.flower.cyan": "青色花朵盾徽", -+ "block.minecraft.banner.flower.gray": "灰色花朵盾徽", -+ "block.minecraft.banner.flower.green": "绿色花朵盾徽", -+ "block.minecraft.banner.flower.light_blue": "淡蓝色花朵盾徽", -+ "block.minecraft.banner.flower.light_gray": "淡灰色花朵盾徽", -+ "block.minecraft.banner.flower.lime": "黄绿色花朵盾徽", -+ "block.minecraft.banner.flower.magenta": "品红色花朵盾徽", -+ "block.minecraft.banner.flower.orange": "橙色花朵盾徽", -+ "block.minecraft.banner.flower.pink": "粉红色花朵盾徽", -+ "block.minecraft.banner.flower.purple": "紫色花朵盾徽", -+ "block.minecraft.banner.flower.red": "红色花朵盾徽", -+ "block.minecraft.banner.flower.white": "白色花朵盾徽", -+ "block.minecraft.banner.flower.yellow": "黄色花朵盾徽", -+ "block.minecraft.banner.globe.black": "黑色地球", -+ "block.minecraft.banner.globe.blue": "蓝色地球", -+ "block.minecraft.banner.globe.brown": "棕色地球", -+ "block.minecraft.banner.globe.cyan": "青色地球", -+ "block.minecraft.banner.globe.gray": "灰色地球", -+ "block.minecraft.banner.globe.green": "绿色地球", -+ "block.minecraft.banner.globe.light_blue": "淡蓝色地球", -+ "block.minecraft.banner.globe.light_gray": "淡灰色地球", -+ "block.minecraft.banner.globe.lime": "黄绿色地球", -+ "block.minecraft.banner.globe.magenta": "品红色地球", -+ "block.minecraft.banner.globe.orange": "橙色地球", -+ "block.minecraft.banner.globe.pink": "粉红色地球", -+ "block.minecraft.banner.globe.purple": "紫色地球", -+ "block.minecraft.banner.globe.red": "红色地球", -+ "block.minecraft.banner.globe.white": "白色地球", -+ "block.minecraft.banner.globe.yellow": "黄色地球", -+ "block.minecraft.banner.gradient.black": "黑色自上渐淡", -+ "block.minecraft.banner.gradient.blue": "蓝色自上渐淡", -+ "block.minecraft.banner.gradient.brown": "棕色自上渐淡", -+ "block.minecraft.banner.gradient.cyan": "青色自上渐淡", -+ "block.minecraft.banner.gradient.gray": "灰色自上渐淡", -+ "block.minecraft.banner.gradient.green": "绿色自上渐淡", -+ "block.minecraft.banner.gradient.light_blue": "淡蓝色自上渐淡", -+ "block.minecraft.banner.gradient.light_gray": "淡灰色自上渐淡", -+ "block.minecraft.banner.gradient.lime": "黄绿色自上渐淡", -+ "block.minecraft.banner.gradient.magenta": "品红色自上渐淡", -+ "block.minecraft.banner.gradient.orange": "橙色自上渐淡", -+ "block.minecraft.banner.gradient.pink": "粉红色自上渐淡", -+ "block.minecraft.banner.gradient.purple": "紫色自上渐淡", -+ "block.minecraft.banner.gradient.red": "红色自上渐淡", -+ "block.minecraft.banner.gradient.white": "白色自上渐淡", -+ "block.minecraft.banner.gradient.yellow": "黄色自上渐淡", -+ "block.minecraft.banner.gradient_up.black": "黑色自下渐淡", -+ "block.minecraft.banner.gradient_up.blue": "蓝色自下渐淡", -+ "block.minecraft.banner.gradient_up.brown": "棕色自下渐淡", -+ "block.minecraft.banner.gradient_up.cyan": "青色自下渐淡", -+ "block.minecraft.banner.gradient_up.gray": "灰色自下渐淡", -+ "block.minecraft.banner.gradient_up.green": "绿色自下渐淡", -+ "block.minecraft.banner.gradient_up.light_blue": "淡蓝色自下渐淡", -+ "block.minecraft.banner.gradient_up.light_gray": "淡灰色自下渐淡", -+ "block.minecraft.banner.gradient_up.lime": "黄绿色自下渐淡", -+ "block.minecraft.banner.gradient_up.magenta": "品红色自下渐淡", -+ "block.minecraft.banner.gradient_up.orange": "橙色自下渐淡", -+ "block.minecraft.banner.gradient_up.pink": "粉红色自下渐淡", -+ "block.minecraft.banner.gradient_up.purple": "紫色自下渐淡", -+ "block.minecraft.banner.gradient_up.red": "红色自下渐淡", -+ "block.minecraft.banner.gradient_up.white": "白色自下渐淡", -+ "block.minecraft.banner.gradient_up.yellow": "黄色自下渐淡", -+ "block.minecraft.banner.half_horizontal.black": "黑色上半方形", -+ "block.minecraft.banner.half_horizontal.blue": "蓝色上半方形", -+ "block.minecraft.banner.half_horizontal.brown": "棕色上半方形", -+ "block.minecraft.banner.half_horizontal.cyan": "青色上半方形", -+ "block.minecraft.banner.half_horizontal.gray": "灰色上半方形", -+ "block.minecraft.banner.half_horizontal.green": "绿色上半方形", -+ "block.minecraft.banner.half_horizontal.light_blue": "淡蓝色上半方形", -+ "block.minecraft.banner.half_horizontal.light_gray": "淡灰色上半方形", -+ "block.minecraft.banner.half_horizontal.lime": "黄绿色上半方形", -+ "block.minecraft.banner.half_horizontal.magenta": "品红色上半方形", -+ "block.minecraft.banner.half_horizontal.orange": "橙色上半方形", -+ "block.minecraft.banner.half_horizontal.pink": "粉红色上半方形", -+ "block.minecraft.banner.half_horizontal.purple": "紫色上半方形", -+ "block.minecraft.banner.half_horizontal.red": "红色上半方形", -+ "block.minecraft.banner.half_horizontal.white": "白色上半方形", -+ "block.minecraft.banner.half_horizontal.yellow": "黄色上半方形", -+ "block.minecraft.banner.half_horizontal_bottom.black": "黑色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.blue": "蓝色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.brown": "棕色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.cyan": "青色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.gray": "灰色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.green": "绿色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.light_blue": "淡蓝色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.light_gray": "淡灰色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.lime": "黄绿色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.magenta": "品红色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.orange": "橙色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.pink": "粉红色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.purple": "紫色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.red": "红色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.white": "白色下半方形", -+ "block.minecraft.banner.half_horizontal_bottom.yellow": "黄色下半方形", -+ "block.minecraft.banner.half_vertical.black": "黑色右半方形", -+ "block.minecraft.banner.half_vertical.blue": "蓝色右半方形", -+ "block.minecraft.banner.half_vertical.brown": "棕色右半方形", -+ "block.minecraft.banner.half_vertical.cyan": "青色右半方形", -+ "block.minecraft.banner.half_vertical.gray": "灰色右半方形", -+ "block.minecraft.banner.half_vertical.green": "绿色右半方形", -+ "block.minecraft.banner.half_vertical.light_blue": "淡蓝色右半方形", -+ "block.minecraft.banner.half_vertical.light_gray": "淡灰色右半方形", -+ "block.minecraft.banner.half_vertical.lime": "黄绿色右半方形", -+ "block.minecraft.banner.half_vertical.magenta": "品红色右半方形", -+ "block.minecraft.banner.half_vertical.orange": "橙色右半方形", -+ "block.minecraft.banner.half_vertical.pink": "粉红色右半方形", -+ "block.minecraft.banner.half_vertical.purple": "紫色右半方形", -+ "block.minecraft.banner.half_vertical.red": "红色右半方形", -+ "block.minecraft.banner.half_vertical.white": "白色右半方形", -+ "block.minecraft.banner.half_vertical.yellow": "黄色右半方形", -+ "block.minecraft.banner.half_vertical_right.black": "黑色左半方形", -+ "block.minecraft.banner.half_vertical_right.blue": "蓝色左半方形", -+ "block.minecraft.banner.half_vertical_right.brown": "棕色左半方形", -+ "block.minecraft.banner.half_vertical_right.cyan": "青色左半方形", -+ "block.minecraft.banner.half_vertical_right.gray": "灰色左半方形", -+ "block.minecraft.banner.half_vertical_right.green": "绿色左半方形", -+ "block.minecraft.banner.half_vertical_right.light_blue": "淡蓝色左半方形", -+ "block.minecraft.banner.half_vertical_right.light_gray": "淡灰色左半方形", -+ "block.minecraft.banner.half_vertical_right.lime": "黄绿色左半方形", -+ "block.minecraft.banner.half_vertical_right.magenta": "品红色左半方形", -+ "block.minecraft.banner.half_vertical_right.orange": "橙色左半方形", -+ "block.minecraft.banner.half_vertical_right.pink": "粉红色左半方形", -+ "block.minecraft.banner.half_vertical_right.purple": "紫色左半方形", -+ "block.minecraft.banner.half_vertical_right.red": "红色左半方形", -+ "block.minecraft.banner.half_vertical_right.white": "白色左半方形", -+ "block.minecraft.banner.half_vertical_right.yellow": "黄色左半方形", -+ "block.minecraft.banner.mojang.black": "黑色Mojang徽标", -+ "block.minecraft.banner.mojang.blue": "蓝色Mojang徽标", -+ "block.minecraft.banner.mojang.brown": "棕色Mojang徽标", -+ "block.minecraft.banner.mojang.cyan": "青色Mojang徽标", -+ "block.minecraft.banner.mojang.gray": "灰色Mojang徽标", -+ "block.minecraft.banner.mojang.green": "绿色Mojang徽标", -+ "block.minecraft.banner.mojang.light_blue": "淡蓝色Mojang徽标", -+ "block.minecraft.banner.mojang.light_gray": "淡灰色Mojang徽标", -+ "block.minecraft.banner.mojang.lime": "黄绿色Mojang徽标", -+ "block.minecraft.banner.mojang.magenta": "品红色Mojang徽标", -+ "block.minecraft.banner.mojang.orange": "橙色Mojang徽标", -+ "block.minecraft.banner.mojang.pink": "粉红色Mojang徽标", -+ "block.minecraft.banner.mojang.purple": "紫色Mojang徽标", -+ "block.minecraft.banner.mojang.red": "红色Mojang徽标", -+ "block.minecraft.banner.mojang.white": "白色Mojang徽标", -+ "block.minecraft.banner.mojang.yellow": "黄色Mojang徽标", -+ "block.minecraft.banner.piglin.black": "黑色猪鼻", -+ "block.minecraft.banner.piglin.blue": "蓝色猪鼻", -+ "block.minecraft.banner.piglin.brown": "棕色猪鼻", -+ "block.minecraft.banner.piglin.cyan": "青色猪鼻", -+ "block.minecraft.banner.piglin.gray": "灰色猪鼻", -+ "block.minecraft.banner.piglin.green": "绿色猪鼻", -+ "block.minecraft.banner.piglin.light_blue": "淡蓝色猪鼻", -+ "block.minecraft.banner.piglin.light_gray": "淡灰色猪鼻", -+ "block.minecraft.banner.piglin.lime": "黄绿色猪鼻", -+ "block.minecraft.banner.piglin.magenta": "品红色猪鼻", -+ "block.minecraft.banner.piglin.orange": "橙色猪鼻", -+ "block.minecraft.banner.piglin.pink": "粉红色猪鼻", -+ "block.minecraft.banner.piglin.purple": "紫色猪鼻", -+ "block.minecraft.banner.piglin.red": "红色猪鼻", -+ "block.minecraft.banner.piglin.white": "白色猪鼻", -+ "block.minecraft.banner.piglin.yellow": "黄色猪鼻", -+ "block.minecraft.banner.rhombus.black": "黑色菱形", -+ "block.minecraft.banner.rhombus.blue": "蓝色菱形", -+ "block.minecraft.banner.rhombus.brown": "棕色菱形", -+ "block.minecraft.banner.rhombus.cyan": "青色菱形", -+ "block.minecraft.banner.rhombus.gray": "灰色菱形", -+ "block.minecraft.banner.rhombus.green": "绿色菱形", -+ "block.minecraft.banner.rhombus.light_blue": "淡蓝色菱形", -+ "block.minecraft.banner.rhombus.light_gray": "淡灰色菱形", -+ "block.minecraft.banner.rhombus.lime": "黄绿色菱形", -+ "block.minecraft.banner.rhombus.magenta": "品红色菱形", -+ "block.minecraft.banner.rhombus.orange": "橙色菱形", -+ "block.minecraft.banner.rhombus.pink": "粉红色菱形", -+ "block.minecraft.banner.rhombus.purple": "紫色菱形", -+ "block.minecraft.banner.rhombus.red": "红色菱形", -+ "block.minecraft.banner.rhombus.white": "白色菱形", -+ "block.minecraft.banner.rhombus.yellow": "黄色菱形", -+ "block.minecraft.banner.skull.black": "黑色头颅盾徽", -+ "block.minecraft.banner.skull.blue": "蓝色头颅盾徽", -+ "block.minecraft.banner.skull.brown": "棕色头颅盾徽", -+ "block.minecraft.banner.skull.cyan": "青色头颅盾徽", -+ "block.minecraft.banner.skull.gray": "灰色头颅盾徽", -+ "block.minecraft.banner.skull.green": "绿色头颅盾徽", -+ "block.minecraft.banner.skull.light_blue": "淡蓝色头颅盾徽", -+ "block.minecraft.banner.skull.light_gray": "淡灰色头颅盾徽", -+ "block.minecraft.banner.skull.lime": "黄绿色头颅盾徽", -+ "block.minecraft.banner.skull.magenta": "品红色头颅盾徽", -+ "block.minecraft.banner.skull.orange": "橙色头颅盾徽", -+ "block.minecraft.banner.skull.pink": "粉红色头颅盾徽", -+ "block.minecraft.banner.skull.purple": "紫色头颅盾徽", -+ "block.minecraft.banner.skull.red": "红色头颅盾徽", -+ "block.minecraft.banner.skull.white": "白色头颅盾徽", -+ "block.minecraft.banner.skull.yellow": "黄色头颅盾徽", -+ "block.minecraft.banner.small_stripes.black": "黑竖条纹", -+ "block.minecraft.banner.small_stripes.blue": "蓝竖条纹", -+ "block.minecraft.banner.small_stripes.brown": "棕竖条纹", -+ "block.minecraft.banner.small_stripes.cyan": "青竖条纹", -+ "block.minecraft.banner.small_stripes.gray": "灰竖条纹", -+ "block.minecraft.banner.small_stripes.green": "绿竖条纹", -+ "block.minecraft.banner.small_stripes.light_blue": "淡蓝竖条纹", -+ "block.minecraft.banner.small_stripes.light_gray": "淡灰竖条纹", -+ "block.minecraft.banner.small_stripes.lime": "黄绿竖条纹", -+ "block.minecraft.banner.small_stripes.magenta": "品红竖条纹", -+ "block.minecraft.banner.small_stripes.orange": "橙竖条纹", -+ "block.minecraft.banner.small_stripes.pink": "粉红竖条纹", -+ "block.minecraft.banner.small_stripes.purple": "紫竖条纹", -+ "block.minecraft.banner.small_stripes.red": "红竖条纹", -+ "block.minecraft.banner.small_stripes.white": "白竖条纹", -+ "block.minecraft.banner.small_stripes.yellow": "黄竖条纹", -+ "block.minecraft.banner.square_bottom_left.black": "右底黑方", -+ "block.minecraft.banner.square_bottom_left.blue": "右底蓝方", -+ "block.minecraft.banner.square_bottom_left.brown": "右底棕方", -+ "block.minecraft.banner.square_bottom_left.cyan": "右底青方", -+ "block.minecraft.banner.square_bottom_left.gray": "右底灰方", -+ "block.minecraft.banner.square_bottom_left.green": "右底绿方", -+ "block.minecraft.banner.square_bottom_left.light_blue": "右底淡蓝方", -+ "block.minecraft.banner.square_bottom_left.light_gray": "右底淡灰方", -+ "block.minecraft.banner.square_bottom_left.lime": "右底黄绿方", -+ "block.minecraft.banner.square_bottom_left.magenta": "右底品红方", -+ "block.minecraft.banner.square_bottom_left.orange": "右底橙方", -+ "block.minecraft.banner.square_bottom_left.pink": "右底粉红方", -+ "block.minecraft.banner.square_bottom_left.purple": "右底紫方", -+ "block.minecraft.banner.square_bottom_left.red": "右底红方", -+ "block.minecraft.banner.square_bottom_left.white": "右底白方", -+ "block.minecraft.banner.square_bottom_left.yellow": "右底黄方", -+ "block.minecraft.banner.square_bottom_right.black": "左底黑方", -+ "block.minecraft.banner.square_bottom_right.blue": "左底蓝方", -+ "block.minecraft.banner.square_bottom_right.brown": "左底棕方", -+ "block.minecraft.banner.square_bottom_right.cyan": "左底青方", -+ "block.minecraft.banner.square_bottom_right.gray": "左底灰方", -+ "block.minecraft.banner.square_bottom_right.green": "左底绿方", -+ "block.minecraft.banner.square_bottom_right.light_blue": "左底淡蓝方", -+ "block.minecraft.banner.square_bottom_right.light_gray": "左底淡灰方", -+ "block.minecraft.banner.square_bottom_right.lime": "左底黄绿方", -+ "block.minecraft.banner.square_bottom_right.magenta": "左底品红方", -+ "block.minecraft.banner.square_bottom_right.orange": "左底橙方", -+ "block.minecraft.banner.square_bottom_right.pink": "左底粉红方", -+ "block.minecraft.banner.square_bottom_right.purple": "左底紫方", -+ "block.minecraft.banner.square_bottom_right.red": "左底红方", -+ "block.minecraft.banner.square_bottom_right.white": "左底白方", -+ "block.minecraft.banner.square_bottom_right.yellow": "左底黄方", -+ "block.minecraft.banner.square_top_left.black": "右顶黑方", -+ "block.minecraft.banner.square_top_left.blue": "右顶蓝方", -+ "block.minecraft.banner.square_top_left.brown": "右顶棕方", -+ "block.minecraft.banner.square_top_left.cyan": "右顶青方", -+ "block.minecraft.banner.square_top_left.gray": "右顶灰方", -+ "block.minecraft.banner.square_top_left.green": "右顶绿方", -+ "block.minecraft.banner.square_top_left.light_blue": "右顶淡蓝方", -+ "block.minecraft.banner.square_top_left.light_gray": "右顶淡灰方", -+ "block.minecraft.banner.square_top_left.lime": "右顶黄绿方", -+ "block.minecraft.banner.square_top_left.magenta": "右顶品红方", -+ "block.minecraft.banner.square_top_left.orange": "右顶橙方", -+ "block.minecraft.banner.square_top_left.pink": "右顶粉红方", -+ "block.minecraft.banner.square_top_left.purple": "右顶紫方", -+ "block.minecraft.banner.square_top_left.red": "右顶红方", -+ "block.minecraft.banner.square_top_left.white": "右顶白方", -+ "block.minecraft.banner.square_top_left.yellow": "右顶黄方", -+ "block.minecraft.banner.square_top_right.black": "左顶黑方", -+ "block.minecraft.banner.square_top_right.blue": "左顶蓝方", -+ "block.minecraft.banner.square_top_right.brown": "左顶棕方", -+ "block.minecraft.banner.square_top_right.cyan": "左顶青方", -+ "block.minecraft.banner.square_top_right.gray": "左顶灰方", -+ "block.minecraft.banner.square_top_right.green": "左顶绿方", -+ "block.minecraft.banner.square_top_right.light_blue": "左顶淡蓝方", -+ "block.minecraft.banner.square_top_right.light_gray": "左顶淡灰方", -+ "block.minecraft.banner.square_top_right.lime": "左顶黄绿方", -+ "block.minecraft.banner.square_top_right.magenta": "左顶品红方", -+ "block.minecraft.banner.square_top_right.orange": "左顶橙方", -+ "block.minecraft.banner.square_top_right.pink": "左顶粉红方", -+ "block.minecraft.banner.square_top_right.purple": "左顶紫方", -+ "block.minecraft.banner.square_top_right.red": "左顶红方", -+ "block.minecraft.banner.square_top_right.white": "左顶白方", -+ "block.minecraft.banner.square_top_right.yellow": "左顶黄方", -+ "block.minecraft.banner.straight_cross.black": "黑正十字", -+ "block.minecraft.banner.straight_cross.blue": "蓝正十字", -+ "block.minecraft.banner.straight_cross.brown": "棕正十字", -+ "block.minecraft.banner.straight_cross.cyan": "青正十字", -+ "block.minecraft.banner.straight_cross.gray": "灰正十字", -+ "block.minecraft.banner.straight_cross.green": "绿正十字", -+ "block.minecraft.banner.straight_cross.light_blue": "淡蓝正十字", -+ "block.minecraft.banner.straight_cross.light_gray": "淡灰正十字", -+ "block.minecraft.banner.straight_cross.lime": "黄绿正十字", -+ "block.minecraft.banner.straight_cross.magenta": "品红正十字", -+ "block.minecraft.banner.straight_cross.orange": "橙正十字", -+ "block.minecraft.banner.straight_cross.pink": "粉红正十字", -+ "block.minecraft.banner.straight_cross.purple": "紫正十字", -+ "block.minecraft.banner.straight_cross.red": "红正十字", -+ "block.minecraft.banner.straight_cross.white": "白正十字", -+ "block.minecraft.banner.straight_cross.yellow": "黄正十字", -+ "block.minecraft.banner.stripe_bottom.black": "底黑横条", -+ "block.minecraft.banner.stripe_bottom.blue": "底蓝横条", -+ "block.minecraft.banner.stripe_bottom.brown": "底棕横条", -+ "block.minecraft.banner.stripe_bottom.cyan": "底青横条", -+ "block.minecraft.banner.stripe_bottom.gray": "底灰横条", -+ "block.minecraft.banner.stripe_bottom.green": "底绿横条", -+ "block.minecraft.banner.stripe_bottom.light_blue": "底淡蓝横条", -+ "block.minecraft.banner.stripe_bottom.light_gray": "底淡灰横条", -+ "block.minecraft.banner.stripe_bottom.lime": "底黄绿横条", -+ "block.minecraft.banner.stripe_bottom.magenta": "底品红横条", -+ "block.minecraft.banner.stripe_bottom.orange": "底橙横条", -+ "block.minecraft.banner.stripe_bottom.pink": "底粉红横条", -+ "block.minecraft.banner.stripe_bottom.purple": "底紫横条", -+ "block.minecraft.banner.stripe_bottom.red": "底红横条", -+ "block.minecraft.banner.stripe_bottom.white": "底白横条", -+ "block.minecraft.banner.stripe_bottom.yellow": "底黄横条", -+ "block.minecraft.banner.stripe_center.black": "中黑竖条", -+ "block.minecraft.banner.stripe_center.blue": "中蓝竖条", -+ "block.minecraft.banner.stripe_center.brown": "中棕竖条", -+ "block.minecraft.banner.stripe_center.cyan": "中青竖条", -+ "block.minecraft.banner.stripe_center.gray": "中灰竖条", -+ "block.minecraft.banner.stripe_center.green": "中绿竖条", -+ "block.minecraft.banner.stripe_center.light_blue": "中淡蓝竖条", -+ "block.minecraft.banner.stripe_center.light_gray": "中淡灰竖条", -+ "block.minecraft.banner.stripe_center.lime": "中黄绿竖条", -+ "block.minecraft.banner.stripe_center.magenta": "中品红竖条", -+ "block.minecraft.banner.stripe_center.orange": "中橙竖条", -+ "block.minecraft.banner.stripe_center.pink": "中粉红竖条", -+ "block.minecraft.banner.stripe_center.purple": "中紫竖条", -+ "block.minecraft.banner.stripe_center.red": "中红竖条", -+ "block.minecraft.banner.stripe_center.white": "中白竖条", -+ "block.minecraft.banner.stripe_center.yellow": "中黄竖条", -+ "block.minecraft.banner.stripe_downleft.black": "左黑斜条", -+ "block.minecraft.banner.stripe_downleft.blue": "左蓝斜条", -+ "block.minecraft.banner.stripe_downleft.brown": "左棕斜条", -+ "block.minecraft.banner.stripe_downleft.cyan": "左青斜条", -+ "block.minecraft.banner.stripe_downleft.gray": "左灰斜条", -+ "block.minecraft.banner.stripe_downleft.green": "左绿斜条", -+ "block.minecraft.banner.stripe_downleft.light_blue": "左淡蓝斜条", -+ "block.minecraft.banner.stripe_downleft.light_gray": "左淡灰斜条", -+ "block.minecraft.banner.stripe_downleft.lime": "左黄绿斜条", -+ "block.minecraft.banner.stripe_downleft.magenta": "左品红斜条", -+ "block.minecraft.banner.stripe_downleft.orange": "左橙斜条", -+ "block.minecraft.banner.stripe_downleft.pink": "左粉红斜条", -+ "block.minecraft.banner.stripe_downleft.purple": "左紫斜条", -+ "block.minecraft.banner.stripe_downleft.red": "左红斜条", -+ "block.minecraft.banner.stripe_downleft.white": "左白斜条", -+ "block.minecraft.banner.stripe_downleft.yellow": "左黄斜条", -+ "block.minecraft.banner.stripe_downright.black": "右黑斜条", -+ "block.minecraft.banner.stripe_downright.blue": "右蓝斜条", -+ "block.minecraft.banner.stripe_downright.brown": "右棕斜条", -+ "block.minecraft.banner.stripe_downright.cyan": "右青斜条", -+ "block.minecraft.banner.stripe_downright.gray": "右灰斜条", -+ "block.minecraft.banner.stripe_downright.green": "右绿斜条", -+ "block.minecraft.banner.stripe_downright.light_blue": "右淡蓝斜条", -+ "block.minecraft.banner.stripe_downright.light_gray": "右淡灰斜条", -+ "block.minecraft.banner.stripe_downright.lime": "右黄绿斜条", -+ "block.minecraft.banner.stripe_downright.magenta": "右品红斜条", -+ "block.minecraft.banner.stripe_downright.orange": "右橙斜条", -+ "block.minecraft.banner.stripe_downright.pink": "右粉红斜条", -+ "block.minecraft.banner.stripe_downright.purple": "右紫斜条", -+ "block.minecraft.banner.stripe_downright.red": "右红斜条", -+ "block.minecraft.banner.stripe_downright.white": "右白斜条", -+ "block.minecraft.banner.stripe_downright.yellow": "右黄斜条", -+ "block.minecraft.banner.stripe_left.black": "右黑竖条", -+ "block.minecraft.banner.stripe_left.blue": "右蓝竖条", -+ "block.minecraft.banner.stripe_left.brown": "右棕竖条", -+ "block.minecraft.banner.stripe_left.cyan": "右青竖条", -+ "block.minecraft.banner.stripe_left.gray": "右灰竖条", -+ "block.minecraft.banner.stripe_left.green": "右绿竖条", -+ "block.minecraft.banner.stripe_left.light_blue": "右淡蓝竖条", -+ "block.minecraft.banner.stripe_left.light_gray": "右淡灰竖条", -+ "block.minecraft.banner.stripe_left.lime": "右黄绿竖条", -+ "block.minecraft.banner.stripe_left.magenta": "右品红竖条", -+ "block.minecraft.banner.stripe_left.orange": "右橙竖条", -+ "block.minecraft.banner.stripe_left.pink": "右粉红竖条", -+ "block.minecraft.banner.stripe_left.purple": "右紫竖条", -+ "block.minecraft.banner.stripe_left.red": "右红竖条", -+ "block.minecraft.banner.stripe_left.white": "右白竖条", -+ "block.minecraft.banner.stripe_left.yellow": "右黄竖条", -+ "block.minecraft.banner.stripe_middle.black": "中黑横条", -+ "block.minecraft.banner.stripe_middle.blue": "中蓝横条", -+ "block.minecraft.banner.stripe_middle.brown": "中棕横条", -+ "block.minecraft.banner.stripe_middle.cyan": "中青横条", -+ "block.minecraft.banner.stripe_middle.gray": "中灰横条", -+ "block.minecraft.banner.stripe_middle.green": "中绿横条", -+ "block.minecraft.banner.stripe_middle.light_blue": "中淡蓝横条", -+ "block.minecraft.banner.stripe_middle.light_gray": "中淡灰横条", -+ "block.minecraft.banner.stripe_middle.lime": "中黄绿横条", -+ "block.minecraft.banner.stripe_middle.magenta": "中品红横条", -+ "block.minecraft.banner.stripe_middle.orange": "中橙横条", -+ "block.minecraft.banner.stripe_middle.pink": "中粉红横条", -+ "block.minecraft.banner.stripe_middle.purple": "中紫横条", -+ "block.minecraft.banner.stripe_middle.red": "中红横条", -+ "block.minecraft.banner.stripe_middle.white": "中白横条", -+ "block.minecraft.banner.stripe_middle.yellow": "中黄横条", -+ "block.minecraft.banner.stripe_right.black": "左黑竖条", -+ "block.minecraft.banner.stripe_right.blue": "左蓝竖条", -+ "block.minecraft.banner.stripe_right.brown": "左棕竖条", -+ "block.minecraft.banner.stripe_right.cyan": "左青竖条", -+ "block.minecraft.banner.stripe_right.gray": "左灰竖条", -+ "block.minecraft.banner.stripe_right.green": "左绿竖条", -+ "block.minecraft.banner.stripe_right.light_blue": "左淡蓝竖条", -+ "block.minecraft.banner.stripe_right.light_gray": "左淡灰竖条", -+ "block.minecraft.banner.stripe_right.lime": "左黄绿竖条", -+ "block.minecraft.banner.stripe_right.magenta": "左品红竖条", -+ "block.minecraft.banner.stripe_right.orange": "左橙竖条", -+ "block.minecraft.banner.stripe_right.pink": "左粉红竖条", -+ "block.minecraft.banner.stripe_right.purple": "左紫竖条", -+ "block.minecraft.banner.stripe_right.red": "左红竖条", -+ "block.minecraft.banner.stripe_right.white": "左白竖条", -+ "block.minecraft.banner.stripe_right.yellow": "左黄竖条", -+ "block.minecraft.banner.stripe_top.black": "顶黑横条", -+ "block.minecraft.banner.stripe_top.blue": "顶蓝横条", -+ "block.minecraft.banner.stripe_top.brown": "顶棕横条", -+ "block.minecraft.banner.stripe_top.cyan": "顶青横条", -+ "block.minecraft.banner.stripe_top.gray": "顶灰横条", -+ "block.minecraft.banner.stripe_top.green": "顶绿横条", -+ "block.minecraft.banner.stripe_top.light_blue": "顶淡蓝横条", -+ "block.minecraft.banner.stripe_top.light_gray": "顶淡灰横条", -+ "block.minecraft.banner.stripe_top.lime": "顶黄绿横条", -+ "block.minecraft.banner.stripe_top.magenta": "顶品红横条", -+ "block.minecraft.banner.stripe_top.orange": "顶橙横条", -+ "block.minecraft.banner.stripe_top.pink": "顶粉红横条", -+ "block.minecraft.banner.stripe_top.purple": "顶紫横条", -+ "block.minecraft.banner.stripe_top.red": "顶红横条", -+ "block.minecraft.banner.stripe_top.white": "顶白横条", -+ "block.minecraft.banner.stripe_top.yellow": "顶黄横条", -+ "block.minecraft.banner.triangle_bottom.black": "底黑三角", -+ "block.minecraft.banner.triangle_bottom.blue": "底蓝三角", -+ "block.minecraft.banner.triangle_bottom.brown": "底棕三角", -+ "block.minecraft.banner.triangle_bottom.cyan": "底青三角", -+ "block.minecraft.banner.triangle_bottom.gray": "底灰三角", -+ "block.minecraft.banner.triangle_bottom.green": "底绿三角", -+ "block.minecraft.banner.triangle_bottom.light_blue": "底淡蓝三角", -+ "block.minecraft.banner.triangle_bottom.light_gray": "底淡灰三角", -+ "block.minecraft.banner.triangle_bottom.lime": "底黄绿三角", -+ "block.minecraft.banner.triangle_bottom.magenta": "底品红三角", -+ "block.minecraft.banner.triangle_bottom.orange": "底橙三角", -+ "block.minecraft.banner.triangle_bottom.pink": "底粉红三角", -+ "block.minecraft.banner.triangle_bottom.purple": "底紫三角", -+ "block.minecraft.banner.triangle_bottom.red": "底红三角", -+ "block.minecraft.banner.triangle_bottom.white": "底白三角", -+ "block.minecraft.banner.triangle_bottom.yellow": "底黄三角", -+ "block.minecraft.banner.triangle_top.black": "顶黑三角", -+ "block.minecraft.banner.triangle_top.blue": "顶蓝三角", -+ "block.minecraft.banner.triangle_top.brown": "顶棕三角", -+ "block.minecraft.banner.triangle_top.cyan": "顶青三角", -+ "block.minecraft.banner.triangle_top.gray": "灰色顶三角", -+ "block.minecraft.banner.triangle_top.green": "顶绿三角", -+ "block.minecraft.banner.triangle_top.light_blue": "淡蓝色顶三角", -+ "block.minecraft.banner.triangle_top.light_gray": "顶淡灰三角", -+ "block.minecraft.banner.triangle_top.lime": "黄绿色顶三角", -+ "block.minecraft.banner.triangle_top.magenta": "品红色顶三角", -+ "block.minecraft.banner.triangle_top.orange": "橙色顶三角", -+ "block.minecraft.banner.triangle_top.pink": "粉红色顶三角", -+ "block.minecraft.banner.triangle_top.purple": "顶紫三角", -+ "block.minecraft.banner.triangle_top.red": "顶红三角", -+ "block.minecraft.banner.triangle_top.white": "白色顶三角", -+ "block.minecraft.banner.triangle_top.yellow": "黄色顶三角", -+ "block.minecraft.banner.triangles_bottom.black": "黑色底波纹", -+ "block.minecraft.banner.triangles_bottom.blue": "蓝色底波纹", -+ "block.minecraft.banner.triangles_bottom.brown": "棕色底波纹", -+ "block.minecraft.banner.triangles_bottom.cyan": "青色底波纹", -+ "block.minecraft.banner.triangles_bottom.gray": "灰色底波纹", -+ "block.minecraft.banner.triangles_bottom.green": "绿色底波纹", -+ "block.minecraft.banner.triangles_bottom.light_blue": "淡蓝色底波纹", -+ "block.minecraft.banner.triangles_bottom.light_gray": "淡灰色底波纹", -+ "block.minecraft.banner.triangles_bottom.lime": "黄绿色底波纹", -+ "block.minecraft.banner.triangles_bottom.magenta": "品红色底波纹", -+ "block.minecraft.banner.triangles_bottom.orange": "橙色底波纹", -+ "block.minecraft.banner.triangles_bottom.pink": "粉红色底波纹", -+ "block.minecraft.banner.triangles_bottom.purple": "紫色底波纹", -+ "block.minecraft.banner.triangles_bottom.red": "红色底波纹", -+ "block.minecraft.banner.triangles_bottom.white": "白色底波纹", -+ "block.minecraft.banner.triangles_bottom.yellow": "黄色底波纹", -+ "block.minecraft.banner.triangles_top.black": "黑色顶波纹", -+ "block.minecraft.banner.triangles_top.blue": "蓝色顶波纹", -+ "block.minecraft.banner.triangles_top.brown": "棕色顶波纹", -+ "block.minecraft.banner.triangles_top.cyan": "青色顶波纹", -+ "block.minecraft.banner.triangles_top.gray": "灰色顶波纹", -+ "block.minecraft.banner.triangles_top.green": "绿色顶波纹", -+ "block.minecraft.banner.triangles_top.light_blue": "淡蓝色顶波纹", -+ "block.minecraft.banner.triangles_top.light_gray": "淡灰色顶波纹", -+ "block.minecraft.banner.triangles_top.lime": "黄绿色顶波纹", -+ "block.minecraft.banner.triangles_top.magenta": "品红色顶波纹", -+ "block.minecraft.banner.triangles_top.orange": "橙色顶波纹", -+ "block.minecraft.banner.triangles_top.pink": "粉红色顶波纹", -+ "block.minecraft.banner.triangles_top.purple": "紫色顶波纹", -+ "block.minecraft.banner.triangles_top.red": "红色顶波纹", -+ "block.minecraft.banner.triangles_top.white": "白色顶波纹", -+ "block.minecraft.banner.triangles_top.yellow": "黄色顶波纹", -+ "block.minecraft.barrel": "木桶", -+ "block.minecraft.barrier": "屏障", -+ "block.minecraft.basalt": "玄武岩", -+ "block.minecraft.beacon": "信标", -+ "block.minecraft.beacon.primary": "主效果", -+ "block.minecraft.beacon.secondary": "辅助效果", -+ "block.minecraft.bed.no_sleep": "你只能在夜间或雷暴中入眠", -+ "block.minecraft.bed.not_safe": "你现在不能休息,周围有怪物在游荡", -+ "block.minecraft.bed.obstructed": "这张床已被阻挡", -+ "block.minecraft.bed.occupied": "这张床已被占用", -+ "block.minecraft.bed.too_far_away": "你现在不能休息,床太远了", -+ "block.minecraft.bedrock": "基岩", -+ "block.minecraft.bee_nest": "蜂巢", -+ "block.minecraft.beehive": "蜂箱", -+ "block.minecraft.beetroots": "甜菜根", -+ "block.minecraft.bell": "钟", -+ "block.minecraft.big_dripleaf": "大型垂滴叶", -+ "block.minecraft.big_dripleaf_stem": "大型垂滴叶茎", -+ "block.minecraft.birch_button": "白桦木按钮", -+ "block.minecraft.birch_door": "白桦木门", -+ "block.minecraft.birch_fence": "白桦木栅栏", -+ "block.minecraft.birch_fence_gate": "白桦木栅栏门", -+ "block.minecraft.birch_hanging_sign": "悬挂式白桦木告示牌", -+ "block.minecraft.birch_leaves": "白桦树叶", -+ "block.minecraft.birch_log": "白桦原木", -+ "block.minecraft.birch_planks": "白桦木板", -+ "block.minecraft.birch_pressure_plate": "白桦木压力板", -+ "block.minecraft.birch_sapling": "白桦树苗", -+ "block.minecraft.birch_sign": "白桦木告示牌", -+ "block.minecraft.birch_slab": "白桦木台阶", -+ "block.minecraft.birch_stairs": "白桦木楼梯", -+ "block.minecraft.birch_trapdoor": "白桦木活板门", -+ "block.minecraft.birch_wall_hanging_sign": "墙上的悬挂式白桦木告示牌", -+ "block.minecraft.birch_wall_sign": "墙上的白桦木告示牌", -+ "block.minecraft.birch_wood": "白桦木", -+ "block.minecraft.black_banner": "黑色旗帜", -+ "block.minecraft.black_bed": "黑色床", -+ "block.minecraft.black_candle": "黑色蜡烛", -+ "block.minecraft.black_candle_cake": "插上黑色蜡烛的蛋糕", -+ "block.minecraft.black_carpet": "黑色地毯", -+ "block.minecraft.black_concrete": "黑色混凝土", -+ "block.minecraft.black_concrete_powder": "黑色混凝土粉末", -+ "block.minecraft.black_glazed_terracotta": "黑色带釉陶瓦", -+ "block.minecraft.black_shulker_box": "黑色潜影盒", -+ "block.minecraft.black_stained_glass": "黑色染色玻璃", -+ "block.minecraft.black_stained_glass_pane": "黑色染色玻璃板", -+ "block.minecraft.black_terracotta": "黑色陶瓦", -+ "block.minecraft.black_wool": "黑色羊毛", -+ "block.minecraft.blackstone": "黑石", -+ "block.minecraft.blackstone_slab": "黑石台阶", -+ "block.minecraft.blackstone_stairs": "黑石楼梯", -+ "block.minecraft.blackstone_wall": "黑石墙", -+ "block.minecraft.blast_furnace": "高炉", -+ "block.minecraft.blue_banner": "蓝色旗帜", -+ "block.minecraft.blue_bed": "蓝色床", -+ "block.minecraft.blue_candle": "蓝色蜡烛", -+ "block.minecraft.blue_candle_cake": "插上蓝色蜡烛的蛋糕", -+ "block.minecraft.blue_carpet": "蓝色地毯", -+ "block.minecraft.blue_concrete": "蓝色混凝土", -+ "block.minecraft.blue_concrete_powder": "蓝色混凝土粉末", -+ "block.minecraft.blue_glazed_terracotta": "蓝色带釉陶瓦", -+ "block.minecraft.blue_ice": "蓝冰", -+ "block.minecraft.blue_orchid": "兰花", -+ "block.minecraft.blue_shulker_box": "蓝色潜影盒", -+ "block.minecraft.blue_stained_glass": "蓝色染色玻璃", -+ "block.minecraft.blue_stained_glass_pane": "蓝色染色玻璃板", -+ "block.minecraft.blue_terracotta": "蓝色陶瓦", -+ "block.minecraft.blue_wool": "蓝色羊毛", -+ "block.minecraft.bone_block": "骨块", -+ "block.minecraft.bookshelf": "书架", -+ "block.minecraft.brain_coral": "脑纹珊瑚", -+ "block.minecraft.brain_coral_block": "脑纹珊瑚块", -+ "block.minecraft.brain_coral_fan": "脑纹珊瑚扇", -+ "block.minecraft.brain_coral_wall_fan": "墙上的脑纹珊瑚扇", -+ "block.minecraft.brewing_stand": "酿造台", -+ "block.minecraft.brick_slab": "红砖台阶", -+ "block.minecraft.brick_stairs": "红砖楼梯", -+ "block.minecraft.brick_wall": "红砖墙", -+ "block.minecraft.bricks": "红砖块", -+ "block.minecraft.brown_banner": "棕色旗帜", -+ "block.minecraft.brown_bed": "棕色床", -+ "block.minecraft.brown_candle": "棕色蜡烛", -+ "block.minecraft.brown_candle_cake": "插上棕色蜡烛的蛋糕", -+ "block.minecraft.brown_carpet": "棕色地毯", -+ "block.minecraft.brown_concrete": "棕色混凝土", -+ "block.minecraft.brown_concrete_powder": "棕色混凝土粉末", -+ "block.minecraft.brown_glazed_terracotta": "棕色带釉陶瓦", -+ "block.minecraft.brown_mushroom": "棕色蘑菇", -+ "block.minecraft.brown_mushroom_block": "棕色蘑菇方块", -+ "block.minecraft.brown_shulker_box": "棕色潜影盒", -+ "block.minecraft.brown_stained_glass": "棕色染色玻璃", -+ "block.minecraft.brown_stained_glass_pane": "棕色染色玻璃板", -+ "block.minecraft.brown_terracotta": "棕色陶瓦", -+ "block.minecraft.brown_wool": "棕色羊毛", -+ "block.minecraft.bubble_column": "气泡柱", -+ "block.minecraft.bubble_coral": "气泡珊瑚", -+ "block.minecraft.bubble_coral_block": "气泡珊瑚块", -+ "block.minecraft.bubble_coral_fan": "气泡珊瑚扇", -+ "block.minecraft.bubble_coral_wall_fan": "墙上的气泡珊瑚扇", -+ "block.minecraft.budding_amethyst": "紫水晶母岩", -+ "block.minecraft.cactus": "仙人掌", -+ "block.minecraft.cake": "蛋糕", -+ "block.minecraft.calcite": "方解石", -+ "block.minecraft.calibrated_sculk_sensor": "校频幽匿感测体", -+ "block.minecraft.campfire": "营火", -+ "block.minecraft.candle": "蜡烛", -+ "block.minecraft.candle_cake": "插上蜡烛的蛋糕", -+ "block.minecraft.carrots": "胡萝卜", -+ "block.minecraft.cartography_table": "制图台", -+ "block.minecraft.carved_pumpkin": "雕刻南瓜", -+ "block.minecraft.cauldron": "炼药锅", -+ "block.minecraft.cave_air": "洞穴空气", -+ "block.minecraft.cave_vines": "洞穴藤蔓", -+ "block.minecraft.cave_vines_plant": "洞穴藤蔓植株", -+ "block.minecraft.chain": "锁链", -+ "block.minecraft.chain_command_block": "连锁型命令方块", -+ "block.minecraft.cherry_button": "樱花木按钮", -+ "block.minecraft.cherry_door": "樱花木门", -+ "block.minecraft.cherry_fence": "樱花木栅栏", -+ "block.minecraft.cherry_fence_gate": "樱花木栅栏门", -+ "block.minecraft.cherry_hanging_sign": "悬挂式樱花木告示牌", -+ "block.minecraft.cherry_leaves": "樱花树叶", -+ "block.minecraft.cherry_log": "樱花原木", -+ "block.minecraft.cherry_planks": "樱花木板", -+ "block.minecraft.cherry_pressure_plate": "樱花木压力板", -+ "block.minecraft.cherry_sapling": "樱花树苗", -+ "block.minecraft.cherry_sign": "樱花木告示牌", -+ "block.minecraft.cherry_slab": "樱花木台阶", -+ "block.minecraft.cherry_stairs": "樱花木楼梯", -+ "block.minecraft.cherry_trapdoor": "樱花木活板门", -+ "block.minecraft.cherry_wall_hanging_sign": "墙上的悬挂式樱花木告示牌", -+ "block.minecraft.cherry_wall_sign": "墙上的樱花木告示牌", -+ "block.minecraft.cherry_wood": "樱花木", -+ "block.minecraft.chest": "箱子", -+ "block.minecraft.chipped_anvil": "开裂的铁砧", -+ "block.minecraft.chiseled_bookshelf": "雕纹书架", -+ "block.minecraft.chiseled_deepslate": "雕纹深板岩", -+ "block.minecraft.chiseled_nether_bricks": "雕纹下界砖块", -+ "block.minecraft.chiseled_polished_blackstone": "雕纹磨制黑石", -+ "block.minecraft.chiseled_quartz_block": "雕纹石英块", -+ "block.minecraft.chiseled_red_sandstone": "雕纹红砂岩", -+ "block.minecraft.chiseled_sandstone": "雕纹砂岩", -+ "block.minecraft.chiseled_stone_bricks": "雕纹石砖", -+ "block.minecraft.chorus_flower": "紫颂花", -+ "block.minecraft.chorus_plant": "紫颂植株", -+ "block.minecraft.clay": "黏土块", -+ "block.minecraft.coal_block": "煤炭块", -+ "block.minecraft.coal_ore": "煤矿石", -+ "block.minecraft.coarse_dirt": "砂土", -+ "block.minecraft.cobbled_deepslate": "深板岩圆石", -+ "block.minecraft.cobbled_deepslate_slab": "深板岩圆石台阶", -+ "block.minecraft.cobbled_deepslate_stairs": "深板岩圆石楼梯", -+ "block.minecraft.cobbled_deepslate_wall": "深板岩圆石墙", -+ "block.minecraft.cobblestone": "圆石", -+ "block.minecraft.cobblestone_slab": "圆石台阶", -+ "block.minecraft.cobblestone_stairs": "圆石楼梯", -+ "block.minecraft.cobblestone_wall": "圆石墙", -+ "block.minecraft.cobweb": "蜘蛛网", -+ "block.minecraft.cocoa": "可可果", -+ "block.minecraft.command_block": "命令方块", -+ "block.minecraft.comparator": "红石比较器", -+ "block.minecraft.composter": "堆肥桶", -+ "block.minecraft.conduit": "潮涌核心", -+ "block.minecraft.copper_block": "铜块", -+ "block.minecraft.copper_ore": "铜矿石", -+ "block.minecraft.cornflower": "矢车菊", -+ "block.minecraft.cracked_deepslate_bricks": "裂纹深板岩砖", -+ "block.minecraft.cracked_deepslate_tiles": "裂纹深板岩瓦", -+ "block.minecraft.cracked_nether_bricks": "裂纹下界砖块", -+ "block.minecraft.cracked_polished_blackstone_bricks": "裂纹磨制黑石砖", -+ "block.minecraft.cracked_stone_bricks": "裂纹石砖", -+ "block.minecraft.crafting_table": "工作台", -+ "block.minecraft.creeper_head": "苦力怕的头", -+ "block.minecraft.creeper_wall_head": "墙上的苦力怕的头", -+ "block.minecraft.crimson_button": "绯红木按钮", -+ "block.minecraft.crimson_door": "绯红木门", -+ "block.minecraft.crimson_fence": "绯红木栅栏", -+ "block.minecraft.crimson_fence_gate": "绯红木栅栏门", -+ "block.minecraft.crimson_fungus": "绯红菌", -+ "block.minecraft.crimson_hanging_sign": "悬挂式绯红木告示牌", -+ "block.minecraft.crimson_hyphae": "绯红菌核", -+ "block.minecraft.crimson_nylium": "绯红菌岩", -+ "block.minecraft.crimson_planks": "绯红木板", -+ "block.minecraft.crimson_pressure_plate": "绯红木压力板", -+ "block.minecraft.crimson_roots": "绯红菌索", -+ "block.minecraft.crimson_sign": "绯红木告示牌", -+ "block.minecraft.crimson_slab": "绯红木台阶", -+ "block.minecraft.crimson_stairs": "绯红木楼梯", -+ "block.minecraft.crimson_stem": "绯红菌柄", -+ "block.minecraft.crimson_trapdoor": "绯红木活板门", -+ "block.minecraft.crimson_wall_hanging_sign": "墙上的悬挂式绯红木告示牌", -+ "block.minecraft.crimson_wall_sign": "墙上的绯红木告示牌", -+ "block.minecraft.crying_obsidian": "哭泣的黑曜石", -+ "block.minecraft.cut_copper": "切制铜块", -+ "block.minecraft.cut_copper_slab": "切制铜台阶", -+ "block.minecraft.cut_copper_stairs": "切制铜楼梯", -+ "block.minecraft.cut_red_sandstone": "切制红砂岩", -+ "block.minecraft.cut_red_sandstone_slab": "切制红砂岩台阶", -+ "block.minecraft.cut_sandstone": "切制砂岩", -+ "block.minecraft.cut_sandstone_slab": "切制砂岩台阶", -+ "block.minecraft.cyan_banner": "青色旗帜", -+ "block.minecraft.cyan_bed": "青色床", -+ "block.minecraft.cyan_candle": "青色蜡烛", -+ "block.minecraft.cyan_candle_cake": "插上青色蜡烛的蛋糕", -+ "block.minecraft.cyan_carpet": "青色地毯", -+ "block.minecraft.cyan_concrete": "青色混凝土", -+ "block.minecraft.cyan_concrete_powder": "青色混凝土粉末", -+ "block.minecraft.cyan_glazed_terracotta": "青色带釉陶瓦", -+ "block.minecraft.cyan_shulker_box": "青色潜影盒", -+ "block.minecraft.cyan_stained_glass": "青色染色玻璃", -+ "block.minecraft.cyan_stained_glass_pane": "青色染色玻璃板", -+ "block.minecraft.cyan_terracotta": "青色陶瓦", -+ "block.minecraft.cyan_wool": "青色羊毛", -+ "block.minecraft.damaged_anvil": "损坏的铁砧", -+ "block.minecraft.dandelion": "蒲公英", -+ "block.minecraft.dark_oak_button": "深色橡木按钮", -+ "block.minecraft.dark_oak_door": "深色橡木门", -+ "block.minecraft.dark_oak_fence": "深色橡木栅栏", -+ "block.minecraft.dark_oak_fence_gate": "深色橡木栅栏门", -+ "block.minecraft.dark_oak_hanging_sign": "悬挂式深色橡木告示牌", -+ "block.minecraft.dark_oak_leaves": "深色橡树树叶", -+ "block.minecraft.dark_oak_log": "深色橡木原木", -+ "block.minecraft.dark_oak_planks": "深色橡木木板", -+ "block.minecraft.dark_oak_pressure_plate": "深色橡木压力板", -+ "block.minecraft.dark_oak_sapling": "深色橡树树苗", -+ "block.minecraft.dark_oak_sign": "深色橡木告示牌", -+ "block.minecraft.dark_oak_slab": "深色橡木台阶", -+ "block.minecraft.dark_oak_stairs": "深色橡木楼梯", -+ "block.minecraft.dark_oak_trapdoor": "深色橡木活板门", -+ "block.minecraft.dark_oak_wall_hanging_sign": "墙上的悬挂式深色橡木告示牌", -+ "block.minecraft.dark_oak_wall_sign": "墙上的深色橡木告示牌", -+ "block.minecraft.dark_oak_wood": "深色橡木", -+ "block.minecraft.dark_prismarine": "暗海晶石", -+ "block.minecraft.dark_prismarine_slab": "暗海晶石台阶", -+ "block.minecraft.dark_prismarine_stairs": "暗海晶石楼梯", -+ "block.minecraft.daylight_detector": "阳光探测器", -+ "block.minecraft.dead_brain_coral": "失活的脑纹珊瑚", -+ "block.minecraft.dead_brain_coral_block": "失活的脑纹珊瑚块", -+ "block.minecraft.dead_brain_coral_fan": "失活的脑纹珊瑚扇", -+ "block.minecraft.dead_brain_coral_wall_fan": "墙上的失活脑纹珊瑚扇", -+ "block.minecraft.dead_bubble_coral": "失活的气泡珊瑚", -+ "block.minecraft.dead_bubble_coral_block": "失活的气泡珊瑚块", -+ "block.minecraft.dead_bubble_coral_fan": "失活的气泡珊瑚扇", -+ "block.minecraft.dead_bubble_coral_wall_fan": "墙上的失活气泡珊瑚扇", -+ "block.minecraft.dead_bush": "枯萎的灌木", -+ "block.minecraft.dead_fire_coral": "失活的火珊瑚", -+ "block.minecraft.dead_fire_coral_block": "失活的火珊瑚块", -+ "block.minecraft.dead_fire_coral_fan": "失活的火珊瑚扇", -+ "block.minecraft.dead_fire_coral_wall_fan": "墙上的失活火珊瑚扇", -+ "block.minecraft.dead_horn_coral": "失活的鹿角珊瑚", -+ "block.minecraft.dead_horn_coral_block": "失活的鹿角珊瑚块", -+ "block.minecraft.dead_horn_coral_fan": "失活的鹿角珊瑚扇", -+ "block.minecraft.dead_horn_coral_wall_fan": "墙上的失活鹿角珊瑚扇", -+ "block.minecraft.dead_tube_coral": "失活的管珊瑚", -+ "block.minecraft.dead_tube_coral_block": "失活的管珊瑚块", -+ "block.minecraft.dead_tube_coral_fan": "失活的管珊瑚扇", -+ "block.minecraft.dead_tube_coral_wall_fan": "墙上的失活管珊瑚扇", -+ "block.minecraft.decorated_pot": "饰纹陶罐", -+ "block.minecraft.deepslate": "深板岩", -+ "block.minecraft.deepslate_brick_slab": "深板岩砖台阶", -+ "block.minecraft.deepslate_brick_stairs": "深板岩砖楼梯", -+ "block.minecraft.deepslate_brick_wall": "深板岩砖墙", -+ "block.minecraft.deepslate_bricks": "深板岩砖", -+ "block.minecraft.deepslate_coal_ore": "深层煤矿石", -+ "block.minecraft.deepslate_copper_ore": "深层铜矿石", -+ "block.minecraft.deepslate_diamond_ore": "深层钻石矿石", -+ "block.minecraft.deepslate_emerald_ore": "深层绿宝石矿石", -+ "block.minecraft.deepslate_gold_ore": "深层金矿石", -+ "block.minecraft.deepslate_iron_ore": "深层铁矿石", -+ "block.minecraft.deepslate_lapis_ore": "深层青金石矿石", -+ "block.minecraft.deepslate_redstone_ore": "深层红石矿石", -+ "block.minecraft.deepslate_tile_slab": "深板岩瓦台阶", -+ "block.minecraft.deepslate_tile_stairs": "深板岩瓦楼梯", -+ "block.minecraft.deepslate_tile_wall": "深板岩瓦墙", -+ "block.minecraft.deepslate_tiles": "深板岩瓦", -+ "block.minecraft.detector_rail": "探测铁轨", -+ "block.minecraft.diamond_block": "钻石块", -+ "block.minecraft.diamond_ore": "钻石矿石", -+ "block.minecraft.diorite": "闪长岩", -+ "block.minecraft.diorite_slab": "闪长岩台阶", -+ "block.minecraft.diorite_stairs": "闪长岩楼梯", -+ "block.minecraft.diorite_wall": "闪长岩墙", -+ "block.minecraft.dirt": "泥土", -+ "block.minecraft.dirt_path": "土径", -+ "block.minecraft.dispenser": "发射器", -+ "block.minecraft.dragon_egg": "龙蛋", -+ "block.minecraft.dragon_head": "龙首", -+ "block.minecraft.dragon_wall_head": "墙上的龙首", -+ "block.minecraft.dried_kelp_block": "干海带块", -+ "block.minecraft.dripstone_block": "滴水石块", -+ "block.minecraft.dropper": "投掷器", -+ "block.minecraft.emerald_block": "绿宝石块", -+ "block.minecraft.emerald_ore": "绿宝石矿石", -+ "block.minecraft.enchanting_table": "附魔台", -+ "block.minecraft.end_gateway": "末地折跃门", -+ "block.minecraft.end_portal": "末地传送门", -+ "block.minecraft.end_portal_frame": "末地传送门框架", -+ "block.minecraft.end_rod": "末地烛", -+ "block.minecraft.end_stone": "末地石", -+ "block.minecraft.end_stone_brick_slab": "末地石砖台阶", -+ "block.minecraft.end_stone_brick_stairs": "末地石砖楼梯", -+ "block.minecraft.end_stone_brick_wall": "末地石砖墙", -+ "block.minecraft.end_stone_bricks": "末地石砖", -+ "block.minecraft.ender_chest": "末影箱", -+ "block.minecraft.exposed_copper": "斑驳的铜块", -+ "block.minecraft.exposed_cut_copper": "斑驳的切制铜块", -+ "block.minecraft.exposed_cut_copper_slab": "斑驳的切制铜台阶", -+ "block.minecraft.exposed_cut_copper_stairs": "斑驳的切制铜楼梯", -+ "block.minecraft.farmland": "耕地", -+ "block.minecraft.fern": "蕨", -+ "block.minecraft.fire": "火", -+ "block.minecraft.fire_coral": "火珊瑚", -+ "block.minecraft.fire_coral_block": "火珊瑚块", -+ "block.minecraft.fire_coral_fan": "火珊瑚扇", -+ "block.minecraft.fire_coral_wall_fan": "墙上的火珊瑚扇", -+ "block.minecraft.fletching_table": "制箭台", -+ "block.minecraft.flower_pot": "花盆", -+ "block.minecraft.flowering_azalea": "盛开的杜鹃花丛", -+ "block.minecraft.flowering_azalea_leaves": "盛开的杜鹃树叶", -+ "block.minecraft.frogspawn": "青蛙卵", -+ "block.minecraft.frosted_ice": "霜冰", -+ "block.minecraft.furnace": "熔炉", -+ "block.minecraft.gilded_blackstone": "镶金黑石", -+ "block.minecraft.glass": "玻璃", -+ "block.minecraft.glass_pane": "玻璃板", -+ "block.minecraft.glow_lichen": "发光地衣", -+ "block.minecraft.glowstone": "荧石", -+ "block.minecraft.gold_block": "金块", -+ "block.minecraft.gold_ore": "金矿石", -+ "block.minecraft.granite": "花岗岩", -+ "block.minecraft.granite_slab": "花岗岩台阶", -+ "block.minecraft.granite_stairs": "花岗岩楼梯", -+ "block.minecraft.granite_wall": "花岗岩墙", -+ "block.minecraft.grass": "草", -+ "block.minecraft.grass_block": "草方块", -+ "block.minecraft.gravel": "沙砾", -+ "block.minecraft.gray_banner": "灰色旗帜", -+ "block.minecraft.gray_bed": "灰色床", -+ "block.minecraft.gray_candle": "灰色蜡烛", -+ "block.minecraft.gray_candle_cake": "插上灰色蜡烛的蛋糕", -+ "block.minecraft.gray_carpet": "灰色地毯", -+ "block.minecraft.gray_concrete": "灰色混凝土", -+ "block.minecraft.gray_concrete_powder": "灰色混凝土粉末", -+ "block.minecraft.gray_glazed_terracotta": "灰色带釉陶瓦", -+ "block.minecraft.gray_shulker_box": "灰色潜影盒", -+ "block.minecraft.gray_stained_glass": "灰色染色玻璃", -+ "block.minecraft.gray_stained_glass_pane": "灰色染色玻璃板", -+ "block.minecraft.gray_terracotta": "灰色陶瓦", -+ "block.minecraft.gray_wool": "灰色羊毛", -+ "block.minecraft.green_banner": "绿色旗帜", -+ "block.minecraft.green_bed": "绿色床", -+ "block.minecraft.green_candle": "绿色蜡烛", -+ "block.minecraft.green_candle_cake": "插上绿色蜡烛的蛋糕", -+ "block.minecraft.green_carpet": "绿色地毯", -+ "block.minecraft.green_concrete": "绿色混凝土", -+ "block.minecraft.green_concrete_powder": "绿色混凝土粉末", -+ "block.minecraft.green_glazed_terracotta": "绿色带釉陶瓦", -+ "block.minecraft.green_shulker_box": "绿色潜影盒", -+ "block.minecraft.green_stained_glass": "绿色染色玻璃", -+ "block.minecraft.green_stained_glass_pane": "绿色染色玻璃板", -+ "block.minecraft.green_terracotta": "绿色陶瓦", -+ "block.minecraft.green_wool": "绿色羊毛", -+ "block.minecraft.grindstone": "砂轮", -+ "block.minecraft.hanging_roots": "垂根", -+ "block.minecraft.hay_block": "干草捆", -+ "block.minecraft.heavy_weighted_pressure_plate": "重质测重压力板", -+ "block.minecraft.honey_block": "蜂蜜块", -+ "block.minecraft.honeycomb_block": "蜜脾块", -+ "block.minecraft.hopper": "漏斗", -+ "block.minecraft.horn_coral": "鹿角珊瑚", -+ "block.minecraft.horn_coral_block": "鹿角珊瑚块", -+ "block.minecraft.horn_coral_fan": "鹿角珊瑚扇", -+ "block.minecraft.horn_coral_wall_fan": "墙上的鹿角珊瑚扇", -+ "block.minecraft.ice": "冰", -+ "block.minecraft.infested_chiseled_stone_bricks": "虫蚀雕纹石砖", -+ "block.minecraft.infested_cobblestone": "虫蚀圆石", -+ "block.minecraft.infested_cracked_stone_bricks": "虫蚀裂纹石砖", -+ "block.minecraft.infested_deepslate": "虫蚀深板岩", -+ "block.minecraft.infested_mossy_stone_bricks": "虫蚀苔石砖", -+ "block.minecraft.infested_stone": "虫蚀石头", -+ "block.minecraft.infested_stone_bricks": "虫蚀石砖", -+ "block.minecraft.iron_bars": "铁栏杆", -+ "block.minecraft.iron_block": "铁块", -+ "block.minecraft.iron_door": "铁门", -+ "block.minecraft.iron_ore": "铁矿石", -+ "block.minecraft.iron_trapdoor": "铁活板门", -+ "block.minecraft.jack_o_lantern": "南瓜灯", -+ "block.minecraft.jigsaw": "拼图方块", -+ "block.minecraft.jukebox": "唱片机", -+ "block.minecraft.jungle_button": "丛林木按钮", -+ "block.minecraft.jungle_door": "丛林木门", -+ "block.minecraft.jungle_fence": "丛林木栅栏", -+ "block.minecraft.jungle_fence_gate": "丛林木栅栏门", -+ "block.minecraft.jungle_hanging_sign": "悬挂式丛林木告示牌", -+ "block.minecraft.jungle_leaves": "丛林树叶", -+ "block.minecraft.jungle_log": "丛林原木", -+ "block.minecraft.jungle_planks": "丛林木板", -+ "block.minecraft.jungle_pressure_plate": "丛林木压力板", -+ "block.minecraft.jungle_sapling": "丛林树苗", -+ "block.minecraft.jungle_sign": "丛林木告示牌", -+ "block.minecraft.jungle_slab": "丛林木台阶", -+ "block.minecraft.jungle_stairs": "丛林木楼梯", -+ "block.minecraft.jungle_trapdoor": "丛林木活板门", -+ "block.minecraft.jungle_wall_hanging_sign": "墙上的悬挂式丛林木告示牌", -+ "block.minecraft.jungle_wall_sign": "墙上的丛林木告示牌", -+ "block.minecraft.jungle_wood": "丛林木", -+ "block.minecraft.kelp": "海带", -+ "block.minecraft.kelp_plant": "海带植株", -+ "block.minecraft.ladder": "梯子", -+ "block.minecraft.lantern": "灯笼", -+ "block.minecraft.lapis_block": "青金石块", -+ "block.minecraft.lapis_ore": "青金石矿石", -+ "block.minecraft.large_amethyst_bud": "大型紫晶芽", -+ "block.minecraft.large_fern": "大型蕨", -+ "block.minecraft.lava": "熔岩", -+ "block.minecraft.lava_cauldron": "装有熔岩的炼药锅", -+ "block.minecraft.lectern": "讲台", -+ "block.minecraft.lever": "拉杆", -+ "block.minecraft.light": "光源方块", -+ "block.minecraft.light_blue_banner": "淡蓝色旗帜", -+ "block.minecraft.light_blue_bed": "淡蓝色床", -+ "block.minecraft.light_blue_candle": "淡蓝色蜡烛", -+ "block.minecraft.light_blue_candle_cake": "插上淡蓝色蜡烛的蛋糕", -+ "block.minecraft.light_blue_carpet": "淡蓝色地毯", -+ "block.minecraft.light_blue_concrete": "淡蓝色混凝土", -+ "block.minecraft.light_blue_concrete_powder": "淡蓝色混凝土粉末", -+ "block.minecraft.light_blue_glazed_terracotta": "淡蓝色带釉陶瓦", -+ "block.minecraft.light_blue_shulker_box": "淡蓝色潜影盒", -+ "block.minecraft.light_blue_stained_glass": "淡蓝色染色玻璃", -+ "block.minecraft.light_blue_stained_glass_pane": "淡蓝色染色玻璃板", -+ "block.minecraft.light_blue_terracotta": "淡蓝色陶瓦", -+ "block.minecraft.light_blue_wool": "淡蓝色羊毛", -+ "block.minecraft.light_gray_banner": "淡灰色旗帜", -+ "block.minecraft.light_gray_bed": "淡灰色床", -+ "block.minecraft.light_gray_candle": "淡灰色蜡烛", -+ "block.minecraft.light_gray_candle_cake": "插上淡灰色蜡烛的蛋糕", -+ "block.minecraft.light_gray_carpet": "淡灰色地毯", -+ "block.minecraft.light_gray_concrete": "淡灰色混凝土", -+ "block.minecraft.light_gray_concrete_powder": "淡灰色混凝土粉末", -+ "block.minecraft.light_gray_glazed_terracotta": "淡灰色带釉陶瓦", -+ "block.minecraft.light_gray_shulker_box": "淡灰色潜影盒", -+ "block.minecraft.light_gray_stained_glass": "淡灰色染色玻璃", -+ "block.minecraft.light_gray_stained_glass_pane": "淡灰色染色玻璃板", -+ "block.minecraft.light_gray_terracotta": "淡灰色陶瓦", -+ "block.minecraft.light_gray_wool": "淡灰色羊毛", -+ "block.minecraft.light_weighted_pressure_plate": "轻质测重压力板", -+ "block.minecraft.lightning_rod": "避雷针", -+ "block.minecraft.lilac": "丁香", -+ "block.minecraft.lily_of_the_valley": "铃兰", -+ "block.minecraft.lily_pad": "睡莲", -+ "block.minecraft.lime_banner": "黄绿色旗帜", -+ "block.minecraft.lime_bed": "黄绿色床", -+ "block.minecraft.lime_candle": "黄绿色蜡烛", -+ "block.minecraft.lime_candle_cake": "插上黄绿色蜡烛的蛋糕", -+ "block.minecraft.lime_carpet": "黄绿色地毯", -+ "block.minecraft.lime_concrete": "黄绿色混凝土", -+ "block.minecraft.lime_concrete_powder": "黄绿色混凝土粉末", -+ "block.minecraft.lime_glazed_terracotta": "黄绿色带釉陶瓦", -+ "block.minecraft.lime_shulker_box": "黄绿色潜影盒", -+ "block.minecraft.lime_stained_glass": "黄绿色染色玻璃", -+ "block.minecraft.lime_stained_glass_pane": "黄绿色染色玻璃板", -+ "block.minecraft.lime_terracotta": "黄绿色陶瓦", -+ "block.minecraft.lime_wool": "黄绿色羊毛", -+ "block.minecraft.lodestone": "磁石", -+ "block.minecraft.loom": "织布机", -+ "block.minecraft.magenta_banner": "品红色旗帜", -+ "block.minecraft.magenta_bed": "品红色床", -+ "block.minecraft.magenta_candle": "品红色蜡烛", -+ "block.minecraft.magenta_candle_cake": "插上品红色蜡烛的蛋糕", -+ "block.minecraft.magenta_carpet": "品红色地毯", -+ "block.minecraft.magenta_concrete": "品红色混凝土", -+ "block.minecraft.magenta_concrete_powder": "品红色混凝土粉末", -+ "block.minecraft.magenta_glazed_terracotta": "品红色带釉陶瓦", -+ "block.minecraft.magenta_shulker_box": "品红色潜影盒", -+ "block.minecraft.magenta_stained_glass": "品红色染色玻璃", -+ "block.minecraft.magenta_stained_glass_pane": "品红色染色玻璃板", -+ "block.minecraft.magenta_terracotta": "品红色陶瓦", -+ "block.minecraft.magenta_wool": "品红色羊毛", -+ "block.minecraft.magma_block": "岩浆块", -+ "block.minecraft.mangrove_button": "红树木按钮", -+ "block.minecraft.mangrove_door": "红树木门", -+ "block.minecraft.mangrove_fence": "红树木栅栏", -+ "block.minecraft.mangrove_fence_gate": "红树木栅栏门", -+ "block.minecraft.mangrove_hanging_sign": "悬挂式红树木告示牌", -+ "block.minecraft.mangrove_leaves": "红树树叶", -+ "block.minecraft.mangrove_log": "红树原木", -+ "block.minecraft.mangrove_planks": "红树木板", -+ "block.minecraft.mangrove_pressure_plate": "红树木压力板", -+ "block.minecraft.mangrove_propagule": "红树胎生苗", -+ "block.minecraft.mangrove_roots": "红树根", -+ "block.minecraft.mangrove_sign": "红树木告示牌", -+ "block.minecraft.mangrove_slab": "红树木台阶", -+ "block.minecraft.mangrove_stairs": "红树木楼梯", -+ "block.minecraft.mangrove_trapdoor": "红树木活板门", -+ "block.minecraft.mangrove_wall_hanging_sign": "墙上的悬挂式红树木告示牌", -+ "block.minecraft.mangrove_wall_sign": "墙上的红树木告示牌", -+ "block.minecraft.mangrove_wood": "红树木", -+ "block.minecraft.medium_amethyst_bud": "中型紫晶芽", -+ "block.minecraft.melon": "西瓜", -+ "block.minecraft.melon_stem": "西瓜茎", -+ "block.minecraft.moss_block": "苔藓块", -+ "block.minecraft.moss_carpet": "覆地苔藓", -+ "block.minecraft.mossy_cobblestone": "苔石", -+ "block.minecraft.mossy_cobblestone_slab": "苔石台阶", -+ "block.minecraft.mossy_cobblestone_stairs": "苔石楼梯", -+ "block.minecraft.mossy_cobblestone_wall": "苔石墙", -+ "block.minecraft.mossy_stone_brick_slab": "苔石砖台阶", -+ "block.minecraft.mossy_stone_brick_stairs": "苔石砖楼梯", -+ "block.minecraft.mossy_stone_brick_wall": "苔石砖墙", -+ "block.minecraft.mossy_stone_bricks": "苔石砖", -+ "block.minecraft.moving_piston": "移动的活塞", -+ "block.minecraft.mud": "泥巴", -+ "block.minecraft.mud_brick_slab": "泥砖台阶", -+ "block.minecraft.mud_brick_stairs": "泥砖楼梯", -+ "block.minecraft.mud_brick_wall": "泥砖墙", -+ "block.minecraft.mud_bricks": "泥砖", -+ "block.minecraft.muddy_mangrove_roots": "沾泥的红树根", -+ "block.minecraft.mushroom_stem": "蘑菇柄", -+ "block.minecraft.mycelium": "菌丝体", -+ "block.minecraft.nether_brick_fence": "下界砖栅栏", -+ "block.minecraft.nether_brick_slab": "下界砖台阶", -+ "block.minecraft.nether_brick_stairs": "下界砖楼梯", -+ "block.minecraft.nether_brick_wall": "下界砖墙", -+ "block.minecraft.nether_bricks": "下界砖块", -+ "block.minecraft.nether_gold_ore": "下界金矿石", -+ "block.minecraft.nether_portal": "下界传送门", -+ "block.minecraft.nether_quartz_ore": "下界石英矿石", -+ "block.minecraft.nether_sprouts": "下界苗", -+ "block.minecraft.nether_wart": "下界疣", -+ "block.minecraft.nether_wart_block": "下界疣块", -+ "block.minecraft.netherite_block": "下界合金块", -+ "block.minecraft.netherrack": "下界岩", -+ "block.minecraft.note_block": "音符盒", -+ "block.minecraft.oak_button": "橡木按钮", -+ "block.minecraft.oak_door": "橡木门", -+ "block.minecraft.oak_fence": "橡木栅栏", -+ "block.minecraft.oak_fence_gate": "橡木栅栏门", -+ "block.minecraft.oak_hanging_sign": "悬挂式橡木告示牌", -+ "block.minecraft.oak_leaves": "橡树树叶", -+ "block.minecraft.oak_log": "橡木原木", -+ "block.minecraft.oak_planks": "橡木木板", -+ "block.minecraft.oak_pressure_plate": "橡木压力板", -+ "block.minecraft.oak_sapling": "橡树树苗", -+ "block.minecraft.oak_sign": "橡木告示牌", -+ "block.minecraft.oak_slab": "橡木台阶", -+ "block.minecraft.oak_stairs": "橡木楼梯", -+ "block.minecraft.oak_trapdoor": "橡木活板门", -+ "block.minecraft.oak_wall_hanging_sign": "墙上的悬挂式橡木告示牌", -+ "block.minecraft.oak_wall_sign": "墙上的橡木告示牌", -+ "block.minecraft.oak_wood": "橡木", -+ "block.minecraft.observer": "侦测器", -+ "block.minecraft.obsidian": "黑曜石", -+ "block.minecraft.ochre_froglight": "赭黄蛙明灯", -+ "block.minecraft.ominous_banner": "灾厄旗帜", -+ "block.minecraft.orange_banner": "橙色旗帜", -+ "block.minecraft.orange_bed": "橙色床", -+ "block.minecraft.orange_candle": "橙色蜡烛", -+ "block.minecraft.orange_candle_cake": "插上橙色蜡烛的蛋糕", -+ "block.minecraft.orange_carpet": "橙色地毯", -+ "block.minecraft.orange_concrete": "橙色混凝土", -+ "block.minecraft.orange_concrete_powder": "橙色混凝土粉末", -+ "block.minecraft.orange_glazed_terracotta": "橙色带釉陶瓦", -+ "block.minecraft.orange_shulker_box": "橙色潜影盒", -+ "block.minecraft.orange_stained_glass": "橙色染色玻璃", -+ "block.minecraft.orange_stained_glass_pane": "橙色染色玻璃板", -+ "block.minecraft.orange_terracotta": "橙色陶瓦", -+ "block.minecraft.orange_tulip": "橙色郁金香", -+ "block.minecraft.orange_wool": "橙色羊毛", -+ "block.minecraft.oxeye_daisy": "滨菊", -+ "block.minecraft.oxidized_copper": "氧化的铜块", -+ "block.minecraft.oxidized_cut_copper": "氧化的切制铜块", -+ "block.minecraft.oxidized_cut_copper_slab": "氧化的切制铜台阶", -+ "block.minecraft.oxidized_cut_copper_stairs": "氧化的切制铜楼梯", -+ "block.minecraft.packed_ice": "浮冰", -+ "block.minecraft.packed_mud": "泥坯", -+ "block.minecraft.pearlescent_froglight": "珠光蛙明灯", -+ "block.minecraft.peony": "牡丹", -+ "block.minecraft.petrified_oak_slab": "石化橡木台阶", -+ "block.minecraft.piglin_head": "猪灵的头", -+ "block.minecraft.piglin_wall_head": "墙上的猪灵的头", -+ "block.minecraft.pink_banner": "粉红色旗帜", -+ "block.minecraft.pink_bed": "粉红色床", -+ "block.minecraft.pink_candle": "粉红色蜡烛", -+ "block.minecraft.pink_candle_cake": "插上粉红色蜡烛的蛋糕", -+ "block.minecraft.pink_carpet": "粉红色地毯", -+ "block.minecraft.pink_concrete": "粉红色混凝土", -+ "block.minecraft.pink_concrete_powder": "粉红色混凝土粉末", -+ "block.minecraft.pink_glazed_terracotta": "粉红色带釉陶瓦", -+ "block.minecraft.pink_petals": "粉红色花簇", -+ "block.minecraft.pink_shulker_box": "粉红色潜影盒", -+ "block.minecraft.pink_stained_glass": "粉红色染色玻璃", -+ "block.minecraft.pink_stained_glass_pane": "粉红色染色玻璃板", -+ "block.minecraft.pink_terracotta": "粉红色陶瓦", -+ "block.minecraft.pink_tulip": "粉红色郁金香", -+ "block.minecraft.pink_wool": "粉红色羊毛", -+ "block.minecraft.piston": "活塞", -+ "block.minecraft.piston_head": "活塞头", -+ "block.minecraft.pitcher_crop": "瓶子草植株", -+ "block.minecraft.pitcher_plant": "瓶子草", -+ "block.minecraft.player_head": "玩家的头", -+ "block.minecraft.player_head.named": "%s的头", -+ "block.minecraft.player_wall_head": "墙上的玩家的头", -+ "block.minecraft.podzol": "灰化土", -+ "block.minecraft.pointed_dripstone": "滴水石锥", -+ "block.minecraft.polished_andesite": "磨制安山岩", -+ "block.minecraft.polished_andesite_slab": "磨制安山岩台阶", -+ "block.minecraft.polished_andesite_stairs": "磨制安山岩楼梯", -+ "block.minecraft.polished_basalt": "磨制玄武岩", -+ "block.minecraft.polished_blackstone": "磨制黑石", -+ "block.minecraft.polished_blackstone_brick_slab": "磨制黑石砖台阶", -+ "block.minecraft.polished_blackstone_brick_stairs": "磨制黑石砖楼梯", -+ "block.minecraft.polished_blackstone_brick_wall": "磨制黑石砖墙", -+ "block.minecraft.polished_blackstone_bricks": "磨制黑石砖", -+ "block.minecraft.polished_blackstone_button": "磨制黑石按钮", -+ "block.minecraft.polished_blackstone_pressure_plate": "磨制黑石压力板", -+ "block.minecraft.polished_blackstone_slab": "磨制黑石台阶", -+ "block.minecraft.polished_blackstone_stairs": "磨制黑石楼梯", -+ "block.minecraft.polished_blackstone_wall": "磨制黑石墙", -+ "block.minecraft.polished_deepslate": "磨制深板岩", -+ "block.minecraft.polished_deepslate_slab": "磨制深板岩台阶", -+ "block.minecraft.polished_deepslate_stairs": "磨制深板岩楼梯", -+ "block.minecraft.polished_deepslate_wall": "磨制深板岩墙", -+ "block.minecraft.polished_diorite": "磨制闪长岩", -+ "block.minecraft.polished_diorite_slab": "磨制闪长岩台阶", -+ "block.minecraft.polished_diorite_stairs": "磨制闪长岩楼梯", -+ "block.minecraft.polished_granite": "磨制花岗岩", -+ "block.minecraft.polished_granite_slab": "磨制花岗岩台阶", -+ "block.minecraft.polished_granite_stairs": "磨制花岗岩楼梯", -+ "block.minecraft.poppy": "虞美人", -+ "block.minecraft.potatoes": "马铃薯", -+ "block.minecraft.potted_acacia_sapling": "金合欢树苗盆栽", -+ "block.minecraft.potted_allium": "绒球葱盆栽", -+ "block.minecraft.potted_azalea_bush": "杜鹃花丛盆栽", -+ "block.minecraft.potted_azure_bluet": "蓝花美耳草盆栽", -+ "block.minecraft.potted_bamboo": "竹子盆栽", -+ "block.minecraft.potted_birch_sapling": "白桦树苗盆栽", -+ "block.minecraft.potted_blue_orchid": "兰花盆栽", -+ "block.minecraft.potted_brown_mushroom": "棕色蘑菇盆栽", -+ "block.minecraft.potted_cactus": "仙人掌盆栽", -+ "block.minecraft.potted_cherry_sapling": "樱花树苗盆栽", -+ "block.minecraft.potted_cornflower": "矢车菊盆栽", -+ "block.minecraft.potted_crimson_fungus": "绯红菌盆栽", -+ "block.minecraft.potted_crimson_roots": "绯红菌索盆栽", -+ "block.minecraft.potted_dandelion": "蒲公英盆栽", -+ "block.minecraft.potted_dark_oak_sapling": "深色橡树树苗盆栽", -+ "block.minecraft.potted_dead_bush": "枯萎的灌木盆栽", -+ "block.minecraft.potted_fern": "蕨盆栽", -+ "block.minecraft.potted_flowering_azalea_bush": "盛开的杜鹃花丛盆栽", -+ "block.minecraft.potted_jungle_sapling": "丛林树苗盆栽", -+ "block.minecraft.potted_lily_of_the_valley": "铃兰盆栽", -+ "block.minecraft.potted_mangrove_propagule": "红树胎生苗盆栽", -+ "block.minecraft.potted_oak_sapling": "橡树树苗盆栽", -+ "block.minecraft.potted_orange_tulip": "橙色郁金香盆栽", -+ "block.minecraft.potted_oxeye_daisy": "滨菊盆栽", -+ "block.minecraft.potted_pink_tulip": "粉红色郁金香盆栽", -+ "block.minecraft.potted_poppy": "虞美人盆栽", -+ "block.minecraft.potted_red_mushroom": "红色蘑菇盆栽", -+ "block.minecraft.potted_red_tulip": "红色郁金香盆栽", -+ "block.minecraft.potted_spruce_sapling": "云杉树苗盆栽", -+ "block.minecraft.potted_torchflower": "火把花盆栽", -+ "block.minecraft.potted_warped_fungus": "诡异菌盆栽", -+ "block.minecraft.potted_warped_roots": "诡异菌索盆栽", -+ "block.minecraft.potted_white_tulip": "白色郁金香盆栽", -+ "block.minecraft.potted_wither_rose": "凋灵玫瑰盆栽", -+ "block.minecraft.powder_snow": "细雪", -+ "block.minecraft.powder_snow_cauldron": "装有细雪的炼药锅", -+ "block.minecraft.powered_rail": "动力铁轨", -+ "block.minecraft.prismarine": "海晶石", -+ "block.minecraft.prismarine_brick_slab": "海晶石砖台阶", -+ "block.minecraft.prismarine_brick_stairs": "海晶石砖楼梯", -+ "block.minecraft.prismarine_bricks": "海晶石砖", -+ "block.minecraft.prismarine_slab": "海晶石台阶", -+ "block.minecraft.prismarine_stairs": "海晶石楼梯", -+ "block.minecraft.prismarine_wall": "海晶石墙", -+ "block.minecraft.pumpkin": "南瓜", -+ "block.minecraft.pumpkin_stem": "南瓜茎", -+ "block.minecraft.purple_banner": "紫色旗帜", -+ "block.minecraft.purple_bed": "紫色床", -+ "block.minecraft.purple_candle": "紫色蜡烛", -+ "block.minecraft.purple_candle_cake": "插上紫色蜡烛的蛋糕", -+ "block.minecraft.purple_carpet": "紫色地毯", -+ "block.minecraft.purple_concrete": "紫色混凝土", -+ "block.minecraft.purple_concrete_powder": "紫色混凝土粉末", -+ "block.minecraft.purple_glazed_terracotta": "紫色带釉陶瓦", -+ "block.minecraft.purple_shulker_box": "紫色潜影盒", -+ "block.minecraft.purple_stained_glass": "紫色染色玻璃", -+ "block.minecraft.purple_stained_glass_pane": "紫色染色玻璃板", -+ "block.minecraft.purple_terracotta": "紫色陶瓦", -+ "block.minecraft.purple_wool": "紫色羊毛", -+ "block.minecraft.purpur_block": "紫珀块", -+ "block.minecraft.purpur_pillar": "紫珀柱", -+ "block.minecraft.purpur_slab": "紫珀台阶", -+ "block.minecraft.purpur_stairs": "紫珀楼梯", -+ "block.minecraft.quartz_block": "石英块", -+ "block.minecraft.quartz_bricks": "石英砖", -+ "block.minecraft.quartz_pillar": "石英柱", -+ "block.minecraft.quartz_slab": "石英台阶", -+ "block.minecraft.quartz_stairs": "石英楼梯", -+ "block.minecraft.rail": "铁轨", -+ "block.minecraft.raw_copper_block": "粗铜块", -+ "block.minecraft.raw_gold_block": "粗金块", -+ "block.minecraft.raw_iron_block": "粗铁块", -+ "block.minecraft.red_banner": "红色旗帜", -+ "block.minecraft.red_bed": "红色床", -+ "block.minecraft.red_candle": "红色蜡烛", -+ "block.minecraft.red_candle_cake": "插上红色蜡烛的蛋糕", -+ "block.minecraft.red_carpet": "红色地毯", -+ "block.minecraft.red_concrete": "红色混凝土", -+ "block.minecraft.red_concrete_powder": "红色混凝土粉末", -+ "block.minecraft.red_glazed_terracotta": "红色带釉陶瓦", -+ "block.minecraft.red_mushroom": "红色蘑菇", -+ "block.minecraft.red_mushroom_block": "红色蘑菇方块", -+ "block.minecraft.red_nether_brick_slab": "红色下界砖台阶", -+ "block.minecraft.red_nether_brick_stairs": "红色下界砖楼梯", -+ "block.minecraft.red_nether_brick_wall": "红色下界砖墙", -+ "block.minecraft.red_nether_bricks": "红色下界砖块", -+ "block.minecraft.red_sand": "红沙", -+ "block.minecraft.red_sandstone": "红砂岩", -+ "block.minecraft.red_sandstone_slab": "红砂岩台阶", -+ "block.minecraft.red_sandstone_stairs": "红砂岩楼梯", -+ "block.minecraft.red_sandstone_wall": "红砂岩墙", -+ "block.minecraft.red_shulker_box": "红色潜影盒", -+ "block.minecraft.red_stained_glass": "红色染色玻璃", -+ "block.minecraft.red_stained_glass_pane": "红色染色玻璃板", -+ "block.minecraft.red_terracotta": "红色陶瓦", -+ "block.minecraft.red_tulip": "红色郁金香", -+ "block.minecraft.red_wool": "红色羊毛", -+ "block.minecraft.redstone_block": "红石块", -+ "block.minecraft.redstone_lamp": "红石灯", -+ "block.minecraft.redstone_ore": "红石矿石", -+ "block.minecraft.redstone_torch": "红石火把", -+ "block.minecraft.redstone_wall_torch": "墙上的红石火把", -+ "block.minecraft.redstone_wire": "红石线", -+ "block.minecraft.reinforced_deepslate": "强化深板岩", -+ "block.minecraft.repeater": "红石中继器", -+ "block.minecraft.repeating_command_block": "循环型命令方块", -+ "block.minecraft.respawn_anchor": "重生锚", -+ "block.minecraft.rooted_dirt": "缠根泥土", -+ "block.minecraft.rose_bush": "玫瑰丛", -+ "block.minecraft.sand": "沙子", -+ "block.minecraft.sandstone": "砂岩", -+ "block.minecraft.sandstone_slab": "砂岩台阶", -+ "block.minecraft.sandstone_stairs": "砂岩楼梯", -+ "block.minecraft.sandstone_wall": "砂岩墙", -+ "block.minecraft.scaffolding": "脚手架", -+ "block.minecraft.sculk": "幽匿块", -+ "block.minecraft.sculk_catalyst": "幽匿催发体", -+ "block.minecraft.sculk_sensor": "幽匿感测体", -+ "block.minecraft.sculk_shrieker": "幽匿尖啸体", -+ "block.minecraft.sculk_vein": "幽匿脉络", -+ "block.minecraft.sea_lantern": "海晶灯", -+ "block.minecraft.sea_pickle": "海泡菜", -+ "block.minecraft.seagrass": "海草", -+ "block.minecraft.set_spawn": "已设置重生点", -+ "block.minecraft.shroomlight": "菌光体", -+ "block.minecraft.shulker_box": "潜影盒", -+ "block.minecraft.skeleton_skull": "骷髅头颅", -+ "block.minecraft.skeleton_wall_skull": "墙上的骷髅头颅", -+ "block.minecraft.slime_block": "黏液块", -+ "block.minecraft.small_amethyst_bud": "小型紫晶芽", -+ "block.minecraft.small_dripleaf": "小型垂滴叶", -+ "block.minecraft.smithing_table": "锻造台", -+ "block.minecraft.smoker": "烟熏炉", -+ "block.minecraft.smooth_basalt": "平滑玄武岩", -+ "block.minecraft.smooth_quartz": "平滑石英块", -+ "block.minecraft.smooth_quartz_slab": "平滑石英台阶", -+ "block.minecraft.smooth_quartz_stairs": "平滑石英楼梯", -+ "block.minecraft.smooth_red_sandstone": "平滑红砂岩", -+ "block.minecraft.smooth_red_sandstone_slab": "平滑红砂岩台阶", -+ "block.minecraft.smooth_red_sandstone_stairs": "平滑红砂岩楼梯", -+ "block.minecraft.smooth_sandstone": "平滑砂岩", -+ "block.minecraft.smooth_sandstone_slab": "平滑砂岩台阶", -+ "block.minecraft.smooth_sandstone_stairs": "平滑砂岩楼梯", -+ "block.minecraft.smooth_stone": "平滑石头", -+ "block.minecraft.smooth_stone_slab": "平滑石头台阶", -+ "block.minecraft.sniffer_egg": "嗅探兽蛋", -+ "block.minecraft.snow": "雪", -+ "block.minecraft.snow_block": "雪块", -+ "block.minecraft.soul_campfire": "灵魂营火", -+ "block.minecraft.soul_fire": "灵魂火", -+ "block.minecraft.soul_lantern": "灵魂灯笼", -+ "block.minecraft.soul_sand": "灵魂沙", -+ "block.minecraft.soul_soil": "灵魂土", -+ "block.minecraft.soul_torch": "灵魂火把", -+ "block.minecraft.soul_wall_torch": "墙上的灵魂火把", -+ "block.minecraft.spawn.not_valid": "你的床或已充能的重生锚不存在或已被阻挡", -+ "block.minecraft.spawner": "刷怪笼", -+ "block.minecraft.spawner.desc1": "用刷怪蛋交互时:", -+ "block.minecraft.spawner.desc2": "设置生物种类", -+ "block.minecraft.sponge": "海绵", -+ "block.minecraft.spore_blossom": "孢子花", -+ "block.minecraft.spruce_button": "云杉木按钮", -+ "block.minecraft.spruce_door": "云杉木门", -+ "block.minecraft.spruce_fence": "云杉木栅栏", -+ "block.minecraft.spruce_fence_gate": "云杉木栅栏门", -+ "block.minecraft.spruce_hanging_sign": "悬挂式云杉木告示牌", -+ "block.minecraft.spruce_leaves": "云杉树叶", -+ "block.minecraft.spruce_log": "云杉原木", -+ "block.minecraft.spruce_planks": "云杉木板", -+ "block.minecraft.spruce_pressure_plate": "云杉木压力板", -+ "block.minecraft.spruce_sapling": "云杉树苗", -+ "block.minecraft.spruce_sign": "云杉木告示牌", -+ "block.minecraft.spruce_slab": "云杉木台阶", -+ "block.minecraft.spruce_stairs": "云杉木楼梯", -+ "block.minecraft.spruce_trapdoor": "云杉木活板门", -+ "block.minecraft.spruce_wall_hanging_sign": "墙上的悬挂式云杉木告示牌", -+ "block.minecraft.spruce_wall_sign": "墙上的云杉木告示牌", -+ "block.minecraft.spruce_wood": "云杉木", -+ "block.minecraft.sticky_piston": "黏性活塞", -+ "block.minecraft.stone": "石头", -+ "block.minecraft.stone_brick_slab": "石砖台阶", -+ "block.minecraft.stone_brick_stairs": "石砖楼梯", -+ "block.minecraft.stone_brick_wall": "石砖墙", -+ "block.minecraft.stone_bricks": "石砖", -+ "block.minecraft.stone_button": "石头按钮", -+ "block.minecraft.stone_pressure_plate": "石头压力板", -+ "block.minecraft.stone_slab": "石头台阶", -+ "block.minecraft.stone_stairs": "石头楼梯", -+ "block.minecraft.stonecutter": "切石机", -+ "block.minecraft.stripped_acacia_log": "去皮金合欢原木", -+ "block.minecraft.stripped_acacia_wood": "去皮金合欢木", -+ "block.minecraft.stripped_bamboo_block": "去皮竹块", -+ "block.minecraft.stripped_birch_log": "去皮白桦原木", -+ "block.minecraft.stripped_birch_wood": "去皮白桦木", -+ "block.minecraft.stripped_cherry_log": "去皮樱花原木", -+ "block.minecraft.stripped_cherry_wood": "去皮樱花木", -+ "block.minecraft.stripped_crimson_hyphae": "去皮绯红菌核", -+ "block.minecraft.stripped_crimson_stem": "去皮绯红菌柄", -+ "block.minecraft.stripped_dark_oak_log": "去皮深色橡木原木", -+ "block.minecraft.stripped_dark_oak_wood": "去皮深色橡木", -+ "block.minecraft.stripped_jungle_log": "去皮丛林原木", -+ "block.minecraft.stripped_jungle_wood": "去皮丛林木", -+ "block.minecraft.stripped_mangrove_log": "去皮红树原木", -+ "block.minecraft.stripped_mangrove_wood": "去皮红树木", -+ "block.minecraft.stripped_oak_log": "去皮橡木原木", -+ "block.minecraft.stripped_oak_wood": "去皮橡木", -+ "block.minecraft.stripped_spruce_log": "去皮云杉原木", -+ "block.minecraft.stripped_spruce_wood": "去皮云杉木", -+ "block.minecraft.stripped_warped_hyphae": "去皮诡异菌核", -+ "block.minecraft.stripped_warped_stem": "去皮诡异菌柄", -+ "block.minecraft.structure_block": "结构方块", -+ "block.minecraft.structure_void": "结构空位", -+ "block.minecraft.sugar_cane": "甘蔗", -+ "block.minecraft.sunflower": "向日葵", -+ "block.minecraft.suspicious_gravel": "可疑的沙砾", -+ "block.minecraft.suspicious_sand": "可疑的沙子", -+ "block.minecraft.sweet_berry_bush": "甜浆果丛", -+ "block.minecraft.tall_grass": "高草丛", -+ "block.minecraft.tall_seagrass": "高海草", -+ "block.minecraft.target": "标靶", -+ "block.minecraft.terracotta": "陶瓦", -+ "block.minecraft.tinted_glass": "遮光玻璃", -+ "block.minecraft.tnt": "TNT", -+ "block.minecraft.torch": "火把", -+ "block.minecraft.torchflower": "火把花", -+ "block.minecraft.torchflower_crop": "火把花植株", -+ "block.minecraft.trapped_chest": "陷阱箱", -+ "block.minecraft.tripwire": "绊线", -+ "block.minecraft.tripwire_hook": "绊线钩", -+ "block.minecraft.tube_coral": "管珊瑚", -+ "block.minecraft.tube_coral_block": "管珊瑚块", -+ "block.minecraft.tube_coral_fan": "管珊瑚扇", -+ "block.minecraft.tube_coral_wall_fan": "墙上的管珊瑚扇", -+ "block.minecraft.tuff": "凝灰岩", -+ "block.minecraft.turtle_egg": "海龟蛋", -+ "block.minecraft.twisting_vines": "缠怨藤", -+ "block.minecraft.twisting_vines_plant": "缠怨藤植株", -+ "block.minecraft.verdant_froglight": "青翠蛙明灯", -+ "block.minecraft.vine": "藤蔓", -+ "block.minecraft.void_air": "虚空空气", -+ "block.minecraft.wall_torch": "墙上的火把", -+ "block.minecraft.warped_button": "诡异木按钮", -+ "block.minecraft.warped_door": "诡异木门", -+ "block.minecraft.warped_fence": "诡异木栅栏", -+ "block.minecraft.warped_fence_gate": "诡异木栅栏门", -+ "block.minecraft.warped_fungus": "诡异菌", -+ "block.minecraft.warped_hanging_sign": "悬挂式诡异木告示牌", -+ "block.minecraft.warped_hyphae": "诡异菌核", -+ "block.minecraft.warped_nylium": "诡异菌岩", -+ "block.minecraft.warped_planks": "诡异木板", -+ "block.minecraft.warped_pressure_plate": "诡异木压力板", -+ "block.minecraft.warped_roots": "诡异菌索", -+ "block.minecraft.warped_sign": "诡异木告示牌", -+ "block.minecraft.warped_slab": "诡异木台阶", -+ "block.minecraft.warped_stairs": "诡异木楼梯", -+ "block.minecraft.warped_stem": "诡异菌柄", -+ "block.minecraft.warped_trapdoor": "诡异木活板门", -+ "block.minecraft.warped_wall_hanging_sign": "墙上的悬挂式诡异木告示牌", -+ "block.minecraft.warped_wall_sign": "墙上的诡异木告示牌", -+ "block.minecraft.warped_wart_block": "诡异疣块", -+ "block.minecraft.water": "水", -+ "block.minecraft.water_cauldron": "装有水的炼药锅", -+ "block.minecraft.waxed_copper_block": "涂蜡铜块", -+ "block.minecraft.waxed_cut_copper": "涂蜡切制铜块", -+ "block.minecraft.waxed_cut_copper_slab": "涂蜡切制铜台阶", -+ "block.minecraft.waxed_cut_copper_stairs": "涂蜡切制铜楼梯", -+ "block.minecraft.waxed_exposed_copper": "斑驳的涂蜡铜块", -+ "block.minecraft.waxed_exposed_cut_copper": "斑驳的涂蜡切制铜块", -+ "block.minecraft.waxed_exposed_cut_copper_slab": "斑驳的涂蜡切制铜台阶", -+ "block.minecraft.waxed_exposed_cut_copper_stairs": "斑驳的涂蜡切制铜楼梯", -+ "block.minecraft.waxed_oxidized_copper": "氧化的涂蜡铜块", -+ "block.minecraft.waxed_oxidized_cut_copper": "氧化的涂蜡切制铜块", -+ "block.minecraft.waxed_oxidized_cut_copper_slab": "氧化的涂蜡切制铜台阶", -+ "block.minecraft.waxed_oxidized_cut_copper_stairs": "氧化的涂蜡切制铜楼梯", -+ "block.minecraft.waxed_weathered_copper": "锈蚀的涂蜡铜块", -+ "block.minecraft.waxed_weathered_cut_copper": "锈蚀的涂蜡切制铜块", -+ "block.minecraft.waxed_weathered_cut_copper_slab": "锈蚀的涂蜡切制铜台阶", -+ "block.minecraft.waxed_weathered_cut_copper_stairs": "锈蚀的涂蜡切制铜楼梯", -+ "block.minecraft.weathered_copper": "锈蚀的铜块", -+ "block.minecraft.weathered_cut_copper": "锈蚀的切制铜块", -+ "block.minecraft.weathered_cut_copper_slab": "锈蚀的切制铜台阶", -+ "block.minecraft.weathered_cut_copper_stairs": "锈蚀的切制铜楼梯", -+ "block.minecraft.weeping_vines": "垂泪藤", -+ "block.minecraft.weeping_vines_plant": "垂泪藤植株", -+ "block.minecraft.wet_sponge": "湿海绵", -+ "block.minecraft.wheat": "小麦植株", -+ "block.minecraft.white_banner": "白色旗帜", -+ "block.minecraft.white_bed": "白色床", -+ "block.minecraft.white_candle": "白色蜡烛", -+ "block.minecraft.white_candle_cake": "插上白色蜡烛的蛋糕", -+ "block.minecraft.white_carpet": "白色地毯", -+ "block.minecraft.white_concrete": "白色混凝土", -+ "block.minecraft.white_concrete_powder": "白色混凝土粉末", -+ "block.minecraft.white_glazed_terracotta": "白色带釉陶瓦", -+ "block.minecraft.white_shulker_box": "白色潜影盒", -+ "block.minecraft.white_stained_glass": "白色染色玻璃", -+ "block.minecraft.white_stained_glass_pane": "白色染色玻璃板", -+ "block.minecraft.white_terracotta": "白色陶瓦", -+ "block.minecraft.white_tulip": "白色郁金香", -+ "block.minecraft.white_wool": "白色羊毛", -+ "block.minecraft.wither_rose": "凋灵玫瑰", -+ "block.minecraft.wither_skeleton_skull": "凋灵骷髅头颅", -+ "block.minecraft.wither_skeleton_wall_skull": "墙上的凋灵骷髅头颅", -+ "block.minecraft.yellow_banner": "黄色旗帜", -+ "block.minecraft.yellow_bed": "黄色床", -+ "block.minecraft.yellow_candle": "黄色蜡烛", -+ "block.minecraft.yellow_candle_cake": "插上黄色蜡烛的蛋糕", -+ "block.minecraft.yellow_carpet": "黄色地毯", -+ "block.minecraft.yellow_concrete": "黄色混凝土", -+ "block.minecraft.yellow_concrete_powder": "黄色混凝土粉末", -+ "block.minecraft.yellow_glazed_terracotta": "黄色带釉陶瓦", -+ "block.minecraft.yellow_shulker_box": "黄色潜影盒", -+ "block.minecraft.yellow_stained_glass": "黄色染色玻璃", -+ "block.minecraft.yellow_stained_glass_pane": "黄色染色玻璃板", -+ "block.minecraft.yellow_terracotta": "黄色陶瓦", -+ "block.minecraft.yellow_wool": "黄色羊毛", -+ "block.minecraft.zombie_head": "僵尸的头", -+ "block.minecraft.zombie_wall_head": "墙上的僵尸的头", -+ "book.byAuthor": "%1$s 著", -+ "book.editTitle": "输入书名:", -+ "book.finalizeButton": "署名并关闭", -+ "book.finalizeWarning": "注意!在你署名后,它将不能再被修改。", -+ "book.generation.0": "原稿", -+ "book.generation.1": "原稿的副本", -+ "book.generation.2": "副本的副本", -+ "book.generation.3": "破烂不堪", -+ "book.invalid.tag": "* 无效的书本标签 *", -+ "book.pageIndicator": "第%1$s页/共%2$s页", -+ "book.signButton": "署名", -+ "build.tooHigh": "建筑高度限制是%s", -+ "chat.cannotSend": "无法发送聊天消息", -+ "chat.coordinates": "%s, %s, %s", -+ "chat.coordinates.tooltip": "点此传送", -+ "chat.copy": "复制到剪贴板", -+ "chat.copy.click": "单击复制到剪切板", -+ "chat.deleted_marker": "此聊天消息已被服务器删除。", -+ "chat.disabled.chain_broken": "由于消息链损坏,聊天已被禁用。请尝试重新连接。", -+ "chat.disabled.expiredProfileKey": "由于个人信息公钥过期,聊天已被禁用。请尝试重新连接。", -+ "chat.disabled.launcher": "聊天在启动器选项中被禁用。无法发送消息。", -+ "chat.disabled.missingProfileKey": "由于个人信息公钥丢失,聊天已被禁用。请尝试重新连接。", -+ "chat.disabled.options": "聊天在客户端选项中被禁用。", -+ "chat.disabled.profile": "聊天在账户设置中被禁用。再按一次“%s”以获取更多信息。", -+ "chat.disabled.profile.moreInfo": "聊天在账户设置中被禁用。无法发送或查看消息。", -+ "chat.editBox": "聊天", -+ "chat.filtered": "被服务器过滤。", -+ "chat.filtered_full": "服务器已向部分玩家隐藏了你的消息。", -+ "chat.link.confirm": "你确定要打开以下网页?", -+ "chat.link.confirmTrusted": "你想要打开这个链接或将其复制到你的剪贴板吗?", -+ "chat.link.open": "在浏览器中打开", -+ "chat.link.warning": "永远不要打开你不信任的人提供的链接!", -+ "chat.queue": "[+%s行待发送]", -+ "chat.square_brackets": "[%s]", -+ "chat.tag.modified": "消息被服务器修改。原文:", -+ "chat.tag.not_secure": "未经验证的消息。无法举报。", -+ "chat.tag.system": "服务器消息。无法举报。", -+ "chat.tag.system_single_player": "服务器消息。", -+ "chat.type.admin": "[%s: %s]", -+ "chat.type.advancement.challenge": "%s完成了挑战%s", -+ "chat.type.advancement.goal": "%s达成了目标%s", -+ "chat.type.advancement.task": "%s取得了进度%s", -+ "chat.type.announcement": "[%s] %s", -+ "chat.type.emote": "* %s %s", -+ "chat.type.team.hover": "发送队伍消息", -+ "chat.type.team.sent": "-> %s <%s> %s", -+ "chat.type.team.text": "%s <%s> %s", -+ "chat.type.text": "<%s> %s", -+ "chat.type.text.narrate": "%s说%s", -+ "chat_screen.message": "要发送的消息:%s", -+ "chat_screen.title": "聊天屏幕", -+ "chat_screen.usage": "输入消息并按Enter键发送", -+ "clear.failed.multiple": "未能从%s名玩家找到任何物品", -+ "clear.failed.single": "未能从玩家%s找到任何物品", -+ "color.minecraft.black": "黑色", -+ "color.minecraft.blue": "蓝色", -+ "color.minecraft.brown": "棕色", -+ "color.minecraft.cyan": "青色", -+ "color.minecraft.gray": "灰色", -+ "color.minecraft.green": "绿色", -+ "color.minecraft.light_blue": "淡蓝色", -+ "color.minecraft.light_gray": "淡灰色", -+ "color.minecraft.lime": "黄绿色", -+ "color.minecraft.magenta": "品红色", -+ "color.minecraft.orange": "橙色", -+ "color.minecraft.pink": "粉红色", -+ "color.minecraft.purple": "紫色", -+ "color.minecraft.red": "红色", -+ "color.minecraft.white": "白色", -+ "color.minecraft.yellow": "黄色", -+ "command.context.here": "<--[此处]", -+ "command.context.parse_error": "在第%2$s个字符处发现%1$s:%3$s", -+ "command.exception": "无法解析命令:%s", -+ "command.expected.separator": "参数后应有空格分隔,但发现了紧邻的数据", -+ "command.failed": "试图执行该命令时出现意外错误", -+ "command.unknown.argument": "错误的命令参数", -+ "command.unknown.command": "未知或不完整的命令,错误见下", -+ "commands.advancement.advancementNotFound": "没有找到名为“%s”的进度", -+ "commands.advancement.criterionNotFound": "进度%1$s并不包含条件“%2$s”", -+ "commands.advancement.grant.criterion.to.many.failure": "无法将进度%2$s的达成条件“%1$s”赋予%3$s名玩家,因为他们已达成此条件", -+ "commands.advancement.grant.criterion.to.many.success": "已将进度%2$s的达成条件“%1$s”赋予%3$s名玩家", -+ "commands.advancement.grant.criterion.to.one.failure": "无法将进度%2$s的达成条件“%1$s”赋予%3$s,因为该玩家已达成此条件", -+ "commands.advancement.grant.criterion.to.one.success": "已将进度%2$s的达成条件“%1$s”赋予%3$s", -+ "commands.advancement.grant.many.to.many.failure": "无法将%s项进度赋予%s名玩家,因为他们已达成这些进度", -+ "commands.advancement.grant.many.to.many.success": "已将%s项进度赋予%s名玩家", -+ "commands.advancement.grant.many.to.one.failure": "无法将%s项进度赋予%s,因为该玩家已达成这些进度", -+ "commands.advancement.grant.many.to.one.success": "已将%s项进度赋予%s", -+ "commands.advancement.grant.one.to.many.failure": "无法将进度%s赋予%s名玩家,因为他们已达成此进度", -+ "commands.advancement.grant.one.to.many.success": "已将进度%s赋予%s名玩家", -+ "commands.advancement.grant.one.to.one.failure": "无法将进度%s赋予%s,因为该玩家已达成此进度", -+ "commands.advancement.grant.one.to.one.success": "已将进度%s赋予%s", -+ "commands.advancement.revoke.criterion.to.many.failure": "无法撤销%3$s名玩家关于进度%2$s的达成条件“%1$s”,因为该玩家并未达成此条件", -+ "commands.advancement.revoke.criterion.to.many.success": "已撤销%3$s名玩家关于进度%2$s的达成条件“%1$s”", -+ "commands.advancement.revoke.criterion.to.one.failure": "无法撤销%3$s关于进度%2$s的达成条件“%1$s”,因为该玩家并未达成此条件", -+ "commands.advancement.revoke.criterion.to.one.success": "已撤销%3$s关于进度%2$s的达成条件“%1$s”", -+ "commands.advancement.revoke.many.to.many.failure": "无法撤销%2$s名玩家的%1$s项进度,因为他们并未达成此进度", -+ "commands.advancement.revoke.many.to.many.success": "已撤销%2$s名玩家的%1$s项进度", -+ "commands.advancement.revoke.many.to.one.failure": "无法撤销%2$s的%1$s项进度,因为该玩家并未达成这些进度", -+ "commands.advancement.revoke.many.to.one.success": "已撤销%2$s的%1$s项进度", -+ "commands.advancement.revoke.one.to.many.failure": "无法撤销%2$s名玩家的进度%1$s,因为他们并未达成此进度", -+ "commands.advancement.revoke.one.to.many.success": "已撤销%2$s名玩家的进度%1$s", -+ "commands.advancement.revoke.one.to.one.failure": "无法撤销%2$s的进度%1$s,因为该玩家并未达成此进度", -+ "commands.advancement.revoke.one.to.one.success": "已撤销%2$s的进度%1$s", -+ "commands.attribute.base_value.get.success": "实体%2$s的属性%1$s的基值为%3$s", -+ "commands.attribute.base_value.set.success": "实体%2$s的属性%1$s的基值已设置为%3$s", -+ "commands.attribute.failed.entity": "%s不是此命令的有效实体", -+ "commands.attribute.failed.modifier_already_present": "实体%3$s的属性%2$s已存在修饰符%1$s", -+ "commands.attribute.failed.no_attribute": "实体%s没有属性%s", -+ "commands.attribute.failed.no_modifier": "实体%2$s的属性%1$s无修饰符%3$s", -+ "commands.attribute.modifier.add.success": "为实体%3$s的属性%2$s添加了修饰符%1$s", -+ "commands.attribute.modifier.remove.success": "为实体%3$s的属性%2$s移除了修饰符%1$s", -+ "commands.attribute.modifier.value.get.success": "实体%3$s的属性%2$s中修饰符%1$s值为%4$s", -+ "commands.attribute.value.get.success": "实体%2$s的属性%1$s的值为%3$s", -+ "commands.ban.failed": "无变化,该玩家已被封禁", -+ "commands.ban.success": "已封禁%s:%s", -+ "commands.banip.failed": "无变化,该IP地址已被封禁", -+ "commands.banip.info": "此次封禁涉及%s名玩家:%s", -+ "commands.banip.invalid": "无效的IP地址或未知的玩家", -+ "commands.banip.success": "已封禁IP地址%s:%s", -+ "commands.banlist.entry": "%s被%s封禁:%s", -+ "commands.banlist.list": "共有%s条封禁:", -+ "commands.banlist.none": "没有玩家被封禁", -+ "commands.bossbar.create.failed": "ID为“%s”的Boss栏已经存在", -+ "commands.bossbar.create.success": "已创建自定义Boss栏%s", -+ "commands.bossbar.get.max": "自定义Boss栏%s的最大值为%s", -+ "commands.bossbar.get.players.none": "自定义Boss栏%s目前没有在线玩家", -+ "commands.bossbar.get.players.some": "自定义Boss栏%s当前在线的%s名玩家有:%s", -+ "commands.bossbar.get.value": "自定义Boss栏%s的数值为%s", -+ "commands.bossbar.get.visible.hidden": "自定义Boss栏%s现为隐藏", -+ "commands.bossbar.get.visible.visible": "自定义Boss栏%s现为可见", -+ "commands.bossbar.list.bars.none": "无运行中的自定义Boss栏", -+ "commands.bossbar.list.bars.some": "有%s项运行中的自定义Boss栏:%s", -+ "commands.bossbar.remove.success": "已移除自定义Boss栏%s", -+ "commands.bossbar.set.color.success": "已更改自定义Boss栏%s的颜色", -+ "commands.bossbar.set.color.unchanged": "无变化,这本就是这个Boss栏的颜色", -+ "commands.bossbar.set.max.success": "自定义Boss栏%s的最大值已改为%s", -+ "commands.bossbar.set.max.unchanged": "无变化,这本就是这个Boss栏的最大值", -+ "commands.bossbar.set.name.success": "已重命名自定义Boss栏%s", -+ "commands.bossbar.set.name.unchanged": "无变化,这本就是这个Boss栏的名称", -+ "commands.bossbar.set.players.success.none": "自定义Boss栏%s在当下没有在线玩家", -+ "commands.bossbar.set.players.success.some": "自定义Boss栏%s在当下在线的%s名玩家有:%s", -+ "commands.bossbar.set.players.unchanged": "无变化,这些玩家已经在Boss栏上,没有玩家需要被添加或移除", -+ "commands.bossbar.set.style.success": "已改变自定义Boss栏%s的样式", -+ "commands.bossbar.set.style.unchanged": "无变化,这本就是这个Boss栏的样式", -+ "commands.bossbar.set.value.success": "自定义Boss栏%s的值已改为%s", -+ "commands.bossbar.set.value.unchanged": "无变化,这本就是这个Boss栏的值", -+ "commands.bossbar.set.visibility.unchanged.hidden": "无变化,该Boss栏原已隐藏", -+ "commands.bossbar.set.visibility.unchanged.visible": "无变化,该Boss栏原已可见", -+ "commands.bossbar.set.visible.success.hidden": "已将自定义Boss栏%s改为隐藏", -+ "commands.bossbar.set.visible.success.visible": "已将自定义Boss栏%s改为可见", -+ "commands.bossbar.unknown": "不存在ID为“%s”的Boss栏", -+ "commands.clear.success.multiple": "已移除%2$s名玩家的%1$s个物品", -+ "commands.clear.success.single": "已移除玩家%2$s的%1$s个物品", -+ "commands.clear.test.multiple": "已在%2$s名玩家身上找到%1$s个匹配的物品", -+ "commands.clear.test.single": "已在玩家%2$s身上找到%1$s个匹配的物品", -+ "commands.clone.failed": "未复制任何方块", -+ "commands.clone.overlap": "源区域和目标区域不能重叠", -+ "commands.clone.success": "已成功复制%s个方块", -+ "commands.clone.toobig": "指定区域内的方块太多(最大值为%s,指定值为%s)", -+ "commands.damage.invulnerable": "对象免疫指定的伤害类型", -+ "commands.damage.success": "已将%s伤害施加于%s", -+ "commands.data.block.get": "位于%2$s, %3$s, %4$s的方块的%1$s乘以%5$s倍率后的值为%6$s", -+ "commands.data.block.invalid": "目标方块不是方块实体", -+ "commands.data.block.modified": "已修改%s, %s, %s处的方块数据", -+ "commands.data.block.query": "%s, %s, %s拥有以下方块数据:%s", -+ "commands.data.entity.get": "%2$s的%1$s在乘以倍率%3$s后的值是%4$s", -+ "commands.data.entity.invalid": "无法修改玩家数据", -+ "commands.data.entity.modified": "已修改%s的实体数据", -+ "commands.data.entity.query": "%s拥有以下实体数据:%s", -+ "commands.data.get.invalid": "无法获取%s,只接受数字标签", -+ "commands.data.get.multiple": "该参数只接受单个NBT值", -+ "commands.data.get.unknown": "无法获取%s,标签不存在", -+ "commands.data.merge.failed": "无变化,所指定的属性已有这些值", -+ "commands.data.modify.expected_list": "应为列表,实际为:%s", -+ "commands.data.modify.expected_object": "应为对象,实际为:%s", -+ "commands.data.modify.expected_value": "应为值,实际值:%s", -+ "commands.data.modify.invalid_index": "无效的列表索引:%s", -+ "commands.data.modify.invalid_substring": "无效的子串索引:%s到%s", -+ "commands.data.storage.get": "在存储%2$s中的%1$s在乘以倍率%3$s后的值是%4$s", -+ "commands.data.storage.modified": "已修改存储%s", -+ "commands.data.storage.query": "存储%s含有以下内容:%s", -+ "commands.datapack.disable.failed": "数据包“%s”并未启用!", -+ "commands.datapack.enable.failed": "数据包“%s”已经启用!", -+ "commands.datapack.enable.failed.no_flags": "无法启用数据包“%s”,因为所需的功能未在此世界启用:%s!", -+ "commands.datapack.list.available.none": "已无更多可用的数据包", -+ "commands.datapack.list.available.success": "共有%s个数据包可用:%s", -+ "commands.datapack.list.enabled.none": "没有启用中的数据包", -+ "commands.datapack.list.enabled.success": "已启用%s个数据包:%s", -+ "commands.datapack.modify.disable": "正在禁用数据包%s", -+ "commands.datapack.modify.enable": "正在启用数据包%s", -+ "commands.datapack.unknown": "未知的数据包 “%s”", -+ "commands.debug.alreadyRunning": "刻分析器原已开启", -+ "commands.debug.function.noRecursion": "无法从函数内部开始追踪", -+ "commands.debug.function.success.multiple": "已追踪%2$s个函数内的%1$s条命令至输出文件%3$s", -+ "commands.debug.function.success.single": "已追踪函数“%2$s”内的%1$s条命令至输出文件%3$s", -+ "commands.debug.function.traceFailed": "追踪函数失败", -+ "commands.debug.notRunning": "尚未启动刻分析器", -+ "commands.debug.started": "已开始刻分析", -+ "commands.debug.stopped": "已停止刻分析,用时%s秒和%s刻(每秒%s刻)", -+ "commands.defaultgamemode.success": "默认游戏模式现在为%s", -+ "commands.deop.failed": "无变化,此玩家不是管理员", -+ "commands.deop.success": "%s不再是服务器管理员了", -+ "commands.difficulty.failure": "难度未变,它原来就是%s", -+ "commands.difficulty.query": "目前难度为%s", -+ "commands.difficulty.success": "难度已被设置为%s", -+ "commands.drop.no_held_items": "该实体无法持有任何物品", -+ "commands.drop.no_loot_table": "实体%s没有战利品表", -+ "commands.drop.success.multiple": "掉落了%s个物品", -+ "commands.drop.success.multiple_with_table": "从战利品表%2$s中掉落了%1$s个物品", -+ "commands.drop.success.single": "掉落了%s个%s", -+ "commands.drop.success.single_with_table": "从战利品表%3$s中掉落了%1$s个%2$s", -+ "commands.effect.clear.everything.failed": "对象没有可以移除的效果", -+ "commands.effect.clear.everything.success.multiple": "已移除%s个对象的所有效果", -+ "commands.effect.clear.everything.success.single": "已移除%s的所有效果", -+ "commands.effect.clear.specific.failed": "对象没有指定效果", -+ "commands.effect.clear.specific.success.multiple": "已移除%2$s个对象的%1$s效果", -+ "commands.effect.clear.specific.success.single": "已移除%2$s的%1$s效果", -+ "commands.effect.give.failed": "无法应用此效果(对象对效果免疫,或已有更强的效果)", -+ "commands.effect.give.success.multiple": "已将%s效果应用于%s个对象", -+ "commands.effect.give.success.single": "已将%s效果应用于%s", -+ "commands.enchant.failed": "无变化,对象未持有物品或其不支持此魔咒", -+ "commands.enchant.failed.entity": "%s不是此命令的有效实体", -+ "commands.enchant.failed.incompatible": "%s不支持此魔咒", -+ "commands.enchant.failed.itemless": "%s未手持任何物品", -+ "commands.enchant.failed.level": "%s高于该魔咒支持的最高等级%s", -+ "commands.enchant.success.multiple": "已将%s魔咒应用于%s个实体", -+ "commands.enchant.success.single": "已将%s魔咒应用于%s的物品上", -+ "commands.execute.blocks.toobig": "指定区域内的方块太多(最大值为%s,指定值为%s)", -+ "commands.execute.conditional.fail": "测试失败", -+ "commands.execute.conditional.fail_count": "测试失败,计数:%s", -+ "commands.execute.conditional.pass": "测试通过", -+ "commands.execute.conditional.pass_count": "测试通过,计数:%s", -+ "commands.experience.add.levels.success.multiple": "已给予%2$s名玩家%1$s级经验", -+ "commands.experience.add.levels.success.single": "已给予%2$s %1$s级经验", -+ "commands.experience.add.points.success.multiple": "已给予%2$s名玩家%1$s点经验", -+ "commands.experience.add.points.success.single": "已给予%2$s %1$s点经验", -+ "commands.experience.query.levels": "%s拥有%s级经验", -+ "commands.experience.query.points": "%s拥有%s点经验值", -+ "commands.experience.set.levels.success.multiple": "已将%2$s名玩家的经验等级设为%1$s", -+ "commands.experience.set.levels.success.single": "已将%2$s的经验等级设为%1$s", -+ "commands.experience.set.points.invalid": "无法将此玩家的经验值设置为超过现有等级的最大值", -+ "commands.experience.set.points.success.multiple": "已将%2$s名玩家的经验值设为%1$s", -+ "commands.experience.set.points.success.single": "已将%2$s的经验值设为%1$s", -+ "commands.fill.failed": "没有方块被填充", -+ "commands.fill.success": "已成功填充%s个方块", -+ "commands.fill.toobig": "指定区域内的方块太多(最大值为%s,指定值为%s)", -+ "commands.fillbiome.success": "已在%s, %s, %s与%s, %s, %s之间设置生物群系", -+ "commands.fillbiome.success.count": "已在%2$s, %3$s, %4$s与%5$s, %6$s, %7$s之间设置%1$s个生物群系单元", -+ "commands.fillbiome.toobig": "指定范围内的方块过多(最大值为%s,指定值为%s)", -+ "commands.forceload.added.failure": "没有被标记为强制加载的区块", -+ "commands.forceload.added.multiple": "已将%2$s中的%3$s至%4$s间的%1$s个区块标记为强制加载", -+ "commands.forceload.added.none": "在%s中未找到强制加载的区块", -+ "commands.forceload.added.single": "已将%2$s中的区块%1$s标记为强制加载", -+ "commands.forceload.list.multiple": "在%2$s内找到%1$s个强制加载的区块:%3$s", -+ "commands.forceload.list.single": "在%s内找到一个强制加载的区块:%s", -+ "commands.forceload.query.failure": "在%2$s中%1$s内的区块未被标记为强制加载", -+ "commands.forceload.query.success": "在%2$s中%1$s内的区块被标记为强制加载", -+ "commands.forceload.removed.all": "已解除标记%s内所有的强制加载区块", -+ "commands.forceload.removed.failure": "没有强制加载的区块被移除", -+ "commands.forceload.removed.multiple": "已将%2$s中的%3$s至%4$s间的%1$s个区块取消强制加载", -+ "commands.forceload.removed.single": "已将%2$s中的区块%1$s解除强制加载", -+ "commands.forceload.toobig": "指定区域内区块过多(最大值为%s,指定值为%s)", -+ "commands.function.success.multiple": "已执行%2$s个函数中的%1$s条命令", -+ "commands.function.success.multiple.result": "执行了%s个函数", -+ "commands.function.success.single": "已执行函数%2$s中的%1$s条命令", -+ "commands.function.success.single.result": "函数“%2$s”返回了%1$s", -+ "commands.gamemode.success.other": "已将%s的游戏模式改为%s", -+ "commands.gamemode.success.self": "已将自己的游戏模式设置为%s", -+ "commands.gamerule.query": "游戏规则%s目前为:%s", -+ "commands.gamerule.set": "游戏规则%s已被设为:%s", -+ "commands.give.failed.toomanyitems": "最多只能给予%s个%s", -+ "commands.give.success.multiple": "已将%s个%s给予%s名玩家", -+ "commands.give.success.single": "已将%s个%s给予%s", -+ "commands.help.failed": "未知的命令或权限不足", -+ "commands.item.block.set.success": "已用%4$s替换了位于%1$s, %2$s, %3$s的槽位", -+ "commands.item.entity.set.success.multiple": "已用%2$s替换了%1$s个实体的槽位", -+ "commands.item.entity.set.success.single": "已用%2$s替换了%1$s的槽位", -+ "commands.item.source.no_such_slot": "来源没有%s槽位", -+ "commands.item.source.not_a_container": "在来源位置%s, %s, %s上的不是一个容器", -+ "commands.item.target.no_changed.known_item": "没有对象在%2$s槽位接受了物品%1$s", -+ "commands.item.target.no_changes": "没有对象在%s槽位接受了物品", -+ "commands.item.target.no_such_slot": "对象没有%s槽位", -+ "commands.item.target.not_a_container": "在目标位置%s, %s, %s上的不是一个容器", -+ "commands.jfr.dump.failed": "无法转储JFR记录:%s", -+ "commands.jfr.start.failed": "无法开始JFR分析", -+ "commands.jfr.started": "已开始JFR分析", -+ "commands.jfr.stopped": "JFR分析已结束,已转储至%s", -+ "commands.kick.success": "已踢出%s:%s", -+ "commands.kill.success.multiple": "杀死了%s个实体", -+ "commands.kill.success.single": "杀死了%s", -+ "commands.list.nameAndId": "%s(%s)", -+ "commands.list.players": "当前共有%s名玩家在线(最大玩家数为%s):%s", -+ "commands.locate.biome.not_found": "无法在合理距离内找到类型为“%s”的生物群系", -+ "commands.locate.biome.success": "最近的%s位于%s(%s个方块外)", -+ "commands.locate.poi.not_found": "无法在合理距离内找到类型为“%s”的兴趣点", -+ "commands.locate.poi.success": "最近的%s位于%s(%s个方块外)", -+ "commands.locate.structure.invalid": "没有类型为“%s”的结构", -+ "commands.locate.structure.not_found": "无法在附近找到类型为“%s”的结构", -+ "commands.locate.structure.success": "最近的%s位于%s(%s个方块外)", -+ "commands.message.display.incoming": "%s悄悄地对你说:%s", -+ "commands.message.display.outgoing": "你悄悄地对%s说:%s", -+ "commands.op.failed": "无变化,该玩家已是管理员", -+ "commands.op.success": "已将%s设为服务器管理员", -+ "commands.pardon.failed": "无变化,该玩家未被封禁", -+ "commands.pardon.success": "已解封%s", -+ "commands.pardonip.failed": "无变化,该IP地址未被封禁", -+ "commands.pardonip.invalid": "无效的IP地址", -+ "commands.pardonip.success": "已解封IP地址%s", -+ "commands.particle.failed": "该粒子无法被任何玩家看见", -+ "commands.particle.success": "正在显示粒子%s", -+ "commands.perf.alreadyRunning": "性能分析器已在运行", -+ "commands.perf.notRunning": "性能分析器尚未启动", -+ "commands.perf.reportFailed": "生成调试报告失败", -+ "commands.perf.reportSaved": "已在%s生成调试报告", -+ "commands.perf.started": "已开始时长为10秒的性能分析测试(使用“/perf stop”以提前结束)", -+ "commands.perf.stopped": "已停止性能分析,用时%s秒和%s刻(每秒%s刻)", -+ "commands.place.feature.failed": "放置地物失败", -+ "commands.place.feature.invalid": "没有类型为“%s”的地物", -+ "commands.place.feature.success": "已在%2$s, %3$s, %4$s处放置“%1$s”", -+ "commands.place.jigsaw.failed": "生成拼图失败", -+ "commands.place.jigsaw.invalid": "没有类型为“%s”的模板池", -+ "commands.place.jigsaw.success": "已在%s, %s, %s处生成拼图", -+ "commands.place.structure.failed": "放置结构失败", -+ "commands.place.structure.invalid": "没有类型为“%s”的结构", -+ "commands.place.structure.success": "已在%2$s, %3$s, %4$s处生成结构“%1$s”", -+ "commands.place.template.failed": "放置模板失败", -+ "commands.place.template.invalid": "没有ID为“%s”的模板", -+ "commands.place.template.success": "已在%2$s, %3$s, %4$s处加载模板“%1$s”", -+ "commands.playsound.failed": "声音过远而无法被听见", -+ "commands.playsound.success.multiple": "已将声音%s播放给%s名玩家", -+ "commands.playsound.success.single": "已将声音%s播放给%s", -+ "commands.publish.alreadyPublished": "已存在开放于%s端口的多人游戏", -+ "commands.publish.failed": "无法建立本地游戏", -+ "commands.publish.started": "本地游戏已在端口%s上开启", -+ "commands.publish.success": "多人游戏已在%s端口上开启", -+ "commands.recipe.give.failed": "没有新配方被解锁", -+ "commands.recipe.give.success.multiple": "已为%2$s名玩家解锁了%1$s条配方", -+ "commands.recipe.give.success.single": "已为%2$s解锁了%1$s条配方", -+ "commands.recipe.take.failed": "没有可遗忘的配方", -+ "commands.recipe.take.success.multiple": "已剥夺%2$s名玩家的%1$s条配方", -+ "commands.recipe.take.success.single": "已剥夺%2$s的%1$s条配方", -+ "commands.reload.failure": "重新加载失败,保留原有数据", -+ "commands.reload.success": "重新加载中!", -+ "commands.ride.already_riding": "%s已经在骑乘%s", -+ "commands.ride.dismount.success": "%s已停止骑乘%s", -+ "commands.ride.mount.failure.cant_ride_players": "玩家无法被骑乘", -+ "commands.ride.mount.failure.generic": "%s无法骑乘%s", -+ "commands.ride.mount.failure.loop": "无法让实体骑乘自身或其乘客", -+ "commands.ride.mount.failure.wrong_dimension": "无法骑乘位于不同维度的实体", -+ "commands.ride.mount.success": "%s已开始骑乘%s", -+ "commands.ride.not_riding": "%s未骑乘任何载具", -+ "commands.save.alreadyOff": "已经关闭世界保存", -+ "commands.save.alreadyOn": "已经打开世界保存", -+ "commands.save.disabled": "自动保存已禁用", -+ "commands.save.enabled": "自动保存已启用", -+ "commands.save.failed": "无法保存游戏(硬盘空间是否足够?)", -+ "commands.save.saving": "正在保存游戏(这可能需要一些时间!)", -+ "commands.save.success": "游戏已保存", -+ "commands.schedule.cleared.failure": "没有ID为%s的计划", -+ "commands.schedule.cleared.success": "已移除%s个ID为%s的计划", -+ "commands.schedule.created.function": "已将函数“%s”计划在%s刻后,即游戏时间%s时执行", -+ "commands.schedule.created.tag": "已将标签“%s”计划在%s刻后,即游戏时间%s时执行", -+ "commands.schedule.same_tick": "无法将函数计划在当前刻", -+ "commands.scoreboard.objectives.add.duplicate": "已经存在同名记分项", -+ "commands.scoreboard.objectives.add.success": "创建了新的记分项%s", -+ "commands.scoreboard.objectives.display.alreadyEmpty": "无变化,该显示位置本就是空的", -+ "commands.scoreboard.objectives.display.alreadySet": "无变化,该显示位置已经存在该记分项", -+ "commands.scoreboard.objectives.display.cleared": "清空了显示位置%s的所有记分项", -+ "commands.scoreboard.objectives.display.set": "已将显示位置%s设置为展示记分项%s", -+ "commands.scoreboard.objectives.list.empty": "不存在任何记分项", -+ "commands.scoreboard.objectives.list.success": "共有%s个记分项:%s", -+ "commands.scoreboard.objectives.modify.displayname": "已将%s的显示名称更改为%s", -+ "commands.scoreboard.objectives.modify.rendertype": "已更改记分项%s的渲染类型", -+ "commands.scoreboard.objectives.remove.success": "移除了记分项%s", -+ "commands.scoreboard.players.add.success.multiple": "将%3$s个实体的%2$s增加了%1$s", -+ "commands.scoreboard.players.add.success.single": "将%3$s的%2$s增加了%1$s(现在是%4$s)", -+ "commands.scoreboard.players.enable.failed": "无变化,触发器原已开启", -+ "commands.scoreboard.players.enable.invalid": "只能启用trigger类记分项", -+ "commands.scoreboard.players.enable.success.multiple": "已为%2$s个实体启用了触发器%1$s", -+ "commands.scoreboard.players.enable.success.single": "已为%2$s启用了触发器%1$s", -+ "commands.scoreboard.players.get.null": "无法获取%2$s的%1$s的值,其尚未被赋值", -+ "commands.scoreboard.players.get.success": "%1$s在%3$s记分项里拥有%2$s分", -+ "commands.scoreboard.players.list.empty": "没有正被追踪的实体", -+ "commands.scoreboard.players.list.entity.empty": "%s无可显示的分数", -+ "commands.scoreboard.players.list.entity.entry": "%s:%s", -+ "commands.scoreboard.players.list.entity.success": "%s拥有%s项分数:", -+ "commands.scoreboard.players.list.success": "共有%s个正被追踪的实体:%s", -+ "commands.scoreboard.players.operation.success.multiple": "更新了%2$s个实体的%1$s", -+ "commands.scoreboard.players.operation.success.single": "已将%2$s的%1$s设为%3$s", -+ "commands.scoreboard.players.remove.success.multiple": "已将%3$s个实体的%2$s分数减少%1$s", -+ "commands.scoreboard.players.remove.success.single": "已将%3$s的%2$s减少%1$s(现在是%4$s)", -+ "commands.scoreboard.players.reset.all.multiple": "重置了%s个实体的所有分数", -+ "commands.scoreboard.players.reset.all.single": "重置了%s的所有分数", -+ "commands.scoreboard.players.reset.specific.multiple": "重置了%2$s个实体的%1$s", -+ "commands.scoreboard.players.reset.specific.single": "重置了%2$s的%1$s", -+ "commands.scoreboard.players.set.success.multiple": "已将%2$s个实体的%1$s设为%3$s", -+ "commands.scoreboard.players.set.success.single": "已将%2$s的%1$s分数设为%3$s", -+ "commands.seed.success": "种子:%s", -+ "commands.setblock.failed": "无法放置方块", -+ "commands.setblock.success": "更改了位于%s, %s, %s的方块", -+ "commands.setidletimeout.success": "玩家的闲置限时现在为%s分钟", -+ "commands.setworldspawn.success": "已将世界的出生点设置为%s, %s, %s [%s]", -+ "commands.spawnpoint.success.multiple": "已将%6$s名玩家在%5$s的出生点设为%1$s, %2$s, %3$s [%4$s]", -+ "commands.spawnpoint.success.single": "已将%6$s在%5$s的出生点设为%1$s, %2$s, %3$s [%4$s]", -+ "commands.spectate.not_spectator": "%s尚未处于旁观模式", -+ "commands.spectate.self": "不能旁观自己", -+ "commands.spectate.success.started": "正在旁观%s", -+ "commands.spectate.success.stopped": "不再旁观实体", -+ "commands.spreadplayers.failed.entities": "%1$s个实体未能围绕%2$s, %3$s分散(空间过小而实体过多,请将分散间距设为%4$s以下)", -+ "commands.spreadplayers.failed.invalid.height": "无效的maxHeight值:%s,应高于世界最小高度%s", -+ "commands.spreadplayers.failed.teams": "%1$s支队伍未能围绕%2$s, %3$s分散(空间过小而实体过多,请将分散间距设为%4$s以下)", -+ "commands.spreadplayers.success.entities": "已将%1$s名玩家围绕%2$s, %3$s分散,平均距离为%4$s个方块", -+ "commands.spreadplayers.success.teams": "已将%1$s支队伍围绕%2$s, %3$s分散,平均距离为%4$s个方块", -+ "commands.stop.stopping": "正在关闭服务器", -+ "commands.stopsound.success.source.any": "已停止播放所有“%s”的声音", -+ "commands.stopsound.success.source.sound": "已停止播放来源为“%2$s”的音效“%1$s”", -+ "commands.stopsound.success.sourceless.any": "已停止播放所有声音", -+ "commands.stopsound.success.sourceless.sound": "已停止播放声音“%s”", -+ "commands.summon.failed": "无法召唤实体", -+ "commands.summon.failed.uuid": "UUID重复,无法召唤实体", -+ "commands.summon.invalidPosition": "无效的召唤坐标", -+ "commands.summon.success": "召唤了新的%s", -+ "commands.tag.add.failed": "对象已拥有此标签或拥有过多标签", -+ "commands.tag.add.success.multiple": "已为%2$s个实体添加了标签“%1$s”", -+ "commands.tag.add.success.single": "已为%2$s添加了标签“%1$s”", -+ "commands.tag.list.multiple.empty": "%s个实体没有任何标签", -+ "commands.tag.list.multiple.success": "%s个实体拥有共计%s项标签:%s", -+ "commands.tag.list.single.empty": "%s没有标签", -+ "commands.tag.list.single.success": "%s拥有%s个标签:%s", -+ "commands.tag.remove.failed": "对象没有这个标签", -+ "commands.tag.remove.success.multiple": "已移除%2$s个实体的标签“%1$s”", -+ "commands.tag.remove.success.single": "已移除%2$s的标签“%1$s”", -+ "commands.team.add.duplicate": "已经存在同名队伍", -+ "commands.team.add.success": "已创建队伍%s", -+ "commands.team.empty.success": "已将%s名成员从队伍%s中移除", -+ "commands.team.empty.unchanged": "无变化,该队伍本就是空的", -+ "commands.team.join.success.multiple": "已将%s名成员加入队伍%s", -+ "commands.team.join.success.single": "已将%s加入队伍%s", -+ "commands.team.leave.success.multiple": "已将%s名成员从所有队伍中移除", -+ "commands.team.leave.success.single": "已将%s从所有队伍中移除", -+ "commands.team.list.members.empty": "队伍%s中没有成员", -+ "commands.team.list.members.success": "队伍%s含有%s名成员:%s", -+ "commands.team.list.teams.empty": "没有队伍存在", -+ "commands.team.list.teams.success": "共有%s支队伍:%s", -+ "commands.team.option.collisionRule.success": "队伍%s的碰撞规则现在是“%s”", -+ "commands.team.option.collisionRule.unchanged": "无变化,碰撞规则已经是此值", -+ "commands.team.option.color.success": "队伍%s的颜色已更新为%s", -+ "commands.team.option.color.unchanged": "无变化,此队伍本就为此颜色", -+ "commands.team.option.deathMessageVisibility.success": "队伍%s的死亡消息可见性现在为“%s”", -+ "commands.team.option.deathMessageVisibility.unchanged": "无变化,死亡消息的可见性已经是此值", -+ "commands.team.option.friendlyfire.alreadyDisabled": "无变化,友军伤害本就在此队伍上禁用", -+ "commands.team.option.friendlyfire.alreadyEnabled": "无变化,友军伤害本就在此队伍上启用", -+ "commands.team.option.friendlyfire.disabled": "已禁用队伍%s的友军伤害", -+ "commands.team.option.friendlyfire.enabled": "已启用队伍%s的友军伤害", -+ "commands.team.option.name.success": "已更新队伍%s的名称", -+ "commands.team.option.name.unchanged": "无变化。此队伍本就为该名称。", -+ "commands.team.option.nametagVisibility.success": "队伍%s的名称标签可见性现在为“%s”", -+ "commands.team.option.nametagVisibility.unchanged": "无变化,名称标签的可见性已经是此值", -+ "commands.team.option.prefix.success": "队伍前缀已设为%s", -+ "commands.team.option.seeFriendlyInvisibles.alreadyDisabled": "无变化,此队伍本就不可看见隐身的队友", -+ "commands.team.option.seeFriendlyInvisibles.alreadyEnabled": "无变化,此队伍本就可以看见隐身的队友", -+ "commands.team.option.seeFriendlyInvisibles.disabled": "队伍%s不再可以看见隐身的队友了", -+ "commands.team.option.seeFriendlyInvisibles.enabled": "队伍%s现在可以看见隐身的队友了", -+ "commands.team.option.suffix.success": "队伍后缀已设为%s", -+ "commands.team.remove.success": "移除了队伍%s", -+ "commands.teammsg.failed.noteam": "你必须在一支队伍内才能发出队伍消息", -+ "commands.teleport.invalidPosition": "无效的传送坐标", -+ "commands.teleport.success.entity.multiple": "已传送%s个实体至%s", -+ "commands.teleport.success.entity.single": "已将%s传送至%s", -+ "commands.teleport.success.location.multiple": "已传送%s个实体至%s, %s, %s", -+ "commands.teleport.success.location.single": "已将%s传送到%s, %s, %s", -+ "commands.time.query": "当前时间为%s", -+ "commands.time.set": "已将时间设为%s", -+ "commands.title.cleared.multiple": "已清除%s名玩家的标题", -+ "commands.title.cleared.single": "已清除%s的标题", -+ "commands.title.reset.multiple": "已重置%s名玩家的标题设置", -+ "commands.title.reset.single": "已重置%s的标题设置", -+ "commands.title.show.actionbar.multiple": "正在向%s名玩家显示新的动作栏标题", -+ "commands.title.show.actionbar.single": "正在向%s显示新的动作栏标题", -+ "commands.title.show.subtitle.multiple": "正在向%s名玩家显示新的副标题", -+ "commands.title.show.subtitle.single": "正在向%s显示新的副标题", -+ "commands.title.show.title.multiple": "正在向%s名玩家显示新的标题", -+ "commands.title.show.title.single": "正在向%s显示新的标题", -+ "commands.title.times.multiple": "已更改%s名玩家的标题显示时间", -+ "commands.title.times.single": "已更改%s的标题显示时间", -+ "commands.trigger.add.success": "已触发%s(数值已增加%s)", -+ "commands.trigger.failed.invalid": "你只能触发'trigger'类型的记分项", -+ "commands.trigger.failed.unprimed": "你尚无法触发这个记分项", -+ "commands.trigger.set.success": "已触发%s(数值已设为%s)", -+ "commands.trigger.simple.success": "已触发%s", -+ "commands.weather.set.clear": "天气已设为晴天", -+ "commands.weather.set.rain": "天气已设为雨天", -+ "commands.weather.set.thunder": "天气已设为雷雨", -+ "commands.whitelist.add.failed": "玩家已在白名单内", -+ "commands.whitelist.add.success": "已将%s加入白名单", -+ "commands.whitelist.alreadyOff": "白名单原已关闭", -+ "commands.whitelist.alreadyOn": "白名单原已开启", -+ "commands.whitelist.disabled": "白名单已关闭", -+ "commands.whitelist.enabled": "白名单已开启", -+ "commands.whitelist.list": "白名单中共有%s名玩家:%s", -+ "commands.whitelist.none": "白名单中没有玩家", -+ "commands.whitelist.reloaded": "已重新读取白名单", -+ "commands.whitelist.remove.failed": "玩家不在白名单内", -+ "commands.whitelist.remove.success": "已将%s移出白名单", -+ "commands.worldborder.center.failed": "无变化,世界边界中心点已经在该处", -+ "commands.worldborder.center.success": "已将世界边界的中心设为%s, %s", -+ "commands.worldborder.damage.amount.failed": "无变化,世界边界伤害已经是此值", -+ "commands.worldborder.damage.amount.success": "已将世界边界的伤害设置为%s每秒每个方块", -+ "commands.worldborder.damage.buffer.failed": "无变化,世界边界伤害缓冲区已经是此距离", -+ "commands.worldborder.damage.buffer.success": "已将世界边界的伤害缓冲区设置为%s个方块", -+ "commands.worldborder.get": "世界边界的当前宽度为%s个方块", -+ "commands.worldborder.set.failed.big": "世界边界的宽度不能大于%s格", -+ "commands.worldborder.set.failed.far": "世界边界的位置不能远于%s格", -+ "commands.worldborder.set.failed.nochange": "无变化,世界边界已经是此大小", -+ "commands.worldborder.set.failed.small": "世界边界的宽度不能小于1格", -+ "commands.worldborder.set.grow": "正在将世界边界的宽度扩大为%s个方块,时间%s秒", -+ "commands.worldborder.set.immediate": "已将世界边界的宽度设为%s", -+ "commands.worldborder.set.shrink": "正在将世界边界的宽度缩小为%s个方块,用时%s秒", -+ "commands.worldborder.warning.distance.failed": "无变化,世界边界伤害警告区已经是此距离", -+ "commands.worldborder.warning.distance.success": "已将世界边界的警告距离设置为%s个方块", -+ "commands.worldborder.warning.time.failed": "无变化,世界边界警告时间已经是此时长", -+ "commands.worldborder.warning.time.success": "已将世界边界的警告时间设置为%s秒", -+ "compliance.playtime.greaterThan24Hours": "你的游戏时长已超过24小时", -+ "compliance.playtime.hours": "你的游戏时长已达%s小时", -+ "compliance.playtime.message": "适度游戏益脑,沉迷游戏伤身", -+ "connect.aborted": "连接中断", -+ "connect.authorizing": "登录中…", -+ "connect.connecting": "正在连接到服务器…", -+ "connect.encrypting": "通讯加密中…", -+ "connect.failed": "无法连接至服务器", -+ "connect.joining": "加入世界中…", -+ "connect.negotiating": "连接协商中…", -+ "container.barrel": "木桶", -+ "container.beacon": "信标", -+ "container.blast_furnace": "高炉", -+ "container.brewing": "酿造台", -+ "container.cartography_table": "制图台", -+ "container.chest": "箱子", -+ "container.chestDouble": "大型箱子", -+ "container.crafting": "合成", -+ "container.creative": "物品选栏", -+ "container.dispenser": "发射器", -+ "container.dropper": "投掷器", -+ "container.enchant": "附魔", -+ "container.enchant.clue": "%s…?", -+ "container.enchant.lapis.many": "花费:%s颗青金石", -+ "container.enchant.lapis.one": "花费:1颗青金石", -+ "container.enchant.level.many": "+ %s级经验", -+ "container.enchant.level.one": "+ 1级经验", -+ "container.enchant.level.requirement": "等级要求:%s", -+ "container.enderchest": "末影箱", -+ "container.furnace": "熔炉", -+ "container.grindstone_title": "修复和祛魔", -+ "container.hopper": "漏斗", -+ "container.inventory": "物品栏", -+ "container.isLocked": "%s已被上锁!", -+ "container.lectern": "讲台", -+ "container.loom": "织布机", -+ "container.repair": "修复和命名", -+ "container.repair.cost": "附魔花费:%1$s", -+ "container.repair.expensive": "过于昂贵!", -+ "container.shulkerBox": "潜影盒", -+ "container.shulkerBox.more": "还有%s项未显示…", -+ "container.smoker": "烟熏炉", -+ "container.spectatorCantOpen": "无法打开:战利品尚未生成。", -+ "container.stonecutter": "切石机", -+ "container.upgrade": "升级装备", -+ "container.upgrade.error_tooltip": "该物品无法用此方式升级", -+ "container.upgrade.missing_template_tooltip": "放入锻造模板", -+ "controls.keybinds": "按键绑定…", -+ "controls.keybinds.duplicateKeybinds": "该按键也用于:\n%s", -+ "controls.keybinds.title": "按键绑定", -+ "controls.reset": "重置", -+ "controls.resetAll": "重置按键", -+ "controls.title": "按键控制", -+ "createWorld.customize.buffet.biome": "请选择一种生物群系", -+ "createWorld.customize.buffet.title": "自定义自选世界", -+ "createWorld.customize.custom.baseSize": "基准深度大小", -+ "createWorld.customize.custom.biomeDepthOffset": "生物群系深度偏移量", -+ "createWorld.customize.custom.biomeDepthWeight": "生物群系深度比重", -+ "createWorld.customize.custom.biomeScaleOffset": "生物群系规模偏移量", -+ "createWorld.customize.custom.biomeScaleWeight": "生物群系规模比重", -+ "createWorld.customize.custom.biomeSize": "生物群系规模", -+ "createWorld.customize.custom.center": "中心高度", -+ "createWorld.customize.custom.confirm1": "这将覆盖你的当前", -+ "createWorld.customize.custom.confirm2": "的设置且不能恢复。", -+ "createWorld.customize.custom.confirmTitle": "警告!", -+ "createWorld.customize.custom.coordinateScale": "平面比例", -+ "createWorld.customize.custom.count": "生成尝试次数", -+ "createWorld.customize.custom.defaults": "默认", -+ "createWorld.customize.custom.depthNoiseScaleExponent": "高度差异指数", -+ "createWorld.customize.custom.depthNoiseScaleX": "X轴高度差异值", -+ "createWorld.customize.custom.depthNoiseScaleZ": "Z轴高度差异值", -+ "createWorld.customize.custom.dungeonChance": "地牢数量", -+ "createWorld.customize.custom.fixedBiome": "生物群系", -+ "createWorld.customize.custom.heightScale": "高度比例", -+ "createWorld.customize.custom.lavaLakeChance": "熔岩湖密度", -+ "createWorld.customize.custom.lowerLimitScale": "规模下限", -+ "createWorld.customize.custom.mainNoiseScaleX": "主地形差异值 X", -+ "createWorld.customize.custom.mainNoiseScaleY": "主地形差异值 Y", -+ "createWorld.customize.custom.mainNoiseScaleZ": "主地形差异值 Z", -+ "createWorld.customize.custom.maxHeight": "最高高度", -+ "createWorld.customize.custom.minHeight": "最低高度", -+ "createWorld.customize.custom.next": "下一页", -+ "createWorld.customize.custom.page0": "基本设置", -+ "createWorld.customize.custom.page1": "矿物设置", -+ "createWorld.customize.custom.page2": "高级设置(仅限专业玩家!)", -+ "createWorld.customize.custom.page3": "额外高级设置(仅限专业玩家!)", -+ "createWorld.customize.custom.preset.caveChaos": "混沌洞穴", -+ "createWorld.customize.custom.preset.caveDelight": "探洞者的喜悦", -+ "createWorld.customize.custom.preset.drought": "旱地", -+ "createWorld.customize.custom.preset.goodLuck": "祝你好运", -+ "createWorld.customize.custom.preset.isleLand": "空岛", -+ "createWorld.customize.custom.preset.mountains": "山脉狂魔", -+ "createWorld.customize.custom.preset.waterWorld": "水世界", -+ "createWorld.customize.custom.presets": "预设", -+ "createWorld.customize.custom.presets.title": "自定义世界预设", -+ "createWorld.customize.custom.prev": "上一页", -+ "createWorld.customize.custom.randomize": "随机", -+ "createWorld.customize.custom.riverSize": "河流规模", -+ "createWorld.customize.custom.seaLevel": "海平面", -+ "createWorld.customize.custom.size": "生成规模", -+ "createWorld.customize.custom.spread": "扩散高度", -+ "createWorld.customize.custom.stretchY": "高度伸展", -+ "createWorld.customize.custom.upperLimitScale": "规模上限", -+ "createWorld.customize.custom.useCaves": "洞穴", -+ "createWorld.customize.custom.useDungeons": "地牢", -+ "createWorld.customize.custom.useLavaLakes": "熔岩湖", -+ "createWorld.customize.custom.useLavaOceans": "熔岩海", -+ "createWorld.customize.custom.useMansions": "林地府邸", -+ "createWorld.customize.custom.useMineShafts": "废弃矿井", -+ "createWorld.customize.custom.useMonuments": "海底神殿", -+ "createWorld.customize.custom.useOceanRuins": "海底废墟", -+ "createWorld.customize.custom.useRavines": "峡谷", -+ "createWorld.customize.custom.useStrongholds": "要塞", -+ "createWorld.customize.custom.useTemples": "神殿", -+ "createWorld.customize.custom.useVillages": "村庄", -+ "createWorld.customize.custom.useWaterLakes": "湖泊", -+ "createWorld.customize.custom.waterLakeChance": "湖泊密度", -+ "createWorld.customize.flat.height": "高度", -+ "createWorld.customize.flat.layer": "%s", -+ "createWorld.customize.flat.layer.bottom": "底层 - %s", -+ "createWorld.customize.flat.layer.top": "顶层 - %s", -+ "createWorld.customize.flat.removeLayer": "移除层面", -+ "createWorld.customize.flat.tile": "此层的材料", -+ "createWorld.customize.flat.title": "自定义超平坦世界", -+ "createWorld.customize.presets": "预设", -+ "createWorld.customize.presets.list": "另外,这里是些我们早期制作好的!", -+ "createWorld.customize.presets.select": "使用预设", -+ "createWorld.customize.presets.share": "想要与别人分享你的预设方案吗?使用下面的输入框吧!", -+ "createWorld.customize.presets.title": "选择一种预设", -+ "createWorld.preparing": "正在准备生成世界…", -+ "createWorld.tab.game.title": "游戏", -+ "createWorld.tab.more.title": "更多", -+ "createWorld.tab.world.title": "世界", -+ "credits_and_attribution.button.attribution": "著作权说明", -+ "credits_and_attribution.button.credits": "鸣谢名单", -+ "credits_and_attribution.button.licenses": "许可协议", -+ "credits_and_attribution.screen.title": "鸣谢与著作权说明", -+ "dataPack.bundle.description": "启用实验性收纳袋物品", -+ "dataPack.bundle.name": "收纳袋", -+ "dataPack.title": "选择数据包", -+ "dataPack.update_1_20.description": "Minecraft 1.20的新功能与内容", -+ "dataPack.update_1_20.name": "1.20更新", -+ "dataPack.validation.back": "返回", -+ "dataPack.validation.failed": "数据包验证失败!", -+ "dataPack.validation.reset": "重置为默认", -+ "dataPack.validation.working": "正在验证已选的数据包…", -+ "dataPack.vanilla.description": "Minecraft的默认数据包", -+ "dataPack.vanilla.name": "默认", -+ "datapackFailure.safeMode": "安全模式", -+ "datapackFailure.safeMode.failed.description": "这个世界包含无效或损坏的存档数据。", -+ "datapackFailure.safeMode.failed.title": "无法在安全模式下加载世界。", -+ "datapackFailure.title": "当前选中的数据包中出现了错误,导致世界无法加载。\n你可以尝试仅加载原版数据包(“安全模式”)或回到标题屏幕手动修复该问题。", -+ "death.attack.anvil": "%1$s被坠落的铁砧压扁了", -+ "death.attack.anvil.player": "%1$s在与%2$s战斗时被坠落的铁砧压扁了", -+ "death.attack.arrow": "%1$s被%2$s射杀", -+ "death.attack.arrow.item": "%1$s被%2$s用%3$s射杀", -+ "death.attack.badRespawnPoint.link": "刻意的游戏设计", -+ "death.attack.badRespawnPoint.message": "%1$s被%2$s杀死了", -+ "death.attack.cactus": "%1$s被戳死了", -+ "death.attack.cactus.player": "%1$s在试图逃离%2$s时撞上了仙人掌", -+ "death.attack.cramming": "%1$s因被过度挤压而死", -+ "death.attack.cramming.player": "%1$s被%2$s挤扁了", -+ "death.attack.dragonBreath": "%1$s被龙息烤熟了", -+ "death.attack.dragonBreath.player": "%1$s被%2$s的龙息烤熟了", -+ "death.attack.drown": "%1$s淹死了", -+ "death.attack.drown.player": "%1$s在试图逃离%2$s时淹死了", -+ "death.attack.dryout": "%1$s因脱水而死", -+ "death.attack.dryout.player": "%1$s在试图逃离%2$s时因脱水而死", -+ "death.attack.even_more_magic": "%1$s被不为人知的魔法杀死了", -+ "death.attack.explosion": "%1$s爆炸了", -+ "death.attack.explosion.player": "%1$s被%2$s炸死了", -+ "death.attack.explosion.player.item": "%1$s被%2$s用%3$s炸死了", -+ "death.attack.fall": "%1$s落地过猛", -+ "death.attack.fall.player": "%1$s在试图逃离%2$s时落地过猛", -+ "death.attack.fallingBlock": "%1$s被下落的方块压扁了", -+ "death.attack.fallingBlock.player": "%1$s在与%2$s战斗时被下落的方块压扁了", -+ "death.attack.fallingStalactite": "%1$s被坠落的钟乳石刺穿了", -+ "death.attack.fallingStalactite.player": "%1$s在与%2$s战斗时被坠落的钟乳石刺穿了", -+ "death.attack.fireball": "%1$s被%2$s用火球烧死了", -+ "death.attack.fireball.item": "%1$s被%2$s用%3$s发射的火球烧死了", -+ "death.attack.fireworks": "%1$s随着一声巨响消失了", -+ "death.attack.fireworks.item": "%1$s随着%2$s用%3$s发射的烟花发出的巨响消失了", -+ "death.attack.fireworks.player": "%1$s在与%2$s战斗时随着一声巨响消失了", -+ "death.attack.flyIntoWall": "%1$s感受到了动能", -+ "death.attack.flyIntoWall.player": "%1$s在试图逃离%2$s时感受到了动能", -+ "death.attack.freeze": "%1$s被冻死了", -+ "death.attack.freeze.player": "%1$s被%2$s冻死了", -+ "death.attack.generic": "%1$s死了", -+ "death.attack.generic.player": "%1$s死于%2$s", -+ "death.attack.genericKill": "%1$s被杀死了", -+ "death.attack.genericKill.player": "%1$s在与%2$s战斗时被杀死了", -+ "death.attack.hotFloor": "%1$s发现了地板是熔岩做的", -+ "death.attack.hotFloor.player": "%1$s因%2$s而步入危险之地", -+ "death.attack.inFire": "%1$s浴火焚身", -+ "death.attack.inFire.player": "%1$s在与%2$s战斗时踏入了火中", -+ "death.attack.inWall": "%1$s在墙里窒息而亡", -+ "death.attack.inWall.player": "%1$s在与%2$s战斗时在墙里窒息而亡", -+ "death.attack.indirectMagic": "%1$s被%2$s使用的魔法杀死了", -+ "death.attack.indirectMagic.item": "%1$s被%2$s用%3$s杀死了", -+ "death.attack.lava": "%1$s试图在熔岩里游泳", -+ "death.attack.lava.player": "%1$s在逃离%2$s时试图在熔岩里游泳", -+ "death.attack.lightningBolt": "%1$s被闪电击中", -+ "death.attack.lightningBolt.player": "%1$s在与%2$s战斗时被闪电击中", -+ "death.attack.magic": "%1$s被魔法杀死了", -+ "death.attack.magic.player": "%1$s在试图逃离%2$s时被魔法杀死了", -+ "death.attack.message_too_long": "抱歉!消息过长,无法完整显示。截断后的消息为:%s", -+ "death.attack.mob": "%1$s被%2$s杀死了", -+ "death.attack.mob.item": "%1$s被%2$s用%3$s杀死了", -+ "death.attack.onFire": "%1$s被烧死了", -+ "death.attack.onFire.item": "%1$s在与持有%3$s的%2$s战斗时被烤得酥脆", -+ "death.attack.onFire.player": "%1$s在与%2$s战斗时被烤得酥脆", -+ "death.attack.outOfWorld": "%1$s掉出了这个世界", -+ "death.attack.outOfWorld.player": "%1$s与%2$s不共戴天", -+ "death.attack.outsideBorder": "%1$s脱离了这个世界", -+ "death.attack.outsideBorder.player": "%1$s在与%2$s战斗时脱离了这个世界", -+ "death.attack.player": "%1$s被%2$s杀死了", -+ "death.attack.player.item": "%1$s被%2$s用%3$s杀死了", -+ "death.attack.sonic_boom": "%1$s被一道音波尖啸抹除了", -+ "death.attack.sonic_boom.item": "%1$s在试图逃离持有%3$s的%2$s时被一道音波尖啸抹除了", -+ "death.attack.sonic_boom.player": "%1$s在试图逃离%2$s时被一道音波尖啸抹除了", -+ "death.attack.stalagmite": "%1$s被石笋刺穿了", -+ "death.attack.stalagmite.player": "%1$s在与%2$s战斗时被石笋刺穿了", -+ "death.attack.starve": "%1$s饿死了", -+ "death.attack.starve.player": "%1$s在与%2$s战斗时饿死了", -+ "death.attack.sting": "%1$s被蛰死了", -+ "death.attack.sting.item": "%1$s被%2$s用%3$s蛰死了", -+ "death.attack.sting.player": "%1$s被%2$s蛰死了", -+ "death.attack.sweetBerryBush": "%1$s被甜浆果丛刺死了", -+ "death.attack.sweetBerryBush.player": "%1$s在试图逃离%2$s时被甜浆果丛刺死了", -+ "death.attack.thorns": "%1$s在试图伤害%2$s时被杀", -+ "death.attack.thorns.item": "%1$s在试图伤害%2$s时被%3$s杀死", -+ "death.attack.thrown": "%1$s被%2$s给砸死了", -+ "death.attack.thrown.item": "%1$s被%2$s用%3$s给砸死了", -+ "death.attack.trident": "%1$s被%2$s刺穿了", -+ "death.attack.trident.item": "%1$s被%2$s用%3$s刺穿了", -+ "death.attack.wither": "%1$s凋零了", -+ "death.attack.wither.player": "%1$s在与%2$s战斗时凋零了", -+ "death.attack.witherSkull": "%1$s被%2$s发射的头颅射杀", -+ "death.attack.witherSkull.item": "%1$s被%2$s用%3$s发射的头颅射杀", -+ "death.fell.accident.generic": "%1$s从高处摔了下来", -+ "death.fell.accident.ladder": "%1$s从梯子上摔了下来", -+ "death.fell.accident.other_climbable": "%1$s在攀爬时摔了下来", -+ "death.fell.accident.scaffolding": "%1$s从脚手架上摔了下来", -+ "death.fell.accident.twisting_vines": "%1$s从缠怨藤上摔了下来", -+ "death.fell.accident.vines": "%1$s从藤蔓上摔了下来", -+ "death.fell.accident.weeping_vines": "%1$s从垂泪藤上摔了下来", -+ "death.fell.assist": "%1$s因为%2$s注定要摔死", -+ "death.fell.assist.item": "%1$s因为%2$s使用了%3$s注定要摔死", -+ "death.fell.finish": "%1$s摔伤得太重并被%2$s完结了生命", -+ "death.fell.finish.item": "%1$s摔伤得太重并被%2$s用%3$s完结了生命", -+ "death.fell.killer": "%1$s注定要摔死", -+ "deathScreen.quit.confirm": "你确定要退出吗?", -+ "deathScreen.respawn": "重生", -+ "deathScreen.score": "分数", -+ "deathScreen.spectate": "旁观世界", -+ "deathScreen.title": "你死了!", -+ "deathScreen.title.hardcore": "游戏结束!", -+ "deathScreen.titleScreen": "标题屏幕", -+ "debug.advanced_tooltips.help": "F3 + H = 显示高级提示框", -+ "debug.advanced_tooltips.off": "高级提示框:隐藏", -+ "debug.advanced_tooltips.on": "高级提示框:显示", -+ "debug.chunk_boundaries.help": "F3 + G = 显示区块边界", -+ "debug.chunk_boundaries.off": "区块边界:隐藏", -+ "debug.chunk_boundaries.on": "区块边界:显示", -+ "debug.clear_chat.help": "F3 + D = 清空聊天记录", -+ "debug.copy_location.help": "F3 + C = 用/tp命令的形式复制你的位置,按住F3 + C使游戏崩溃", -+ "debug.copy_location.message": "坐标已复制到剪贴板", -+ "debug.crash.message": "F3 + C已被按下。若不放开按键则会使游戏崩溃。", -+ "debug.crash.warning": "将在%s秒后崩溃…", -+ "debug.creative_spectator.error": "你没有切换游戏模式的权限", -+ "debug.creative_spectator.help": "F3 + N = 在上一个模式和旁观模式间切换", -+ "debug.dump_dynamic_textures": "已将动态纹理保存至%s", -+ "debug.dump_dynamic_textures.help": "F3 + S = 转储动态纹理", -+ "debug.gamemodes.error": "你没有权限打开游戏模式切换器", -+ "debug.gamemodes.help": "F3 + F4 = 打开游戏模式切换器", -+ "debug.gamemodes.press_f4": "[ F4 ]", -+ "debug.gamemodes.select_next": "%s 下一个", -+ "debug.help.help": "F3 + Q = 显示此列表", -+ "debug.help.message": "按键设置:", -+ "debug.inspect.client.block": "客户端方块数据已复制到剪贴板", -+ "debug.inspect.client.entity": "客户端实体数据已复制到剪贴板", -+ "debug.inspect.help": "F3 + I = 将实体或方块的数据复制到剪贴板", -+ "debug.inspect.server.block": "服务端方块数据已复制到剪贴板", -+ "debug.inspect.server.entity": "服务端实体数据已复制到剪贴板", -+ "debug.pause.help": "F3 + Esc = 暂停但不显示菜单(如果可以暂停的话)", -+ "debug.pause_focus.help": "F3 + P = 失去焦点时暂停", -+ "debug.pause_focus.off": "失去焦点时暂停:停用", -+ "debug.pause_focus.on": "失去焦点时暂停:启用", -+ "debug.prefix": "[调试]:", -+ "debug.profiling.help": "F3 + L = 开始/停止分析", -+ "debug.profiling.start": "分析已启动%s秒。使用F3 + L以提前结束", -+ "debug.profiling.stop": "分析已结束。结果已保存至%s", -+ "debug.reload_chunks.help": "F3 + A = 重新加载区块", -+ "debug.reload_chunks.message": "重新加载所有区块中", -+ "debug.reload_resourcepacks.help": "F3 + T = 重新加载资源包", -+ "debug.reload_resourcepacks.message": "已重新加载资源包", -+ "debug.show_hitboxes.help": "F3 + B = 显示判定箱", -+ "debug.show_hitboxes.off": "判定箱:隐藏", -+ "debug.show_hitboxes.on": "判定箱:显示", -+ "demo.day.1": "此试玩版会在5个游戏日后结束,尽力而为吧!", -+ "demo.day.2": "第二天", -+ "demo.day.3": "第三天", -+ "demo.day.4": "第四天", -+ "demo.day.5": "这是你游戏内的最后一天!", -+ "demo.day.6": "你已经度过了5个游戏日的试玩时间,按下%s来为你的成果截图留念。", -+ "demo.day.warning": "你的试玩时间即将结束!", -+ "demo.demoExpired": "试玩的时间结束了!", -+ "demo.help.buy": "即刻购买!", -+ "demo.help.fullWrapped": "这个试玩将会持续游戏内5天的时间(现实时间大约为1小时40分钟)。查看进度来获得提示!祝你玩得开心!", -+ "demo.help.inventory": "按%1$s来打开你的物品栏", -+ "demo.help.jump": "按%1$s来跳跃", -+ "demo.help.later": "继续游戏!", -+ "demo.help.movement": "按%1$s,%2$s,%3$s,%4$s以及鼠标来移动", -+ "demo.help.movementMouse": "使用鼠标来查看四周", -+ "demo.help.movementShort": "通过按下%1$s,%2$s,%3$s,%4$s来移动", -+ "demo.help.title": "Minecraft试玩模式", -+ "demo.remainingTime": "剩余时间:%s", -+ "demo.reminder": "试玩时间已经结束,请购买游戏来继续或开始一个新的世界!", -+ "difficulty.lock.question": "你确定你要锁定这个世界的难度吗?这会将这个世界的难度锁定为%1$s,并且永远无法再次改变难度。", -+ "difficulty.lock.title": "锁定世界难度", -+ "disconnect.closed": "连接已关闭", -+ "disconnect.disconnected": "被服务器中断连接", -+ "disconnect.endOfStream": "数据流终止", -+ "disconnect.exceeded_packet_rate": "由于超出数据包速率限制而被踢出游戏", -+ "disconnect.genericReason": "%s", -+ "disconnect.ignoring_status_request": "忽略状态请求", -+ "disconnect.kicked": "你已被踢出游戏", -+ "disconnect.loginFailed": "登录失败", -+ "disconnect.loginFailedInfo": "登录失败:%s", -+ "disconnect.loginFailedInfo.insufficientPrivileges": "多人游戏已被禁用,请检查你的Microsoft账户设置。", -+ "disconnect.loginFailedInfo.invalidSession": "无效会话(请尝试重启游戏及启动器)", -+ "disconnect.loginFailedInfo.serversUnavailable": "暂时无法连接到身份验证服务器,请稍后再试。", -+ "disconnect.loginFailedInfo.userBanned": "你已被禁止进行多人游戏", -+ "disconnect.lost": "连接已丢失", -+ "disconnect.overflow": "缓冲区溢出", -+ "disconnect.quitting": "正在退出", -+ "disconnect.spam": "由于滥发消息而被踢出游戏", -+ "disconnect.timeout": "连接超时", -+ "disconnect.unknownHost": "未知的主机", -+ "editGamerule.default": "默认:%s", -+ "editGamerule.title": "编辑游戏规则", -+ "effect.duration.infinite": "∞", -+ "effect.minecraft.absorption": "伤害吸收", -+ "effect.minecraft.bad_omen": "不祥之兆", -+ "effect.minecraft.blindness": "失明", -+ "effect.minecraft.conduit_power": "潮涌能量", -+ "effect.minecraft.darkness": "黑暗", -+ "effect.minecraft.dolphins_grace": "海豚的恩惠", -+ "effect.minecraft.fire_resistance": "抗火", -+ "effect.minecraft.glowing": "发光", -+ "effect.minecraft.haste": "急迫", -+ "effect.minecraft.health_boost": "生命提升", -+ "effect.minecraft.hero_of_the_village": "村庄英雄", -+ "effect.minecraft.hunger": "饥饿", -+ "effect.minecraft.instant_damage": "瞬间伤害", -+ "effect.minecraft.instant_health": "瞬间治疗", -+ "effect.minecraft.invisibility": "隐身", -+ "effect.minecraft.jump_boost": "跳跃提升", -+ "effect.minecraft.levitation": "飘浮", -+ "effect.minecraft.luck": "幸运", -+ "effect.minecraft.mining_fatigue": "挖掘疲劳", -+ "effect.minecraft.nausea": "反胃", -+ "effect.minecraft.night_vision": "夜视", -+ "effect.minecraft.poison": "中毒", -+ "effect.minecraft.regeneration": "生命恢复", -+ "effect.minecraft.resistance": "抗性提升", -+ "effect.minecraft.saturation": "饱和", -+ "effect.minecraft.slow_falling": "缓降", -+ "effect.minecraft.slowness": "缓慢", -+ "effect.minecraft.speed": "迅捷", -+ "effect.minecraft.strength": "力量", -+ "effect.minecraft.unluck": "霉运", -+ "effect.minecraft.water_breathing": "水下呼吸", -+ "effect.minecraft.weakness": "虚弱", -+ "effect.minecraft.wither": "凋零", -+ "effect.none": "无效果", -+ "enchantment.level.1": "I", -+ "enchantment.level.10": "X", -+ "enchantment.level.2": "II", -+ "enchantment.level.3": "III", -+ "enchantment.level.4": "IV", -+ "enchantment.level.5": "V", -+ "enchantment.level.6": "VI", -+ "enchantment.level.7": "VII", -+ "enchantment.level.8": "VIII", -+ "enchantment.level.9": "IX", -+ "enchantment.minecraft.aqua_affinity": "水下速掘", -+ "enchantment.minecraft.bane_of_arthropods": "节肢杀手", -+ "enchantment.minecraft.binding_curse": "绑定诅咒", -+ "enchantment.minecraft.blast_protection": "爆炸保护", -+ "enchantment.minecraft.channeling": "引雷", -+ "enchantment.minecraft.depth_strider": "深海探索者", -+ "enchantment.minecraft.efficiency": "效率", -+ "enchantment.minecraft.feather_falling": "摔落缓冲", -+ "enchantment.minecraft.fire_aspect": "火焰附加", -+ "enchantment.minecraft.fire_protection": "火焰保护", -+ "enchantment.minecraft.flame": "火矢", -+ "enchantment.minecraft.fortune": "时运", -+ "enchantment.minecraft.frost_walker": "冰霜行者", -+ "enchantment.minecraft.impaling": "穿刺", -+ "enchantment.minecraft.infinity": "无限", -+ "enchantment.minecraft.knockback": "击退", -+ "enchantment.minecraft.looting": "抢夺", -+ "enchantment.minecraft.loyalty": "忠诚", -+ "enchantment.minecraft.luck_of_the_sea": "海之眷顾", -+ "enchantment.minecraft.lure": "饵钓", -+ "enchantment.minecraft.mending": "经验修补", -+ "enchantment.minecraft.multishot": "多重射击", -+ "enchantment.minecraft.piercing": "穿透", -+ "enchantment.minecraft.power": "力量", -+ "enchantment.minecraft.projectile_protection": "弹射物保护", -+ "enchantment.minecraft.protection": "保护", -+ "enchantment.minecraft.punch": "冲击", -+ "enchantment.minecraft.quick_charge": "快速装填", -+ "enchantment.minecraft.respiration": "水下呼吸", -+ "enchantment.minecraft.riptide": "激流", -+ "enchantment.minecraft.sharpness": "锋利", -+ "enchantment.minecraft.silk_touch": "精准采集", -+ "enchantment.minecraft.smite": "亡灵杀手", -+ "enchantment.minecraft.soul_speed": "灵魂疾行", -+ "enchantment.minecraft.sweeping": "横扫之刃", -+ "enchantment.minecraft.swift_sneak": "迅捷潜行", -+ "enchantment.minecraft.thorns": "荆棘", -+ "enchantment.minecraft.unbreaking": "耐久", -+ "enchantment.minecraft.vanishing_curse": "消失诅咒", -+ "entity.minecraft.allay": "悦灵", -+ "entity.minecraft.area_effect_cloud": "区域效果云", -+ "entity.minecraft.armor_stand": "盔甲架", -+ "entity.minecraft.arrow": "箭", -+ "entity.minecraft.axolotl": "美西螈", -+ "entity.minecraft.bat": "蝙蝠", -+ "entity.minecraft.bee": "蜜蜂", -+ "entity.minecraft.blaze": "烈焰人", -+ "entity.minecraft.block_display": "方块展示实体", -+ "entity.minecraft.boat": "船", -+ "entity.minecraft.camel": "骆驼", -+ "entity.minecraft.cat": "猫", -+ "entity.minecraft.cave_spider": "洞穴蜘蛛", -+ "entity.minecraft.chest_boat": "运输船", -+ "entity.minecraft.chest_minecart": "运输矿车", -+ "entity.minecraft.chicken": "鸡", -+ "entity.minecraft.cod": "鳕鱼", -+ "entity.minecraft.command_block_minecart": "命令方块矿车", -+ "entity.minecraft.cow": "牛", -+ "entity.minecraft.creeper": "苦力怕", -+ "entity.minecraft.dolphin": "海豚", -+ "entity.minecraft.donkey": "驴", -+ "entity.minecraft.dragon_fireball": "末影龙火球", -+ "entity.minecraft.drowned": "溺尸", -+ "entity.minecraft.egg": "掷出的鸡蛋", -+ "entity.minecraft.elder_guardian": "远古守卫者", -+ "entity.minecraft.end_crystal": "末地水晶", -+ "entity.minecraft.ender_dragon": "末影龙", -+ "entity.minecraft.ender_pearl": "掷出的末影珍珠", -+ "entity.minecraft.enderman": "末影人", -+ "entity.minecraft.endermite": "末影螨", -+ "entity.minecraft.evoker": "唤魔者", -+ "entity.minecraft.evoker_fangs": "唤魔者尖牙", -+ "entity.minecraft.experience_bottle": "掷出的附魔之瓶", -+ "entity.minecraft.experience_orb": "经验球", -+ "entity.minecraft.eye_of_ender": "末影之眼", -+ "entity.minecraft.falling_block": "下落的方块", -+ "entity.minecraft.falling_block_type": "下落的%s", -+ "entity.minecraft.fireball": "火球", -+ "entity.minecraft.firework_rocket": "烟花火箭", -+ "entity.minecraft.fishing_bobber": "浮漂", -+ "entity.minecraft.fox": "狐狸", -+ "entity.minecraft.frog": "青蛙", -+ "entity.minecraft.furnace_minecart": "动力矿车", -+ "entity.minecraft.ghast": "恶魂", -+ "entity.minecraft.giant": "巨人", -+ "entity.minecraft.glow_item_frame": "荧光物品展示框", -+ "entity.minecraft.glow_squid": "发光鱿鱼", -+ "entity.minecraft.goat": "山羊", -+ "entity.minecraft.guardian": "守卫者", -+ "entity.minecraft.hoglin": "疣猪兽", -+ "entity.minecraft.hopper_minecart": "漏斗矿车", -+ "entity.minecraft.horse": "马", -+ "entity.minecraft.husk": "尸壳", -+ "entity.minecraft.illusioner": "幻术师", -+ "entity.minecraft.interaction": "交互实体", -+ "entity.minecraft.iron_golem": "铁傀儡", -+ "entity.minecraft.item": "物品", -+ "entity.minecraft.item_display": "物品展示实体", -+ "entity.minecraft.item_frame": "物品展示框", -+ "entity.minecraft.killer_bunny": "杀手兔", -+ "entity.minecraft.leash_knot": "拴绳结", -+ "entity.minecraft.lightning_bolt": "闪电束", -+ "entity.minecraft.llama": "羊驼", -+ "entity.minecraft.llama_spit": "羊驼唾沫", -+ "entity.minecraft.magma_cube": "岩浆怪", -+ "entity.minecraft.marker": "标记", -+ "entity.minecraft.minecart": "矿车", -+ "entity.minecraft.mooshroom": "哞菇", -+ "entity.minecraft.mule": "骡", -+ "entity.minecraft.ocelot": "豹猫", -+ "entity.minecraft.painting": "画", -+ "entity.minecraft.panda": "熊猫", -+ "entity.minecraft.parrot": "鹦鹉", -+ "entity.minecraft.phantom": "幻翼", -+ "entity.minecraft.pig": "猪", -+ "entity.minecraft.piglin": "猪灵", -+ "entity.minecraft.piglin_brute": "猪灵蛮兵", -+ "entity.minecraft.pillager": "掠夺者", -+ "entity.minecraft.player": "玩家", -+ "entity.minecraft.polar_bear": "北极熊", -+ "entity.minecraft.potion": "药水", -+ "entity.minecraft.pufferfish": "河豚", -+ "entity.minecraft.rabbit": "兔子", -+ "entity.minecraft.ravager": "劫掠兽", -+ "entity.minecraft.salmon": "鲑鱼", -+ "entity.minecraft.sheep": "绵羊", -+ "entity.minecraft.shulker": "潜影贝", -+ "entity.minecraft.shulker_bullet": "潜影弹", -+ "entity.minecraft.silverfish": "蠹虫", -+ "entity.minecraft.skeleton": "骷髅", -+ "entity.minecraft.skeleton_horse": "骷髅马", -+ "entity.minecraft.slime": "史莱姆", -+ "entity.minecraft.small_fireball": "小火球", -+ "entity.minecraft.sniffer": "嗅探兽", -+ "entity.minecraft.snow_golem": "雪傀儡", -+ "entity.minecraft.snowball": "雪球", -+ "entity.minecraft.spawner_minecart": "刷怪笼矿车", -+ "entity.minecraft.spectral_arrow": "光灵箭", -+ "entity.minecraft.spider": "蜘蛛", -+ "entity.minecraft.squid": "鱿鱼", -+ "entity.minecraft.stray": "流浪者", -+ "entity.minecraft.strider": "炽足兽", -+ "entity.minecraft.tadpole": "蝌蚪", -+ "entity.minecraft.text_display": "文本展示实体", -+ "entity.minecraft.tnt": "被激活的TNT", -+ "entity.minecraft.tnt_minecart": "TNT矿车", -+ "entity.minecraft.trader_llama": "行商羊驼", -+ "entity.minecraft.trident": "三叉戟", -+ "entity.minecraft.tropical_fish": "热带鱼", -+ "entity.minecraft.tropical_fish.predefined.0": "海葵鱼", -+ "entity.minecraft.tropical_fish.predefined.1": "黑刺尾鲷", -+ "entity.minecraft.tropical_fish.predefined.10": "镰鱼", -+ "entity.minecraft.tropical_fish.predefined.11": "华丽蝴蝶鱼", -+ "entity.minecraft.tropical_fish.predefined.12": "鹦嘴鱼", -+ "entity.minecraft.tropical_fish.predefined.13": "额斑刺蝶鱼", -+ "entity.minecraft.tropical_fish.predefined.14": "红丽鱼", -+ "entity.minecraft.tropical_fish.predefined.15": "红唇真蛇鳚", -+ "entity.minecraft.tropical_fish.predefined.16": "红边笛鲷", -+ "entity.minecraft.tropical_fish.predefined.17": "马鲅", -+ "entity.minecraft.tropical_fish.predefined.18": "白条双锯鱼", -+ "entity.minecraft.tropical_fish.predefined.19": "鳞鲀", -+ "entity.minecraft.tropical_fish.predefined.2": "蓝刺尾鲷", -+ "entity.minecraft.tropical_fish.predefined.20": "高鳍鹦嘴鱼", -+ "entity.minecraft.tropical_fish.predefined.21": "黄刺尾鲷", -+ "entity.minecraft.tropical_fish.predefined.3": "蝴蝶鱼", -+ "entity.minecraft.tropical_fish.predefined.4": "丽鱼", -+ "entity.minecraft.tropical_fish.predefined.5": "小丑鱼", -+ "entity.minecraft.tropical_fish.predefined.6": "五彩搏鱼", -+ "entity.minecraft.tropical_fish.predefined.7": "绣雀鲷", -+ "entity.minecraft.tropical_fish.predefined.8": "川纹笛鲷", -+ "entity.minecraft.tropical_fish.predefined.9": "拟羊鱼", -+ "entity.minecraft.tropical_fish.type.betty": "背蒂类", -+ "entity.minecraft.tropical_fish.type.blockfish": "方身类", -+ "entity.minecraft.tropical_fish.type.brinely": "咸水类", -+ "entity.minecraft.tropical_fish.type.clayfish": "陶鱼类", -+ "entity.minecraft.tropical_fish.type.dasher": "速跃类", -+ "entity.minecraft.tropical_fish.type.flopper": "飞翼类", -+ "entity.minecraft.tropical_fish.type.glitter": "闪鳞类", -+ "entity.minecraft.tropical_fish.type.kob": "石首类", -+ "entity.minecraft.tropical_fish.type.snooper": "窥伺类", -+ "entity.minecraft.tropical_fish.type.spotty": "多斑类", -+ "entity.minecraft.tropical_fish.type.stripey": "条纹类", -+ "entity.minecraft.tropical_fish.type.sunstreak": "日纹类", -+ "entity.minecraft.turtle": "海龟", -+ "entity.minecraft.vex": "恼鬼", -+ "entity.minecraft.villager": "村民", -+ "entity.minecraft.villager.armorer": "盔甲匠", -+ "entity.minecraft.villager.butcher": "屠夫", -+ "entity.minecraft.villager.cartographer": "制图师", -+ "entity.minecraft.villager.cleric": "牧师", -+ "entity.minecraft.villager.farmer": "农民", -+ "entity.minecraft.villager.fisherman": "渔夫", -+ "entity.minecraft.villager.fletcher": "制箭师", -+ "entity.minecraft.villager.leatherworker": "皮匠", -+ "entity.minecraft.villager.librarian": "图书管理员", -+ "entity.minecraft.villager.mason": "石匠", -+ "entity.minecraft.villager.nitwit": "傻子", -+ "entity.minecraft.villager.none": "村民", -+ "entity.minecraft.villager.shepherd": "牧羊人", -+ "entity.minecraft.villager.toolsmith": "工具匠", -+ "entity.minecraft.villager.weaponsmith": "武器匠", -+ "entity.minecraft.vindicator": "卫道士", -+ "entity.minecraft.wandering_trader": "流浪商人", -+ "entity.minecraft.warden": "监守者", -+ "entity.minecraft.witch": "女巫", -+ "entity.minecraft.wither": "凋灵", -+ "entity.minecraft.wither_skeleton": "凋灵骷髅", -+ "entity.minecraft.wither_skull": "凋灵之首", -+ "entity.minecraft.wolf": "狼", -+ "entity.minecraft.zoglin": "僵尸疣猪兽", -+ "entity.minecraft.zombie": "僵尸", -+ "entity.minecraft.zombie_horse": "僵尸马", -+ "entity.minecraft.zombie_villager": "僵尸村民", -+ "entity.minecraft.zombified_piglin": "僵尸猪灵", -+ "entity.not_summonable": "无法召唤类型为%s的实体", -+ "event.minecraft.raid": "袭击", -+ "event.minecraft.raid.defeat": "失败", -+ "event.minecraft.raid.raiders_remaining": "剩余%s名袭击者", -+ "event.minecraft.raid.victory": "胜利", -+ "filled_map.buried_treasure": "藏宝图", -+ "filled_map.id": "编号#%s", -+ "filled_map.level": "(等级 %s/%s)", -+ "filled_map.locked": "已锁定", -+ "filled_map.mansion": "林地探险家地图", -+ "filled_map.monument": "海洋探险家地图", -+ "filled_map.scale": "比例尺1:%s", -+ "filled_map.unknown": "未知地图", -+ "flat_world_preset.minecraft.bottomless_pit": "无底深渊", -+ "flat_world_preset.minecraft.classic_flat": "经典平坦", -+ "flat_world_preset.minecraft.desert": "沙漠", -+ "flat_world_preset.minecraft.overworld": "主世界", -+ "flat_world_preset.minecraft.redstone_ready": "红石俱备", -+ "flat_world_preset.minecraft.snowy_kingdom": "雪之王国", -+ "flat_world_preset.minecraft.the_void": "虚空", -+ "flat_world_preset.minecraft.tunnelers_dream": "挖掘工的梦想", -+ "flat_world_preset.minecraft.water_world": "水世界", -+ "flat_world_preset.unknown": "???", -+ "gameMode.adventure": "冒险模式", -+ "gameMode.changed": "你的游戏模式已被更新为%s", -+ "gameMode.creative": "创造模式", -+ "gameMode.hardcore": "极限模式!", -+ "gameMode.spectator": "旁观模式", -+ "gameMode.survival": "生存模式", -+ "gamerule.announceAdvancements": "进度通知", -+ "gamerule.blockExplosionDropDecay": "在方块交互爆炸中,一些方块不会掉落战利品", -+ "gamerule.blockExplosionDropDecay.description": "在与方块交互引起的爆炸中,部分被破坏方块的掉落物会被炸毁。", -+ "gamerule.category.chat": "聊天", -+ "gamerule.category.drops": "掉落", -+ "gamerule.category.misc": "杂项", -+ "gamerule.category.mobs": "生物", -+ "gamerule.category.player": "玩家", -+ "gamerule.category.spawning": "生成", -+ "gamerule.category.updates": "世界更新", -+ "gamerule.commandBlockOutput": "广播命令方块输出", -+ "gamerule.commandModificationBlockLimit": "命令修改方块数量限制", -+ "gamerule.commandModificationBlockLimit.description": "单条命令(如fill和clone)最多能更改的方块数量", -+ "gamerule.disableElytraMovementCheck": "禁用鞘翅移动检测", -+ "gamerule.disableRaids": "禁用袭击", -+ "gamerule.doDaylightCycle": "游戏内时间流逝", -+ "gamerule.doEntityDrops": "非生物实体掉落", -+ "gamerule.doEntityDrops.description": "控制矿车(包括内容物)、物品展示框、船等的物品掉落", -+ "gamerule.doFireTick": "火焰蔓延", -+ "gamerule.doImmediateRespawn": "立即重生", -+ "gamerule.doInsomnia": "生成幻翼", -+ "gamerule.doLimitedCrafting": "合成需要配方", -+ "gamerule.doLimitedCrafting.description": "若启用,玩家只能使用已解锁的配方合成。", -+ "gamerule.doMobLoot": "生物战利品掉落", -+ "gamerule.doMobLoot.description": "控制生物死亡后是否掉落资源,包括经验球。", -+ "gamerule.doMobSpawning": "生成生物", -+ "gamerule.doMobSpawning.description": "一些实体可能有其特定的规则。", -+ "gamerule.doPatrolSpawning": "生成灾厄巡逻队", -+ "gamerule.doTileDrops": "方块掉落", -+ "gamerule.doTileDrops.description": "控制破坏方块后是否掉落资源,包括经验球。", -+ "gamerule.doTraderSpawning": "生成流浪商人", -+ "gamerule.doVinesSpread": "藤蔓蔓延", -+ "gamerule.doVinesSpread.description": "控制藤蔓方块是否会随机向相邻的方块蔓延。不会影响其他藤蔓类方块(例如垂泪藤和缠怨藤等)。", -+ "gamerule.doWardenSpawning": "生成监守者", -+ "gamerule.doWeatherCycle": "天气更替", -+ "gamerule.drowningDamage": "溺水伤害", -+ "gamerule.fallDamage": "摔落伤害", -+ "gamerule.fireDamage": "火焰伤害", -+ "gamerule.forgiveDeadPlayers": "宽恕死亡玩家", -+ "gamerule.forgiveDeadPlayers.description": "愤怒的中立生物将在其目标玩家于附近死亡后息怒。", -+ "gamerule.freezeDamage": "冰冻伤害", -+ "gamerule.globalSoundEvents": "全局声音事件", -+ "gamerule.globalSoundEvents.description": "特定游戏事件(如Boss生成)发生时,声音可在所有地方听见。", -+ "gamerule.keepInventory": "死亡后保留物品栏", -+ "gamerule.lavaSourceConversion": "允许流动熔岩转化为熔岩源", -+ "gamerule.lavaSourceConversion.description": "流动熔岩在两面与熔岩源相邻时转化为熔岩源。", -+ "gamerule.logAdminCommands": "通告管理员命令", -+ "gamerule.maxCommandChainLength": "命令连锁执行数量限制", -+ "gamerule.maxCommandChainLength.description": "应用于命令方块链和函数。", -+ "gamerule.maxEntityCramming": "实体挤压上限", -+ "gamerule.mobExplosionDropDecay": "在生物爆炸中,一些方块不会掉落战利品", -+ "gamerule.mobExplosionDropDecay.description": "在生物引起的爆炸中,部分被破坏方块的掉落物会被炸毁。", -+ "gamerule.mobGriefing": "允许破坏性生物行为", -+ "gamerule.naturalRegeneration": "生命值自然恢复", -+ "gamerule.playersSleepingPercentage": "入睡占比", -+ "gamerule.playersSleepingPercentage.description": "跳过夜晚所需的入睡玩家占比。", -+ "gamerule.randomTickSpeed": "随机刻速率", -+ "gamerule.reducedDebugInfo": "简化调试信息", -+ "gamerule.reducedDebugInfo.description": "限制调试屏幕内容。", -+ "gamerule.sendCommandFeedback": "发送命令反馈", -+ "gamerule.showDeathMessages": "显示死亡消息", -+ "gamerule.snowAccumulationHeight": "积雪厚度", -+ "gamerule.snowAccumulationHeight.description": "降雪时,地面上的雪最多堆积到此处指定的层数。", -+ "gamerule.spawnRadius": "重生点半径", -+ "gamerule.spectatorsGenerateChunks": "允许旁观者生成地形", -+ "gamerule.tntExplosionDropDecay": "在TNT爆炸中,一些方块不会掉落战利品", -+ "gamerule.tntExplosionDropDecay.description": "在TNT引起的爆炸中,部分被破坏方块的掉落物会被炸毁。", -+ "gamerule.universalAnger": "无差别愤怒", -+ "gamerule.universalAnger.description": "愤怒的中立生物将攻击附近的所有玩家,而不再限于激怒它们的玩家。禁用“宽恕死亡玩家”可达到最佳效果。", -+ "gamerule.waterSourceConversion": "允许流动水转化为水源", -+ "gamerule.waterSourceConversion.description": "流动水在两面与水源相邻时转化为水源。", -+ "generator.custom": "自定义", -+ "generator.customized": "旧版自定义", -+ "generator.minecraft.amplified": "放大化", -+ "generator.minecraft.amplified.info": "注意:仅供娱乐!需要强劲的电脑。", -+ "generator.minecraft.debug_all_block_states": "调试模式", -+ "generator.minecraft.flat": "超平坦", -+ "generator.minecraft.large_biomes": "巨型生物群系", -+ "generator.minecraft.normal": "默认", -+ "generator.minecraft.single_biome_surface": "单一生物群系", -+ "generator.single_biome_caves": "洞穴", -+ "generator.single_biome_floating_islands": "浮岛", -+ "gui.abuseReport.error.title": "发送举报时出现问题", -+ "gui.abuseReport.reason.alcohol_tobacco_drugs": "吸毒或饮酒", -+ "gui.abuseReport.reason.alcohol_tobacco_drugs.description": "有人教唆他人涉毒或教唆未成年人饮酒。", -+ "gui.abuseReport.reason.child_sexual_exploitation_or_abuse": "对儿童的性剥削或虐待", -+ "gui.abuseReport.reason.child_sexual_exploitation_or_abuse.description": "有人谈论或以其他方式宣扬涉及儿童的不当行为。", -+ "gui.abuseReport.reason.defamation_impersonation_false_information": "诽谤、冒充他人或散布虚假信息", -+ "gui.abuseReport.reason.defamation_impersonation_false_information.description": "有人以利用或误导他人为目的,损害他人名誉、冒充他人或散布虚假信息。", -+ "gui.abuseReport.reason.description": "描述:", -+ "gui.abuseReport.reason.false_reporting": "不实举报", -+ "gui.abuseReport.reason.harassment_or_bullying": "骚扰或霸凌", -+ "gui.abuseReport.reason.harassment_or_bullying.description": "有人羞辱、攻击、霸凌你或其他人。这包括在未经允许的情况下不断尝试联系你或其他人,或发布你或其他人的隐私信息。", -+ "gui.abuseReport.reason.hate_speech": "仇恨言论", -+ "gui.abuseReport.reason.hate_speech.description": "有人因身份要素(如宗教信仰、种族或性相关)攻击你或其他玩家。", -+ "gui.abuseReport.reason.imminent_harm": "即将发生的伤害行为 — 威胁伤害他人", -+ "gui.abuseReport.reason.imminent_harm.description": "有人威胁要在现实生活中伤害你或其他人。", -+ "gui.abuseReport.reason.narration": "%s:%s", -+ "gui.abuseReport.reason.non_consensual_intimate_imagery": "未经同意发布私密图像", -+ "gui.abuseReport.reason.non_consensual_intimate_imagery.description": "有人谈论、分享私密或亲密的图像,或以其他方式宣扬有关行为。", -+ "gui.abuseReport.reason.self_harm_or_suicide": "即将发生的伤害行为 — 自残或自杀", -+ "gui.abuseReport.reason.self_harm_or_suicide.description": "有人威胁要自残或谈论现实生活中的自残行为。", -+ "gui.abuseReport.reason.terrorism_or_violent_extremism": "恐怖主义或暴力极端主义", -+ "gui.abuseReport.reason.terrorism_or_violent_extremism.description": "有人因政治、宗教、意识形态或其他原因,谈论、宣扬、威胁实施恐怖主义或极端暴力行为。", -+ "gui.abuseReport.reason.title": "选择举报类型", -+ "gui.abuseReport.send.error_message": "发送举报时返回了错误:\n“%s”", -+ "gui.abuseReport.send.generic_error": "发送举报时遇到意外错误。", -+ "gui.abuseReport.send.http_error": "发送举报时发生了意外的HTTP错误。", -+ "gui.abuseReport.send.json_error": "发送举报时遇到了格式错误的负载。", -+ "gui.abuseReport.send.service_unavailable": "无法使用举报服务。请检查是否已联网,然后重试。", -+ "gui.abuseReport.sending.title": "发送举报中…", -+ "gui.abuseReport.sent.title": "举报已发送", -+ "gui.acknowledge": "了解", -+ "gui.advancements": "进度", -+ "gui.all": "全部", -+ "gui.back": "返回", -+ "gui.banned.description": "%s\n\n%s\n\n点击以下链接了解更多:%s", -+ "gui.banned.description.permanent": "你的账户已被永久封禁,无法进行多人游戏或加入Realms。", -+ "gui.banned.description.reason": "我们最近收到举报称你的账户存在不良行为。我们的监督员已审核你的案件并将其认定为%s,这违反了Minecraft社区准则。", -+ "gui.banned.description.reason_id": "代码:%s", -+ "gui.banned.description.reason_id_message": "代码:%s - %s", -+ "gui.banned.description.temporary": "%s期限结束前,你无法进行多人游戏或加入Realms。", -+ "gui.banned.description.temporary.duration": "你的账户已被暂时封禁并将于%s后解封。", -+ "gui.banned.description.unknownreason": "我们最近收到举报称你的账户存在不良行为。我们的监督员已审核你的案件并认定你违反了Minecraft社区准则。", -+ "gui.banned.reason.defamation_impersonation_false_information": "假冒或分享信息以利用或误导他人", -+ "gui.banned.reason.drugs": "推销非法药品", -+ "gui.banned.reason.extreme_violence_or_gore": "描述现实生活中过度暴力或血腥的场面", -+ "gui.banned.reason.false_reporting": "过多虚假或不实举报", -+ "gui.banned.reason.fraud": "欺骗性获取或使用内容", -+ "gui.banned.reason.generic_violation": "违反社区准则", -+ "gui.banned.reason.harassment_or_bullying": "有针对性地、有害地使用辱骂性语言", -+ "gui.banned.reason.hate_speech": "仇恨或歧视言论", -+ "gui.banned.reason.hate_terrorism_notorious_figure": "推广仇恨群体、恐怖组织或不法分子", -+ "gui.banned.reason.imminent_harm_to_person_or_property": "意图在现实生活中造成人身或财产伤害", -+ "gui.banned.reason.nudity_or_pornography": "展示淫秽或色情材料", -+ "gui.banned.reason.sexually_inappropriate": "性相关话题或内容", -+ "gui.banned.reason.spam_or_advertising": "滥发消息或广告宣传", -+ "gui.banned.title.permanent": "账户已被永久封禁", -+ "gui.banned.title.temporary": "账户已被暂时封禁", -+ "gui.cancel": "取消", -+ "gui.chatReport.comments": "留言", -+ "gui.chatReport.describe": "提供详情可以帮助我们更严谨地做出决定。", -+ "gui.chatReport.discard.content": "如果离开,该举报及留言将不会被保留。\n确定要离开吗?", -+ "gui.chatReport.discard.discard": "离开并放弃举报", -+ "gui.chatReport.discard.draft": "保存为草稿", -+ "gui.chatReport.discard.return": "继续编辑", -+ "gui.chatReport.discard.title": "放弃举报和留言?", -+ "gui.chatReport.draft.content": "继续编辑现有举报还是放弃并新建另一份举报?", -+ "gui.chatReport.draft.discard": "放弃", -+ "gui.chatReport.draft.edit": "继续编辑", -+ "gui.chatReport.draft.quittotitle.content": "继续编辑还是放弃?", -+ "gui.chatReport.draft.quittotitle.title": "你有一份聊天举报草稿将在退出时丢失", -+ "gui.chatReport.draft.title": "编辑聊天举报草稿?", -+ "gui.chatReport.more_comments": "请描述发生的状况:", -+ "gui.chatReport.observed_what": "你为什么要举报?", -+ "gui.chatReport.read_info": "了解举报功能", -+ "gui.chatReport.report_sent_msg": "我们已经成功收到你的举报。非常感谢!\n\n我们的团队将尽快进行审核。", -+ "gui.chatReport.select_chat": "选择要举报的聊天消息", -+ "gui.chatReport.select_reason": "选择举报类型", -+ "gui.chatReport.selected_chat": "已选择%s条要举报的聊天消息", -+ "gui.chatReport.send": "发送举报", -+ "gui.chatReport.send.comments_too_long": "请缩短留言", -+ "gui.chatReport.send.no_reason": "请选择举报类型", -+ "gui.chatReport.send.no_reported_messages": "请选择至少一条要举报的聊天消息", -+ "gui.chatReport.send.too_many_messages": "举报中包含的消息过多", -+ "gui.chatReport.title": "举报玩家", -+ "gui.chatSelection.context": "所选消息前后的部分消息将会用于提供额外的辅助信息", -+ "gui.chatSelection.fold": "已隐藏%s条消息", -+ "gui.chatSelection.heading": "%s %s", -+ "gui.chatSelection.join": "%s加入了聊天", -+ "gui.chatSelection.message.narrate": "%1$s在%3$s说:%2$s", -+ "gui.chatSelection.selected": "已选中%s条消息,共%s条", -+ "gui.chatSelection.title": "选择要举报的聊天消息", -+ "gui.continue": "继续", -+ "gui.copy_link_to_clipboard": "复制链接到剪贴板", -+ "gui.days": "%s天", -+ "gui.done": "完成", -+ "gui.down": "向下", -+ "gui.entity_tooltip.type": "类型:%s", -+ "gui.hours": "%s小时", -+ "gui.minutes": "%s分钟", -+ "gui.multiLineEditBox.character_limit": "%s/%s", -+ "gui.narrate.button": "%s按钮", -+ "gui.narrate.editBox": "%s编辑框:%s", -+ "gui.narrate.slider": "%s滑块", -+ "gui.narrate.tab": "%s标签页", -+ "gui.no": "否", -+ "gui.none": "无", -+ "gui.ok": "确定", -+ "gui.proceed": "继续", -+ "gui.recipebook.moreRecipes": "右击以获取更多信息", -+ "gui.recipebook.search_hint": "搜索…", -+ "gui.recipebook.toggleRecipes.all": "显示全部", -+ "gui.recipebook.toggleRecipes.blastable": "仅显示可冶炼", -+ "gui.recipebook.toggleRecipes.craftable": "仅显示可合成", -+ "gui.recipebook.toggleRecipes.smeltable": "仅显示可烧炼", -+ "gui.recipebook.toggleRecipes.smokable": "仅显示可熏制", -+ "gui.socialInteractions.blocking_hint": "使用Microsoft账户管理", -+ "gui.socialInteractions.empty_blocked": "未屏蔽任何玩家的聊天消息", -+ "gui.socialInteractions.empty_hidden": "未隐藏任何玩家的聊天消息", -+ "gui.socialInteractions.hidden_in_chat": "%s的聊天消息将会被隐藏", -+ "gui.socialInteractions.hide": "在聊天中隐藏", -+ "gui.socialInteractions.narration.hide": "隐藏%s发送的消息", -+ "gui.socialInteractions.narration.report": "举报玩家%s", -+ "gui.socialInteractions.narration.show": "显示%s发送的消息", -+ "gui.socialInteractions.report": "举报", -+ "gui.socialInteractions.search_empty": "未找到使用此名称的玩家", -+ "gui.socialInteractions.search_hint": "搜索…", -+ "gui.socialInteractions.server_label.multiple": "%s - %s名玩家", -+ "gui.socialInteractions.server_label.single": "%s - %s名玩家", -+ "gui.socialInteractions.show": "在聊天中显示", -+ "gui.socialInteractions.shown_in_chat": "%s的聊天消息将会被显示", -+ "gui.socialInteractions.status_blocked": "已屏蔽", -+ "gui.socialInteractions.status_blocked_offline": "已屏蔽 - 离线", -+ "gui.socialInteractions.status_hidden": "隐藏", -+ "gui.socialInteractions.status_hidden_offline": "隐藏 - 离线", -+ "gui.socialInteractions.status_offline": "离线", -+ "gui.socialInteractions.tab_all": "全部", -+ "gui.socialInteractions.tab_blocked": "已屏蔽", -+ "gui.socialInteractions.tab_hidden": "已隐藏", -+ "gui.socialInteractions.title": "社交", -+ "gui.socialInteractions.tooltip.hide": "隐藏消息", -+ "gui.socialInteractions.tooltip.report": "举报玩家", -+ "gui.socialInteractions.tooltip.report.disabled": "举报服务不可用", -+ "gui.socialInteractions.tooltip.report.no_messages": "玩家%s没有可举报的消息", -+ "gui.socialInteractions.tooltip.report.not_reportable": "无法举报该玩家,服务器无法验证其聊天消息。", -+ "gui.socialInteractions.tooltip.show": "显示消息", -+ "gui.stats": "统计信息", -+ "gui.toMenu": "返回到服务器列表", -+ "gui.toRealms": "返回到Realm列表", -+ "gui.toTitle": "返回到标题屏幕", -+ "gui.toWorld": "返回到世界列表", -+ "gui.up": "向上", -+ "gui.yes": "是", -+ "hanging_sign.edit": "编辑悬挂式告示牌消息", -+ "instrument.minecraft.admire_goat_horn": "仰慕", -+ "instrument.minecraft.call_goat_horn": "呼唤", -+ "instrument.minecraft.dream_goat_horn": "想象", -+ "instrument.minecraft.feel_goat_horn": "感受", -+ "instrument.minecraft.ponder_goat_horn": "沉思", -+ "instrument.minecraft.seek_goat_horn": "寻觅", -+ "instrument.minecraft.sing_goat_horn": "歌颂", -+ "instrument.minecraft.yearn_goat_horn": "憧憬", -+ "inventory.binSlot": "摧毁物品", -+ "inventory.hotbarInfo": "用%1$s+%2$s来保存快捷栏", -+ "inventory.hotbarSaved": "已保存物品快捷栏(用%1$s+%2$s来加载)", -+ "item.canBreak": "能破坏:", -+ "item.canPlace": "可以放在:", -+ "item.color": "颜色:%s", -+ "item.disabled": "已禁用物品", -+ "item.durability": "耐久度:%s / %s", -+ "item.dyed": "已染色", -+ "item.minecraft.acacia_boat": "金合欢木船", -+ "item.minecraft.acacia_chest_boat": "金合欢木运输船", -+ "item.minecraft.allay_spawn_egg": "悦灵刷怪蛋", -+ "item.minecraft.amethyst_shard": "紫水晶碎片", -+ "item.minecraft.angler_pottery_shard": "垂钓纹样陶片", -+ "item.minecraft.angler_pottery_sherd": "垂钓纹样陶片", -+ "item.minecraft.apple": "苹果", -+ "item.minecraft.archer_pottery_shard": "弓箭纹样陶片", -+ "item.minecraft.archer_pottery_sherd": "弓箭纹样陶片", -+ "item.minecraft.armor_stand": "盔甲架", -+ "item.minecraft.arms_up_pottery_shard": "举臂纹样陶片", -+ "item.minecraft.arms_up_pottery_sherd": "举臂纹样陶片", -+ "item.minecraft.arrow": "箭", -+ "item.minecraft.axolotl_bucket": "美西螈桶", -+ "item.minecraft.axolotl_spawn_egg": "美西螈刷怪蛋", -+ "item.minecraft.baked_potato": "烤马铃薯", -+ "item.minecraft.bamboo_chest_raft": "运输竹筏", -+ "item.minecraft.bamboo_raft": "竹筏", -+ "item.minecraft.bat_spawn_egg": "蝙蝠刷怪蛋", -+ "item.minecraft.bee_spawn_egg": "蜜蜂刷怪蛋", -+ "item.minecraft.beef": "生牛肉", -+ "item.minecraft.beetroot": "甜菜根", -+ "item.minecraft.beetroot_seeds": "甜菜种子", -+ "item.minecraft.beetroot_soup": "甜菜汤", -+ "item.minecraft.birch_boat": "白桦木船", -+ "item.minecraft.birch_chest_boat": "白桦木运输船", -+ "item.minecraft.black_dye": "黑色染料", -+ "item.minecraft.blade_pottery_shard": "利刃纹样陶片", -+ "item.minecraft.blade_pottery_sherd": "利刃纹样陶片", -+ "item.minecraft.blaze_powder": "烈焰粉", -+ "item.minecraft.blaze_rod": "烈焰棒", -+ "item.minecraft.blaze_spawn_egg": "烈焰人刷怪蛋", -+ "item.minecraft.blue_dye": "蓝色染料", -+ "item.minecraft.bone": "骨头", -+ "item.minecraft.bone_meal": "骨粉", -+ "item.minecraft.book": "书", -+ "item.minecraft.bow": "弓", -+ "item.minecraft.bowl": "碗", -+ "item.minecraft.bread": "面包", -+ "item.minecraft.brewer_pottery_shard": "佳酿纹样陶片", -+ "item.minecraft.brewer_pottery_sherd": "佳酿纹样陶片", -+ "item.minecraft.brewing_stand": "酿造台", -+ "item.minecraft.brick": "红砖", -+ "item.minecraft.brown_dye": "棕色染料", -+ "item.minecraft.brush": "刷子", -+ "item.minecraft.bucket": "铁桶", -+ "item.minecraft.bundle": "收纳袋", -+ "item.minecraft.bundle.fullness": "%s/%s", -+ "item.minecraft.burn_pottery_shard": "烈焰纹样陶片", -+ "item.minecraft.burn_pottery_sherd": "烈焰纹样陶片", -+ "item.minecraft.camel_spawn_egg": "骆驼刷怪蛋", -+ "item.minecraft.carrot": "胡萝卜", -+ "item.minecraft.carrot_on_a_stick": "胡萝卜钓竿", -+ "item.minecraft.cat_spawn_egg": "猫刷怪蛋", -+ "item.minecraft.cauldron": "炼药锅", -+ "item.minecraft.cave_spider_spawn_egg": "洞穴蜘蛛刷怪蛋", -+ "item.minecraft.chainmail_boots": "锁链靴子", -+ "item.minecraft.chainmail_chestplate": "锁链胸甲", -+ "item.minecraft.chainmail_helmet": "锁链头盔", -+ "item.minecraft.chainmail_leggings": "锁链护腿", -+ "item.minecraft.charcoal": "木炭", -+ "item.minecraft.cherry_boat": "樱花木船", -+ "item.minecraft.cherry_chest_boat": "樱花木运输船", -+ "item.minecraft.chest_minecart": "运输矿车", -+ "item.minecraft.chicken": "生鸡肉", -+ "item.minecraft.chicken_spawn_egg": "鸡刷怪蛋", -+ "item.minecraft.chorus_fruit": "紫颂果", -+ "item.minecraft.clay_ball": "黏土球", -+ "item.minecraft.clock": "时钟", -+ "item.minecraft.coal": "煤炭", -+ "item.minecraft.cocoa_beans": "可可豆", -+ "item.minecraft.cod": "生鳕鱼", -+ "item.minecraft.cod_bucket": "鳕鱼桶", -+ "item.minecraft.cod_spawn_egg": "鳕鱼刷怪蛋", -+ "item.minecraft.command_block_minecart": "命令方块矿车", -+ "item.minecraft.compass": "指南针", -+ "item.minecraft.cooked_beef": "牛排", -+ "item.minecraft.cooked_chicken": "熟鸡肉", -+ "item.minecraft.cooked_cod": "熟鳕鱼", -+ "item.minecraft.cooked_mutton": "熟羊肉", -+ "item.minecraft.cooked_porkchop": "熟猪排", -+ "item.minecraft.cooked_rabbit": "熟兔肉", -+ "item.minecraft.cooked_salmon": "熟鲑鱼", -+ "item.minecraft.cookie": "曲奇", -+ "item.minecraft.copper_ingot": "铜锭", -+ "item.minecraft.cow_spawn_egg": "牛刷怪蛋", -+ "item.minecraft.creeper_banner_pattern": "旗帜图案", -+ "item.minecraft.creeper_banner_pattern.desc": "苦力怕盾徽", -+ "item.minecraft.creeper_spawn_egg": "苦力怕刷怪蛋", -+ "item.minecraft.crossbow": "弩", -+ "item.minecraft.crossbow.projectile": "弹射物:", -+ "item.minecraft.cyan_dye": "青色染料", -+ "item.minecraft.danger_pottery_shard": "危机纹样陶片", -+ "item.minecraft.danger_pottery_sherd": "危机纹样陶片", -+ "item.minecraft.dark_oak_boat": "深色橡木船", -+ "item.minecraft.dark_oak_chest_boat": "深色橡木运输船", -+ "item.minecraft.debug_stick": "调试棒", -+ "item.minecraft.debug_stick.empty": "%s不具备属性", -+ "item.minecraft.debug_stick.select": "已选择“%s”(%s)", -+ "item.minecraft.debug_stick.update": "“%s”设为%s", -+ "item.minecraft.diamond": "钻石", -+ "item.minecraft.diamond_axe": "钻石斧", -+ "item.minecraft.diamond_boots": "钻石靴子", -+ "item.minecraft.diamond_chestplate": "钻石胸甲", -+ "item.minecraft.diamond_helmet": "钻石头盔", -+ "item.minecraft.diamond_hoe": "钻石锄", -+ "item.minecraft.diamond_horse_armor": "钻石马铠", -+ "item.minecraft.diamond_leggings": "钻石护腿", -+ "item.minecraft.diamond_pickaxe": "钻石镐", -+ "item.minecraft.diamond_shovel": "钻石锹", -+ "item.minecraft.diamond_sword": "钻石剑", -+ "item.minecraft.disc_fragment_5": "唱片残片", -+ "item.minecraft.disc_fragment_5.desc": "音乐唱片 - 5", -+ "item.minecraft.dolphin_spawn_egg": "海豚刷怪蛋", -+ "item.minecraft.donkey_spawn_egg": "驴刷怪蛋", -+ "item.minecraft.dragon_breath": "龙息", -+ "item.minecraft.dried_kelp": "干海带", -+ "item.minecraft.drowned_spawn_egg": "溺尸刷怪蛋", -+ "item.minecraft.echo_shard": "回响碎片", -+ "item.minecraft.egg": "鸡蛋", -+ "item.minecraft.elder_guardian_spawn_egg": "远古守卫者刷怪蛋", -+ "item.minecraft.elytra": "鞘翅", -+ "item.minecraft.emerald": "绿宝石", -+ "item.minecraft.enchanted_book": "附魔书", -+ "item.minecraft.enchanted_golden_apple": "附魔金苹果", -+ "item.minecraft.end_crystal": "末地水晶", -+ "item.minecraft.ender_dragon_spawn_egg": "末影龙刷怪蛋", -+ "item.minecraft.ender_eye": "末影之眼", -+ "item.minecraft.ender_pearl": "末影珍珠", -+ "item.minecraft.enderman_spawn_egg": "末影人刷怪蛋", -+ "item.minecraft.endermite_spawn_egg": "末影螨刷怪蛋", -+ "item.minecraft.evoker_spawn_egg": "唤魔者刷怪蛋", -+ "item.minecraft.experience_bottle": "附魔之瓶", -+ "item.minecraft.explorer_pottery_shard": "探险纹样陶片", -+ "item.minecraft.explorer_pottery_sherd": "探险纹样陶片", -+ "item.minecraft.feather": "羽毛", -+ "item.minecraft.fermented_spider_eye": "发酵蛛眼", -+ "item.minecraft.filled_map": "地图", -+ "item.minecraft.fire_charge": "火焰弹", -+ "item.minecraft.firework_rocket": "烟花火箭", -+ "item.minecraft.firework_rocket.flight": "飞行时间:", -+ "item.minecraft.firework_star": "烟火之星", -+ "item.minecraft.firework_star.black": "黑色", -+ "item.minecraft.firework_star.blue": "蓝色", -+ "item.minecraft.firework_star.brown": "棕色", -+ "item.minecraft.firework_star.custom_color": "自定义", -+ "item.minecraft.firework_star.cyan": "青色", -+ "item.minecraft.firework_star.fade_to": "淡化至", -+ "item.minecraft.firework_star.flicker": "闪烁", -+ "item.minecraft.firework_star.gray": "灰色", -+ "item.minecraft.firework_star.green": "绿色", -+ "item.minecraft.firework_star.light_blue": "淡蓝色", -+ "item.minecraft.firework_star.light_gray": "淡灰色", -+ "item.minecraft.firework_star.lime": "黄绿色", -+ "item.minecraft.firework_star.magenta": "品红色", -+ "item.minecraft.firework_star.orange": "橙色", -+ "item.minecraft.firework_star.pink": "粉红色", -+ "item.minecraft.firework_star.purple": "紫色", -+ "item.minecraft.firework_star.red": "红色", -+ "item.minecraft.firework_star.shape": "未知形状", -+ "item.minecraft.firework_star.shape.burst": "喷发状", -+ "item.minecraft.firework_star.shape.creeper": "苦力怕状", -+ "item.minecraft.firework_star.shape.large_ball": "大型球状", -+ "item.minecraft.firework_star.shape.small_ball": "小型球状", -+ "item.minecraft.firework_star.shape.star": "星形", -+ "item.minecraft.firework_star.trail": "踪迹", -+ "item.minecraft.firework_star.white": "白色", -+ "item.minecraft.firework_star.yellow": "黄色", -+ "item.minecraft.fishing_rod": "钓鱼竿", -+ "item.minecraft.flint": "燧石", -+ "item.minecraft.flint_and_steel": "打火石", -+ "item.minecraft.flower_banner_pattern": "旗帜图案", -+ "item.minecraft.flower_banner_pattern.desc": "花朵盾徽", -+ "item.minecraft.flower_pot": "花盆", -+ "item.minecraft.fox_spawn_egg": "狐狸刷怪蛋", -+ "item.minecraft.friend_pottery_shard": "挚友纹样陶片", -+ "item.minecraft.friend_pottery_sherd": "挚友纹样陶片", -+ "item.minecraft.frog_spawn_egg": "青蛙刷怪蛋", -+ "item.minecraft.furnace_minecart": "动力矿车", -+ "item.minecraft.ghast_spawn_egg": "恶魂刷怪蛋", -+ "item.minecraft.ghast_tear": "恶魂之泪", -+ "item.minecraft.glass_bottle": "玻璃瓶", -+ "item.minecraft.glistering_melon_slice": "闪烁的西瓜片", -+ "item.minecraft.globe_banner_pattern": "旗帜图案", -+ "item.minecraft.globe_banner_pattern.desc": "地球", -+ "item.minecraft.glow_berries": "发光浆果", -+ "item.minecraft.glow_ink_sac": "荧光墨囊", -+ "item.minecraft.glow_item_frame": "荧光物品展示框", -+ "item.minecraft.glow_squid_spawn_egg": "发光鱿鱼刷怪蛋", -+ "item.minecraft.glowstone_dust": "荧石粉", -+ "item.minecraft.goat_horn": "山羊角", -+ "item.minecraft.goat_spawn_egg": "山羊刷怪蛋", -+ "item.minecraft.gold_ingot": "金锭", -+ "item.minecraft.gold_nugget": "金粒", -+ "item.minecraft.golden_apple": "金苹果", -+ "item.minecraft.golden_axe": "金斧", -+ "item.minecraft.golden_boots": "金靴子", -+ "item.minecraft.golden_carrot": "金胡萝卜", -+ "item.minecraft.golden_chestplate": "金胸甲", -+ "item.minecraft.golden_helmet": "金头盔", -+ "item.minecraft.golden_hoe": "金锄", -+ "item.minecraft.golden_horse_armor": "金马铠", -+ "item.minecraft.golden_leggings": "金护腿", -+ "item.minecraft.golden_pickaxe": "金镐", -+ "item.minecraft.golden_shovel": "金锹", -+ "item.minecraft.golden_sword": "金剑", -+ "item.minecraft.gray_dye": "灰色染料", -+ "item.minecraft.green_dye": "绿色染料", -+ "item.minecraft.guardian_spawn_egg": "守卫者刷怪蛋", -+ "item.minecraft.gunpowder": "火药", -+ "item.minecraft.heart_of_the_sea": "海洋之心", -+ "item.minecraft.heart_pottery_shard": "爱心纹样陶片", -+ "item.minecraft.heart_pottery_sherd": "爱心纹样陶片", -+ "item.minecraft.heartbreak_pottery_shard": "心碎纹样陶片", -+ "item.minecraft.heartbreak_pottery_sherd": "心碎纹样陶片", -+ "item.minecraft.hoglin_spawn_egg": "疣猪兽刷怪蛋", -+ "item.minecraft.honey_bottle": "蜂蜜瓶", -+ "item.minecraft.honeycomb": "蜜脾", -+ "item.minecraft.hopper_minecart": "漏斗矿车", -+ "item.minecraft.horse_spawn_egg": "马刷怪蛋", -+ "item.minecraft.howl_pottery_shard": "狼嚎纹样陶片", -+ "item.minecraft.howl_pottery_sherd": "狼嚎纹样陶片", -+ "item.minecraft.husk_spawn_egg": "尸壳刷怪蛋", -+ "item.minecraft.ink_sac": "墨囊", -+ "item.minecraft.iron_axe": "铁斧", -+ "item.minecraft.iron_boots": "铁靴子", -+ "item.minecraft.iron_chestplate": "铁胸甲", -+ "item.minecraft.iron_golem_spawn_egg": "铁傀儡刷怪蛋", -+ "item.minecraft.iron_helmet": "铁头盔", -+ "item.minecraft.iron_hoe": "铁锄", -+ "item.minecraft.iron_horse_armor": "铁马铠", -+ "item.minecraft.iron_ingot": "铁锭", -+ "item.minecraft.iron_leggings": "铁护腿", -+ "item.minecraft.iron_nugget": "铁粒", -+ "item.minecraft.iron_pickaxe": "铁镐", -+ "item.minecraft.iron_shovel": "铁锹", -+ "item.minecraft.iron_sword": "铁剑", -+ "item.minecraft.item_frame": "物品展示框", -+ "item.minecraft.jungle_boat": "丛林木船", -+ "item.minecraft.jungle_chest_boat": "丛林木运输船", -+ "item.minecraft.knowledge_book": "知识之书", -+ "item.minecraft.lapis_lazuli": "青金石", -+ "item.minecraft.lava_bucket": "熔岩桶", -+ "item.minecraft.lead": "拴绳", -+ "item.minecraft.leather": "皮革", -+ "item.minecraft.leather_boots": "皮革靴子", -+ "item.minecraft.leather_chestplate": "皮革外套", -+ "item.minecraft.leather_helmet": "皮革帽子", -+ "item.minecraft.leather_horse_armor": "皮革马铠", -+ "item.minecraft.leather_leggings": "皮革裤子", -+ "item.minecraft.light_blue_dye": "淡蓝色染料", -+ "item.minecraft.light_gray_dye": "淡灰色染料", -+ "item.minecraft.lime_dye": "黄绿色染料", -+ "item.minecraft.lingering_potion": "滞留药水", -+ "item.minecraft.lingering_potion.effect.awkward": "滞留型粗制的药水", -+ "item.minecraft.lingering_potion.effect.empty": "不可合成的滞留型药水", -+ "item.minecraft.lingering_potion.effect.fire_resistance": "滞留型抗火药水", -+ "item.minecraft.lingering_potion.effect.harming": "滞留型伤害药水", -+ "item.minecraft.lingering_potion.effect.healing": "滞留型治疗药水", -+ "item.minecraft.lingering_potion.effect.invisibility": "滞留型隐身药水", -+ "item.minecraft.lingering_potion.effect.leaping": "滞留型跳跃药水", -+ "item.minecraft.lingering_potion.effect.levitation": "滞留型飘浮药水", -+ "item.minecraft.lingering_potion.effect.luck": "滞留型幸运药水", -+ "item.minecraft.lingering_potion.effect.mundane": "滞留型平凡的药水", -+ "item.minecraft.lingering_potion.effect.night_vision": "滞留型夜视药水", -+ "item.minecraft.lingering_potion.effect.poison": "滞留型剧毒药水", -+ "item.minecraft.lingering_potion.effect.regeneration": "滞留型再生药水", -+ "item.minecraft.lingering_potion.effect.slow_falling": "滞留型缓降药水", -+ "item.minecraft.lingering_potion.effect.slowness": "滞留型迟缓药水", -+ "item.minecraft.lingering_potion.effect.strength": "滞留型力量药水", -+ "item.minecraft.lingering_potion.effect.swiftness": "滞留型迅捷药水", -+ "item.minecraft.lingering_potion.effect.thick": "滞留型浓稠的药水", -+ "item.minecraft.lingering_potion.effect.turtle_master": "滞留型神龟药水", -+ "item.minecraft.lingering_potion.effect.water": "滞留型水瓶", -+ "item.minecraft.lingering_potion.effect.water_breathing": "滞留型水肺药水", -+ "item.minecraft.lingering_potion.effect.weakness": "滞留型虚弱药水", -+ "item.minecraft.llama_spawn_egg": "羊驼刷怪蛋", -+ "item.minecraft.lodestone_compass": "磁石指针", -+ "item.minecraft.magenta_dye": "品红色染料", -+ "item.minecraft.magma_cream": "岩浆膏", -+ "item.minecraft.magma_cube_spawn_egg": "岩浆怪刷怪蛋", -+ "item.minecraft.mangrove_boat": "红树木船", -+ "item.minecraft.mangrove_chest_boat": "红树木运输船", -+ "item.minecraft.map": "空地图", -+ "item.minecraft.melon_seeds": "西瓜种子", -+ "item.minecraft.melon_slice": "西瓜片", -+ "item.minecraft.milk_bucket": "奶桶", -+ "item.minecraft.minecart": "矿车", -+ "item.minecraft.miner_pottery_shard": "采矿纹样陶片", -+ "item.minecraft.miner_pottery_sherd": "采矿纹样陶片", -+ "item.minecraft.mojang_banner_pattern": "旗帜图案", -+ "item.minecraft.mojang_banner_pattern.desc": "Mojang徽标", -+ "item.minecraft.mooshroom_spawn_egg": "哞菇刷怪蛋", -+ "item.minecraft.mourner_pottery_shard": "悲恸纹样陶片", -+ "item.minecraft.mourner_pottery_sherd": "悲恸纹样陶片", -+ "item.minecraft.mule_spawn_egg": "骡刷怪蛋", -+ "item.minecraft.mushroom_stew": "蘑菇煲", -+ "item.minecraft.music_disc_11": "音乐唱片", -+ "item.minecraft.music_disc_11.desc": "C418 - 11", -+ "item.minecraft.music_disc_13": "音乐唱片", -+ "item.minecraft.music_disc_13.desc": "C418 - 13", -+ "item.minecraft.music_disc_5": "音乐唱片", -+ "item.minecraft.music_disc_5.desc": "Samuel Åberg - 5", -+ "item.minecraft.music_disc_blocks": "音乐唱片", -+ "item.minecraft.music_disc_blocks.desc": "C418 - blocks", -+ "item.minecraft.music_disc_cat": "音乐唱片", -+ "item.minecraft.music_disc_cat.desc": "C418 - cat", -+ "item.minecraft.music_disc_chirp": "音乐唱片", -+ "item.minecraft.music_disc_chirp.desc": "C418 - chirp", -+ "item.minecraft.music_disc_far": "音乐唱片", -+ "item.minecraft.music_disc_far.desc": "C418 - far", -+ "item.minecraft.music_disc_mall": "音乐唱片", -+ "item.minecraft.music_disc_mall.desc": "C418 - mall", -+ "item.minecraft.music_disc_mellohi": "音乐唱片", -+ "item.minecraft.music_disc_mellohi.desc": "C418 - mellohi", -+ "item.minecraft.music_disc_otherside": "音乐唱片", -+ "item.minecraft.music_disc_otherside.desc": "Lena Raine - otherside", -+ "item.minecraft.music_disc_pigstep": "音乐唱片", -+ "item.minecraft.music_disc_pigstep.desc": "Lena Raine - Pigstep", -+ "item.minecraft.music_disc_relic": "音乐唱片", -+ "item.minecraft.music_disc_relic.desc": "Aaron Cherof - Relic", -+ "item.minecraft.music_disc_stal": "音乐唱片", -+ "item.minecraft.music_disc_stal.desc": "C418 - stal", -+ "item.minecraft.music_disc_strad": "音乐唱片", -+ "item.minecraft.music_disc_strad.desc": "C418 - strad", -+ "item.minecraft.music_disc_wait": "音乐唱片", -+ "item.minecraft.music_disc_wait.desc": "C418 - wait", -+ "item.minecraft.music_disc_ward": "音乐唱片", -+ "item.minecraft.music_disc_ward.desc": "C418 - ward", -+ "item.minecraft.mutton": "生羊肉", -+ "item.minecraft.name_tag": "命名牌", -+ "item.minecraft.nautilus_shell": "鹦鹉螺壳", -+ "item.minecraft.nether_brick": "下界砖", -+ "item.minecraft.nether_star": "下界之星", -+ "item.minecraft.nether_wart": "下界疣", -+ "item.minecraft.netherite_axe": "下界合金斧", -+ "item.minecraft.netherite_boots": "下界合金靴子", -+ "item.minecraft.netherite_chestplate": "下界合金胸甲", -+ "item.minecraft.netherite_helmet": "下界合金头盔", -+ "item.minecraft.netherite_hoe": "下界合金锄", -+ "item.minecraft.netherite_ingot": "下界合金锭", -+ "item.minecraft.netherite_leggings": "下界合金护腿", -+ "item.minecraft.netherite_pickaxe": "下界合金镐", -+ "item.minecraft.netherite_scrap": "下界合金碎片", -+ "item.minecraft.netherite_shovel": "下界合金锹", -+ "item.minecraft.netherite_sword": "下界合金剑", -+ "item.minecraft.oak_boat": "橡木船", -+ "item.minecraft.oak_chest_boat": "橡木运输船", -+ "item.minecraft.ocelot_spawn_egg": "豹猫刷怪蛋", -+ "item.minecraft.orange_dye": "橙色染料", -+ "item.minecraft.painting": "画", -+ "item.minecraft.panda_spawn_egg": "熊猫刷怪蛋", -+ "item.minecraft.paper": "纸", -+ "item.minecraft.parrot_spawn_egg": "鹦鹉刷怪蛋", -+ "item.minecraft.phantom_membrane": "幻翼膜", -+ "item.minecraft.phantom_spawn_egg": "幻翼刷怪蛋", -+ "item.minecraft.pig_spawn_egg": "猪刷怪蛋", -+ "item.minecraft.piglin_banner_pattern": "旗帜图案", -+ "item.minecraft.piglin_banner_pattern.desc": "猪鼻", -+ "item.minecraft.piglin_brute_spawn_egg": "猪灵蛮兵刷怪蛋", -+ "item.minecraft.piglin_spawn_egg": "猪灵刷怪蛋", -+ "item.minecraft.pillager_spawn_egg": "掠夺者刷怪蛋", -+ "item.minecraft.pink_dye": "粉红色染料", -+ "item.minecraft.pitcher_plant": "瓶子草", -+ "item.minecraft.pitcher_pod": "瓶子草荚果", -+ "item.minecraft.plenty_pottery_shard": "富饶纹样陶片", -+ "item.minecraft.plenty_pottery_sherd": "富饶纹样陶片", -+ "item.minecraft.poisonous_potato": "毒马铃薯", -+ "item.minecraft.polar_bear_spawn_egg": "北极熊刷怪蛋", -+ "item.minecraft.popped_chorus_fruit": "爆裂紫颂果", -+ "item.minecraft.porkchop": "生猪排", -+ "item.minecraft.potato": "马铃薯", -+ "item.minecraft.potion": "药水", -+ "item.minecraft.potion.effect.awkward": "粗制的药水", -+ "item.minecraft.potion.effect.empty": "不可合成的药水", -+ "item.minecraft.potion.effect.fire_resistance": "抗火药水", -+ "item.minecraft.potion.effect.harming": "伤害药水", -+ "item.minecraft.potion.effect.healing": "治疗药水", -+ "item.minecraft.potion.effect.invisibility": "隐身药水", -+ "item.minecraft.potion.effect.leaping": "跳跃药水", -+ "item.minecraft.potion.effect.levitation": "飘浮药水", -+ "item.minecraft.potion.effect.luck": "幸运药水", -+ "item.minecraft.potion.effect.mundane": "平凡的药水", -+ "item.minecraft.potion.effect.night_vision": "夜视药水", -+ "item.minecraft.potion.effect.poison": "剧毒药水", -+ "item.minecraft.potion.effect.regeneration": "再生药水", -+ "item.minecraft.potion.effect.slow_falling": "缓降药水", -+ "item.minecraft.potion.effect.slowness": "迟缓药水", -+ "item.minecraft.potion.effect.strength": "力量药水", -+ "item.minecraft.potion.effect.swiftness": "迅捷药水", -+ "item.minecraft.potion.effect.thick": "浓稠的药水", -+ "item.minecraft.potion.effect.turtle_master": "神龟药水", -+ "item.minecraft.potion.effect.water": "水瓶", -+ "item.minecraft.potion.effect.water_breathing": "水肺药水", -+ "item.minecraft.potion.effect.weakness": "虚弱药水", -+ "item.minecraft.pottery_shard_archer": "弓箭纹样陶片", -+ "item.minecraft.pottery_shard_arms_up": "举臂纹样陶片", -+ "item.minecraft.pottery_shard_prize": "珍宝纹样陶片", -+ "item.minecraft.pottery_shard_skull": "头颅纹样陶片", -+ "item.minecraft.powder_snow_bucket": "细雪桶", -+ "item.minecraft.prismarine_crystals": "海晶砂粒", -+ "item.minecraft.prismarine_shard": "海晶碎片", -+ "item.minecraft.prize_pottery_shard": "珍宝纹样陶片", -+ "item.minecraft.prize_pottery_sherd": "珍宝纹样陶片", -+ "item.minecraft.pufferfish": "河豚", -+ "item.minecraft.pufferfish_bucket": "河豚桶", -+ "item.minecraft.pufferfish_spawn_egg": "河豚刷怪蛋", -+ "item.minecraft.pumpkin_pie": "南瓜派", -+ "item.minecraft.pumpkin_seeds": "南瓜种子", -+ "item.minecraft.purple_dye": "紫色染料", -+ "item.minecraft.quartz": "下界石英", -+ "item.minecraft.rabbit": "生兔肉", -+ "item.minecraft.rabbit_foot": "兔子脚", -+ "item.minecraft.rabbit_hide": "兔子皮", -+ "item.minecraft.rabbit_spawn_egg": "兔子刷怪蛋", -+ "item.minecraft.rabbit_stew": "兔肉煲", -+ "item.minecraft.ravager_spawn_egg": "劫掠兽刷怪蛋", -+ "item.minecraft.raw_copper": "粗铜", -+ "item.minecraft.raw_gold": "粗金", -+ "item.minecraft.raw_iron": "粗铁", -+ "item.minecraft.recovery_compass": "追溯指针", -+ "item.minecraft.red_dye": "红色染料", -+ "item.minecraft.redstone": "红石粉", -+ "item.minecraft.rotten_flesh": "腐肉", -+ "item.minecraft.saddle": "鞍", -+ "item.minecraft.salmon": "生鲑鱼", -+ "item.minecraft.salmon_bucket": "鲑鱼桶", -+ "item.minecraft.salmon_spawn_egg": "鲑鱼刷怪蛋", -+ "item.minecraft.scute": "鳞甲", -+ "item.minecraft.sheaf_pottery_shard": "麦捆纹样陶片", -+ "item.minecraft.sheaf_pottery_sherd": "麦捆纹样陶片", -+ "item.minecraft.shears": "剪刀", -+ "item.minecraft.sheep_spawn_egg": "绵羊刷怪蛋", -+ "item.minecraft.shelter_pottery_shard": "树荫纹样陶片", -+ "item.minecraft.shelter_pottery_sherd": "树荫纹样陶片", -+ "item.minecraft.shield": "盾牌", -+ "item.minecraft.shield.black": "黑色盾牌", -+ "item.minecraft.shield.blue": "蓝色盾牌", -+ "item.minecraft.shield.brown": "棕色盾牌", -+ "item.minecraft.shield.cyan": "青色盾牌", -+ "item.minecraft.shield.gray": "灰色盾牌", -+ "item.minecraft.shield.green": "绿色盾牌", -+ "item.minecraft.shield.light_blue": "淡蓝色盾牌", -+ "item.minecraft.shield.light_gray": "淡灰色盾牌", -+ "item.minecraft.shield.lime": "黄绿色盾牌", -+ "item.minecraft.shield.magenta": "品红色盾牌", -+ "item.minecraft.shield.orange": "橙色盾牌", -+ "item.minecraft.shield.pink": "粉红色盾牌", -+ "item.minecraft.shield.purple": "紫色盾牌", -+ "item.minecraft.shield.red": "红色盾牌", -+ "item.minecraft.shield.white": "白色盾牌", -+ "item.minecraft.shield.yellow": "黄色盾牌", -+ "item.minecraft.shulker_shell": "潜影壳", -+ "item.minecraft.shulker_spawn_egg": "潜影贝刷怪蛋", -+ "item.minecraft.sign": "告示牌", -+ "item.minecraft.silverfish_spawn_egg": "蠹虫刷怪蛋", -+ "item.minecraft.skeleton_horse_spawn_egg": "骷髅马刷怪蛋", -+ "item.minecraft.skeleton_spawn_egg": "骷髅刷怪蛋", -+ "item.minecraft.skull_banner_pattern": "旗帜图案", -+ "item.minecraft.skull_banner_pattern.desc": "头颅盾徽", -+ "item.minecraft.skull_pottery_shard": "头颅纹样陶片", -+ "item.minecraft.skull_pottery_sherd": "头颅纹样陶片", -+ "item.minecraft.slime_ball": "黏液球", -+ "item.minecraft.slime_spawn_egg": "史莱姆刷怪蛋", -+ "item.minecraft.smithing_template": "锻造模板", -+ "item.minecraft.smithing_template.applies_to": "可应用于:", -+ "item.minecraft.smithing_template.armor_trim.additions_slot_description": "放入铸锭或晶体", -+ "item.minecraft.smithing_template.armor_trim.applies_to": "盔甲", -+ "item.minecraft.smithing_template.armor_trim.base_slot_description": "放入盔甲", -+ "item.minecraft.smithing_template.armor_trim.ingredients": "铸锭或晶体", -+ "item.minecraft.smithing_template.ingredients": "所需原材料:", -+ "item.minecraft.smithing_template.netherite_upgrade.additions_slot_description": "放入下界合金锭", -+ "item.minecraft.smithing_template.netherite_upgrade.applies_to": "钻石装备", -+ "item.minecraft.smithing_template.netherite_upgrade.base_slot_description": "放入钻石盔甲、武器或工具", -+ "item.minecraft.smithing_template.netherite_upgrade.ingredients": "下界合金锭", -+ "item.minecraft.smithing_template.upgrade": "已有升级:", -+ "item.minecraft.sniffer_spawn_egg": "嗅探兽刷怪蛋", -+ "item.minecraft.snort_pottery_shard": "嗅探纹样陶片", -+ "item.minecraft.snort_pottery_sherd": "嗅探纹样陶片", -+ "item.minecraft.snow_golem_spawn_egg": "雪傀儡刷怪蛋", -+ "item.minecraft.snowball": "雪球", -+ "item.minecraft.spectral_arrow": "光灵箭", -+ "item.minecraft.spider_eye": "蜘蛛眼", -+ "item.minecraft.spider_spawn_egg": "蜘蛛刷怪蛋", -+ "item.minecraft.splash_potion": "喷溅药水", -+ "item.minecraft.splash_potion.effect.awkward": "喷溅型粗制的药水", -+ "item.minecraft.splash_potion.effect.empty": "不可合成的喷溅型药水", -+ "item.minecraft.splash_potion.effect.fire_resistance": "喷溅型抗火药水", -+ "item.minecraft.splash_potion.effect.harming": "喷溅型伤害药水", -+ "item.minecraft.splash_potion.effect.healing": "喷溅型治疗药水", -+ "item.minecraft.splash_potion.effect.invisibility": "喷溅型隐身药水", -+ "item.minecraft.splash_potion.effect.leaping": "喷溅型跳跃药水", -+ "item.minecraft.splash_potion.effect.levitation": "喷溅型飘浮药水", -+ "item.minecraft.splash_potion.effect.luck": "喷溅型幸运药水", -+ "item.minecraft.splash_potion.effect.mundane": "喷溅型平凡的药水", -+ "item.minecraft.splash_potion.effect.night_vision": "喷溅型夜视药水", -+ "item.minecraft.splash_potion.effect.poison": "喷溅型剧毒药水", -+ "item.minecraft.splash_potion.effect.regeneration": "喷溅型再生药水", -+ "item.minecraft.splash_potion.effect.slow_falling": "喷溅型缓降药水", -+ "item.minecraft.splash_potion.effect.slowness": "喷溅型迟缓药水", -+ "item.minecraft.splash_potion.effect.strength": "喷溅型力量药水", -+ "item.minecraft.splash_potion.effect.swiftness": "喷溅型迅捷药水", -+ "item.minecraft.splash_potion.effect.thick": "喷溅型浓稠的药水", -+ "item.minecraft.splash_potion.effect.turtle_master": "喷溅型神龟药水", -+ "item.minecraft.splash_potion.effect.water": "喷溅型水瓶", -+ "item.minecraft.splash_potion.effect.water_breathing": "喷溅型水肺药水", -+ "item.minecraft.splash_potion.effect.weakness": "喷溅型虚弱药水", -+ "item.minecraft.spruce_boat": "云杉木船", -+ "item.minecraft.spruce_chest_boat": "云杉木运输船", -+ "item.minecraft.spyglass": "望远镜", -+ "item.minecraft.squid_spawn_egg": "鱿鱼刷怪蛋", -+ "item.minecraft.stick": "木棍", -+ "item.minecraft.stone_axe": "石斧", -+ "item.minecraft.stone_hoe": "石锄", -+ "item.minecraft.stone_pickaxe": "石镐", -+ "item.minecraft.stone_shovel": "石锹", -+ "item.minecraft.stone_sword": "石剑", -+ "item.minecraft.stray_spawn_egg": "流浪者刷怪蛋", -+ "item.minecraft.strider_spawn_egg": "炽足兽刷怪蛋", -+ "item.minecraft.string": "线", -+ "item.minecraft.sugar": "糖", -+ "item.minecraft.suspicious_stew": "谜之炖菜", -+ "item.minecraft.sweet_berries": "甜浆果", -+ "item.minecraft.tadpole_bucket": "蝌蚪桶", -+ "item.minecraft.tadpole_spawn_egg": "蝌蚪刷怪蛋", -+ "item.minecraft.tipped_arrow": "药箭", -+ "item.minecraft.tipped_arrow.effect.awkward": "药箭", -+ "item.minecraft.tipped_arrow.effect.empty": "不可合成的药箭", -+ "item.minecraft.tipped_arrow.effect.fire_resistance": "抗火之箭", -+ "item.minecraft.tipped_arrow.effect.harming": "伤害之箭", -+ "item.minecraft.tipped_arrow.effect.healing": "治疗之箭", -+ "item.minecraft.tipped_arrow.effect.invisibility": "隐身之箭", -+ "item.minecraft.tipped_arrow.effect.leaping": "跳跃之箭", -+ "item.minecraft.tipped_arrow.effect.levitation": "飘浮之箭", -+ "item.minecraft.tipped_arrow.effect.luck": "幸运之箭", -+ "item.minecraft.tipped_arrow.effect.mundane": "药箭", -+ "item.minecraft.tipped_arrow.effect.night_vision": "夜视之箭", -+ "item.minecraft.tipped_arrow.effect.poison": "剧毒之箭", -+ "item.minecraft.tipped_arrow.effect.regeneration": "再生之箭", -+ "item.minecraft.tipped_arrow.effect.slow_falling": "缓降之箭", -+ "item.minecraft.tipped_arrow.effect.slowness": "迟缓之箭", -+ "item.minecraft.tipped_arrow.effect.strength": "力量之箭", -+ "item.minecraft.tipped_arrow.effect.swiftness": "迅捷之箭", -+ "item.minecraft.tipped_arrow.effect.thick": "药箭", -+ "item.minecraft.tipped_arrow.effect.turtle_master": "神龟之箭", -+ "item.minecraft.tipped_arrow.effect.water": "喷溅之箭", -+ "item.minecraft.tipped_arrow.effect.water_breathing": "水肺之箭", -+ "item.minecraft.tipped_arrow.effect.weakness": "虚弱之箭", -+ "item.minecraft.tnt_minecart": "TNT矿车", -+ "item.minecraft.torchflower_seeds": "火把花种子", -+ "item.minecraft.totem_of_undying": "不死图腾", -+ "item.minecraft.trader_llama_spawn_egg": "行商羊驼刷怪蛋", -+ "item.minecraft.trident": "三叉戟", -+ "item.minecraft.tropical_fish": "热带鱼", -+ "item.minecraft.tropical_fish_bucket": "热带鱼桶", -+ "item.minecraft.tropical_fish_spawn_egg": "热带鱼刷怪蛋", -+ "item.minecraft.turtle_helmet": "海龟壳", -+ "item.minecraft.turtle_spawn_egg": "海龟刷怪蛋", -+ "item.minecraft.vex_spawn_egg": "恼鬼刷怪蛋", -+ "item.minecraft.villager_spawn_egg": "村民刷怪蛋", -+ "item.minecraft.vindicator_spawn_egg": "卫道士刷怪蛋", -+ "item.minecraft.wandering_trader_spawn_egg": "流浪商人刷怪蛋", -+ "item.minecraft.warden_spawn_egg": "监守者刷怪蛋", -+ "item.minecraft.warped_fungus_on_a_stick": "诡异菌钓竿", -+ "item.minecraft.water_bucket": "水桶", -+ "item.minecraft.wheat": "小麦", -+ "item.minecraft.wheat_seeds": "小麦种子", -+ "item.minecraft.white_dye": "白色染料", -+ "item.minecraft.witch_spawn_egg": "女巫刷怪蛋", -+ "item.minecraft.wither_skeleton_spawn_egg": "凋灵骷髅刷怪蛋", -+ "item.minecraft.wither_spawn_egg": "凋灵刷怪蛋", -+ "item.minecraft.wolf_spawn_egg": "狼刷怪蛋", -+ "item.minecraft.wooden_axe": "木斧", -+ "item.minecraft.wooden_hoe": "木锄", -+ "item.minecraft.wooden_pickaxe": "木镐", -+ "item.minecraft.wooden_shovel": "木锹", -+ "item.minecraft.wooden_sword": "木剑", -+ "item.minecraft.writable_book": "书与笔", -+ "item.minecraft.written_book": "成书", -+ "item.minecraft.yellow_dye": "黄色染料", -+ "item.minecraft.zoglin_spawn_egg": "僵尸疣猪兽刷怪蛋", -+ "item.minecraft.zombie_horse_spawn_egg": "僵尸马刷怪蛋", -+ "item.minecraft.zombie_spawn_egg": "僵尸刷怪蛋", -+ "item.minecraft.zombie_villager_spawn_egg": "僵尸村民刷怪蛋", -+ "item.minecraft.zombified_piglin_spawn_egg": "僵尸猪灵刷怪蛋", -+ "item.modifiers.chest": "穿在身上时:", -+ "item.modifiers.feet": "穿在脚上时:", -+ "item.modifiers.head": "戴在头上时:", -+ "item.modifiers.legs": "穿在腿上时:", -+ "item.modifiers.mainhand": "在主手时:", -+ "item.modifiers.offhand": "在副手时:", -+ "item.nbt_tags": "NBT:%s个标签", -+ "item.unbreakable": "无法破坏", -+ "itemGroup.buildingBlocks": "建筑方块", -+ "itemGroup.coloredBlocks": "染色方块", -+ "itemGroup.combat": "战斗用品", -+ "itemGroup.consumables": "消耗品", -+ "itemGroup.crafting": "合成用品", -+ "itemGroup.foodAndDrink": "食物与饮品", -+ "itemGroup.functional": "功能方块", -+ "itemGroup.hotbar": "已保存的快捷栏", -+ "itemGroup.ingredients": "原材料", -+ "itemGroup.inventory": "生存模式物品栏", -+ "itemGroup.natural": "自然方块", -+ "itemGroup.op": "管理员用品", -+ "itemGroup.redstone": "红石方块", -+ "itemGroup.search": "搜索物品", -+ "itemGroup.spawnEggs": "刷怪蛋", -+ "itemGroup.tools": "工具与实用物品", -+ "item_modifier.unknown": "未知的物品修饰器:%s", -+ "jigsaw_block.final_state": "转变为:", -+ "jigsaw_block.generate": "生成", -+ "jigsaw_block.joint.aligned": "固定", -+ "jigsaw_block.joint.rollable": "可旋转", -+ "jigsaw_block.joint_label": "拼接类型:", -+ "jigsaw_block.keep_jigsaws": "保留拼图", -+ "jigsaw_block.levels": "层数:%s", -+ "jigsaw_block.name": "名称:", -+ "jigsaw_block.pool": "目标池:", -+ "jigsaw_block.target": "目标名称:", -+ "key.advancements": "进度", -+ "key.attack": "攻击/摧毁", -+ "key.back": "向后移动", -+ "key.categories.creative": "创造模式", -+ "key.categories.gameplay": "游戏内容", -+ "key.categories.inventory": "物品栏", -+ "key.categories.misc": "杂项", -+ "key.categories.movement": "移动", -+ "key.categories.multiplayer": "多人游戏", -+ "key.categories.ui": "游戏界面", -+ "key.chat": "打开聊天栏", -+ "key.command": "输入命令", -+ "key.drop": "丢弃所选物品", -+ "key.forward": "向前移动", -+ "key.fullscreen": "切换全屏显示", -+ "key.hotbar.1": "快捷栏1", -+ "key.hotbar.2": "快捷栏2", -+ "key.hotbar.3": "快捷栏3", -+ "key.hotbar.4": "快捷栏4", -+ "key.hotbar.5": "快捷栏5", -+ "key.hotbar.6": "快捷栏6", -+ "key.hotbar.7": "快捷栏7", -+ "key.hotbar.8": "快捷栏8", -+ "key.hotbar.9": "快捷栏9", -+ "key.inventory": "开启/关闭物品栏", -+ "key.jump": "跳跃", -+ "key.keyboard.apostrophe": "'", -+ "key.keyboard.backslash": "\\", -+ "key.keyboard.backspace": "Backspace", -+ "key.keyboard.caps.lock": "Caps Lock", -+ "key.keyboard.comma": ",", -+ "key.keyboard.delete": "Delete", -+ "key.keyboard.down": "下方向键", -+ "key.keyboard.end": "End", -+ "key.keyboard.enter": "Enter", -+ "key.keyboard.equal": "=", -+ "key.keyboard.escape": "Esc", -+ "key.keyboard.f1": "F1", -+ "key.keyboard.f10": "F10", -+ "key.keyboard.f11": "F11", -+ "key.keyboard.f12": "F12", -+ "key.keyboard.f13": "F13", -+ "key.keyboard.f14": "F14", -+ "key.keyboard.f15": "F15", -+ "key.keyboard.f16": "F16", -+ "key.keyboard.f17": "F17", -+ "key.keyboard.f18": "F18", -+ "key.keyboard.f19": "F19", -+ "key.keyboard.f2": "F2", -+ "key.keyboard.f20": "F20", -+ "key.keyboard.f21": "F21", -+ "key.keyboard.f22": "F22", -+ "key.keyboard.f23": "F23", -+ "key.keyboard.f24": "F24", -+ "key.keyboard.f25": "F25", -+ "key.keyboard.f3": "F3", -+ "key.keyboard.f4": "F4", -+ "key.keyboard.f5": "F5", -+ "key.keyboard.f6": "F6", -+ "key.keyboard.f7": "F7", -+ "key.keyboard.f8": "F8", -+ "key.keyboard.f9": "F9", -+ "key.keyboard.grave.accent": "`", -+ "key.keyboard.home": "Home", -+ "key.keyboard.insert": "Insert", -+ "key.keyboard.keypad.0": "小键盘 0", -+ "key.keyboard.keypad.1": "小键盘 1", -+ "key.keyboard.keypad.2": "小键盘 2", -+ "key.keyboard.keypad.3": "小键盘 3", -+ "key.keyboard.keypad.4": "小键盘 4", -+ "key.keyboard.keypad.5": "小键盘 5", -+ "key.keyboard.keypad.6": "小键盘 6", -+ "key.keyboard.keypad.7": "小键盘 7", -+ "key.keyboard.keypad.8": "小键盘 8", -+ "key.keyboard.keypad.9": "小键盘 9", -+ "key.keyboard.keypad.add": "小键盘 +", -+ "key.keyboard.keypad.decimal": "小键盘 .", -+ "key.keyboard.keypad.divide": "小键盘 /", -+ "key.keyboard.keypad.enter": "小键盘 Enter", -+ "key.keyboard.keypad.equal": "小键盘 =", -+ "key.keyboard.keypad.multiply": "小键盘 *", -+ "key.keyboard.keypad.subtract": "小键盘 -", -+ "key.keyboard.left": "左方向键", -+ "key.keyboard.left.alt": "左Alt", -+ "key.keyboard.left.bracket": "[", -+ "key.keyboard.left.control": "左Ctrl", -+ "key.keyboard.left.shift": "左Shift", -+ "key.keyboard.left.win": "左Win", -+ "key.keyboard.menu": "菜单", -+ "key.keyboard.minus": "-", -+ "key.keyboard.num.lock": "Num Lock", -+ "key.keyboard.page.down": "Page Down", -+ "key.keyboard.page.up": "Page Up", -+ "key.keyboard.pause": "Pause", -+ "key.keyboard.period": ".", -+ "key.keyboard.print.screen": "Print Screen", -+ "key.keyboard.right": "右方向键", -+ "key.keyboard.right.alt": "右Alt", -+ "key.keyboard.right.bracket": "]", -+ "key.keyboard.right.control": "右Ctrl", -+ "key.keyboard.right.shift": "右Shift", -+ "key.keyboard.right.win": "右Win", -+ "key.keyboard.scroll.lock": "Scroll Lock", -+ "key.keyboard.semicolon": ";", -+ "key.keyboard.slash": "/", -+ "key.keyboard.space": "空格", -+ "key.keyboard.tab": "Tab", -+ "key.keyboard.unknown": "未指定", -+ "key.keyboard.up": "上方向键", -+ "key.keyboard.world.1": "World 1", -+ "key.keyboard.world.2": "World 2", -+ "key.left": "向左移动", -+ "key.loadToolbarActivator": "加载快捷栏", -+ "key.mouse": "鼠标按键%1$s", -+ "key.mouse.left": "左键", -+ "key.mouse.middle": "中键", -+ "key.mouse.right": "右键", -+ "key.pickItem": "选取方块", -+ "key.playerlist": "显示玩家列表", -+ "key.right": "向右移动", -+ "key.saveToolbarActivator": "保存快捷栏", -+ "key.screenshot": "截图", -+ "key.smoothCamera": "切换电影视角", -+ "key.sneak": "潜行", -+ "key.socialInteractions": "社交屏幕", -+ "key.spectatorOutlines": "高亮玩家(旁观者)", -+ "key.sprint": "疾跑", -+ "key.swapOffhand": "与副手交换物品", -+ "key.togglePerspective": "切换视角", -+ "key.use": "使用物品/放置方块", -+ "lanServer.otherPlayers": "对其他玩家的设置", -+ "lanServer.port": "端口号", -+ "lanServer.port.invalid": "端口无效。\n请将编辑框留空,或输入一个介于1024和65535之间的数字。", -+ "lanServer.port.invalid.new": "端口无效。\n请将编辑框留空,或输入一个介于%s和%s之间的数字。", -+ "lanServer.port.unavailable": "端口不可用。\n请将编辑框留空,或输入一个介于1024和65535之间的新数字。", -+ "lanServer.port.unavailable.new": "端口不可用。\n请将编辑框留空,或输入一个介于%s和%s之间的新数字。", -+ "lanServer.scanning": "正在你的本地网络中寻找游戏", -+ "lanServer.start": "创建局域网世界", -+ "lanServer.title": "局域网世界", -+ "language.code": "zho-Hans_CN", -+ "language.name": "简体中文", -+ "language.region": "中国大陆", -+ "lectern.take_book": "取书", -+ "mco.account.privacy.info": "阅读更多关于Mojang和其隐私政策的信息", -+ "mco.account.privacyinfo": "Mojang实施许多方法来促进儿童及其隐私权的保护,包括履行《儿童在线隐私权保护法案》(COPPA)与《通用数据保护条例》(GDPR)。\n\n在访问你的Realms账户之前,你可能需要征得父母的同意。\n\n如果你拥有的是旧版的Minecraft账户(登录时使用用户名),你需要将其合并到Mojang账户以访问Realms。", -+ "mco.account.update": "更新账户", -+ "mco.activity.noactivity": "过去%s天无动态", -+ "mco.activity.title": "玩家动态", -+ "mco.backup.button.download": "下载最新版本", -+ "mco.backup.button.reset": "重置世界", -+ "mco.backup.button.restore": "还原", -+ "mco.backup.button.upload": "上传世界", -+ "mco.backup.changes.tooltip": "更改", -+ "mco.backup.entry": "备份(%s)", -+ "mco.backup.entry.description": "描述", -+ "mco.backup.entry.enabledPack": "已启用的包", -+ "mco.backup.entry.gameDifficulty": "游戏难度", -+ "mco.backup.entry.gameMode": "游戏模式", -+ "mco.backup.entry.gameServerVersion": "游戏服务器版本", -+ "mco.backup.entry.name": "名称", -+ "mco.backup.entry.seed": "种子", -+ "mco.backup.entry.templateName": "模板名称", -+ "mco.backup.entry.undefined": "未定义的更改", -+ "mco.backup.entry.uploaded": "已上传", -+ "mco.backup.entry.worldType": "世界类型", -+ "mco.backup.generate.world": "生成世界", -+ "mco.backup.info.title": "自上次备份以来发生的更改", -+ "mco.backup.nobackups": "这个Realm目前没有任何备份。", -+ "mco.backup.restoring": "正在还原你的Realm", -+ "mco.backup.unknown": "未知", -+ "mco.brokenworld.download": "下载", -+ "mco.brokenworld.downloaded": "已下载", -+ "mco.brokenworld.message.line1": "请重置或选择另一个世界。", -+ "mco.brokenworld.message.line2": "你可以选择将世界下载至单人模式中。", -+ "mco.brokenworld.minigame.title": "此小游戏已不受支持", -+ "mco.brokenworld.nonowner.error": "请等待Realms所有者重置世界", -+ "mco.brokenworld.nonowner.title": "世界已过期", -+ "mco.brokenworld.play": "开始游戏", -+ "mco.brokenworld.reset": "重置", -+ "mco.brokenworld.title": "当前的世界已不受支持", -+ "mco.client.incompatible.msg.line1": "你的客户端与Realms不兼容。", -+ "mco.client.incompatible.msg.line2": "请使用最新版本的Minecraft。", -+ "mco.client.incompatible.msg.line3": "Realms不与快照版本兼容。", -+ "mco.client.incompatible.title": "客户端不兼容!", -+ "mco.configure.current.minigame": "当前", -+ "mco.configure.world.activityfeed.disabled": "玩家推送暂不可用", -+ "mco.configure.world.backup": "世界备份", -+ "mco.configure.world.buttons.activity": "玩家动态", -+ "mco.configure.world.buttons.close": "关闭Realm", -+ "mco.configure.world.buttons.delete": "删除", -+ "mco.configure.world.buttons.done": "完成", -+ "mco.configure.world.buttons.edit": "设置", -+ "mco.configure.world.buttons.invite": "邀请玩家", -+ "mco.configure.world.buttons.moreoptions": "更多选项", -+ "mco.configure.world.buttons.open": "开启Realm", -+ "mco.configure.world.buttons.options": "世界选项", -+ "mco.configure.world.buttons.players": "玩家", -+ "mco.configure.world.buttons.resetworld": "重置世界", -+ "mco.configure.world.buttons.settings": "设置", -+ "mco.configure.world.buttons.subscription": "订阅", -+ "mco.configure.world.buttons.switchminigame": "切换小游戏", -+ "mco.configure.world.close.question.line1": "你的Realm将变为不可用。", -+ "mco.configure.world.close.question.line2": "你确定要继续吗?", -+ "mco.configure.world.closing": "关闭Realm中…", -+ "mco.configure.world.commandBlocks": "命令方块", -+ "mco.configure.world.delete.button": "删除Realm", -+ "mco.configure.world.delete.question.line1": "你的Realm将被永久删除", -+ "mco.configure.world.delete.question.line2": "你确定要继续吗?", -+ "mco.configure.world.description": "Realm描述", -+ "mco.configure.world.edit.slot.name": "世界名称", -+ "mco.configure.world.edit.subscreen.adventuremap": "由于你当前的世界是冒险世界,有些选项将会被禁用", -+ "mco.configure.world.edit.subscreen.experience": "由于你当前的世界是体验世界,有些选项将会被禁用", -+ "mco.configure.world.edit.subscreen.inspiration": "由于你当前的世界是示例世界,有些选项将会被禁用", -+ "mco.configure.world.forceGameMode": "强制游戏模式", -+ "mco.configure.world.invite.narration": "你有%s个新邀请", -+ "mco.configure.world.invite.profile.name": "名称", -+ "mco.configure.world.invited": "已邀请", -+ "mco.configure.world.invited.number": "已邀请(%s)", -+ "mco.configure.world.invites.normal.tooltip": "普通玩家", -+ "mco.configure.world.invites.ops.tooltip": "管理员", -+ "mco.configure.world.invites.remove.tooltip": "移除", -+ "mco.configure.world.leave.question.line1": "如果你离开这个Realm,你只有被再次邀请才能看见它。", -+ "mco.configure.world.leave.question.line2": "你确定要继续吗?", -+ "mco.configure.world.location": "位置", -+ "mco.configure.world.minigame": "当前:%s", -+ "mco.configure.world.name": "Realm名称", -+ "mco.configure.world.opening": "开启Realm中…", -+ "mco.configure.world.players.error": "不存在该指定名称的玩家", -+ "mco.configure.world.players.inviting": "邀请玩家中…", -+ "mco.configure.world.players.title": "玩家", -+ "mco.configure.world.pvp": "玩家对战", -+ "mco.configure.world.reset.question.line1": "你的Realm将被重新生成,而当前的Realm将会消失", -+ "mco.configure.world.reset.question.line2": "你确定要继续吗?", -+ "mco.configure.world.resourcepack.question.line1": "这个Realm需要一个自定义的资源包。", -+ "mco.configure.world.resourcepack.question.line2": "你想要自动下载并安装它吗?", -+ "mco.configure.world.restore.download.question.line1": "世界将会被下载并添加至你的单人游戏世界中", -+ "mco.configure.world.restore.download.question.line2": "你确定要继续吗?", -+ "mco.configure.world.restore.question.line1": "你的Realm世界将被还原至“%s”(%s)", -+ "mco.configure.world.restore.question.line2": "你确定要继续吗?", -+ "mco.configure.world.settings.title": "设置", -+ "mco.configure.world.slot": "世界%s", -+ "mco.configure.world.slot.empty": "空", -+ "mco.configure.world.slot.switch.question.line1": "你的Realm将会被切换至另一个世界", -+ "mco.configure.world.slot.switch.question.line2": "你确定要继续吗?", -+ "mco.configure.world.slot.tooltip": "切换到世界", -+ "mco.configure.world.slot.tooltip.active": "加入", -+ "mco.configure.world.slot.tooltip.minigame": "切换至小游戏", -+ "mco.configure.world.spawnAnimals": "生成动物", -+ "mco.configure.world.spawnMonsters": "生成怪物", -+ "mco.configure.world.spawnNPCs": "生成NPC", -+ "mco.configure.world.spawnProtection": "重生点保护", -+ "mco.configure.world.spawn_toggle.message": "关闭此选项后,所有现有的该类型实体将会被移除", -+ "mco.configure.world.spawn_toggle.message.npc": "关闭此选项后,所有现有的该类型实体将会被移除,例如村民", -+ "mco.configure.world.spawn_toggle.title": "警告!", -+ "mco.configure.world.status": "状态", -+ "mco.configure.world.subscription.day": "天", -+ "mco.configure.world.subscription.days": "天", -+ "mco.configure.world.subscription.expired": "已过期", -+ "mco.configure.world.subscription.extend": "延长订阅时间", -+ "mco.configure.world.subscription.less_than_a_day": "不足一天", -+ "mco.configure.world.subscription.month": "月", -+ "mco.configure.world.subscription.months": "月", -+ "mco.configure.world.subscription.recurring.daysleft": "自动续期剩余时间:", -+ "mco.configure.world.subscription.recurring.info": "对你的Realms订阅所做的更改(如续期和取消自动续订)将在下个结算日生效。", -+ "mco.configure.world.subscription.remaining.days": "%1$s天", -+ "mco.configure.world.subscription.remaining.months": "%1$s个月", -+ "mco.configure.world.subscription.remaining.months.days": "%1$s个月%2$s天", -+ "mco.configure.world.subscription.start": "开始日期", -+ "mco.configure.world.subscription.timeleft": "剩余时间", -+ "mco.configure.world.subscription.title": "订阅信息", -+ "mco.configure.world.subscription.unknown": "未知", -+ "mco.configure.world.switch.slot": "创建世界", -+ "mco.configure.world.switch.slot.subtitle": "这个世界是空的,请选择创建世界的方式", -+ "mco.configure.world.title": "配置Realm:", -+ "mco.configure.world.uninvite.player": "你确定要取消邀请“%s”吗?", -+ "mco.configure.world.uninvite.question": "你确定要取消邀请吗", -+ "mco.configure.worlds.title": "世界", -+ "mco.connect.authorizing": "登录中…", -+ "mco.connect.connecting": "正在连接至Realm…", -+ "mco.connect.failed": "无法连接至Realm", -+ "mco.connect.success": "完成", -+ "mco.create.world": "创建", -+ "mco.create.world.error": "你必须输入一个名字!", -+ "mco.create.world.reset.title": "正在创建世界…", -+ "mco.create.world.skip": "跳过", -+ "mco.create.world.subtitle": "可选,选择你想放进新Realm的世界", -+ "mco.create.world.wait": "正在创建Realm…", -+ "mco.download.cancelled": "下载已取消", -+ "mco.download.confirmation.line1": "你将下载的世界大小大于%s", -+ "mco.download.confirmation.line2": "你无法再次将这个世界上传至Realm", -+ "mco.download.done": "下载完成", -+ "mco.download.downloading": "正在下载", -+ "mco.download.extracting": "提取中", -+ "mco.download.failed": "下载失败", -+ "mco.download.percent": "%s %%", -+ "mco.download.preparing": "正在准备下载", -+ "mco.download.resourcePack.fail": "资源包下载失败!", -+ "mco.download.speed": "(%s/s)", -+ "mco.download.title": "下载最新的世界", -+ "mco.error.invalid.session.message": "请尝试重启Minecraft", -+ "mco.error.invalid.session.title": "无效的会话ID", -+ "mco.errorMessage.6001": "客户端已过期!", -+ "mco.errorMessage.6002": "尚未同意服务条款", -+ "mco.errorMessage.6003": "超过下载限制", -+ "mco.errorMessage.6004": "超过上传限制", -+ "mco.errorMessage.6005": "世界已锁定", -+ "mco.errorMessage.6006": "世界已过期", -+ "mco.errorMessage.6007": "玩家所在的Realm数量过多", -+ "mco.errorMessage.6008": "无效的Realm名称", -+ "mco.errorMessage.6009": "无效的Realm描述", -+ "mco.errorMessage.connectionFailure": "出错了,请稍后再试。", -+ "mco.errorMessage.generic": "出现错误:", -+ "mco.errorMessage.realmsService": "出现错误(%s):", -+ "mco.errorMessage.realmsService.realmsError": "Realms(%s):", -+ "mco.errorMessage.serviceBusy": "Realms目前处于繁忙状态。\n请在几分钟后再次尝试连接至你的Realm。", -+ "mco.gui.button": "按钮", -+ "mco.gui.ok": "确定", -+ "mco.info": "信息!", -+ "mco.invites.button.accept": "接受", -+ "mco.invites.button.reject": "拒绝", -+ "mco.invites.nopending": "没有未决邀请!", -+ "mco.invites.pending": "新邀请!", -+ "mco.invites.title": "未决邀请", -+ "mco.minigame.world.changeButton": "选择另一个小游戏", -+ "mco.minigame.world.info.line1": "这会暂时将你的世界替换成一个小游戏!", -+ "mco.minigame.world.info.line2": "之后你可以毫无损失地返回到原来的世界。", -+ "mco.minigame.world.noSelection": "请选择", -+ "mco.minigame.world.restore": "正在结束小游戏…", -+ "mco.minigame.world.restore.question.line1": "小游戏将结束而你的Realm将被恢复。", -+ "mco.minigame.world.restore.question.line2": "你确定要继续吗?", -+ "mco.minigame.world.selected": "所选的小游戏:", -+ "mco.minigame.world.slot.screen.title": "切换世界中…", -+ "mco.minigame.world.startButton": "切换", -+ "mco.minigame.world.starting.screen.title": "正在开始小游戏…", -+ "mco.minigame.world.stopButton": "结束小游戏", -+ "mco.minigame.world.switch.new": "选择另一个小游戏?", -+ "mco.minigame.world.switch.title": "切换小游戏", -+ "mco.minigame.world.title": "将Realm切换成小游戏", -+ "mco.news": "Realms新闻", -+ "mco.notification.dismiss": "关闭", -+ "mco.notification.visitUrl.buttonText.default": "打开链接", -+ "mco.notification.visitUrl.message.default": "请访问下方的链接", -+ "mco.question": "问题", -+ "mco.reset.world.adventure": "冒险", -+ "mco.reset.world.experience": "体验", -+ "mco.reset.world.generate": "新的世界", -+ "mco.reset.world.inspiration": "示例", -+ "mco.reset.world.resetting.screen.title": "重置世界中…", -+ "mco.reset.world.seed": "世界种子(可选)", -+ "mco.reset.world.template": "世界模板", -+ "mco.reset.world.title": "重置世界", -+ "mco.reset.world.upload": "上传世界", -+ "mco.reset.world.warning": "这将替换掉你目前的Realm世界!", -+ "mco.selectServer.buy": "购买Realm服务器!", -+ "mco.selectServer.close": "关闭", -+ "mco.selectServer.closed": "已关闭的Realm", -+ "mco.selectServer.closeserver": "关闭Realm", -+ "mco.selectServer.configure": "配置Realm", -+ "mco.selectServer.configureRealm": "配置Realm", -+ "mco.selectServer.create": "创建Realm", -+ "mco.selectServer.expired": "已过期的Realm", -+ "mco.selectServer.expiredList": "你的订阅已过期", -+ "mco.selectServer.expiredRenew": "续期", -+ "mco.selectServer.expiredSubscribe": "订阅", -+ "mco.selectServer.expiredTrial": "你的试用期已结束", -+ "mco.selectServer.expires.day": "将在一天内过期", -+ "mco.selectServer.expires.days": "将在%s天后过期", -+ "mco.selectServer.expires.soon": "即将过期", -+ "mco.selectServer.leave": "退出Realm", -+ "mco.selectServer.mapOnlySupportedForVersion": "此地图不受%s支持", -+ "mco.selectServer.minigame": "小游戏:", -+ "mco.selectServer.minigameNotSupportedInVersion": "无法在%s中进行这个小游戏", -+ "mco.selectServer.note": "注意:", -+ "mco.selectServer.open": "已开启的Realm", -+ "mco.selectServer.openserver": "开启Realm", -+ "mco.selectServer.play": "开始游戏", -+ "mco.selectServer.popup": "Realms是一种安全、简单地享受在线Minecraft世界的方式,可支持多达十位好友同时在线。它也支持多种小游戏与海量自定义世界!只有Realm的所有者需要付款。", -+ "mco.selectServer.purchase": "添加Realm", -+ "mco.selectServer.trial": "获取试用期!", -+ "mco.selectServer.uninitialized": "点击以创建新的Realm!", -+ "mco.template.button.publisher": "发布者", -+ "mco.template.button.select": "选择", -+ "mco.template.button.trailer": "预告片", -+ "mco.template.default.name": "世界模板", -+ "mco.template.info.tooltip": "发布者的网站", -+ "mco.template.name": "模板", -+ "mco.template.select.failure": "我们无法接收这个分类的内容。\n请检查你的网络连接,或稍后再试。", -+ "mco.template.select.narrate.authors": "作者:%s", -+ "mco.template.select.narrate.version": "版本:%s", -+ "mco.template.select.none": "抱歉,看来这个分类目前是空的。\n请之后再回来检查是否有新内容,如果你是一名创作者的话,\n%s。", -+ "mco.template.select.none.linkTitle": "尝试自己提交一些自己的内容", -+ "mco.template.title": "世界模板", -+ "mco.template.title.minigame": "Realm小游戏", -+ "mco.template.trailer.tooltip": "地图预告", -+ "mco.terms.buttons.agree": "同意", -+ "mco.terms.buttons.disagree": "不同意", -+ "mco.terms.sentence.1": "我已阅读并接受Minecraft Realms的", -+ "mco.terms.sentence.2": "服务条款", -+ "mco.terms.title": "Realms服务条款", -+ "mco.time.daysAgo": "%1$s天前", -+ "mco.time.hoursAgo": "%1$s小时前", -+ "mco.time.minutesAgo": "%1$s分钟前", -+ "mco.time.now": "刚刚", -+ "mco.time.secondsAgo": "%1$s秒前", -+ "mco.trial.message.line1": "你想获得属于自己的Realm吗?", -+ "mco.trial.message.line2": "点击此处来获得更多消息!", -+ "mco.upload.button.name": "上传", -+ "mco.upload.cancelled": "上传已取消", -+ "mco.upload.close.failure": "无法关闭服务器,请稍后再试", -+ "mco.upload.done": "上传已完成", -+ "mco.upload.entry.cheats": "%1$s, %2$s", -+ "mco.upload.entry.id": "%1$s(%2$s)", -+ "mco.upload.failed": "上传失败!(%s)", -+ "mco.upload.hardcore": "不能上传极限模式世界!", -+ "mco.upload.preparing": "正在准备你的世界数据", -+ "mco.upload.select.world.none": "找不到单人游戏世界!", -+ "mco.upload.select.world.subtitle": "请选择一个单人游戏世界来上传", -+ "mco.upload.select.world.title": "上传世界", -+ "mco.upload.size.failure.line1": "“%s”太大了!", -+ "mco.upload.size.failure.line2": "你想上传%s的地图,超过了%s的限制。", -+ "mco.upload.uploading": "正在上传“%s”", -+ "mco.upload.verifying": "正在验证你的世界", -+ "mco.warning": "警告!", -+ "mco.worldSlot.minigame": "小游戏", -+ "menu.convertingLevel": "转换世界中", -+ "menu.disconnect": "断开连接", -+ "menu.game": "游戏菜单", -+ "menu.generatingLevel": "生成世界中", -+ "menu.generatingTerrain": "生成地形中", -+ "menu.loadingForcedChunks": "正在加载维度%s中的强制加载区块", -+ "menu.loadingLevel": "加载世界中", -+ "menu.modded": "(已修改)", -+ "menu.multiplayer": "多人游戏", -+ "menu.online": "Minecraft Realms", -+ "menu.options": "选项…", -+ "menu.paused": "游戏暂停", -+ "menu.playdemo": "开始试玩世界", -+ "menu.playerReporting": "举报玩家", -+ "menu.preparingSpawn": "准备生成区域中:%s%%", -+ "menu.quit": "退出游戏", -+ "menu.reportBugs": "报告漏洞", -+ "menu.resetdemo": "重置试玩世界", -+ "menu.respawning": "重生中", -+ "menu.returnToGame": "回到游戏", -+ "menu.returnToMenu": "保存并退回到标题屏幕", -+ "menu.savingChunks": "保存区块中", -+ "menu.savingLevel": "保存世界中", -+ "menu.sendFeedback": "提供反馈", -+ "menu.shareToLan": "对局域网开放", -+ "menu.singleplayer": "单人游戏", -+ "menu.working": "处理中…", -+ "merchant.current_level": "商人的当前等级", -+ "merchant.deprecated": "村民每天最多补货两次。", -+ "merchant.level.1": "新手", -+ "merchant.level.2": "学徒", -+ "merchant.level.3": "老手", -+ "merchant.level.4": "专家", -+ "merchant.level.5": "大师", -+ "merchant.next_level": "商人的下一等级", -+ "merchant.trades": "交易", -+ "mirror.front_back": "↑ ↓", -+ "mirror.left_right": "← →", -+ "mirror.none": "|", -+ "mount.onboard": "按下%1$s来脱离", -+ "multiplayer.applyingPack": "正在应用资源包", -+ "multiplayer.disconnect.authservers_down": "身份验证服务器目前处于宕机状态。请稍后再试,抱歉!", -+ "multiplayer.disconnect.banned": "你已被此服务器封禁", -+ "multiplayer.disconnect.banned.expiration": "\n你的封禁将于%s解除", -+ "multiplayer.disconnect.banned.reason": "你已被此服务器封禁。\n原因:%s", -+ "multiplayer.disconnect.banned_ip.expiration": "\n你的封禁将于%s解除", -+ "multiplayer.disconnect.banned_ip.reason": "你的IP已被此服务器封禁。\n原因:%s", -+ "multiplayer.disconnect.chat_validation_failed": "聊天消息验证失败", -+ "multiplayer.disconnect.duplicate_login": "你已在异地登录", -+ "multiplayer.disconnect.expired_public_key": "个人信息公钥过期。请确保系统时间同步,然后尝试重启游戏。", -+ "multiplayer.disconnect.flying": "此服务器未启用飞行", -+ "multiplayer.disconnect.generic": "连接中断", -+ "multiplayer.disconnect.idling": "你的未操作时间过长!", -+ "multiplayer.disconnect.illegal_characters": "非法聊天字符", -+ "multiplayer.disconnect.incompatible": "客户端不兼容!请使用%s", -+ "multiplayer.disconnect.invalid_entity_attacked": "试图攻击无效实体", -+ "multiplayer.disconnect.invalid_packet": "服务器发送了一个无效的数据包", -+ "multiplayer.disconnect.invalid_player_data": "无效的玩家数据", -+ "multiplayer.disconnect.invalid_player_movement": "收到了包含非法玩家移动的数据包", -+ "multiplayer.disconnect.invalid_public_key_signature": "无效的个人信息公钥签名。\n请尝试重启游戏。", -+ "multiplayer.disconnect.invalid_public_key_signature.new": "无效的个人信息公钥签名。\n请尝试重启游戏。", -+ "multiplayer.disconnect.invalid_vehicle_movement": "收到了包含非法载具移动的数据包", -+ "multiplayer.disconnect.ip_banned": "你的IP地址已被此服务器封禁", -+ "multiplayer.disconnect.kicked": "被管理员踢出游戏", -+ "multiplayer.disconnect.missing_tags": "从服务器接收到不完整标签集。\n请联系服务器管理员。", -+ "multiplayer.disconnect.name_taken": "此名称已被占用", -+ "multiplayer.disconnect.not_whitelisted": "你不在此服务器的白名单中!", -+ "multiplayer.disconnect.out_of_order_chat": "接收到了乱序的聊天数据包。你的系统时间是否被更改过?", -+ "multiplayer.disconnect.outdated_client": "客户端不兼容!请使用%s", -+ "multiplayer.disconnect.outdated_server": "客户端不兼容!请使用%s", -+ "multiplayer.disconnect.server_full": "服务器已满!", -+ "multiplayer.disconnect.server_shutdown": "服务器已关闭", -+ "multiplayer.disconnect.slow_login": "登录超时", -+ "multiplayer.disconnect.too_many_pending_chats": "没有确认的聊天消息过多", -+ "multiplayer.disconnect.unexpected_query_response": "从客户端收到未知的自定义数据", -+ "multiplayer.disconnect.unsigned_chat": "接收到了缺少签名或含有无效签名的聊天数据包。", -+ "multiplayer.disconnect.unverified_username": "验证用户名失败!", -+ "multiplayer.downloadingStats": "正在获取统计信息…", -+ "multiplayer.downloadingTerrain": "加载地形中…", -+ "multiplayer.lan.server_found": "发现新的服务器:%s", -+ "multiplayer.message_not_delivered": "无法发送聊天消息,请检查服务器日志:%s", -+ "multiplayer.player.joined": "%s加入了游戏", -+ "multiplayer.player.joined.renamed": "%s(之前被称为%s)加入了游戏", -+ "multiplayer.player.left": "%s退出了游戏", -+ "multiplayer.player.list.narration": "在线玩家:%s", -+ "multiplayer.requiredTexturePrompt.disconnect": "服务器需要自定义资源包", -+ "multiplayer.requiredTexturePrompt.line1": "这个服务器需要使用自定义的资源包。", -+ "multiplayer.requiredTexturePrompt.line2": "拒绝使用该自定义资源包将会断开你与该服务器间的连接。", -+ "multiplayer.socialInteractions.not_available": "社交屏幕仅在多人游戏世界中可用", -+ "multiplayer.status.and_more": "…及其他%s名玩家…", -+ "multiplayer.status.cancelled": "已取消", -+ "multiplayer.status.cannot_connect": "无法连接到服务器", -+ "multiplayer.status.cannot_resolve": "无法解析主机名", -+ "multiplayer.status.finished": "已完成", -+ "multiplayer.status.incompatible": "版本不兼容!", -+ "multiplayer.status.motd.narration": "服务器信息:%s", -+ "multiplayer.status.no_connection": "(无连接)", -+ "multiplayer.status.old": "旧版", -+ "multiplayer.status.online": "在线", -+ "multiplayer.status.ping": "%s毫秒", -+ "multiplayer.status.ping.narration": "延迟为%s毫秒", -+ "multiplayer.status.pinging": "检测中…", -+ "multiplayer.status.player_count.narration": "%s名玩家在线,共%s名玩家", -+ "multiplayer.status.quitting": "正在退出", -+ "multiplayer.status.request_handled": "状态请求已处理", -+ "multiplayer.status.unknown": "???", -+ "multiplayer.status.unrequested": "收到了未请求的状态", -+ "multiplayer.status.version.narration": "服务器版本:%s", -+ "multiplayer.stopSleeping": "起床", -+ "multiplayer.texturePrompt.failure.line1": "无法应用服务器资源包", -+ "multiplayer.texturePrompt.failure.line2": "所有依赖自定义资源包的功能都有可能不按预期工作", -+ "multiplayer.texturePrompt.line1": "这个服务器推荐使用自定义的资源包。", -+ "multiplayer.texturePrompt.line2": "你想要自动下载和安装它吗?", -+ "multiplayer.texturePrompt.serverPrompt": "%s\n\n来自服务器的消息:\n%s", -+ "multiplayer.title": "多人游戏", -+ "multiplayer.unsecureserver.toast": "该服务器上发送的消息可能会被修改,可能无法反映原始消息", -+ "multiplayer.unsecureserver.toast.title": "无法验证聊天消息", -+ "multiplayerWarning.check": "不再显示此屏幕", -+ "multiplayerWarning.header": "警告:在线游戏由第三方提供", -+ "multiplayerWarning.message": "警告:在线游戏由非Mojang Studios或Microsoft拥有、运作和监督的第三方服务器提供。 在线游戏的过程中,你可能会收到不受规制的聊天消息或者其他不一定适合所有人的用户生成内容。", -+ "narration.button": "按钮:%s", -+ "narration.button.usage.focused": "按下Enter键激活", -+ "narration.button.usage.hovered": "单击鼠标左键激活", -+ "narration.checkbox": "复选框:%s", -+ "narration.checkbox.usage.focused": "按下Enter键切换", -+ "narration.checkbox.usage.hovered": "单击鼠标左键切换", -+ "narration.component_list.usage": "按下Tab键移动到下一个屏幕控件", -+ "narration.cycle_button.usage.focused": "按下Enter键切换到%s", -+ "narration.cycle_button.usage.hovered": "单击鼠标左键切换到%s", -+ "narration.edit_box": "编辑框:%s", -+ "narration.recipe": "%s的配方", -+ "narration.recipe.usage": "单击鼠标左键选中", -+ "narration.recipe.usage.more": "单击鼠标右键显示更多配方", -+ "narration.selection.usage": "使用上下方向键移动到另一项", -+ "narration.slider.usage.focused": "使用左右方向键更改数值", -+ "narration.slider.usage.hovered": "拖动滑块更改数值", -+ "narration.suggestion": "已选中%2$s项建议中的第%1$s项:%3$s", -+ "narration.suggestion.tooltip": "已选中%2$s项建议中的第%1$s项:%3$s(%4$s)", -+ "narration.tab_navigation.usage": "按下Ctrl键和Tab键切换标签页", -+ "narrator.button.accessibility": "辅助功能", -+ "narrator.button.difficulty_lock": "难度锁定", -+ "narrator.button.difficulty_lock.locked": "已锁定", -+ "narrator.button.difficulty_lock.unlocked": "未锁定", -+ "narrator.button.language": "语言", -+ "narrator.controls.bound": "%s已绑定至%s", -+ "narrator.controls.reset": "重置%s按钮", -+ "narrator.controls.unbound": "%s未绑定", -+ "narrator.joining": "加入中", -+ "narrator.loading": "加载中:%s", -+ "narrator.loading.done": "完成", -+ "narrator.position.list": "已选中列表的第%s行,共%s行", -+ "narrator.position.object_list": "已选中一列控件中的第%s项,共%s项", -+ "narrator.position.screen": "屏幕控件,第%s个,共%s个", -+ "narrator.position.tab": "已选中第%s个标签页,共%s个", -+ "narrator.ready_to_play": "准备就绪", -+ "narrator.screen.title": "标题屏幕", -+ "narrator.screen.usage": "使用鼠标指针或Tab键选择屏幕控件", -+ "narrator.select": "已选择:%s", -+ "narrator.select.world": "已选择%s,上次进入游戏:%s,%s,%s,版本:%s", -+ "narrator.select.world_info": "已选择%s,上次进入游戏:%s,%s", -+ "narrator.toast.disabled": "复述功能已关闭", -+ "narrator.toast.enabled": "复述功能已开启", -+ "optimizeWorld.confirm.description": "这将尝试用最新的游戏格式储存所有数据来达到优化世界的效果。取决于世界的状况,这可能会花费不少时间。一旦完成,进行游戏时可能会更流畅,但你的世界将不再与旧版游戏兼容。你确定要继续吗?", -+ "optimizeWorld.confirm.title": "优化世界", -+ "optimizeWorld.info.converted": "已更新的区块数:%s", -+ "optimizeWorld.info.skipped": "已忽略的区块数:%s", -+ "optimizeWorld.info.total": "总区块数:%s", -+ "optimizeWorld.stage.counting": "统计区块中…", -+ "optimizeWorld.stage.failed": "失败了!:(", -+ "optimizeWorld.stage.finished": "即将完成…", -+ "optimizeWorld.stage.upgrading": "更新所有区块中…", -+ "optimizeWorld.title": "正在优化世界“%s”", -+ "options.accessibility.high_contrast": "高对比度", -+ "options.accessibility.high_contrast.error.tooltip": "高对比度资源包不可用", -+ "options.accessibility.high_contrast.tooltip": "提高UI控件的对比度", -+ "options.accessibility.link": "辅助功能指南", -+ "options.accessibility.panorama_speed": "全景图滚动速度", -+ "options.accessibility.text_background": "文本背景", -+ "options.accessibility.text_background.chat": "聊天", -+ "options.accessibility.text_background.everywhere": "全局", -+ "options.accessibility.text_background_opacity": "文本背景不透明度", -+ "options.accessibility.title": "辅助功能设置…", -+ "options.allowServerListing": "允许列入服务器玩家列表", -+ "options.allowServerListing.tooltip": "服务器可能会公开列出目前在线的玩家。\n若关闭此选项,你的名字将不会显示在此列表中。", -+ "options.ao": "平滑光照", -+ "options.ao.max": "最大", -+ "options.ao.min": "最小", -+ "options.ao.off": "关", -+ "options.attack.crosshair": "十字准星", -+ "options.attack.hotbar": "快捷栏", -+ "options.attackIndicator": "攻击指示器", -+ "options.audioDevice": "设备", -+ "options.audioDevice.default": "系统默认", -+ "options.autoJump": "自动跳跃", -+ "options.autoSuggestCommands": "命令提示", -+ "options.autosaveIndicator": "自动保存指示器", -+ "options.biomeBlendRadius": "生物群系过渡距离", -+ "options.biomeBlendRadius.1": "关(最快)", -+ "options.biomeBlendRadius.11": "11×11(极高)", -+ "options.biomeBlendRadius.13": "13×13(显著)", -+ "options.biomeBlendRadius.15": "15×15(最高)", -+ "options.biomeBlendRadius.3": "3×3(快)", -+ "options.biomeBlendRadius.5": "5×5(普通)", -+ "options.biomeBlendRadius.7": "7×7(高)", -+ "options.biomeBlendRadius.9": "9×9(很高)", -+ "options.chat.color": "颜色", -+ "options.chat.delay": "聊天延迟:%s秒", -+ "options.chat.delay_none": "聊天延迟:无", -+ "options.chat.height.focused": "聚焦高度", -+ "options.chat.height.unfocused": "淡化高度", -+ "options.chat.line_spacing": "行距", -+ "options.chat.links": "网页链接", -+ "options.chat.links.prompt": "链接提示", -+ "options.chat.opacity": "聊天文本不透明度", -+ "options.chat.scale": "聊天文本大小", -+ "options.chat.title": "聊天设置…", -+ "options.chat.visibility": "聊天", -+ "options.chat.visibility.full": "显示", -+ "options.chat.visibility.hidden": "隐藏", -+ "options.chat.visibility.system": "仅限命令", -+ "options.chat.width": "宽度", -+ "options.chunks": "%s个区块", -+ "options.clouds.fancy": "高品质", -+ "options.clouds.fast": "流畅", -+ "options.controls": "控制…", -+ "options.credits_and_attribution": "鸣谢与著作权说明…", -+ "options.customizeTitle": "自定义世界设置", -+ "options.damageTiltStrength": "受伤抖动效果", -+ "options.damageTiltStrength.tooltip": "受到伤害导致的游戏视角晃动程度。", -+ "options.darkMojangStudiosBackgroundColor": "黑白徽标", -+ "options.darkMojangStudiosBackgroundColor.tooltip": "将Mojang Studios加载屏幕的背景颜色更改为黑色。", -+ "options.darknessEffectScale": "黑暗脉动效果", -+ "options.darknessEffectScale.tooltip": "控制监守者或幽匿尖啸体给予的黑暗脉动效果程度。", -+ "options.difficulty": "难度", -+ "options.difficulty.easy": "简单", -+ "options.difficulty.easy.info": "敌对生物会生成,但伤害较低。生命值在饥饿值耗尽后最终会降至5颗心。", -+ "options.difficulty.hard": "困难", -+ "options.difficulty.hard.info": "敌对生物会生成,伤害较高。饥饿值耗尽后最终会饿死。", -+ "options.difficulty.hardcore": "极限", -+ "options.difficulty.normal": "普通", -+ "options.difficulty.normal.info": "敌对生物会生成,伤害适中。生命值在饥饿值耗尽后最终会降至半颗心。", -+ "options.difficulty.online": "服务器难度", -+ "options.difficulty.peaceful": "和平", -+ "options.difficulty.peaceful.info": "不会生成绝大多数敌对生物。饥饿值不会消耗,且生命值会自然恢复。", -+ "options.directionalAudio": "定向音频", -+ "options.directionalAudio.off.tooltip": "经典立体声", -+ "options.directionalAudio.on.tooltip": "通过基于HRTF算法的定向环绕音频来改善立体音效的模拟。需使用与HRTF兼容的音频硬件,推荐在使用时佩戴耳机。", -+ "options.discrete_mouse_scroll": "离散滚动", -+ "options.entityDistanceScaling": "实体渲染距离", -+ "options.entityShadows": "实体阴影", -+ "options.forceUnicodeFont": "强制使用Unicode字体", -+ "options.fov": "视场角", -+ "options.fov.max": "广角", -+ "options.fov.min": "中", -+ "options.fovEffectScale": "视场角效果", -+ "options.fovEffectScale.tooltip": "控制游戏机制效果改变视场角的程度。", -+ "options.framerate": "%s fps", -+ "options.framerateLimit": "最大帧率", -+ "options.framerateLimit.max": "无限制", -+ "options.fullscreen": "全屏", -+ "options.fullscreen.current": "当前分辨率", -+ "options.fullscreen.resolution": "全屏分辨率", -+ "options.fullscreen.unavailable": "设置不可用", -+ "options.gamma": "亮度", -+ "options.gamma.default": "默认", -+ "options.gamma.max": "明亮", -+ "options.gamma.min": "昏暗", -+ "options.generic_value": "%s:%s", -+ "options.glintSpeed": "附魔光效闪烁速度", -+ "options.glintSpeed.tooltip": "控制附魔物品的光效闪动速度。", -+ "options.glintStrength": "附魔光效闪烁强度", -+ "options.glintStrength.tooltip": "控制附魔物品光效的透明程度。", -+ "options.graphics": "图像品质", -+ "options.graphics.fabulous": "极佳!", -+ "options.graphics.fabulous.tooltip": "%s画质使用屏幕着色器绘制天气、云以及半透明方块和水后面的粒子。\n这也许会在便携设备和4K显示屏上造成严重的性能负担。", -+ "options.graphics.fancy": "高品质", -+ "options.graphics.fancy.tooltip": "高品质画质会为大多数设备平衡性能和质量。\n天气、云和粒子可能不会在半透明方块或水的后面显示。", -+ "options.graphics.fast": "流畅", -+ "options.graphics.fast.tooltip": "流畅画质将减少雨雪的可见数量。\n树叶等方块的透明效果将被禁用。", -+ "options.graphics.warning.accept": "在不受支持的情况下继续", -+ "options.graphics.warning.cancel": "算了", -+ "options.graphics.warning.message": "检测到你的图形设备不支持%s画质选项。\n\n你可以忽略此提示并继续,但如果你依然选择使用%s画质,你的设备将不会受到支持。", -+ "options.graphics.warning.renderer": "检测到渲染器:[%s]", -+ "options.graphics.warning.title": "图形设备不受支持", -+ "options.graphics.warning.vendor": "检测到厂商:[%s]", -+ "options.graphics.warning.version": "检测到OpenGL版本:[%s]", -+ "options.guiScale": "界面尺寸", -+ "options.guiScale.auto": "自动", -+ "options.hidden": "隐藏", -+ "options.hideLightningFlashes": "隐藏闪电的闪烁效果", -+ "options.hideLightningFlashes.tooltip": "关闭闪电造成的天空闪烁效果。闪电本身仍将可见。", -+ "options.hideMatchedNames": "隐藏匹配的名称", -+ "options.hideMatchedNames.tooltip": "第三方服务器也许会发送非标准格式的聊天消息。\n此设置打开后,被隐藏的玩家将会被根据发言者的名字匹配。", -+ "options.invertMouse": "鼠标反转", -+ "options.key.hold": "按住", -+ "options.key.toggle": "切换", -+ "options.language": "语言…", -+ "options.languageWarning": "语言翻译不一定100%%准确", -+ "options.mainHand": "主手", -+ "options.mainHand.left": "左手", -+ "options.mainHand.right": "右手", -+ "options.mipmapLevels": "Mipmap级别", -+ "options.modelPart.cape": "披风", -+ "options.modelPart.hat": "帽子", -+ "options.modelPart.jacket": "外套", -+ "options.modelPart.left_pants_leg": "左裤腿", -+ "options.modelPart.left_sleeve": "左袖", -+ "options.modelPart.right_pants_leg": "右裤腿", -+ "options.modelPart.right_sleeve": "右袖", -+ "options.mouseWheelSensitivity": "滚轮灵敏度", -+ "options.mouse_settings": "鼠标设置…", -+ "options.mouse_settings.title": "鼠标设置", -+ "options.multiplayer.title": "多人游戏设置…", -+ "options.multiplier": "%s×", -+ "options.narrator": "复述功能", -+ "options.narrator.all": "复述所有", -+ "options.narrator.chat": "复述聊天消息", -+ "options.narrator.notavailable": "不可用", -+ "options.narrator.off": "关", -+ "options.narrator.system": "复述系统消息", -+ "options.notifications.display_time": "通知显示时长", -+ "options.notifications.display_time.tooltip": "影响所有通知在屏幕上停留的时长。", -+ "options.off": "关", -+ "options.off.composed": "%s:关", -+ "options.on": "开", -+ "options.on.composed": "%s:开", -+ "options.online": "在线选项…", -+ "options.online.title": "在线选项", -+ "options.onlyShowSecureChat": "仅显示安全的聊天", -+ "options.onlyShowSecureChat.tooltip": "仅显示来自其他玩家的可验证为该玩家发送且未被修改的消息。", -+ "options.operatorItemsTab": "管理员用品标签页", -+ "options.particles": "粒子效果", -+ "options.particles.all": "全部", -+ "options.particles.decreased": "少量", -+ "options.particles.minimal": "最少", -+ "options.percent_add_value": "%s:+%s%%", -+ "options.percent_value": "%s:%s%%", -+ "options.pixel_value": "%s:%spx", -+ "options.prioritizeChunkUpdates": "区块构建器", -+ "options.prioritizeChunkUpdates.byPlayer": "半阻塞", -+ "options.prioritizeChunkUpdates.byPlayer.tooltip": "区块内部的某些行为会导致区块立刻重新编译。这包括放置或破坏方块。", -+ "options.prioritizeChunkUpdates.nearby": "全阻塞", -+ "options.prioritizeChunkUpdates.nearby.tooltip": "附近的区块总会被立刻编译。这可能影响放置或破坏方块时的游戏性能。", -+ "options.prioritizeChunkUpdates.none": "线程化", -+ "options.prioritizeChunkUpdates.none.tooltip": "附近的区块会在并行的线程中编译。这可能导致破坏方块时短暂出现图像空洞。", -+ "options.rawMouseInput": "原始输入", -+ "options.realmsNotifications": "Realms新闻与邀请", -+ "options.reducedDebugInfo": "简化调试信息", -+ "options.renderClouds": "云", -+ "options.renderDistance": "渲染距离", -+ "options.resourcepack": "资源包…", -+ "options.screenEffectScale": "屏幕扭曲效果", -+ "options.screenEffectScale.tooltip": "反胃状态效果和下界传送门所使用的屏幕扭曲效果的强度。\n值较低时,反胃的扭曲效果会被一层绿色的视觉效果替代。", -+ "options.sensitivity": "鼠标灵敏度", -+ "options.sensitivity.max": "超高速!!!", -+ "options.sensitivity.min": "*哈欠*", -+ "options.showSubtitles": "显示字幕", -+ "options.simulationDistance": "模拟距离", -+ "options.skinCustomisation": "自定义皮肤…", -+ "options.skinCustomisation.title": "自定义皮肤", -+ "options.sounds": "音乐和声音…", -+ "options.sounds.title": "音乐和声音选项", -+ "options.telemetry": "遥测数据…", -+ "options.telemetry.button": "数据收集", -+ "options.telemetry.button.tooltip": "“%s”仅包含必要数据。\n“%s”包含可选数据和必要数据。", -+ "options.telemetry.state.all": "全部", -+ "options.telemetry.state.minimal": "最少", -+ "options.telemetry.state.none": "无", -+ "options.title": "选项", -+ "options.touchscreen": "触屏模式", -+ "options.video": "视频设置…", -+ "options.videoTitle": "视频设置", -+ "options.viewBobbing": "视角摇晃", -+ "options.visible": "显示", -+ "options.vsync": "垂直同步", -+ "outOfMemory.message": "Minecraft已耗尽内存。\n\n原因可能是游戏存在漏洞或Java虚拟机未获得足够的内存分配。\n\n为了避免存档损坏,游戏已退出。我们已尝试腾出足够的内存来使你返回主菜单进而返回游戏,但这可能并未奏效。\n\n如果你再次看见此消息,请重启游戏。", -+ "outOfMemory.title": "内存溢出!", -+ "pack.available.title": "可用", -+ "pack.copyFailure": "复制包失败", -+ "pack.dropConfirm": "你确定要将这些包添加进Minecraft中吗?", -+ "pack.dropInfo": "将文件拖放到这个窗口内来添加包", -+ "pack.folderInfo": "(请将包放在这里)", -+ "pack.incompatible": "不兼容", -+ "pack.incompatible.confirm.new": "这个包是为更新的Minecraft版本所打造的,在此版本可能不会正常工作。", -+ "pack.incompatible.confirm.old": "这个包是为更旧的Minecraft版本所打造的,在此版本可能不会正常工作。", -+ "pack.incompatible.confirm.title": "你确定要加载此包吗?", -+ "pack.incompatible.new": "(适用于新版本的Minecraft)", -+ "pack.incompatible.old": "(适用于旧版本的Minecraft)", -+ "pack.nameAndSource": "%s(%s)", -+ "pack.openFolder": "打开包文件夹", -+ "pack.selected.title": "已选", -+ "pack.source.builtin": "内置", -+ "pack.source.feature": "功能", -+ "pack.source.local": "本地", -+ "pack.source.server": "服务器", -+ "pack.source.world": "世界", -+ "painting.dimensions": "%s×%s", -+ "painting.minecraft.alban.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.alban.title": "Albanian", -+ "painting.minecraft.aztec.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.aztec.title": "de_aztec", -+ "painting.minecraft.aztec2.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.aztec2.title": "de_aztec", -+ "painting.minecraft.bomb.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.bomb.title": "Target Successfully Bombed", -+ "painting.minecraft.burning_skull.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.burning_skull.title": "Skull On Fire", -+ "painting.minecraft.bust.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.bust.title": "Bust", -+ "painting.minecraft.courbet.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.courbet.title": "Bonjour Monsieur Courbet", -+ "painting.minecraft.creebet.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.creebet.title": "Creebet", -+ "painting.minecraft.donkey_kong.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.donkey_kong.title": "Kong", -+ "painting.minecraft.earth.author": "Mojang", -+ "painting.minecraft.earth.title": "Earth", -+ "painting.minecraft.fighters.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.fighters.title": "Fighters", -+ "painting.minecraft.fire.author": "Mojang", -+ "painting.minecraft.fire.title": "Fire", -+ "painting.minecraft.graham.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.graham.title": "Graham", -+ "painting.minecraft.kebab.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.kebab.title": "Kebab med tre pepperoni", -+ "painting.minecraft.match.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.match.title": "Match", -+ "painting.minecraft.pigscene.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.pigscene.title": "Pigscene", -+ "painting.minecraft.plant.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.plant.title": "Paradisträd", -+ "painting.minecraft.pointer.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.pointer.title": "Pointer", -+ "painting.minecraft.pool.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.pool.title": "The Pool", -+ "painting.minecraft.sea.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.sea.title": "Seaside", -+ "painting.minecraft.skeleton.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.skeleton.title": "Mortal Coil", -+ "painting.minecraft.skull_and_roses.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.skull_and_roses.title": "Skull and Roses", -+ "painting.minecraft.stage.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.stage.title": "The Stage Is Set", -+ "painting.minecraft.sunset.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.sunset.title": "sunset_dense", -+ "painting.minecraft.void.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.void.title": "The void", -+ "painting.minecraft.wanderer.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.wanderer.title": "Wanderer", -+ "painting.minecraft.wasteland.author": "Kristoffer Zetterstrand", -+ "painting.minecraft.wasteland.title": "Wasteland", -+ "painting.minecraft.water.author": "Mojang", -+ "painting.minecraft.water.title": "Water", -+ "painting.minecraft.wind.author": "Mojang", -+ "painting.minecraft.wind.title": "Wind", -+ "painting.minecraft.wither.author": "Mojang", -+ "painting.minecraft.wither.title": "Wither", -+ "painting.random": "随机样式", -+ "parsing.bool.expected": "应为布尔型", -+ "parsing.bool.invalid": "无效的布尔型数据,应为“true”或“false”却出现了“%s”", -+ "parsing.double.expected": "应为双精度浮点型", -+ "parsing.double.invalid": "无效的双精度浮点型数据“%s”", -+ "parsing.expected": "应为“%s”", -+ "parsing.float.expected": "应为浮点型", -+ "parsing.float.invalid": "无效的浮点型数据“%s”", -+ "parsing.int.expected": "应为整型", -+ "parsing.int.invalid": "无效的整型数据“%s”", -+ "parsing.long.expected": "应为长整型", -+ "parsing.long.invalid": "无效的长整型数据“%s”", -+ "parsing.quote.escape": "双引号内的字符串包含无效的转义序列“\\%s”", -+ "parsing.quote.expected.end": "字符串的双引号不成对", -+ "parsing.quote.expected.start": "字符串的开头需要双引号", -+ "particle.notFound": "未知的粒子:%s", -+ "permissions.requires.entity": "需要一个实体来执行此命令", -+ "permissions.requires.player": "需要一名玩家来执行此命令", -+ "potion.potency.1": "II", -+ "potion.potency.2": "III", -+ "potion.potency.3": "IV", -+ "potion.potency.4": "V", -+ "potion.potency.5": "VI", -+ "potion.whenDrank": "当生效后:", -+ "potion.withAmplifier": "%s %s", -+ "potion.withDuration": "%s(%s)", -+ "predicate.unknown": "未知的谓词:%s", -+ "quickplay.error.invalid_identifier": "无法找到具有指定标识符的世界", -+ "quickplay.error.realm_connect": "无法连接至Realm", -+ "quickplay.error.realm_permission": "权限不足,无法连接至此Realm", -+ "quickplay.error.title": "无法快速进入游戏", -+ "realms.missing.module.error.text": "当前无法打开Realms,请稍后再试", -+ "realms.missing.snapshot.error.text": "快照当前不支持Realms", -+ "recipe.notFound": "未知的配方:%s", -+ "recipe.toast.description": "请检查你的配方书", -+ "recipe.toast.title": "新配方已解锁!", -+ "record.nowPlaying": "正在播放:%s", -+ "resourcePack.broken_assets": "检测到损坏的资产", -+ "resourcePack.high_contrast.name": "高对比度", -+ "resourcePack.load_fail": "重载资源失败", -+ "resourcePack.programmer_art.name": "Programmer Art", -+ "resourcePack.server.name": "世界指定资源包", -+ "resourcePack.title": "选择资源包", -+ "resourcePack.vanilla.description": "Minecraft的默认观感", -+ "resourcePack.vanilla.name": "默认", -+ "resourcepack.downloading": "正在下载资源包", -+ "resourcepack.progress": "下载文件中(%s MB)…", -+ "resourcepack.requesting": "正在发送请求…", -+ "screenshot.failure": "无法保存截图:%s", -+ "screenshot.success": "已将截图保存为%s", -+ "selectServer.add": "添加服务器", -+ "selectServer.defaultName": "Minecraft服务器", -+ "selectServer.delete": "删除", -+ "selectServer.deleteButton": "删除", -+ "selectServer.deleteQuestion": "你确定要删除此服务器吗?", -+ "selectServer.deleteWarning": "“%s”将会永久消失!(真的很久!)", -+ "selectServer.direct": "直接连接", -+ "selectServer.edit": "编辑", -+ "selectServer.hiddenAddress": "(隐藏)", -+ "selectServer.refresh": "刷新", -+ "selectServer.select": "加入服务器", -+ "selectServer.title": "选择服务器", -+ "selectWorld.access_failure": "加载世界失败", -+ "selectWorld.allowCommands": "允许作弊", -+ "selectWorld.allowCommands.info": "例如/gamemode、/experience等命令", -+ "selectWorld.backupEraseCache": "清除缓存数据", -+ "selectWorld.backupJoinConfirmButton": "创建备份并加载", -+ "selectWorld.backupJoinSkipButton": "我知道我在做什么!", -+ "selectWorld.backupQuestion.customized": "自定义世界已不受支持", -+ "selectWorld.backupQuestion.downgrade": "不支持对存档版本进行降级", -+ "selectWorld.backupQuestion.experimental": "使用“实验性设置”的世界已不受支持", -+ "selectWorld.backupQuestion.snapshot": "你真的想加载此世界吗?", -+ "selectWorld.backupWarning.customized": "不巧,我们在这个版本的Minecraft中不支持自定义世界。我们可以继续加载这个世界并保持原状,但任何新生成的地形将不再被自定义。抱歉给你带来不便。", -+ "selectWorld.backupWarning.downgrade": "这个世界上次是在%s版本中打开的,你正在使用%s版本。降低世界的游戏版本可能会导致存档损坏——我们无法保证它可以被加载和运行。如果你仍要继续,请备份该存档!", -+ "selectWorld.backupWarning.experimental": "这个世界使用的实验性设置可能会随时停止运作。我们无法保证这些设置将来能够加载或运作。务必谨慎!", -+ "selectWorld.backupWarning.snapshot": "这个世界上次是在%s版本中打开的,你正在使用%s版本。请备份你的世界,以防世界崩溃!", -+ "selectWorld.bonusItems": "奖励箱", -+ "selectWorld.cheats": "作弊", -+ "selectWorld.conversion": "必须进行转换!", -+ "selectWorld.conversion.tooltip": "此世界必须在较旧版本(比如1.6.4)中打开,以便安全转换", -+ "selectWorld.create": "创建新的世界", -+ "selectWorld.createDemo": "进入新的试玩世界", -+ "selectWorld.customizeType": "自定义", -+ "selectWorld.dataPacks": "数据包", -+ "selectWorld.data_read": "读取世界数据中…", -+ "selectWorld.delete": "删除", -+ "selectWorld.deleteButton": "删除", -+ "selectWorld.deleteQuestion": "你确定要删除这个世界吗?", -+ "selectWorld.deleteWarning": "“%s”将会永久消失!(真的很久!)", -+ "selectWorld.delete_failure": "删除世界失败", -+ "selectWorld.edit": "编辑", -+ "selectWorld.edit.backup": "进行备份", -+ "selectWorld.edit.backupCreated": "已备份:%s", -+ "selectWorld.edit.backupFailed": "备份失败", -+ "selectWorld.edit.backupFolder": "打开备份文件夹", -+ "selectWorld.edit.backupSize": "大小:%s MB", -+ "selectWorld.edit.export_worldgen_settings": "导出世界生成设置", -+ "selectWorld.edit.export_worldgen_settings.failure": "导出失败", -+ "selectWorld.edit.export_worldgen_settings.success": "导出成功", -+ "selectWorld.edit.openFolder": "打开世界文件夹", -+ "selectWorld.edit.optimize": "优化世界", -+ "selectWorld.edit.resetIcon": "重置图标", -+ "selectWorld.edit.save": "保存", -+ "selectWorld.edit.title": "编辑世界", -+ "selectWorld.enterName": "世界名称", -+ "selectWorld.enterSeed": "世界生成器的种子", -+ "selectWorld.experimental": "实验性", -+ "selectWorld.experimental.details": "详细信息", -+ "selectWorld.experimental.details.entry": "需要的实验性功能:%s", -+ "selectWorld.experimental.details.title": "实验性功能需求", -+ "selectWorld.experimental.message": "请注意!\n此配置需要使用仍在开发中的功能。你的世界可能会崩溃、损坏或不兼容未来的更新。", -+ "selectWorld.experimental.title": "实验性功能警告", -+ "selectWorld.experiments": "实验性内容", -+ "selectWorld.experiments.info": "实验性内容,即仍在开发的新功能。启用时请小心,因为这些内容可能不稳定或会导致损坏。已开启的实验性内容在创建世界后将无法再关闭。", -+ "selectWorld.futureworld.error.text": "当我们试图加载一份来自未来版本的世界时,发生了一些错误。这本就并非万无一失,很抱歉没能成功。", -+ "selectWorld.futureworld.error.title": "出错了!", -+ "selectWorld.gameMode": "游戏模式", -+ "selectWorld.gameMode.adventure": "冒险", -+ "selectWorld.gameMode.adventure.info": "与生存模式相同,但无法放置或破坏方块。", -+ "selectWorld.gameMode.adventure.line1": "与生存模式相同,但无法", -+ "selectWorld.gameMode.adventure.line2": "放置或者移除方块", -+ "selectWorld.gameMode.creative": "创造", -+ "selectWorld.gameMode.creative.info": "无拘无束地探索创造。可以飞行,材料取之不尽,且不受怪物伤害。", -+ "selectWorld.gameMode.creative.line1": "无限的资源、自由地飞翔", -+ "selectWorld.gameMode.creative.line2": "并且能够瞬间破坏方块", -+ "selectWorld.gameMode.hardcore": "极限", -+ "selectWorld.gameMode.hardcore.info": "难度锁定为“困难”的生存模式,且死亡后无法重生。", -+ "selectWorld.gameMode.hardcore.line1": "难度锁定在困难的生存模式", -+ "selectWorld.gameMode.hardcore.line2": "且只有一条生命", -+ "selectWorld.gameMode.spectator": "旁观", -+ "selectWorld.gameMode.spectator.info": "你可以旁观,但不能互动。", -+ "selectWorld.gameMode.spectator.line1": "你可以旁观,但不能互动", -+ "selectWorld.gameMode.survival": "生存", -+ "selectWorld.gameMode.survival.info": "探索未知的世界,尽情建造、收集、合成并与怪物战斗。", -+ "selectWorld.gameMode.survival.line1": "探索世界、收集资源、合成道具、", -+ "selectWorld.gameMode.survival.line2": "提高等级、补充体力和生命值", -+ "selectWorld.gameRules": "游戏规则", -+ "selectWorld.import_worldgen_settings": "导入设置", -+ "selectWorld.import_worldgen_settings.failure": "导入设置时出错", -+ "selectWorld.import_worldgen_settings.select_file": "选择设置文件(.json)", -+ "selectWorld.incompatible_series": "创建于不兼容的版本", -+ "selectWorld.load_folder_access": "无法读取或访问游戏世界存档所在的文件夹!", -+ "selectWorld.loading_list": "加载世界列表中", -+ "selectWorld.locked": "被另一个正在运行的Minecraft实例锁定", -+ "selectWorld.mapFeatures": "生成结构", -+ "selectWorld.mapFeatures.info": "村庄、沉船等", -+ "selectWorld.mapType": "世界类型", -+ "selectWorld.mapType.normal": "普通", -+ "selectWorld.moreWorldOptions": "更多世界选项…", -+ "selectWorld.newWorld": "新的世界", -+ "selectWorld.recreate": "重建", -+ "selectWorld.recreate.customized.text": "自定义世界在这个版本的Minecraft中已不受支持。我们可以尝试用同样的种子与选项重建它,但任何自定义的地形都会丢失。抱歉给你带来不便。", -+ "selectWorld.recreate.customized.title": "自定义世界已不受支持", -+ "selectWorld.recreate.error.text": "尝试重建世界时出错。", -+ "selectWorld.recreate.error.title": "出错了!", -+ "selectWorld.resultFolder": "将会保存于:", -+ "selectWorld.search": "搜索世界", -+ "selectWorld.seedInfo": "留空以生成随机种子", -+ "selectWorld.select": "进入选中的世界", -+ "selectWorld.targetFolder": "存档文件夹:%s", -+ "selectWorld.title": "选择世界", -+ "selectWorld.tooltip.fromNewerVersion1": "世界是在更新的版本中被保存的,", -+ "selectWorld.tooltip.fromNewerVersion2": "加载这个世界可能会产生问题!", -+ "selectWorld.tooltip.snapshot1": "在这个快照中加载它之前,", -+ "selectWorld.tooltip.snapshot2": "不要忘了备份这个世界!", -+ "selectWorld.unable_to_load": "无法加载世界", -+ "selectWorld.version": "版本:", -+ "selectWorld.versionJoinButton": "仍然加载", -+ "selectWorld.versionQuestion": "你真的想加载此世界吗?", -+ "selectWorld.versionUnknown": "未知", -+ "selectWorld.versionWarning": "此世界上次是在%s版本中打开的,在此版本中加载可能会导致数据损坏!", -+ "selectWorld.warning.deprecated.question": "一些使用中的功能已被淘汰并会在将来失效。你确定要继续吗?", -+ "selectWorld.warning.deprecated.title": "警告!这些设置使用了已淘汰的功能", -+ "selectWorld.warning.experimental.question": "这些设置是实验性的,将来可能停止运作。你确定要继续吗?", -+ "selectWorld.warning.experimental.title": "警告!这些设置使用了实验性功能", -+ "selectWorld.world": "世界", -+ "sign.edit": "编辑告示牌消息", -+ "sleep.not_possible": "已入睡玩家的数量不足以跳过夜晚", -+ "sleep.players_sleeping": "%s/%s名玩家已入睡", -+ "sleep.skipping_night": "今夜将在睡梦中度过", -+ "slot.unknown": "未知的槽位“%s”", -+ "soundCategory.ambient": "环境", -+ "soundCategory.block": "方块", -+ "soundCategory.hostile": "敌对生物", -+ "soundCategory.master": "主音量", -+ "soundCategory.music": "音乐", -+ "soundCategory.neutral": "友好生物", -+ "soundCategory.player": "玩家", -+ "soundCategory.record": "唱片机/音符盒", -+ "soundCategory.voice": "声音/语音", -+ "soundCategory.weather": "天气", -+ "spectatorMenu.close": "关闭菜单", -+ "spectatorMenu.next_page": "下一页", -+ "spectatorMenu.previous_page": "上一页", -+ "spectatorMenu.root.prompt": "按下一个键来选择命令,再按一次来使用它。", -+ "spectatorMenu.team_teleport": "传送到队伍成员", -+ "spectatorMenu.team_teleport.prompt": "选择一支队伍作为传送目标", -+ "spectatorMenu.teleport": "传送到玩家", -+ "spectatorMenu.teleport.prompt": "选择一名玩家作为传送目标", -+ "stat.generalButton": "通用", -+ "stat.itemsButton": "物品", -+ "stat.minecraft.animals_bred": "繁殖动物次数", -+ "stat.minecraft.aviate_one_cm": "鞘翅滑行距离", -+ "stat.minecraft.bell_ring": "鸣钟次数", -+ "stat.minecraft.boat_one_cm": "坐船移动距离", -+ "stat.minecraft.clean_armor": "清洗盔甲次数", -+ "stat.minecraft.clean_banner": "清洗旗帜次数", -+ "stat.minecraft.clean_shulker_box": "潜影盒清洗次数", -+ "stat.minecraft.climb_one_cm": "已攀爬距离", -+ "stat.minecraft.crouch_one_cm": "潜行距离", -+ "stat.minecraft.damage_absorbed": "吸收的伤害", -+ "stat.minecraft.damage_blocked_by_shield": "盾牌抵挡的伤害", -+ "stat.minecraft.damage_dealt": "造成伤害", -+ "stat.minecraft.damage_dealt_absorbed": "造成伤害(被吸收)", -+ "stat.minecraft.damage_dealt_resisted": "造成伤害(被抵挡)", -+ "stat.minecraft.damage_resisted": "抵挡的伤害", -+ "stat.minecraft.damage_taken": "受到伤害", -+ "stat.minecraft.deaths": "死亡次数", -+ "stat.minecraft.drop": "物品掉落", -+ "stat.minecraft.eat_cake_slice": "吃掉的蛋糕片数", -+ "stat.minecraft.enchant_item": "物品附魔次数", -+ "stat.minecraft.fall_one_cm": "摔落高度", -+ "stat.minecraft.fill_cauldron": "炼药锅装水次数", -+ "stat.minecraft.fish_caught": "捕鱼数", -+ "stat.minecraft.fly_one_cm": "飞行距离", -+ "stat.minecraft.horse_one_cm": "骑马移动距离", -+ "stat.minecraft.inspect_dispenser": "搜查发射器次数", -+ "stat.minecraft.inspect_dropper": "搜查投掷器次数", -+ "stat.minecraft.inspect_hopper": "搜查漏斗次数", -+ "stat.minecraft.interact_with_anvil": "与铁砧交互次数", -+ "stat.minecraft.interact_with_beacon": "与信标交互次数", -+ "stat.minecraft.interact_with_blast_furnace": "与高炉交互次数", -+ "stat.minecraft.interact_with_brewingstand": "与酿造台交互次数", -+ "stat.minecraft.interact_with_campfire": "与营火交互次数", -+ "stat.minecraft.interact_with_cartography_table": "与制图台交互次数", -+ "stat.minecraft.interact_with_crafting_table": "与工作台交互次数", -+ "stat.minecraft.interact_with_furnace": "与熔炉交互次数", -+ "stat.minecraft.interact_with_grindstone": "与砂轮交互次数", -+ "stat.minecraft.interact_with_lectern": "与讲台交互次数", -+ "stat.minecraft.interact_with_loom": "与织布机交互次数", -+ "stat.minecraft.interact_with_smithing_table": "与锻造台交互次数", -+ "stat.minecraft.interact_with_smoker": "与烟熏炉交互次数", -+ "stat.minecraft.interact_with_stonecutter": "与切石机交互次数", -+ "stat.minecraft.jump": "跳跃次数", -+ "stat.minecraft.junk_fished": "钓到垃圾次数", -+ "stat.minecraft.leave_game": "游戏退出次数", -+ "stat.minecraft.minecart_one_cm": "坐矿车移动距离", -+ "stat.minecraft.mob_kills": "生物击杀数", -+ "stat.minecraft.open_barrel": "木桶打开次数", -+ "stat.minecraft.open_chest": "箱子打开次数", -+ "stat.minecraft.open_enderchest": "末影箱打开次数", -+ "stat.minecraft.open_shulker_box": "潜影盒打开次数", -+ "stat.minecraft.pig_one_cm": "骑猪移动距离", -+ "stat.minecraft.play_noteblock": "音符盒播放次数", -+ "stat.minecraft.play_record": "播放唱片数", -+ "stat.minecraft.play_time": "游戏时长", -+ "stat.minecraft.player_kills": "玩家击杀数", -+ "stat.minecraft.pot_flower": "盆栽种植数", -+ "stat.minecraft.raid_trigger": "触发袭击次数", -+ "stat.minecraft.raid_win": "袭击胜利次数", -+ "stat.minecraft.ring_bell": "鸣钟次数", -+ "stat.minecraft.sleep_in_bed": "躺在床上的次数", -+ "stat.minecraft.sneak_time": "潜行时间", -+ "stat.minecraft.sprint_one_cm": "疾跑距离", -+ "stat.minecraft.strider_one_cm": "骑炽足兽移动距离", -+ "stat.minecraft.swim_one_cm": "游泳距离", -+ "stat.minecraft.talked_to_villager": "村民交互次数", -+ "stat.minecraft.target_hit": "击中标靶次数", -+ "stat.minecraft.time_since_death": "自上次死亡", -+ "stat.minecraft.time_since_rest": "自上次入眠", -+ "stat.minecraft.total_world_time": "世界打开时间", -+ "stat.minecraft.traded_with_villager": "村民交易次数", -+ "stat.minecraft.treasure_fished": "钓到宝藏次数", -+ "stat.minecraft.trigger_trapped_chest": "陷阱箱触发次数", -+ "stat.minecraft.tune_noteblock": "音符盒调音次数", -+ "stat.minecraft.use_cauldron": "从炼药锅取水次数", -+ "stat.minecraft.walk_on_water_one_cm": "水面行走距离", -+ "stat.minecraft.walk_one_cm": "行走距离", -+ "stat.minecraft.walk_under_water_one_cm": "水下行走距离", -+ "stat.mobsButton": "生物", -+ "stat_type.minecraft.broken": "损坏次数", -+ "stat_type.minecraft.crafted": "合成次数", -+ "stat_type.minecraft.dropped": "丢弃个数", -+ "stat_type.minecraft.killed": "你杀死了%s只%s", -+ "stat_type.minecraft.killed.none": "你从来没有杀死过%s", -+ "stat_type.minecraft.killed_by": "%s杀死了你%s次", -+ "stat_type.minecraft.killed_by.none": "你从来没有被%s杀死过", -+ "stat_type.minecraft.mined": "开采次数", -+ "stat_type.minecraft.picked_up": "拾起个数", -+ "stat_type.minecraft.used": "使用次数", -+ "stats.tooltip.type.statistic": "统计", -+ "structure_block.button.detect_size": "探测", -+ "structure_block.button.load": "加载", -+ "structure_block.button.save": "保存", -+ "structure_block.custom_data": "自定义数据标签名", -+ "structure_block.detect_size": "探测结构大小和位置:", -+ "structure_block.hover.corner": "角落:%s", -+ "structure_block.hover.data": "数据:%s", -+ "structure_block.hover.load": "加载:%s", -+ "structure_block.hover.save": "保存:%s", -+ "structure_block.include_entities": "包括实体:", -+ "structure_block.integrity": "结构完整性及种子", -+ "structure_block.integrity.integrity": "结构完整性", -+ "structure_block.integrity.seed": "结构种子", -+ "structure_block.invalid_structure_name": "无效的结构名“%s”", -+ "structure_block.load_not_found": "不存在名为“%s”的结构 ", -+ "structure_block.load_prepare": "结构“%s”的加载位置已就绪", -+ "structure_block.load_success": "成功从“%s”中加载结构", -+ "structure_block.mode.corner": "角落模式", -+ "structure_block.mode.data": "数据模式", -+ "structure_block.mode.load": "加载模式", -+ "structure_block.mode.save": "储存模式", -+ "structure_block.mode_info.corner": "角落模式 — 位置和大小标记", -+ "structure_block.mode_info.data": "数据模式 — 游戏逻辑标记", -+ "structure_block.mode_info.load": "加载模式 — 从文件中加载", -+ "structure_block.mode_info.save": "保存模式 — 写入文件", -+ "structure_block.position": "相对位置", -+ "structure_block.position.x": "相对X坐标", -+ "structure_block.position.y": "相对Y坐标", -+ "structure_block.position.z": "相对Z坐标", -+ "structure_block.save_failure": "无法保存结构“%s”", -+ "structure_block.save_success": "成功将结构保存为“%s”", -+ "structure_block.show_air": "显示隐形方块:", -+ "structure_block.show_boundingbox": "显示边框:", -+ "structure_block.size": "结构大小", -+ "structure_block.size.x": "结构X轴大小", -+ "structure_block.size.y": "结构Y轴大小", -+ "structure_block.size.z": "结构Z轴大小", -+ "structure_block.size_failure": "无法检测结构大小。请放置与结构名称对应的角落结构方块", -+ "structure_block.size_success": "“%s”的大小已成功检测", -+ "structure_block.structure_name": "结构名称", -+ "subtitles.ambient.cave": "怪异的噪声", -+ "subtitles.block.amethyst_block.chime": "紫水晶:叮铃", -+ "subtitles.block.amethyst_block.resonate": "紫水晶:共振", -+ "subtitles.block.anvil.destroy": "铁砧:被毁", -+ "subtitles.block.anvil.land": "铁砧:着地", -+ "subtitles.block.anvil.use": "铁砧:使用", -+ "subtitles.block.barrel.close": "木桶:关闭", -+ "subtitles.block.barrel.open": "木桶:打开", -+ "subtitles.block.beacon.activate": "信标:激活", -+ "subtitles.block.beacon.ambient": "信标:嗡嗡作响", -+ "subtitles.block.beacon.deactivate": "信标:失效", -+ "subtitles.block.beacon.power_select": "信标:选择效果", -+ "subtitles.block.beehive.drip": "蜂蜜:滴落", -+ "subtitles.block.beehive.enter": "蜜蜂:入巢", -+ "subtitles.block.beehive.exit": "蜜蜂:离巢", -+ "subtitles.block.beehive.shear": "剪刀:刮削", -+ "subtitles.block.beehive.work": "蜜蜂:工作", -+ "subtitles.block.bell.resonate": "钟:回响", -+ "subtitles.block.bell.use": "钟:响起", -+ "subtitles.block.big_dripleaf.tilt_down": "垂滴叶:折下", -+ "subtitles.block.big_dripleaf.tilt_up": "垂滴叶:升起", -+ "subtitles.block.blastfurnace.fire_crackle": "高炉:噼啪作响", -+ "subtitles.block.brewing_stand.brew": "酿造台:冒泡", -+ "subtitles.block.bubble_column.bubble_pop": "气泡:破裂", -+ "subtitles.block.bubble_column.upwards_ambient": "气泡:上浮", -+ "subtitles.block.bubble_column.upwards_inside": "气泡:飞升", -+ "subtitles.block.bubble_column.whirlpool_ambient": "气泡:旋转", -+ "subtitles.block.bubble_column.whirlpool_inside": "气泡:骤降", -+ "subtitles.block.button.click": "按钮:咔哒", -+ "subtitles.block.cake.add_candle": "蛋糕:吧唧", -+ "subtitles.block.campfire.crackle": "营火:噼啪作响", -+ "subtitles.block.candle.crackle": "蜡烛:噼啪作响", -+ "subtitles.block.chest.close": "箱子:关闭", -+ "subtitles.block.chest.locked": "箱子:锁上", -+ "subtitles.block.chest.open": "箱子:开启", -+ "subtitles.block.chorus_flower.death": "紫颂花:凋零", -+ "subtitles.block.chorus_flower.grow": "紫颂花:生长", -+ "subtitles.block.comparator.click": "比较器:模式变更", -+ "subtitles.block.composter.empty": "堆肥桶:清空", -+ "subtitles.block.composter.fill": "堆肥桶:填充", -+ "subtitles.block.composter.ready": "堆肥桶:堆肥", -+ "subtitles.block.conduit.activate": "潮涌核心:激活", -+ "subtitles.block.conduit.ambient": "潮涌核心:涌动", -+ "subtitles.block.conduit.attack.target": "潮涌核心:攻击", -+ "subtitles.block.conduit.deactivate": "潮涌核心:失效", -+ "subtitles.block.decorated_pot.shatter": "陶罐:碎裂", -+ "subtitles.block.dispenser.dispense": "发射器:发射物品", -+ "subtitles.block.dispenser.fail": "发射器:发射失败", -+ "subtitles.block.door.toggle": "门:嘎吱作响", -+ "subtitles.block.enchantment_table.use": "附魔台:使用", -+ "subtitles.block.end_portal.spawn": "末地传送门:开启", -+ "subtitles.block.end_portal_frame.fill": "末影之眼:嵌入", -+ "subtitles.block.fence_gate.toggle": "栅栏门:嘎吱作响", -+ "subtitles.block.fire.ambient": "火:噼啪作响", -+ "subtitles.block.fire.extinguish": "火:熄灭", -+ "subtitles.block.frogspawn.hatch": "蝌蚪:孵化", -+ "subtitles.block.furnace.fire_crackle": "熔炉:噼啪作响", -+ "subtitles.block.generic.break": "方块:被破坏", -+ "subtitles.block.generic.footsteps": "脚步声", -+ "subtitles.block.generic.hit": "方块:损坏中", -+ "subtitles.block.generic.place": "方块:被放置", -+ "subtitles.block.grindstone.use": "砂轮:使用", -+ "subtitles.block.growing_plant.crop": "植物:被修剪", -+ "subtitles.block.honey_block.slide": "从蜂蜜块滑下", -+ "subtitles.block.iron_trapdoor.close": "活板门:关闭", -+ "subtitles.block.iron_trapdoor.open": "活板门:打开", -+ "subtitles.block.lava.ambient": "熔岩:迸裂", -+ "subtitles.block.lava.extinguish": "熔岩:嘶嘶声", -+ "subtitles.block.lever.click": "拉杆:拉动", -+ "subtitles.block.note_block.note": "音符盒:播放", -+ "subtitles.block.piston.move": "活塞:移动", -+ "subtitles.block.pointed_dripstone.drip_lava": "熔岩:滴落", -+ "subtitles.block.pointed_dripstone.drip_lava_into_cauldron": "熔岩:滴入炼药锅", -+ "subtitles.block.pointed_dripstone.drip_water": "水:滴落", -+ "subtitles.block.pointed_dripstone.drip_water_into_cauldron": "水:滴入炼药锅", -+ "subtitles.block.pointed_dripstone.land": "钟乳石:塌落", -+ "subtitles.block.portal.ambient": "传送门:呼啸", -+ "subtitles.block.portal.travel": "传送门:噪声消散", -+ "subtitles.block.portal.trigger": "传送门:噪声渐响", -+ "subtitles.block.pressure_plate.click": "压力板:咔哒", -+ "subtitles.block.pumpkin.carve": "剪刀:雕刻", -+ "subtitles.block.redstone_torch.burnout": "红石火把:熄灭", -+ "subtitles.block.respawn_anchor.ambient": "传送门:呼啸", -+ "subtitles.block.respawn_anchor.charge": "重生锚:充能", -+ "subtitles.block.respawn_anchor.deplete": "重生锚:耗能", -+ "subtitles.block.respawn_anchor.set_spawn": "重生锚:设置出生点", -+ "subtitles.block.sculk.charge": "幽匿块:冒泡", -+ "subtitles.block.sculk.spread": "幽匿块:蔓延", -+ "subtitles.block.sculk_catalyst.bloom": "幽匿催发体:催发", -+ "subtitles.block.sculk_sensor.clicking": "幽匿感测体:惊动", -+ "subtitles.block.sculk_sensor.clicking_stop": "幽匿感测体:停息", -+ "subtitles.block.sculk_shrieker.shriek": "幽匿尖啸体:尖啸", -+ "subtitles.block.shulker_box.close": "潜影盒:关闭", -+ "subtitles.block.shulker_box.open": "潜影盒:开启", -+ "subtitles.block.sign.waxed_interact_fail": "告示牌:晃动", -+ "subtitles.block.smithing_table.use": "锻造台:使用", -+ "subtitles.block.smoker.smoke": "烟熏炉:烟熏", -+ "subtitles.block.sniffer_egg.crack": "嗅探兽蛋:裂开", -+ "subtitles.block.sniffer_egg.hatch": "嗅探兽蛋:孵化", -+ "subtitles.block.sniffer_egg.plop": "嗅探兽:下蛋", -+ "subtitles.block.sweet_berry_bush.pick_berries": "浆果:弹出", -+ "subtitles.block.trapdoor.toggle": "活板门:嘎吱作响", -+ "subtitles.block.tripwire.attach": "绊线:连接", -+ "subtitles.block.tripwire.click": "绊线:咔哒", -+ "subtitles.block.tripwire.detach": "绊线:断开", -+ "subtitles.block.water.ambient": "水:流动", -+ "subtitles.chiseled_bookshelf.insert": "书:被放入", -+ "subtitles.chiseled_bookshelf.insert_enchanted": "附魔书:被放入", -+ "subtitles.chiseled_bookshelf.take": "书:被取出", -+ "subtitles.chiseled_bookshelf.take_enchanted": "附魔书:被取出", -+ "subtitles.enchant.thorns.hit": "荆棘:反刺", -+ "subtitles.entity.allay.ambient_with_item": "悦灵:搜寻", -+ "subtitles.entity.allay.ambient_without_item": "悦灵:渴求", -+ "subtitles.entity.allay.death": "悦灵:死亡", -+ "subtitles.entity.allay.hurt": "悦灵:受伤", -+ "subtitles.entity.allay.item_given": "悦灵:嬉笑", -+ "subtitles.entity.allay.item_taken": "悦灵:愉悦", -+ "subtitles.entity.allay.item_thrown": "悦灵:投掷", -+ "subtitles.entity.armor_stand.fall": "某物:着地", -+ "subtitles.entity.arrow.hit": "箭:击中", -+ "subtitles.entity.arrow.hit_player": "箭:击中玩家", -+ "subtitles.entity.arrow.shoot": "箭:被射出", -+ "subtitles.entity.axolotl.attack": "美西螈:攻击", -+ "subtitles.entity.axolotl.death": "美西螈:死亡", -+ "subtitles.entity.axolotl.hurt": "美西螈:受伤", -+ "subtitles.entity.axolotl.idle_air": "美西螈:啾啾", -+ "subtitles.entity.axolotl.idle_water": "美西螈:啾啾", -+ "subtitles.entity.axolotl.splash": "美西螈:溅起水花", -+ "subtitles.entity.axolotl.swim": "美西螈:游泳", -+ "subtitles.entity.bat.ambient": "蝙蝠:尖声叫", -+ "subtitles.entity.bat.death": "蝙蝠:死亡", -+ "subtitles.entity.bat.hurt": "蝙蝠:受伤", -+ "subtitles.entity.bat.takeoff": "蝙蝠:起飞", -+ "subtitles.entity.bee.ambient": "蜜蜂:嗡嗡", -+ "subtitles.entity.bee.death": "蜜蜂:死亡", -+ "subtitles.entity.bee.hurt": "蜜蜂:受伤", -+ "subtitles.entity.bee.loop": "蜜蜂:嗡嗡", -+ "subtitles.entity.bee.loop_aggressive": "蜜蜂:愤怒地嗡嗡叫", -+ "subtitles.entity.bee.pollinate": "蜜蜂:高兴地嗡嗡叫", -+ "subtitles.entity.bee.sting": "蜜蜂:刺蛰", -+ "subtitles.entity.blaze.ambient": "烈焰人:呼吸", -+ "subtitles.entity.blaze.burn": "烈焰人:噼啪作响", -+ "subtitles.entity.blaze.death": "烈焰人:死亡", -+ "subtitles.entity.blaze.hurt": "烈焰人:受伤", -+ "subtitles.entity.blaze.shoot": "烈焰人:射击", -+ "subtitles.entity.boat.paddle_land": "划船", -+ "subtitles.entity.boat.paddle_water": "划船", -+ "subtitles.entity.camel.ambient": "骆驼:呼噜", -+ "subtitles.entity.camel.dash": "骆驼:冲刺", -+ "subtitles.entity.camel.dash_ready": "骆驼:恢复体力", -+ "subtitles.entity.camel.death": "骆驼:死亡", -+ "subtitles.entity.camel.eat": "骆驼:进食", -+ "subtitles.entity.camel.hurt": "骆驼:受伤", -+ "subtitles.entity.camel.saddle": "鞍:装备", -+ "subtitles.entity.camel.sit": "骆驼:坐下", -+ "subtitles.entity.camel.stand": "骆驼:站起", -+ "subtitles.entity.camel.step": "骆驼:脚步声", -+ "subtitles.entity.camel.step_sand": "骆驼:踏沙", -+ "subtitles.entity.cat.ambient": "猫:喵~", -+ "subtitles.entity.cat.beg_for_food": "猫:求食", -+ "subtitles.entity.cat.death": "猫:死亡", -+ "subtitles.entity.cat.eat": "猫:进食", -+ "subtitles.entity.cat.hiss": "猫:嘶嘶声", -+ "subtitles.entity.cat.hurt": "猫:受伤", -+ "subtitles.entity.cat.purr": "猫:呼噜声", -+ "subtitles.entity.chicken.ambient": "鸡:咯咯叫", -+ "subtitles.entity.chicken.death": "鸡:死亡", -+ "subtitles.entity.chicken.egg": "鸡:下蛋", -+ "subtitles.entity.chicken.hurt": "鸡:受伤", -+ "subtitles.entity.cod.death": "鳕鱼:死亡", -+ "subtitles.entity.cod.flop": "鳕鱼:扑腾", -+ "subtitles.entity.cod.hurt": "鳕鱼:受伤", -+ "subtitles.entity.cow.ambient": "牛:哞~", -+ "subtitles.entity.cow.death": "牛:死亡", -+ "subtitles.entity.cow.hurt": "牛:受伤", -+ "subtitles.entity.cow.milk": "牛:被挤奶", -+ "subtitles.entity.creeper.death": "苦力怕:死亡", -+ "subtitles.entity.creeper.hurt": "苦力怕:受伤", -+ "subtitles.entity.creeper.primed": "苦力怕:嘶~", -+ "subtitles.entity.dolphin.ambient": "海豚:啾啾", -+ "subtitles.entity.dolphin.ambient_water": "海豚:吹口哨", -+ "subtitles.entity.dolphin.attack": "海豚:攻击", -+ "subtitles.entity.dolphin.death": "海豚:死亡", -+ "subtitles.entity.dolphin.eat": "海豚:进食", -+ "subtitles.entity.dolphin.hurt": "海豚:受伤", -+ "subtitles.entity.dolphin.jump": "海豚:跃起", -+ "subtitles.entity.dolphin.play": "海豚:嬉戏", -+ "subtitles.entity.dolphin.splash": "海豚:溅起水花", -+ "subtitles.entity.dolphin.swim": "海豚:游泳", -+ "subtitles.entity.donkey.ambient": "驴:嘶叫", -+ "subtitles.entity.donkey.angry": "驴:嘶鸣", -+ "subtitles.entity.donkey.chest": "驴:装备箱子", -+ "subtitles.entity.donkey.death": "驴:死亡", -+ "subtitles.entity.donkey.eat": "驴:进食", -+ "subtitles.entity.donkey.hurt": "驴:受伤", -+ "subtitles.entity.drowned.ambient": "溺尸:呻吟", -+ "subtitles.entity.drowned.ambient_water": "溺尸:呻吟", -+ "subtitles.entity.drowned.death": "溺尸:死亡", -+ "subtitles.entity.drowned.hurt": "溺尸:受伤", -+ "subtitles.entity.drowned.shoot": "溺尸:投掷三叉戟", -+ "subtitles.entity.drowned.step": "溺尸:脚步声", -+ "subtitles.entity.drowned.swim": "溺尸:游泳", -+ "subtitles.entity.egg.throw": "鸡蛋:飞出", -+ "subtitles.entity.elder_guardian.ambient": "远古守卫者:低鸣", -+ "subtitles.entity.elder_guardian.ambient_land": "远古守卫者:弹跳", -+ "subtitles.entity.elder_guardian.curse": "远古守卫者:诅咒", -+ "subtitles.entity.elder_guardian.death": "远古守卫者:死亡", -+ "subtitles.entity.elder_guardian.flop": "远古守卫者:扑腾", -+ "subtitles.entity.elder_guardian.hurt": "远古守卫者:受伤", -+ "subtitles.entity.ender_dragon.ambient": "末影龙:咆哮", -+ "subtitles.entity.ender_dragon.death": "末影龙:死亡", -+ "subtitles.entity.ender_dragon.flap": "末影龙:拍打翅膀", -+ "subtitles.entity.ender_dragon.growl": "末影龙:吼叫", -+ "subtitles.entity.ender_dragon.hurt": "末影龙:受伤", -+ "subtitles.entity.ender_dragon.shoot": "末影龙:射击", -+ "subtitles.entity.ender_eye.death": "末影之眼:掉落", -+ "subtitles.entity.ender_eye.launch": "末影之眼:射出", -+ "subtitles.entity.ender_pearl.throw": "末影珍珠:飞出", -+ "subtitles.entity.enderman.ambient": "末影人:低鸣", -+ "subtitles.entity.enderman.death": "末影人:死亡", -+ "subtitles.entity.enderman.hurt": "末影人:受伤", -+ "subtitles.entity.enderman.scream": "末影人:尖叫", -+ "subtitles.entity.enderman.stare": "末影人:喊叫", -+ "subtitles.entity.enderman.teleport": "末影人:传送", -+ "subtitles.entity.endermite.ambient": "末影螨:窜动", -+ "subtitles.entity.endermite.death": "末影螨:死亡", -+ "subtitles.entity.endermite.hurt": "末影螨:受伤", -+ "subtitles.entity.evoker.ambient": "唤魔者:咕哝", -+ "subtitles.entity.evoker.cast_spell": "唤魔者:施法", -+ "subtitles.entity.evoker.celebrate": "唤魔者:欢呼", -+ "subtitles.entity.evoker.death": "唤魔者:死亡", -+ "subtitles.entity.evoker.hurt": "唤魔者:受伤", -+ "subtitles.entity.evoker.prepare_attack": "唤魔者:准备攻击", -+ "subtitles.entity.evoker.prepare_summon": "唤魔者:准备召唤", -+ "subtitles.entity.evoker.prepare_wololo": "唤魔者:准备施咒", -+ "subtitles.entity.evoker_fangs.attack": "尖牙:咬合", -+ "subtitles.entity.experience_orb.pickup": "获得经验", -+ "subtitles.entity.firework_rocket.blast": "烟花:爆裂", -+ "subtitles.entity.firework_rocket.launch": "烟花:发射", -+ "subtitles.entity.firework_rocket.twinkle": "烟火:闪烁", -+ "subtitles.entity.fishing_bobber.retrieve": "浮漂:收回", -+ "subtitles.entity.fishing_bobber.splash": "浮漂:溅起水花", -+ "subtitles.entity.fishing_bobber.throw": "浮漂:甩出", -+ "subtitles.entity.fox.aggro": "狐狸:愤怒", -+ "subtitles.entity.fox.ambient": "狐狸:吱吱叫", -+ "subtitles.entity.fox.bite": "狐狸:撕咬", -+ "subtitles.entity.fox.death": "狐狸:死亡", -+ "subtitles.entity.fox.eat": "狐狸:进食", -+ "subtitles.entity.fox.hurt": "狐狸:受伤", -+ "subtitles.entity.fox.screech": "狐狸:尖声叫", -+ "subtitles.entity.fox.sleep": "狐狸:打鼾", -+ "subtitles.entity.fox.sniff": "狐狸:嗅探", -+ "subtitles.entity.fox.spit": "狐狸:吐出", -+ "subtitles.entity.fox.teleport": "狐狸:传送", -+ "subtitles.entity.frog.ambient": "青蛙:咕呱", -+ "subtitles.entity.frog.death": "青蛙:死亡", -+ "subtitles.entity.frog.eat": "青蛙:进食", -+ "subtitles.entity.frog.hurt": "青蛙:受伤", -+ "subtitles.entity.frog.lay_spawn": "青蛙:产卵", -+ "subtitles.entity.frog.long_jump": "青蛙:跳跃", -+ "subtitles.entity.generic.big_fall": "某物:着地", -+ "subtitles.entity.generic.burn": "燃烧", -+ "subtitles.entity.generic.death": "死亡", -+ "subtitles.entity.generic.drink": "啜饮", -+ "subtitles.entity.generic.eat": "进食", -+ "subtitles.entity.generic.explode": "爆炸", -+ "subtitles.entity.generic.extinguish_fire": "火:熄灭", -+ "subtitles.entity.generic.hurt": "某物:受伤", -+ "subtitles.entity.generic.small_fall": "某物:摔倒", -+ "subtitles.entity.generic.splash": "溅起水花", -+ "subtitles.entity.generic.swim": "游泳", -+ "subtitles.entity.ghast.ambient": "恶魂:哭泣", -+ "subtitles.entity.ghast.death": "恶魂:死亡", -+ "subtitles.entity.ghast.hurt": "恶魂:受伤", -+ "subtitles.entity.ghast.shoot": "恶魂:射击", -+ "subtitles.entity.glow_item_frame.add_item": "荧光物品展示框:填充", -+ "subtitles.entity.glow_item_frame.break": "荧光物品展示框:被破坏", -+ "subtitles.entity.glow_item_frame.place": "荧光物品展示框:被放置", -+ "subtitles.entity.glow_item_frame.remove_item": "荧光物品展示框:清空", -+ "subtitles.entity.glow_item_frame.rotate_item": "荧光物品展示框:转动", -+ "subtitles.entity.glow_squid.ambient": "发光鱿鱼:游泳", -+ "subtitles.entity.glow_squid.death": "发光鱿鱼:死亡", -+ "subtitles.entity.glow_squid.hurt": "发光鱿鱼:受伤", -+ "subtitles.entity.glow_squid.squirt": "发光鱿鱼:喷墨", -+ "subtitles.entity.goat.ambient": "山羊:咩~", -+ "subtitles.entity.goat.death": "山羊:死亡", -+ "subtitles.entity.goat.eat": "山羊:进食", -+ "subtitles.entity.goat.horn_break": "山羊:断角", -+ "subtitles.entity.goat.hurt": "山羊:受伤", -+ "subtitles.entity.goat.long_jump": "山羊:跳跃", -+ "subtitles.entity.goat.milk": "山羊:被挤奶", -+ "subtitles.entity.goat.prepare_ram": "山羊:跺脚", -+ "subtitles.entity.goat.ram_impact": "山羊:冲撞", -+ "subtitles.entity.goat.screaming.ambient": "山羊:喊叫", -+ "subtitles.entity.goat.step": "山羊:脚步声", -+ "subtitles.entity.guardian.ambient": "守卫者:低鸣", -+ "subtitles.entity.guardian.ambient_land": "守卫者:弹跳", -+ "subtitles.entity.guardian.attack": "守卫者:射击", -+ "subtitles.entity.guardian.death": "守卫者:死亡", -+ "subtitles.entity.guardian.flop": "守卫者:扑腾", -+ "subtitles.entity.guardian.hurt": "守卫者:受伤", -+ "subtitles.entity.hoglin.ambient": "疣猪兽:咆哮", -+ "subtitles.entity.hoglin.angry": "疣猪兽:怒吼", -+ "subtitles.entity.hoglin.attack": "疣猪兽:攻击", -+ "subtitles.entity.hoglin.converted_to_zombified": "疣猪兽:转化为僵尸疣猪兽", -+ "subtitles.entity.hoglin.death": "疣猪兽:死亡", -+ "subtitles.entity.hoglin.hurt": "疣猪兽:受伤", -+ "subtitles.entity.hoglin.retreat": "疣猪兽:退缩", -+ "subtitles.entity.hoglin.step": "疣猪兽:脚步声", -+ "subtitles.entity.horse.ambient": "马:嘶鸣", -+ "subtitles.entity.horse.angry": "马:嘶鸣", -+ "subtitles.entity.horse.armor": "马铠:装备", -+ "subtitles.entity.horse.breathe": "马:呼吸", -+ "subtitles.entity.horse.death": "马:死亡", -+ "subtitles.entity.horse.eat": "马:进食", -+ "subtitles.entity.horse.gallop": "马:奔腾", -+ "subtitles.entity.horse.hurt": "马:受伤", -+ "subtitles.entity.horse.jump": "马:跳跃", -+ "subtitles.entity.horse.saddle": "鞍:装备", -+ "subtitles.entity.husk.ambient": "尸壳:低吼", -+ "subtitles.entity.husk.converted_to_zombie": "尸壳:转化为僵尸", -+ "subtitles.entity.husk.death": "尸壳:死亡", -+ "subtitles.entity.husk.hurt": "尸壳:受伤", -+ "subtitles.entity.illusioner.ambient": "幻术师:咕哝", -+ "subtitles.entity.illusioner.cast_spell": "幻术师:施法", -+ "subtitles.entity.illusioner.death": "幻术师:死亡", -+ "subtitles.entity.illusioner.hurt": "幻术师:受伤", -+ "subtitles.entity.illusioner.mirror_move": "幻术师:分影", -+ "subtitles.entity.illusioner.prepare_blindness": "幻术师:准备失明法术", -+ "subtitles.entity.illusioner.prepare_mirror": "幻术师:准备分影法术", -+ "subtitles.entity.iron_golem.attack": "铁傀儡:攻击", -+ "subtitles.entity.iron_golem.damage": "铁傀儡:受损", -+ "subtitles.entity.iron_golem.death": "铁傀儡:死亡", -+ "subtitles.entity.iron_golem.hurt": "铁傀儡:受伤", -+ "subtitles.entity.iron_golem.repair": "铁傀儡:修复", -+ "subtitles.entity.item.break": "物品:损坏", -+ "subtitles.entity.item.pickup": "物品:被拾起", -+ "subtitles.entity.item_frame.add_item": "物品展示框:填充", -+ "subtitles.entity.item_frame.break": "物品展示框:被破坏", -+ "subtitles.entity.item_frame.place": "物品展示框:被放置", -+ "subtitles.entity.item_frame.remove_item": "物品展示框:清空", -+ "subtitles.entity.item_frame.rotate_item": "物品展示框:转动", -+ "subtitles.entity.leash_knot.break": "拴绳结:被破坏", -+ "subtitles.entity.leash_knot.place": "拴绳结:被系上", -+ "subtitles.entity.lightning_bolt.impact": "电闪", -+ "subtitles.entity.lightning_bolt.thunder": "雷鸣", -+ "subtitles.entity.llama.ambient": "羊驼:吼叫", -+ "subtitles.entity.llama.angry": "羊驼:怒吼", -+ "subtitles.entity.llama.chest": "羊驼:装备箱子", -+ "subtitles.entity.llama.death": "羊驼:死亡", -+ "subtitles.entity.llama.eat": "羊驼:进食", -+ "subtitles.entity.llama.hurt": "羊驼:受伤", -+ "subtitles.entity.llama.spit": "羊驼:喷射唾沫", -+ "subtitles.entity.llama.step": "羊驼:脚步声", -+ "subtitles.entity.llama.swag": "羊驼:被装饰", -+ "subtitles.entity.magma_cube.death": "岩浆怪:死亡", -+ "subtitles.entity.magma_cube.hurt": "岩浆怪:受伤", -+ "subtitles.entity.magma_cube.squish": "岩浆怪:挤压", -+ "subtitles.entity.minecart.riding": "矿车:行进", -+ "subtitles.entity.mooshroom.convert": "哞菇:转化", -+ "subtitles.entity.mooshroom.eat": "哞菇:进食", -+ "subtitles.entity.mooshroom.milk": "哞菇:被挤奶", -+ "subtitles.entity.mooshroom.suspicious_milk": "哞菇:被挤出谜之炖菜", -+ "subtitles.entity.mule.ambient": "骡:鸣叫", -+ "subtitles.entity.mule.angry": "骡:嘶鸣", -+ "subtitles.entity.mule.chest": "骡:装备箱子", -+ "subtitles.entity.mule.death": "骡:死亡", -+ "subtitles.entity.mule.eat": "骡:进食", -+ "subtitles.entity.mule.hurt": "骡:受伤", -+ "subtitles.entity.painting.break": "画:被破坏", -+ "subtitles.entity.painting.place": "画:被放置", -+ "subtitles.entity.panda.aggressive_ambient": "熊猫:发怒", -+ "subtitles.entity.panda.ambient": "熊猫:喘息", -+ "subtitles.entity.panda.bite": "熊猫:撕咬", -+ "subtitles.entity.panda.cant_breed": "熊猫:哀鸣", -+ "subtitles.entity.panda.death": "熊猫:死亡", -+ "subtitles.entity.panda.eat": "熊猫:进食", -+ "subtitles.entity.panda.hurt": "熊猫:受伤", -+ "subtitles.entity.panda.pre_sneeze": "熊猫:鼻痒", -+ "subtitles.entity.panda.sneeze": "熊猫:打喷嚏", -+ "subtitles.entity.panda.step": "熊猫:脚步声", -+ "subtitles.entity.panda.worried_ambient": "熊猫:呜咽", -+ "subtitles.entity.parrot.ambient": "鹦鹉:说话", -+ "subtitles.entity.parrot.death": "鹦鹉:死亡", -+ "subtitles.entity.parrot.eats": "鹦鹉:进食", -+ "subtitles.entity.parrot.fly": "鹦鹉:扑翼", -+ "subtitles.entity.parrot.hurts": "鹦鹉:受伤", -+ "subtitles.entity.parrot.imitate.blaze": "鹦鹉:呼吸", -+ "subtitles.entity.parrot.imitate.creeper": "鹦鹉:嘶~", -+ "subtitles.entity.parrot.imitate.drowned": "鹦鹉:呻吟", -+ "subtitles.entity.parrot.imitate.elder_guardian": "鹦鹉:低鸣", -+ "subtitles.entity.parrot.imitate.ender_dragon": "鹦鹉:咆哮", -+ "subtitles.entity.parrot.imitate.endermite": "鹦鹉:窜动", -+ "subtitles.entity.parrot.imitate.evoker": "鹦鹉:咕哝", -+ "subtitles.entity.parrot.imitate.ghast": "鹦鹉:哭泣", -+ "subtitles.entity.parrot.imitate.guardian": "鹦鹉:低鸣", -+ "subtitles.entity.parrot.imitate.hoglin": "鹦鹉:咆哮", -+ "subtitles.entity.parrot.imitate.husk": "鹦鹉:低吼", -+ "subtitles.entity.parrot.imitate.illusioner": "鹦鹉:咕哝", -+ "subtitles.entity.parrot.imitate.magma_cube": "鹦鹉:挤压", -+ "subtitles.entity.parrot.imitate.phantom": "鹦鹉:尖声叫", -+ "subtitles.entity.parrot.imitate.piglin": "鹦鹉:哼叫", -+ "subtitles.entity.parrot.imitate.piglin_brute": "鹦鹉:哼叫", -+ "subtitles.entity.parrot.imitate.pillager": "鹦鹉:咕哝", -+ "subtitles.entity.parrot.imitate.ravager": "鹦鹉:呼噜", -+ "subtitles.entity.parrot.imitate.shulker": "鹦鹉:窥视", -+ "subtitles.entity.parrot.imitate.silverfish": "鹦鹉:嘶嘶", -+ "subtitles.entity.parrot.imitate.skeleton": "鹦鹉:咯咯声", -+ "subtitles.entity.parrot.imitate.slime": "鹦鹉:挤压", -+ "subtitles.entity.parrot.imitate.spider": "鹦鹉:嘶嘶", -+ "subtitles.entity.parrot.imitate.stray": "鹦鹉:咯咯声", -+ "subtitles.entity.parrot.imitate.vex": "鹦鹉:恼人", -+ "subtitles.entity.parrot.imitate.vindicator": "鹦鹉:低语", -+ "subtitles.entity.parrot.imitate.warden": "鹦鹉:呻吟", -+ "subtitles.entity.parrot.imitate.witch": "鹦鹉:暗笑", -+ "subtitles.entity.parrot.imitate.wither": "鹦鹉:愤怒", -+ "subtitles.entity.parrot.imitate.wither_skeleton": "鹦鹉:咯咯声", -+ "subtitles.entity.parrot.imitate.zoglin": "鹦鹉:咆哮", -+ "subtitles.entity.parrot.imitate.zombie": "鹦鹉:低吼", -+ "subtitles.entity.parrot.imitate.zombie_villager": "鹦鹉:低吼", -+ "subtitles.entity.phantom.ambient": "幻翼:尖声叫", -+ "subtitles.entity.phantom.bite": "幻翼:撕咬", -+ "subtitles.entity.phantom.death": "幻翼:死亡", -+ "subtitles.entity.phantom.flap": "幻翼:振翅", -+ "subtitles.entity.phantom.hurt": "幻翼:受伤", -+ "subtitles.entity.phantom.swoop": "幻翼:俯冲", -+ "subtitles.entity.pig.ambient": "猪:哼叫", -+ "subtitles.entity.pig.death": "猪:死亡", -+ "subtitles.entity.pig.hurt": "猪:受伤", -+ "subtitles.entity.pig.saddle": "鞍:装备", -+ "subtitles.entity.piglin.admiring_item": "猪灵:端详物品", -+ "subtitles.entity.piglin.ambient": "猪灵:哼叫", -+ "subtitles.entity.piglin.angry": "猪灵:愤怒地哼叫", -+ "subtitles.entity.piglin.celebrate": "猪灵:庆祝", -+ "subtitles.entity.piglin.converted_to_zombified": "猪灵:转化为僵尸猪灵", -+ "subtitles.entity.piglin.death": "猪灵:死亡", -+ "subtitles.entity.piglin.hurt": "猪灵:受伤", -+ "subtitles.entity.piglin.jealous": "猪灵:羡慕地哼叫", -+ "subtitles.entity.piglin.retreat": "猪灵:退缩", -+ "subtitles.entity.piglin.step": "猪灵:脚步声", -+ "subtitles.entity.piglin_brute.ambient": "猪灵蛮兵:哼叫", -+ "subtitles.entity.piglin_brute.angry": "猪灵蛮兵:愤怒地哼叫", -+ "subtitles.entity.piglin_brute.converted_to_zombified": "猪灵蛮兵:转化为僵尸猪灵", -+ "subtitles.entity.piglin_brute.death": "猪灵蛮兵:死亡", -+ "subtitles.entity.piglin_brute.hurt": "猪灵蛮兵:受伤", -+ "subtitles.entity.piglin_brute.step": "猪灵蛮兵:脚步声", -+ "subtitles.entity.pillager.ambient": "掠夺者:咕哝", -+ "subtitles.entity.pillager.celebrate": "掠夺者:欢呼", -+ "subtitles.entity.pillager.death": "掠夺者:死亡", -+ "subtitles.entity.pillager.hurt": "掠夺者:受伤", -+ "subtitles.entity.player.attack.crit": "暴击", -+ "subtitles.entity.player.attack.knockback": "击退攻击", -+ "subtitles.entity.player.attack.strong": "重击", -+ "subtitles.entity.player.attack.sweep": "横扫攻击", -+ "subtitles.entity.player.attack.weak": "轻击", -+ "subtitles.entity.player.burp": "打嗝", -+ "subtitles.entity.player.death": "玩家:死亡", -+ "subtitles.entity.player.freeze_hurt": "玩家:冻伤", -+ "subtitles.entity.player.hurt": "玩家:受伤", -+ "subtitles.entity.player.hurt_drown": "玩家:溺水", -+ "subtitles.entity.player.hurt_on_fire": "玩家:燃烧", -+ "subtitles.entity.player.levelup": "玩家:升级", -+ "subtitles.entity.polar_bear.ambient": "北极熊:低吼", -+ "subtitles.entity.polar_bear.ambient_baby": "北极熊:哼哼", -+ "subtitles.entity.polar_bear.death": "北极熊:死亡", -+ "subtitles.entity.polar_bear.hurt": "北极熊:受伤", -+ "subtitles.entity.polar_bear.warning": "北极熊:咆哮", -+ "subtitles.entity.potion.splash": "玻璃瓶:碎裂", -+ "subtitles.entity.potion.throw": "玻璃瓶:扔出", -+ "subtitles.entity.puffer_fish.blow_out": "河豚:收缩", -+ "subtitles.entity.puffer_fish.blow_up": "河豚:膨胀", -+ "subtitles.entity.puffer_fish.death": "河豚:死亡", -+ "subtitles.entity.puffer_fish.flop": "河豚:扑腾", -+ "subtitles.entity.puffer_fish.hurt": "河豚:受伤", -+ "subtitles.entity.puffer_fish.sting": "河豚:刺蛰", -+ "subtitles.entity.rabbit.ambient": "兔子:吱吱叫", -+ "subtitles.entity.rabbit.attack": "兔子:攻击", -+ "subtitles.entity.rabbit.death": "兔子:死亡", -+ "subtitles.entity.rabbit.hurt": "兔子:受伤", -+ "subtitles.entity.rabbit.jump": "兔子:跳动", -+ "subtitles.entity.ravager.ambient": "劫掠兽:呼噜", -+ "subtitles.entity.ravager.attack": "劫掠兽:撕咬", -+ "subtitles.entity.ravager.celebrate": "劫掠兽:欢呼", -+ "subtitles.entity.ravager.death": "劫掠兽:死亡", -+ "subtitles.entity.ravager.hurt": "劫掠兽:受伤", -+ "subtitles.entity.ravager.roar": "劫掠兽:咆哮", -+ "subtitles.entity.ravager.step": "劫掠兽:脚步声", -+ "subtitles.entity.ravager.stunned": "劫掠兽:眩晕", -+ "subtitles.entity.salmon.death": "鲑鱼:死亡", -+ "subtitles.entity.salmon.flop": "鲑鱼:扑腾", -+ "subtitles.entity.salmon.hurt": "鲑鱼:受伤", -+ "subtitles.entity.sheep.ambient": "绵羊:咩~", -+ "subtitles.entity.sheep.death": "绵羊:死亡", -+ "subtitles.entity.sheep.hurt": "绵羊:受伤", -+ "subtitles.entity.shulker.ambient": "潜影贝:窥视", -+ "subtitles.entity.shulker.close": "潜影贝:闭合", -+ "subtitles.entity.shulker.death": "潜影贝:死亡", -+ "subtitles.entity.shulker.hurt": "潜影贝:受伤", -+ "subtitles.entity.shulker.open": "潜影贝:打开", -+ "subtitles.entity.shulker.shoot": "潜影贝:射击", -+ "subtitles.entity.shulker.teleport": "潜影贝:传送", -+ "subtitles.entity.shulker_bullet.hit": "潜影弹:爆炸", -+ "subtitles.entity.shulker_bullet.hurt": "潜影弹:碎裂", -+ "subtitles.entity.silverfish.ambient": "蠹虫:嘶嘶", -+ "subtitles.entity.silverfish.death": "蠹虫:死亡", -+ "subtitles.entity.silverfish.hurt": "蠹虫:受伤", -+ "subtitles.entity.skeleton.ambient": "骷髅:咯咯声", -+ "subtitles.entity.skeleton.converted_to_stray": "骷髅:转化为流浪者", -+ "subtitles.entity.skeleton.death": "骷髅:死亡", -+ "subtitles.entity.skeleton.hurt": "骷髅:受伤", -+ "subtitles.entity.skeleton.shoot": "骷髅:射击", -+ "subtitles.entity.skeleton_horse.ambient": "骷髅马:嘶叫", -+ "subtitles.entity.skeleton_horse.death": "骷髅马:死亡", -+ "subtitles.entity.skeleton_horse.hurt": "骷髅马:受伤", -+ "subtitles.entity.skeleton_horse.swim": "骷髅马:游泳", -+ "subtitles.entity.slime.attack": "史莱姆:攻击", -+ "subtitles.entity.slime.death": "史莱姆:死亡", -+ "subtitles.entity.slime.hurt": "史莱姆:受伤", -+ "subtitles.entity.slime.squish": "史莱姆:挤压", -+ "subtitles.entity.sniffer.death": "嗅探兽:死亡", -+ "subtitles.entity.sniffer.digging": "嗅探兽:挖掘", -+ "subtitles.entity.sniffer.digging_stop": "嗅探兽:站起", -+ "subtitles.entity.sniffer.drop_seed": "嗅探兽:丢下种子", -+ "subtitles.entity.sniffer.eat": "嗅探兽:进食", -+ "subtitles.entity.sniffer.egg_crack": "嗅探兽蛋:裂开", -+ "subtitles.entity.sniffer.egg_hatch": "嗅探兽蛋:孵化", -+ "subtitles.entity.sniffer.happy": "嗅探兽:愉悦", -+ "subtitles.entity.sniffer.hurt": "嗅探兽:受伤", -+ "subtitles.entity.sniffer.idle": "嗅探兽:呼噜", -+ "subtitles.entity.sniffer.scenting": "嗅探兽:嗅闻", -+ "subtitles.entity.sniffer.searching": "嗅探兽:搜寻", -+ "subtitles.entity.sniffer.sniffing": "嗅探兽:嗅探", -+ "subtitles.entity.sniffer.step": "嗅探兽:脚步声", -+ "subtitles.entity.snow_golem.death": "雪傀儡:死亡", -+ "subtitles.entity.snow_golem.hurt": "雪傀儡:受伤", -+ "subtitles.entity.snowball.throw": "雪球:飞出", -+ "subtitles.entity.spider.ambient": "蜘蛛:嘶嘶", -+ "subtitles.entity.spider.death": "蜘蛛:死亡", -+ "subtitles.entity.spider.hurt": "蜘蛛:受伤", -+ "subtitles.entity.squid.ambient": "鱿鱼:游泳", -+ "subtitles.entity.squid.death": "鱿鱼:死亡", -+ "subtitles.entity.squid.hurt": "鱿鱼:受伤", -+ "subtitles.entity.squid.squirt": "鱿鱼:喷墨", -+ "subtitles.entity.stray.ambient": "流浪者:咯咯声", -+ "subtitles.entity.stray.death": "流浪者:死亡", -+ "subtitles.entity.stray.hurt": "流浪者:受伤", -+ "subtitles.entity.strider.death": "炽足兽:死亡", -+ "subtitles.entity.strider.eat": "炽足兽:进食", -+ "subtitles.entity.strider.happy": "炽足兽:颤鸣", -+ "subtitles.entity.strider.hurt": "炽足兽:受伤", -+ "subtitles.entity.strider.idle": "炽足兽:啾啾", -+ "subtitles.entity.strider.retreat": "炽足兽:退缩", -+ "subtitles.entity.tadpole.death": "蝌蚪:死亡", -+ "subtitles.entity.tadpole.flop": "蝌蚪:扑腾", -+ "subtitles.entity.tadpole.grow_up": "蝌蚪:成长", -+ "subtitles.entity.tadpole.hurt": "蝌蚪:受伤", -+ "subtitles.entity.tnt.primed": "TNT:嘶嘶作响", -+ "subtitles.entity.tropical_fish.death": "热带鱼:死亡", -+ "subtitles.entity.tropical_fish.flop": "热带鱼:扑腾", -+ "subtitles.entity.tropical_fish.hurt": "热带鱼:受伤", -+ "subtitles.entity.turtle.ambient_land": "海龟:啾啾", -+ "subtitles.entity.turtle.death": "海龟:死亡", -+ "subtitles.entity.turtle.death_baby": "幼年海龟:死亡", -+ "subtitles.entity.turtle.egg_break": "海龟蛋:破裂", -+ "subtitles.entity.turtle.egg_crack": "海龟蛋:裂开", -+ "subtitles.entity.turtle.egg_hatch": "海龟蛋:孵化", -+ "subtitles.entity.turtle.hurt": "海龟:受伤", -+ "subtitles.entity.turtle.hurt_baby": "幼年海龟:受伤", -+ "subtitles.entity.turtle.lay_egg": "海龟:产卵", -+ "subtitles.entity.turtle.shamble": "海龟:爬行", -+ "subtitles.entity.turtle.shamble_baby": "幼年海龟:爬行", -+ "subtitles.entity.turtle.swim": "海龟:游泳", -+ "subtitles.entity.vex.ambient": "恼鬼:恼人", -+ "subtitles.entity.vex.charge": "恼鬼:尖叫", -+ "subtitles.entity.vex.death": "恼鬼:死亡", -+ "subtitles.entity.vex.hurt": "恼鬼:受伤", -+ "subtitles.entity.villager.ambient": "村民:喃喃自语", -+ "subtitles.entity.villager.celebrate": "村民:欢呼", -+ "subtitles.entity.villager.death": "村民:死亡", -+ "subtitles.entity.villager.hurt": "村民:受伤", -+ "subtitles.entity.villager.no": "村民:拒绝", -+ "subtitles.entity.villager.trade": "村民:交易", -+ "subtitles.entity.villager.work_armorer": "盔甲匠:工作", -+ "subtitles.entity.villager.work_butcher": "屠夫:工作", -+ "subtitles.entity.villager.work_cartographer": "制图师:工作", -+ "subtitles.entity.villager.work_cleric": "牧师:工作", -+ "subtitles.entity.villager.work_farmer": "农民:工作", -+ "subtitles.entity.villager.work_fisherman": "渔夫:工作", -+ "subtitles.entity.villager.work_fletcher": "制箭师:工作", -+ "subtitles.entity.villager.work_leatherworker": "皮匠:工作", -+ "subtitles.entity.villager.work_librarian": "图书管理员:工作", -+ "subtitles.entity.villager.work_mason": "石匠:工作", -+ "subtitles.entity.villager.work_shepherd": "牧羊人:工作", -+ "subtitles.entity.villager.work_toolsmith": "工具匠:工作", -+ "subtitles.entity.villager.work_weaponsmith": "武器匠:工作", -+ "subtitles.entity.villager.yes": "村民:同意", -+ "subtitles.entity.vindicator.ambient": "卫道士:低语", -+ "subtitles.entity.vindicator.celebrate": "卫道士:欢呼", -+ "subtitles.entity.vindicator.death": "卫道士:死亡", -+ "subtitles.entity.vindicator.hurt": "卫道士:受伤", -+ "subtitles.entity.wandering_trader.ambient": "流浪商人:喃喃自语", -+ "subtitles.entity.wandering_trader.death": "流浪商人:死亡", -+ "subtitles.entity.wandering_trader.disappeared": "流浪商人:隐身", -+ "subtitles.entity.wandering_trader.drink_milk": "流浪商人:喝奶", -+ "subtitles.entity.wandering_trader.drink_potion": "流浪商人:饮用药水", -+ "subtitles.entity.wandering_trader.hurt": "流浪商人:受伤", -+ "subtitles.entity.wandering_trader.no": "流浪商人:拒绝", -+ "subtitles.entity.wandering_trader.reappeared": "流浪商人:现身", -+ "subtitles.entity.wandering_trader.trade": "流浪商人:交易", -+ "subtitles.entity.wandering_trader.yes": "流浪商人:同意", -+ "subtitles.entity.warden.agitated": "监守者:愤怒地呻吟", -+ "subtitles.entity.warden.ambient": "监守者:呻吟", -+ "subtitles.entity.warden.angry": "监守者:狂怒", -+ "subtitles.entity.warden.attack_impact": "监守者:击中", -+ "subtitles.entity.warden.death": "监守者:死亡", -+ "subtitles.entity.warden.dig": "监守者:掘地", -+ "subtitles.entity.warden.emerge": "监守者:现身", -+ "subtitles.entity.warden.heartbeat": "监守者:心跳声", -+ "subtitles.entity.warden.hurt": "监守者:受伤", -+ "subtitles.entity.warden.listening": "监守者:察觉到某物", -+ "subtitles.entity.warden.listening_angry": "监守者:愤怒地察觉到某物", -+ "subtitles.entity.warden.nearby_close": "监守者:接近", -+ "subtitles.entity.warden.nearby_closer": "监守者:逼近", -+ "subtitles.entity.warden.nearby_closest": "监守者:迫近", -+ "subtitles.entity.warden.roar": "监守者:咆哮", -+ "subtitles.entity.warden.sniff": "监守者:嗅闻", -+ "subtitles.entity.warden.sonic_boom": "监守者:发射音波", -+ "subtitles.entity.warden.sonic_charge": "监守者:蓄力", -+ "subtitles.entity.warden.step": "监守者:脚步声", -+ "subtitles.entity.warden.tendril_clicks": "监守者:卷须颤动", -+ "subtitles.entity.witch.ambient": "女巫:暗笑", -+ "subtitles.entity.witch.celebrate": "女巫:欢呼", -+ "subtitles.entity.witch.death": "女巫:死亡", -+ "subtitles.entity.witch.drink": "女巫:饮用药水", -+ "subtitles.entity.witch.hurt": "女巫:受伤", -+ "subtitles.entity.witch.throw": "女巫:投掷", -+ "subtitles.entity.wither.ambient": "凋灵:愤怒", -+ "subtitles.entity.wither.death": "凋灵:死亡", -+ "subtitles.entity.wither.hurt": "凋灵:受伤", -+ "subtitles.entity.wither.shoot": "凋灵:攻击", -+ "subtitles.entity.wither.spawn": "凋灵:解放", -+ "subtitles.entity.wither_skeleton.ambient": "凋灵骷髅:咯咯声", -+ "subtitles.entity.wither_skeleton.death": "凋灵骷髅:死亡", -+ "subtitles.entity.wither_skeleton.hurt": "凋灵骷髅:受伤", -+ "subtitles.entity.wolf.ambient": "狼:喘息", -+ "subtitles.entity.wolf.death": "狼:死亡", -+ "subtitles.entity.wolf.growl": "狼:嚎叫", -+ "subtitles.entity.wolf.hurt": "狼:受伤", -+ "subtitles.entity.wolf.shake": "狼:摇动", -+ "subtitles.entity.zoglin.ambient": "僵尸疣猪兽:咆哮", -+ "subtitles.entity.zoglin.angry": "僵尸疣猪兽:怒吼", -+ "subtitles.entity.zoglin.attack": "僵尸疣猪兽:攻击", -+ "subtitles.entity.zoglin.death": "僵尸疣猪兽:死亡", -+ "subtitles.entity.zoglin.hurt": "僵尸疣猪兽:受伤", -+ "subtitles.entity.zoglin.step": "僵尸疣猪兽:脚步声", -+ "subtitles.entity.zombie.ambient": "僵尸:低吼", -+ "subtitles.entity.zombie.attack_wooden_door": "门:晃动", -+ "subtitles.entity.zombie.break_wooden_door": "门:毁坏", -+ "subtitles.entity.zombie.converted_to_drowned": "僵尸:转化为溺尸", -+ "subtitles.entity.zombie.death": "僵尸:死亡", -+ "subtitles.entity.zombie.destroy_egg": "海龟蛋:被踩踏", -+ "subtitles.entity.zombie.hurt": "僵尸:受伤", -+ "subtitles.entity.zombie.infect": "僵尸:感染", -+ "subtitles.entity.zombie_horse.ambient": "僵尸马:嘶叫", -+ "subtitles.entity.zombie_horse.death": "僵尸马:死亡", -+ "subtitles.entity.zombie_horse.hurt": "僵尸马:受伤", -+ "subtitles.entity.zombie_villager.ambient": "僵尸村民:低吼", -+ "subtitles.entity.zombie_villager.converted": "僵尸村民:哀嚎", -+ "subtitles.entity.zombie_villager.cure": "僵尸村民:撕心裂肺", -+ "subtitles.entity.zombie_villager.death": "僵尸村民:死亡", -+ "subtitles.entity.zombie_villager.hurt": "僵尸村民:受伤", -+ "subtitles.entity.zombified_piglin.ambient": "僵尸猪灵:呼噜", -+ "subtitles.entity.zombified_piglin.angry": "僵尸猪灵:愤怒地呼噜", -+ "subtitles.entity.zombified_piglin.death": "僵尸猪灵:死亡", -+ "subtitles.entity.zombified_piglin.hurt": "僵尸猪灵:受伤", -+ "subtitles.event.raid.horn": "不祥号角:鸣响", -+ "subtitles.item.armor.equip": "盔甲:装备", -+ "subtitles.item.armor.equip_chain": "锁链盔甲:碰擦", -+ "subtitles.item.armor.equip_diamond": "钻石盔甲:碰擦", -+ "subtitles.item.armor.equip_elytra": "鞘翅:沙沙作响", -+ "subtitles.item.armor.equip_gold": "金盔甲:叮当", -+ "subtitles.item.armor.equip_iron": "铁盔甲:铿锵", -+ "subtitles.item.armor.equip_leather": "皮革盔甲:摩擦", -+ "subtitles.item.armor.equip_netherite": "下界合金盔甲:铿锵", -+ "subtitles.item.armor.equip_turtle": "海龟壳:咕咚", -+ "subtitles.item.axe.scrape": "斧:刮削", -+ "subtitles.item.axe.strip": "斧:削皮", -+ "subtitles.item.axe.wax_off": "脱蜡", -+ "subtitles.item.bone_meal.use": "骨粉:沙沙作响", -+ "subtitles.item.book.page_turn": "书页:沙沙作响", -+ "subtitles.item.book.put": "书:放置", -+ "subtitles.item.bottle.empty": "玻璃瓶:倒空", -+ "subtitles.item.bottle.fill": "玻璃瓶:装满", -+ "subtitles.item.brush.brushing.generic": "刷子:刷扫中", -+ "subtitles.item.brush.brushing.gravel": "刷子:刷扫沙砾中", -+ "subtitles.item.brush.brushing.gravel.complete": "刷子:刷扫沙砾完毕", -+ "subtitles.item.brush.brushing.sand": "刷子:刷扫沙子中", -+ "subtitles.item.brush.brushing.sand.complete": "刷子:刷扫沙子完毕", -+ "subtitles.item.bucket.empty": "铁桶:倒空", -+ "subtitles.item.bucket.fill": "铁桶:装满", -+ "subtitles.item.bucket.fill_axolotl": "美西螈:被装起", -+ "subtitles.item.bucket.fill_fish": "鱼:被捕获", -+ "subtitles.item.bucket.fill_tadpole": "蝌蚪:被捕获", -+ "subtitles.item.bundle.drop_contents": "收纳袋:倒空", -+ "subtitles.item.bundle.insert": "物品:装入袋中", -+ "subtitles.item.bundle.remove_one": "物品:从袋中取出", -+ "subtitles.item.chorus_fruit.teleport": "玩家:传送", -+ "subtitles.item.crop.plant": "作物:种植", -+ "subtitles.item.crossbow.charge": "弩:蓄力", -+ "subtitles.item.crossbow.hit": "箭:击中", -+ "subtitles.item.crossbow.load": "弩:装填", -+ "subtitles.item.crossbow.shoot": "弩:发射", -+ "subtitles.item.dye.use": "染料:染色", -+ "subtitles.item.firecharge.use": "火焰弹:呼啸", -+ "subtitles.item.flintandsteel.use": "打火石:生火", -+ "subtitles.item.glow_ink_sac.use": "荧光墨囊:涂抹", -+ "subtitles.item.goat_horn.play": "山羊角:吹奏", -+ "subtitles.item.hoe.till": "锄:犁地", -+ "subtitles.item.honey_bottle.drink": "吞咽", -+ "subtitles.item.honeycomb.wax_on": "涂蜡", -+ "subtitles.item.ink_sac.use": "墨囊:涂抹", -+ "subtitles.item.lodestone_compass.lock": "磁石指针:绑定磁石", -+ "subtitles.item.nether_wart.plant": "作物:种植", -+ "subtitles.item.shears.shear": "剪刀:剪断", -+ "subtitles.item.shield.block": "盾牌:格挡", -+ "subtitles.item.shovel.flatten": "锹:压地", -+ "subtitles.item.spyglass.stop_using": "望远镜:缩小", -+ "subtitles.item.spyglass.use": "望远镜:放大", -+ "subtitles.item.totem.use": "图腾:发动", -+ "subtitles.item.trident.hit": "三叉戟:突刺", -+ "subtitles.item.trident.hit_ground": "三叉戟:振动", -+ "subtitles.item.trident.return": "三叉戟:收回", -+ "subtitles.item.trident.riptide": "三叉戟:突进", -+ "subtitles.item.trident.throw": "三叉戟:铿锵", -+ "subtitles.item.trident.thunder": "三叉戟:电闪雷鸣", -+ "subtitles.particle.soul_escape": "灵魂:逸散", -+ "subtitles.ui.cartography_table.take_result": "地图:绘制", -+ "subtitles.ui.loom.take_result": "织布机:使用", -+ "subtitles.ui.stonecutter.take_result": "切石机:使用", -+ "subtitles.weather.rain": "雨:落下", -+ "symlink_warning.message": "从含有符号链接的文件夹中加载世界可能导致风险,尤其是在你不清楚链接目标内容时。请访问%s了解更多信息。", -+ "symlink_warning.title": "世界文件夹包含符号链接", -+ "team.collision.always": "总是碰撞", -+ "team.collision.never": "禁用碰撞", -+ "team.collision.pushOtherTeams": "队伍间碰撞", -+ "team.collision.pushOwnTeam": "队伍内碰撞", -+ "team.notFound": "未知的队伍“%s”", -+ "team.visibility.always": "始终显示", -+ "team.visibility.hideForOtherTeams": "对别队隐藏", -+ "team.visibility.hideForOwnTeam": "对本队隐藏", -+ "team.visibility.never": "始终隐藏", -+ "telemetry.event.advancement_made.description": "了解达成进度前后发生的事情有助于我们更好了解及改进游戏流程。", -+ "telemetry.event.advancement_made.title": "达成进度", -+ "telemetry.event.game_load_times.description": "此事件会测量游戏启动各个阶段的时长以帮助我们找到游戏启动的性能优化点。", -+ "telemetry.event.game_load_times.title": "游戏加载时间", -+ "telemetry.event.optional": "%s(可选)", -+ "telemetry.event.performance_metrics.description": "了解Minecraft的整体性能概况,可以帮助我们针对各种机型和操作系统调整与优化游戏。 \n游戏版本信息也包含在其中,以帮助我们比较Minecraft新版本的性能概况。", -+ "telemetry.event.performance_metrics.title": "性能指标", -+ "telemetry.event.required": "%s(必要)", -+ "telemetry.event.world_load_times.description": "对我们而言,了解加入世界所需的时长,以及这一时长随着时间推移会如何变化是很重要的。比如,当我们加入新功能或进行较大的技术更改时,我们需要知道这对加载时间有何影响。", -+ "telemetry.event.world_load_times.title": "世界加载时间", -+ "telemetry.event.world_loaded.description": "了解玩家玩Minecraft时的具体细节(例如游戏模式、客户端或服务端是否被修改,以及游戏版本)能够帮助我们专注于游戏更新以改进玩家最关心的部分。\n世界加载事件将同世界卸载事件一起用于计算游戏会话的持续时长。", -+ "telemetry.event.world_loaded.title": "世界加载", -+ "telemetry.event.world_unloaded.description": "此事件与世界加载事件用于计算世界会话的持续时间。\n持续时间(以秒和刻为单位)在世界会话结束时(退回到标题屏幕,或从服务器断开连接)测量。", -+ "telemetry.event.world_unloaded.title": "世界卸载", -+ "telemetry.property.advancement_game_time.title": "游戏时间(刻)", -+ "telemetry.property.advancement_id.title": "进度ID", -+ "telemetry.property.client_id.title": "客户端ID", -+ "telemetry.property.client_modded.title": "客户端是否被修改", -+ "telemetry.property.dedicated_memory_kb.title": "专用内存(kB)", -+ "telemetry.property.event_timestamp_utc.title": "事件时间戳(UTC)", -+ "telemetry.property.frame_rate_samples.title": "帧率样本(FPS)", -+ "telemetry.property.game_mode.title": "游戏模式", -+ "telemetry.property.game_version.title": "游戏版本", -+ "telemetry.property.launcher_name.title": "启动器名称", -+ "telemetry.property.load_time_bootstrap_ms.title": "启动引导时间(毫秒)", -+ "telemetry.property.load_time_loading_overlay_ms.title": "加载屏幕显示时间(毫秒)", -+ "telemetry.property.load_time_pre_window_ms.title": "窗口开启前时间(毫秒)", -+ "telemetry.property.load_time_total_time_ms.title": "总计加载时间(毫秒)", -+ "telemetry.property.minecraft_session_id.title": "Minecraft会话ID", -+ "telemetry.property.new_world.title": "是否为新的世界", -+ "telemetry.property.number_of_samples.title": "样本数", -+ "telemetry.property.operating_system.title": "操作系统", -+ "telemetry.property.opt_in.title": "可选择加入", -+ "telemetry.property.platform.title": "平台", -+ "telemetry.property.realms_map_content.title": "Realm地图内容(小游戏名称)", -+ "telemetry.property.render_distance.title": "渲染距离", -+ "telemetry.property.render_time_samples.title": "渲染用时样本", -+ "telemetry.property.seconds_since_load.title": "自加载后的时间(秒)", -+ "telemetry.property.server_modded.title": "服务端是否被修改", -+ "telemetry.property.server_type.title": "服务器类型", -+ "telemetry.property.ticks_since_load.title": "自加载后的时间(刻)", -+ "telemetry.property.used_memory_samples.title": "已使用的内存", -+ "telemetry.property.user_id.title": "用户ID", -+ "telemetry.property.world_load_time_ms.title": "世界加载时间(毫秒)", -+ "telemetry.property.world_session_id.title": "世界会话ID", -+ "telemetry_info.button.give_feedback": "提供反馈", -+ "telemetry_info.button.show_data": "打开我的数据", -+ "telemetry_info.property_title": "包含的数据", -+ "telemetry_info.screen.description": "收集这些数据可以让我们了解与玩家有关的实际情况,以便帮助我们改进Minecraft。\n你也可以发送其他反馈来帮助我们持续改进Minecraft。", -+ "telemetry_info.screen.title": "遥测数据收集", -+ "title.32bit.deprecation": "检测到32位系统:未来将需要64位系统,使用32位系统可能将无法进行游戏!", -+ "title.32bit.deprecation.realms": "Minecraft不久后需要64位系统才能运行,届时你将无法使用该设备进行游戏或使用Realms服务。你需要自行取消所有Realms订阅。", -+ "title.32bit.deprecation.realms.check": "不再显示此屏幕", -+ "title.32bit.deprecation.realms.header": "检测到32位系统", -+ "title.multiplayer.disabled": "多人游戏已被禁用,请检查你的Microsoft账户设置。", -+ "title.multiplayer.disabled.banned.permanent": "你的账户已被永久封禁,无法进行多人游戏", -+ "title.multiplayer.disabled.banned.temporary": "你的账户已被暂时封禁,无法进行多人游戏", -+ "title.multiplayer.lan": "多人游戏(局域网)", -+ "title.multiplayer.other": "多人游戏(第三方服务器)", -+ "title.multiplayer.realms": "多人游戏(Realms)", -+ "title.singleplayer": "单人游戏", -+ "translation.test.args": "%s %s", -+ "translation.test.complex": "前缀,%s%2$s 然后是 %s 和 %1$s 最后是 %s 还有 %1$s!", -+ "translation.test.escape": "%%s %%%s %%%%s %%%%%s", -+ "translation.test.invalid": "% 你好", -+ "translation.test.invalid2": "%s 你好", -+ "translation.test.none": "你好,世界!", -+ "translation.test.world": "世界", -+ "trim_material.minecraft.amethyst": "紫水晶质", -+ "trim_material.minecraft.copper": "铜质", -+ "trim_material.minecraft.diamond": "钻石质", -+ "trim_material.minecraft.emerald": "绿宝石质", -+ "trim_material.minecraft.gold": "金质", -+ "trim_material.minecraft.iron": "铁质", -+ "trim_material.minecraft.lapis": "青金石质", -+ "trim_material.minecraft.netherite": "下界合金质", -+ "trim_material.minecraft.quartz": "石英质", -+ "trim_material.minecraft.redstone": "红石质", -+ "trim_pattern.minecraft.coast": "海岸盔甲纹饰", -+ "trim_pattern.minecraft.dune": "沙丘盔甲纹饰", -+ "trim_pattern.minecraft.eye": "眼眸盔甲纹饰", -+ "trim_pattern.minecraft.host": "雇主盔甲纹饰", -+ "trim_pattern.minecraft.raiser": "牧民盔甲纹饰", -+ "trim_pattern.minecraft.rib": "肋骨盔甲纹饰", -+ "trim_pattern.minecraft.sentry": "哨兵盔甲纹饰", -+ "trim_pattern.minecraft.shaper": "塑造盔甲纹饰", -+ "trim_pattern.minecraft.silence": "幽静盔甲纹饰", -+ "trim_pattern.minecraft.snout": "猪鼻盔甲纹饰", -+ "trim_pattern.minecraft.spire": "尖塔盔甲纹饰", -+ "trim_pattern.minecraft.tide": "潮汐盔甲纹饰", -+ "trim_pattern.minecraft.vex": "恼鬼盔甲纹饰", -+ "trim_pattern.minecraft.ward": "监守盔甲纹饰", -+ "trim_pattern.minecraft.wayfinder": "向导盔甲纹饰", -+ "trim_pattern.minecraft.wild": "荒野盔甲纹饰", -+ "tutorial.bundleInsert.description": "右击收纳物品", -+ "tutorial.bundleInsert.title": "使用收纳袋", -+ "tutorial.craft_planks.description": "配方书能提供帮助", -+ "tutorial.craft_planks.title": "合成木板", -+ "tutorial.find_tree.description": "敲击木头来收集它", -+ "tutorial.find_tree.title": "找到一棵树", -+ "tutorial.look.description": "使用你的鼠标来转动", -+ "tutorial.look.title": "观察四周", -+ "tutorial.move.description": "用%s来跳跃", -+ "tutorial.move.title": "用%s、%s、%s和%s来移动", -+ "tutorial.open_inventory.description": "按下%s", -+ "tutorial.open_inventory.title": "打开你的物品栏", -+ "tutorial.punch_tree.description": "按住%s", -+ "tutorial.punch_tree.title": "摧毁树木", -+ "tutorial.socialInteractions.description": "按%s打开", -+ "tutorial.socialInteractions.title": "社交", -+ "upgrade.minecraft.netherite_upgrade": "下界合金升级" -+} diff --git a/patches/server/0090-RNG-Fishing.patch b/patches/server/0090-RNG-Fishing.patch deleted file mode 100644 index f4e698fc..00000000 --- a/patches/server/0090-RNG-Fishing.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 4 Sep 2023 22:09:10 +0800 -Subject: [PATCH] RNG Fishing - - -diff --git a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -index d5cb0d8ad7f7ed18ce38b39f245f5ec2c67042d5..74adc702a4503631df9fa575eee3ef388e8c3937 100644 ---- a/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -+++ b/src/main/java/net/minecraft/world/entity/projectile/FishingHook.java -@@ -528,7 +528,7 @@ public class FishingHook extends Projectile { - } else if (this.nibble > 0) { - LootParams lootparams = (new LootParams.Builder((ServerLevel) this.level())).withParameter(LootContextParams.ORIGIN, this.position()).withParameter(LootContextParams.TOOL, usedItem).withParameter(LootContextParams.THIS_ENTITY, this).withLuck((float) this.luck + entityhuman.getLuck()).create(LootContextParamSets.FISHING); - LootTable loottable = this.level().getServer().reloadableRegistries().getLootTable(BuiltInLootTables.FISHING); -- List list = loottable.getRandomItems(lootparams); -+ List list = org.leavesmc.leaves.LeavesConfig.modify.oldMC.rngFishing ? loottable.getRandomItems(lootparams, this.random) : loottable.getRandomItems(lootparams); // Leaves - world random - - CriteriaTriggers.FISHING_ROD_HOOKED.trigger((ServerPlayer) entityhuman, usedItem, this, list); - Iterator iterator = list.iterator(); diff --git a/patches/server/0091-Wool-Hopper-Counter.patch b/patches/server/0091-Wool-Hopper-Counter.patch deleted file mode 100644 index 590416b1..00000000 --- a/patches/server/0091-Wool-Hopper-Counter.patch +++ /dev/null @@ -1,576 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 4 Sep 2023 00:16:09 +0800 -Subject: [PATCH] Wool Hopper Counter - -This patch is Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet) - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -index 7280d2216dcbe09278a2447eb994c4bd2aa98576..af3846dbb29d4738d0cce20c85d2f40c7ccf4f70 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/HopperBlockEntity.java -@@ -447,7 +447,14 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - private static final java.util.function.BiPredicate IS_EMPTY_TEST = (itemstack, i) -> itemstack.isEmpty(); - // Paper end - Perf: Optimize Hoppers - -+ // Leaves start - hopper counter - private static boolean ejectItems(Level world, BlockPos pos, HopperBlockEntity blockEntity) { -+ if (org.leavesmc.leaves.util.HopperCounter.isEnabled()) { -+ if (woolHopperCounter(world, pos, world.getBlockState(pos), (Container) blockEntity, blockEntity)) { -+ return true; -+ } -+ } -+ // Leaves end - hopper counter - Container iinventory = HopperBlockEntity.getAttachedContainer(world, pos, blockEntity); - - if (iinventory == null) { -@@ -511,6 +518,23 @@ public class HopperBlockEntity extends RandomizableContainerBlockEntity implemen - } - } - -+ // Leaves start - hopper counter -+ private static boolean woolHopperCounter(Level level, BlockPos blockPos, BlockState state, Container container, HopperBlockEntity hopper) { -+ net.minecraft.world.item.DyeColor woolColor = org.leavesmc.leaves.util.WoolUtils.getWoolColorAtPosition(level, blockPos.relative(state.getValue(HopperBlock.FACING))); -+ if (woolColor != null) { -+ for (int i = 0; i < container.getContainerSize(); ++i) { -+ if (!container.getItem(i).isEmpty()) { -+ ItemStack itemstack = container.getItem(i); -+ org.leavesmc.leaves.util.HopperCounter.getCounter(woolColor).add(level.getServer(), itemstack); -+ container.setItem(i, ItemStack.EMPTY); -+ } -+ } -+ return true; -+ } -+ return false; -+ } -+ // Leaves end - hopper counter -+ - private static int[] getSlots(Container inventory, Direction side) { - if (inventory instanceof WorldlyContainer iworldinventory) { - return iworldinventory.getSlotsForFace(side); -diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -index 92a8c3e4fc400988b3d984e7632a8149a2ce152e..99823283690fcc1e6c0f76d8dcbcca9dfb50470d 100644 ---- a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -@@ -34,6 +34,7 @@ public final class LeavesCommand extends Command { - commands.put(Set.of("config"), new ConfigCommand()); - commands.put(Set.of("update"), new UpdateCommand()); - commands.put(Set.of("peaceful"), new PeacefulModeSwitchCommand()); -+ commands.put(Set.of("counter"), new CounterCommand()); - - return commands.entrySet().stream() - .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) -diff --git a/src/main/java/org/leavesmc/leaves/command/subcommands/CounterCommand.java b/src/main/java/org/leavesmc/leaves/command/subcommands/CounterCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..6d0e3c9a95f1e0f32ae6b3149a21abe268b22951 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/subcommands/CounterCommand.java -@@ -0,0 +1,121 @@ -+package org.leavesmc.leaves.command.subcommands; -+ -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.JoinConfiguration; -+import net.kyori.adventure.text.format.NamedTextColor; -+import net.kyori.adventure.text.format.TextColor; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.world.item.DyeColor; -+import org.bukkit.command.CommandSender; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.command.LeavesCommandUtil; -+import org.leavesmc.leaves.command.LeavesSubcommand; -+import org.leavesmc.leaves.util.HopperCounter; -+ -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.Collections; -+import java.util.List; -+ -+public class CounterCommand implements LeavesSubcommand { -+ -+ @Override -+ public boolean execute(CommandSender sender, String subCommand, String[] args) { -+ if (!LeavesConfig.modify.hopperCounter) { -+ return false; -+ } -+ -+ if (args.length < 1) { -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Hopper Counter: ", NamedTextColor.GRAY), -+ Component.text(HopperCounter.isEnabled(), HopperCounter.isEnabled() ? NamedTextColor.AQUA : NamedTextColor.GRAY) -+ )); -+ return true; -+ } -+ -+ if (!HopperCounter.isEnabled()) { -+ if (args[0].equals("enable")) { -+ HopperCounter.setEnabled(true); -+ sender.sendMessage(Component.text("Hopper Counter now is enabled", NamedTextColor.AQUA)); -+ } else { -+ sender.sendMessage(Component.text("Hopper Counter is not enabled", NamedTextColor.RED)); -+ } -+ return true; -+ } -+ -+ DyeColor color = DyeColor.byName(args[0], null); -+ if (color != null) { -+ HopperCounter counter = HopperCounter.getCounter(color); -+ if (args.length < 2) { -+ displayCounter(sender, counter, false); -+ } else { -+ switch (args[1]) { -+ case "reset" -> { -+ counter.reset(MinecraftServer.getServer()); -+ sender.sendMessage(Component.join(JoinConfiguration.noSeparators(), -+ Component.text("Restarted "), -+ Component.text(color.getName(), TextColor.color(color.getTextColor())), -+ Component.text(" counter") -+ )); -+ } -+ case "realtime" -> displayCounter(sender, counter, true); -+ } -+ } -+ return true; -+ } -+ -+ switch (args[0]) { -+ case "reset" -> { -+ HopperCounter.resetAll(MinecraftServer.getServer(), false); -+ sender.sendMessage(Component.text("Restarted all counters")); -+ } -+ case "disable" -> { -+ HopperCounter.setEnabled(false); -+ HopperCounter.resetAll(MinecraftServer.getServer(), true); -+ sender.sendMessage(Component.text("Hopper Counter now is disabled", NamedTextColor.GRAY)); -+ } -+ } -+ -+ return true; -+ } -+ -+ private void displayCounter(CommandSender sender, @NotNull HopperCounter counter, boolean realTime) { -+ for (Component component : counter.format(MinecraftServer.getServer(), realTime)) { -+ sender.sendMessage(component); -+ } -+ } -+ -+ @Override -+ public List tabComplete(CommandSender sender, String subCommand, String[] args) { -+ if (!LeavesConfig.modify.hopperCounter) { -+ return Collections.emptyList(); -+ } -+ -+ switch (args.length) { -+ case 1 -> { -+ if (!HopperCounter.isEnabled()) { -+ return Collections.singletonList("enable"); -+ } -+ -+ List list = new ArrayList<>(Arrays.stream(DyeColor.values()).map(DyeColor::getName).toList()); -+ list.add("reset"); -+ list.add("disable"); -+ return LeavesCommandUtil.getListMatchingLast(sender, args, list); -+ } -+ -+ case 2 -> { -+ if (DyeColor.byName(args[0], null) != null) { -+ return LeavesCommandUtil.getListMatchingLast(sender, args, "reset", "realtime"); -+ } -+ } -+ } -+ -+ return Collections.emptyList(); -+ } -+ -+ @Override -+ public boolean tabCompletes() { -+ return LeavesConfig.modify.hopperCounter; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/util/HopperCounter.java b/src/main/java/org/leavesmc/leaves/util/HopperCounter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..62f20b111944a429ee0f2a51fcdfe4ad7d9a44c3 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/HopperCounter.java -@@ -0,0 +1,337 @@ -+package org.leavesmc.leaves.util; -+ -+import it.unimi.dsi.fastutil.objects.Object2LongLinkedOpenHashMap; -+import it.unimi.dsi.fastutil.objects.Object2LongMap; -+import net.kyori.adventure.text.Component; -+import net.kyori.adventure.text.TextComponent; -+import net.kyori.adventure.text.format.NamedTextColor; -+import net.kyori.adventure.text.format.Style; -+import net.kyori.adventure.text.format.TextColor; -+import net.kyori.adventure.text.format.TextDecoration; -+import net.minecraft.core.Holder; -+import net.minecraft.core.Registry; -+import net.minecraft.core.RegistryAccess; -+import net.minecraft.core.registries.BuiltInRegistries; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.resources.ResourceKey; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.util.context.ContextMap; -+import net.minecraft.world.item.BlockItem; -+import net.minecraft.world.item.DyeColor; -+import net.minecraft.world.item.DyeItem; -+import net.minecraft.world.item.Item; -+import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.item.Items; -+import net.minecraft.world.item.crafting.Ingredient; -+import net.minecraft.world.item.crafting.Recipe; -+import net.minecraft.world.item.crafting.RecipeManager; -+import net.minecraft.world.item.crafting.display.RecipeDisplay; -+import net.minecraft.world.item.crafting.display.SlotDisplayContext; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.AbstractBannerBlock; -+import net.minecraft.world.level.block.BeaconBeamBlock; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.material.MapColor; -+import org.jetbrains.annotations.NotNull; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+ -+import java.util.ArrayList; -+import java.util.Collections; -+import java.util.EnumMap; -+import java.util.List; -+import java.util.Map; -+import java.util.Optional; -+ -+import static java.util.Map.entry; -+ -+// Powered by fabric-carpet(https://github.com/gnembon/fabric-carpet) -+ -+public class HopperCounter { -+ -+ private static boolean enabled = false; -+ private static final Map COUNTERS; -+ -+ static { -+ EnumMap counterMap = new EnumMap<>(DyeColor.class); -+ for (DyeColor color : DyeColor.values()) { -+ counterMap.put(color, new HopperCounter(color)); -+ } -+ COUNTERS = Collections.unmodifiableMap(counterMap); -+ } -+ -+ public final DyeColor color; -+ private final TextComponent coloredName; -+ private final Object2LongMap counter = new Object2LongLinkedOpenHashMap<>(); -+ private long startTick; -+ private long startMillis; -+ -+ private HopperCounter(DyeColor color) { -+ this.startTick = -1; -+ this.color = color; -+ this.coloredName = Component.text(color.getName(), TextColor.color(color.getTextColor())); -+ } -+ -+ public void add(MinecraftServer server, ItemStack stack) { -+ if (startTick < 0) { -+ startTick = server.overworld().getGameTime(); -+ startMillis = System.currentTimeMillis(); -+ } -+ Item item = stack.getItem(); -+ counter.put(item, counter.getLong(item) + stack.getCount()); -+ } -+ -+ public void reset(MinecraftServer server) { -+ counter.clear(); -+ startTick = server.overworld().getGameTime(); -+ startMillis = System.currentTimeMillis(); -+ } -+ -+ public static void resetAll(MinecraftServer server, boolean fresh) { -+ for (HopperCounter counter : COUNTERS.values()) { -+ counter.reset(server); -+ if (fresh) { -+ counter.startTick = -1; -+ } -+ } -+ } -+ -+ public List format(MinecraftServer server, boolean realTime) { -+ long ticks = Math.max(realTime ? (System.currentTimeMillis() - startMillis) / 50 : server.overworld().getGameTime() - startTick, -1); -+ -+ if (startTick < 0 || ticks == -1) { -+ return Collections.singletonList(Component.text().append(coloredName, Component.text(" hasn't started counting yet")).build()); -+ } -+ -+ long total = getTotalItems(); -+ if (total <= 0) { -+ return Collections.singletonList(Component.text() -+ .append(Component.text("No items for "), coloredName) -+ .append(Component.text(" yet ("), Component.text(String.format("%.2f ", ticks / (20.0 * 60.0)), Style.style(TextDecoration.BOLD))) -+ .append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text(")")) -+ .build()); -+ } -+ -+ List items = new ArrayList<>(); -+ items.add(Component.text() -+ .append(Component.text("Items for "), coloredName, Component.text(" ")) -+ .append(Component.text("("), Component.text(String.format("%.2f ", ticks * 1.0 / (20 * 60)), Style.style(TextDecoration.BOLD))) -+ .append(Component.text("min"), Component.text(realTime ? " - real time" : ""), Component.text("), ")) -+ .append(Component.text("total: "), Component.text(total, Style.style(TextDecoration.BOLD)), Component.text(", ")) -+ .append(Component.text("("), Component.text(String.format("%.1f", total * 1.0 * (20 * 60 * 60) / ticks), Style.style(TextDecoration.BOLD))) -+ .append(Component.text("/h):")) -+ .build()); -+ -+ items.addAll(counter.object2LongEntrySet().stream().sorted((e, f) -> Long.compare(f.getLongValue(), e.getLongValue())).map(entry -> { -+ Item item = entry.getKey(); -+ Component name = Component.translatable(item.getDescriptionId()); -+ TextColor textColor = guessColor(server, item); -+ -+ if (textColor != null) { -+ name = name.style(name.style().merge(Style.style(textColor))); -+ } else { -+ name = name.style(name.style().merge(Style.style(TextDecoration.ITALIC))); -+ } -+ -+ long count = entry.getLongValue(); -+ return Component.text() -+ .append(Component.text("- ", NamedTextColor.GRAY)) -+ .append(name) -+ .append(Component.text(": ", NamedTextColor.GRAY)) -+ .append(Component.text(count, Style.style(TextDecoration.BOLD)), Component.text(", ", NamedTextColor.GRAY)) -+ .append(Component.text(String.format("%.1f", count * (20.0 * 60.0 * 60.0) / ticks), Style.style(TextDecoration.BOLD))) -+ .append(Component.text("/h")) -+ .build(); -+ }).toList()); -+ return items; -+ } -+ -+ private static final Map DEFAULTS = Map.ofEntries( -+ entry(Items.DANDELION, Blocks.YELLOW_WOOL), -+ entry(Items.POPPY, Blocks.RED_WOOL), -+ entry(Items.BLUE_ORCHID, Blocks.LIGHT_BLUE_WOOL), -+ entry(Items.ALLIUM, Blocks.MAGENTA_WOOL), -+ entry(Items.AZURE_BLUET, Blocks.SNOW_BLOCK), -+ entry(Items.RED_TULIP, Blocks.RED_WOOL), -+ entry(Items.ORANGE_TULIP, Blocks.ORANGE_WOOL), -+ entry(Items.WHITE_TULIP, Blocks.SNOW_BLOCK), -+ entry(Items.PINK_TULIP, Blocks.PINK_WOOL), -+ entry(Items.OXEYE_DAISY, Blocks.SNOW_BLOCK), -+ entry(Items.CORNFLOWER, Blocks.BLUE_WOOL), -+ entry(Items.WITHER_ROSE, Blocks.BLACK_WOOL), -+ entry(Items.LILY_OF_THE_VALLEY, Blocks.WHITE_WOOL), -+ entry(Items.BROWN_MUSHROOM, Blocks.BROWN_MUSHROOM_BLOCK), -+ entry(Items.RED_MUSHROOM, Blocks.RED_MUSHROOM_BLOCK), -+ entry(Items.STICK, Blocks.OAK_PLANKS), -+ entry(Items.GOLD_INGOT, Blocks.GOLD_BLOCK), -+ entry(Items.IRON_INGOT, Blocks.IRON_BLOCK), -+ entry(Items.DIAMOND, Blocks.DIAMOND_BLOCK), -+ entry(Items.NETHERITE_INGOT, Blocks.NETHERITE_BLOCK), -+ entry(Items.SUNFLOWER, Blocks.YELLOW_WOOL), -+ entry(Items.LILAC, Blocks.MAGENTA_WOOL), -+ entry(Items.ROSE_BUSH, Blocks.RED_WOOL), -+ entry(Items.PEONY, Blocks.PINK_WOOL), -+ entry(Items.CARROT, Blocks.ORANGE_WOOL), -+ entry(Items.APPLE, Blocks.RED_WOOL), -+ entry(Items.WHEAT, Blocks.HAY_BLOCK), -+ entry(Items.PORKCHOP, Blocks.PINK_WOOL), -+ entry(Items.RABBIT, Blocks.PINK_WOOL), -+ entry(Items.CHICKEN, Blocks.WHITE_TERRACOTTA), -+ entry(Items.BEEF, Blocks.NETHERRACK), -+ entry(Items.ENCHANTED_GOLDEN_APPLE, Blocks.GOLD_BLOCK), -+ entry(Items.COD, Blocks.WHITE_TERRACOTTA), -+ entry(Items.SALMON, Blocks.ACACIA_PLANKS), -+ entry(Items.ROTTEN_FLESH, Blocks.BROWN_WOOL), -+ entry(Items.PUFFERFISH, Blocks.YELLOW_TERRACOTTA), -+ entry(Items.TROPICAL_FISH, Blocks.ORANGE_WOOL), -+ entry(Items.POTATO, Blocks.WHITE_TERRACOTTA), -+ entry(Items.MUTTON, Blocks.RED_WOOL), -+ entry(Items.BEETROOT, Blocks.NETHERRACK), -+ entry(Items.MELON_SLICE, Blocks.MELON), -+ entry(Items.POISONOUS_POTATO, Blocks.SLIME_BLOCK), -+ entry(Items.SPIDER_EYE, Blocks.NETHERRACK), -+ entry(Items.GUNPOWDER, Blocks.GRAY_WOOL), -+ entry(Items.TURTLE_SCUTE, Blocks.LIME_WOOL), -+ entry(Items.ARMADILLO_SCUTE, Blocks.ANCIENT_DEBRIS), -+ entry(Items.FEATHER, Blocks.WHITE_WOOL), -+ entry(Items.FLINT, Blocks.BLACK_WOOL), -+ entry(Items.LEATHER, Blocks.SPRUCE_PLANKS), -+ entry(Items.GLOWSTONE_DUST, Blocks.GLOWSTONE), -+ entry(Items.PAPER, Blocks.WHITE_WOOL), -+ entry(Items.BRICK, Blocks.BRICKS), -+ entry(Items.INK_SAC, Blocks.BLACK_WOOL), -+ entry(Items.SNOWBALL, Blocks.SNOW_BLOCK), -+ entry(Items.WATER_BUCKET, Blocks.WATER), -+ entry(Items.LAVA_BUCKET, Blocks.LAVA), -+ entry(Items.MILK_BUCKET, Blocks.WHITE_WOOL), -+ entry(Items.CLAY_BALL, Blocks.CLAY), -+ entry(Items.COCOA_BEANS, Blocks.COCOA), -+ entry(Items.BONE, Blocks.BONE_BLOCK), -+ entry(Items.COD_BUCKET, Blocks.BROWN_TERRACOTTA), -+ entry(Items.PUFFERFISH_BUCKET, Blocks.YELLOW_TERRACOTTA), -+ entry(Items.SALMON_BUCKET, Blocks.PINK_TERRACOTTA), -+ entry(Items.TROPICAL_FISH_BUCKET, Blocks.ORANGE_TERRACOTTA), -+ entry(Items.SUGAR, Blocks.WHITE_WOOL), -+ entry(Items.BLAZE_POWDER, Blocks.GOLD_BLOCK), -+ entry(Items.ENDER_PEARL, Blocks.WARPED_PLANKS), -+ entry(Items.NETHER_STAR, Blocks.DIAMOND_BLOCK), -+ entry(Items.PRISMARINE_CRYSTALS, Blocks.SEA_LANTERN), -+ entry(Items.PRISMARINE_SHARD, Blocks.PRISMARINE), -+ entry(Items.RABBIT_HIDE, Blocks.OAK_PLANKS), -+ entry(Items.CHORUS_FRUIT, Blocks.PURPUR_BLOCK), -+ entry(Items.SHULKER_SHELL, Blocks.SHULKER_BOX), -+ entry(Items.NAUTILUS_SHELL, Blocks.BONE_BLOCK), -+ entry(Items.HEART_OF_THE_SEA, Blocks.CONDUIT), -+ entry(Items.HONEYCOMB, Blocks.HONEYCOMB_BLOCK), -+ entry(Items.NAME_TAG, Blocks.BONE_BLOCK), -+ entry(Items.TOTEM_OF_UNDYING, Blocks.YELLOW_TERRACOTTA), -+ entry(Items.TRIDENT, Blocks.PRISMARINE), -+ entry(Items.GHAST_TEAR, Blocks.WHITE_WOOL), -+ entry(Items.PHANTOM_MEMBRANE, Blocks.BONE_BLOCK), -+ entry(Items.EGG, Blocks.BONE_BLOCK), -+ entry(Items.COPPER_INGOT, Blocks.COPPER_BLOCK), -+ entry(Items.AMETHYST_SHARD, Blocks.AMETHYST_BLOCK) -+ ); -+ -+ @Nullable -+ public static TextColor guessColor(@NotNull MinecraftServer server, Item item) { -+ RegistryAccess registryAccess = server.registryAccess(); -+ TextColor direct = fromItem(item, registryAccess); -+ if (direct != null) { -+ return direct; -+ } -+ -+ ResourceLocation id = registryAccess.lookupOrThrow(Registries.ITEM).getKey(item); -+ if (id == null) { -+ return null; -+ } -+ -+ -+ for (Recipe recipe : getRecipesForOutput(server.getRecipeManager(), id, server.overworld())) { -+ for (Ingredient ingredient : recipe.placementInfo().ingredients()) { -+ Optional> match = ingredient.items().stream().filter(stack -> fromItem(stack.value(), registryAccess) != null).findFirst(); -+ if (match.isPresent()) { -+ return fromItem(match.get().value(), registryAccess); -+ } -+ } -+ } -+ return null; -+ } -+ -+ @NotNull -+ public static List> getRecipesForOutput(@NotNull RecipeManager recipeManager, ResourceLocation id, Level level) { -+ List> results = new ArrayList<>(); -+ ContextMap context = SlotDisplayContext.fromLevel(level); -+ recipeManager.getRecipes().forEach(recipe -> { -+ for (RecipeDisplay recipeDisplay : recipe.value().display()) { -+ recipeDisplay.result().resolveForStacks(context).forEach(stack -> { -+ if (BuiltInRegistries.ITEM.wrapAsHolder(stack.getItem()).unwrapKey().map(ResourceKey::location).orElseThrow(IllegalStateException::new).equals(id)) { -+ results.add(recipe.value()); -+ } -+ }); -+ } -+ }); -+ return results; -+ } -+ -+ @Nullable -+ public static TextColor fromItem(Item item, RegistryAccess registryAccess) { -+ if (DEFAULTS.containsKey(item)) { -+ return TextColor.color(appropriateColor(DEFAULTS.get(item).defaultMapColor().col)); -+ } -+ if (item instanceof DyeItem dye) { -+ return TextColor.color(appropriateColor(dye.getDyeColor().getMapColor().col)); -+ } -+ -+ Block block = null; -+ final Registry itemRegistry = registryAccess.lookupOrThrow(Registries.ITEM); -+ final Registry blockRegistry = registryAccess.lookupOrThrow(Registries.BLOCK); -+ ResourceLocation id = itemRegistry.getKey(item); -+ if (item instanceof BlockItem blockItem) { -+ block = blockItem.getBlock(); -+ } else if (blockRegistry.getOptional(id).isPresent()) { -+ block = blockRegistry.getValue(id); -+ } -+ -+ if (block != null) { -+ if (block instanceof AbstractBannerBlock) { -+ return TextColor.color(appropriateColor(((AbstractBannerBlock) block).getColor().getMapColor().col)); -+ } else if (block instanceof BeaconBeamBlock) { -+ return TextColor.color(appropriateColor(((BeaconBeamBlock) block).getColor().getMapColor().col)); -+ } -+ return TextColor.color(appropriateColor(block.defaultMapColor().col)); -+ } -+ return null; -+ } -+ -+ public static int appropriateColor(int color) { -+ if (color == 0) { -+ return MapColor.SNOW.col; -+ } -+ int r = (color >> 16 & 255); -+ int g = (color >> 8 & 255); -+ int b = (color & 255); -+ if (r < 70) r = 70; -+ if (g < 70) g = 70; -+ if (b < 70) b = 70; -+ return (r << 16) + (g << 8) + b; -+ } -+ -+ public long getTotalItems() { -+ return counter.isEmpty() ? 0 : counter.values().longStream().sum(); -+ } -+ -+ public static HopperCounter getCounter(DyeColor color) { -+ return COUNTERS.get(color); -+ } -+ -+ public static void setEnabled(boolean is) { -+ enabled = is; -+ } -+ -+ public static boolean isEnabled() { -+ return LeavesConfig.modify.hopperCounter && enabled; -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/util/WoolUtils.java b/src/main/java/org/leavesmc/leaves/util/WoolUtils.java -new file mode 100644 -index 0000000000000000000000000000000000000000..a4cd63179a15e5f172c43a2da963e23d7d71fc28 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/WoolUtils.java -@@ -0,0 +1,38 @@ -+package org.leavesmc.leaves.util; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.world.item.DyeColor; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.state.BlockState; -+ -+import java.util.Map; -+ -+import static java.util.Map.entry; -+ -+public class WoolUtils { -+ private static final Map WOOL_BLOCK_TO_DYE = Map.ofEntries( -+ entry(Blocks.WHITE_WOOL, DyeColor.WHITE), -+ entry(Blocks.ORANGE_WOOL, DyeColor.ORANGE), -+ entry(Blocks.MAGENTA_WOOL, DyeColor.MAGENTA), -+ entry(Blocks.LIGHT_BLUE_WOOL, DyeColor.LIGHT_BLUE), -+ entry(Blocks.YELLOW_WOOL, DyeColor.YELLOW), -+ entry(Blocks.LIME_WOOL, DyeColor.LIME), -+ entry(Blocks.PINK_WOOL, DyeColor.PINK), -+ entry(Blocks.GRAY_WOOL, DyeColor.GRAY), -+ entry(Blocks.LIGHT_GRAY_WOOL, DyeColor.LIGHT_GRAY), -+ entry(Blocks.CYAN_WOOL, DyeColor.CYAN), -+ entry(Blocks.PURPLE_WOOL, DyeColor.PURPLE), -+ entry(Blocks.BLUE_WOOL, DyeColor.BLUE), -+ entry(Blocks.BROWN_WOOL, DyeColor.BROWN), -+ entry(Blocks.GREEN_WOOL, DyeColor.GREEN), -+ entry(Blocks.RED_WOOL, DyeColor.RED), -+ entry(Blocks.BLACK_WOOL, DyeColor.BLACK) -+ ); -+ -+ public static DyeColor getWoolColorAtPosition(Level worldIn, BlockPos pos) { -+ BlockState state = worldIn.getBlockState(pos); -+ return WOOL_BLOCK_TO_DYE.get(state.getBlock()); -+ } -+} diff --git a/patches/server/0092-Leaves-Reload-Command.patch b/patches/server/0092-Leaves-Reload-Command.patch deleted file mode 100644 index b5d7ec57..00000000 --- a/patches/server/0092-Leaves-Reload-Command.patch +++ /dev/null @@ -1,46 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 5 Sep 2023 00:07:10 +0800 -Subject: [PATCH] Leaves Reload Command - - -diff --git a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -index 99823283690fcc1e6c0f76d8dcbcca9dfb50470d..bc8720e3277c011b39d6ed2d06252f2b1abdc31e 100644 ---- a/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -+++ b/src/main/java/org/leavesmc/leaves/command/LeavesCommand.java -@@ -35,6 +35,7 @@ public final class LeavesCommand extends Command { - commands.put(Set.of("update"), new UpdateCommand()); - commands.put(Set.of("peaceful"), new PeacefulModeSwitchCommand()); - commands.put(Set.of("counter"), new CounterCommand()); -+ commands.put(Set.of("reload"), new ReloadCommand()); - - return commands.entrySet().stream() - .flatMap(entry -> entry.getKey().stream().map(s -> Map.entry(s, entry.getValue()))) -diff --git a/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java b/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java -new file mode 100644 -index 0000000000000000000000000000000000000000..749718287eb5364001176927248a4094afaf8750 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/command/subcommands/ReloadCommand.java -@@ -0,0 +1,22 @@ -+package org.leavesmc.leaves.command.subcommands; -+ -+import net.minecraft.server.MinecraftServer; -+import org.bukkit.command.Command; -+import org.bukkit.command.CommandSender; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.command.LeavesSubcommand; -+ -+import java.io.File; -+ -+import static net.kyori.adventure.text.Component.text; -+import static net.kyori.adventure.text.format.NamedTextColor.GREEN; -+ -+public class ReloadCommand implements LeavesSubcommand { -+ @Override -+ public boolean execute(CommandSender sender, String subCommand, String[] args) { -+ MinecraftServer server = MinecraftServer.getServer(); -+ LeavesConfig.init((File) server.options.valueOf("leaves-settings")); -+ Command.broadcastCommandMessage(sender, text("Leaves config reload complete.", GREEN)); -+ return false; -+ } -+} diff --git a/patches/server/0095-Villager-infinite-discounts.patch b/patches/server/0095-Villager-infinite-discounts.patch deleted file mode 100644 index 01fc16e8..00000000 --- a/patches/server/0095-Villager-infinite-discounts.patch +++ /dev/null @@ -1,46 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 28 Sep 2023 17:00:22 +0800 -Subject: [PATCH] Villager infinite discounts - - -diff --git a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipType.java b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipType.java -index 90bdbd686560bc2987af6524bf32b0c51ef98887..8444f233b68f056fe6ef9f7f6c40e69aa8222ab7 100644 ---- a/src/main/java/net/minecraft/world/entity/ai/gossip/GossipType.java -+++ b/src/main/java/net/minecraft/world/entity/ai/gossip/GossipType.java -@@ -15,9 +15,9 @@ public enum GossipType implements StringRepresentable { - public static final int REPUTATION_CHANGE_PER_TRADE = 2; - public final String id; - public final int weight; -- public final int max; -+ public int max; // Leaves - not final - public final int decayPerDay; -- public final int decayPerTransfer; -+ public int decayPerTransfer; // Leaves - not final - public static final Codec CODEC = StringRepresentable.fromEnum(GossipType::values); - - private GossipType(final String key, final int multiplier, final int maxReputation, final int decay, final int shareDecrement) { -diff --git a/src/main/java/org/leavesmc/leaves/util/VillagerInfiniteDiscountHelper.java b/src/main/java/org/leavesmc/leaves/util/VillagerInfiniteDiscountHelper.java -new file mode 100644 -index 0000000000000000000000000000000000000000..83a6ef92a097b9c8c29299db1d01ad56eeb87441 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/VillagerInfiniteDiscountHelper.java -@@ -0,0 +1,18 @@ -+package org.leavesmc.leaves.util; -+ -+import net.minecraft.world.entity.ai.gossip.GossipType; -+ -+public class VillagerInfiniteDiscountHelper { -+ -+ public static void doVillagerInfiniteDiscount(boolean value) { -+ if (value) { -+ GossipType.MAJOR_POSITIVE.max = 100; -+ GossipType.MAJOR_POSITIVE.decayPerTransfer = 100; -+ GossipType.MINOR_POSITIVE.max = 200; -+ } else { -+ GossipType.MAJOR_POSITIVE.max = 20; -+ GossipType.MAJOR_POSITIVE.decayPerTransfer = 20; -+ GossipType.MINOR_POSITIVE.max = 25; -+ } -+ } -+} diff --git a/patches/server/0096-CCE-update-suppression.patch b/patches/server/0096-CCE-update-suppression.patch deleted file mode 100644 index 1c4600f1..00000000 --- a/patches/server/0096-CCE-update-suppression.patch +++ /dev/null @@ -1,45 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 28 Sep 2023 17:07:02 +0800 -Subject: [PATCH] CCE update suppression - - -diff --git a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java -index b7c1050dfafa79f3ed86524ce37cd06651cbbdd9..b01918fc97926a3182c21145bb7411e7bc409d35 100644 ---- a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java -@@ -182,7 +182,7 @@ public class ShulkerBoxBlock extends BaseEntityBlock { - protected void onRemove(BlockState state, Level world, BlockPos pos, BlockState newState, boolean moved) { - if (!state.is(newState.getBlock())) { - BlockEntity blockEntity = world.getBlockEntity(pos); -- super.onRemove(state, world, pos, newState, moved); -+ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, world, pos, newState, moved); // Leaves - behaviour 1.21.1- - if (blockEntity instanceof ShulkerBoxBlockEntity) { - world.updateNeighbourForOutputSignal(pos, state.getBlock()); - } -@@ -239,17 +239,21 @@ public class ShulkerBoxBlock extends BaseEntityBlock { - - @Override - protected int getAnalogOutputSignal(BlockState state, Level world, BlockPos pos) { -- // Leaves start - fix update suppression crash -+ // Leaves start - fix update suppression crash - and cce fix - if (org.leavesmc.leaves.LeavesConfig.modify.updateSuppressionCrashFix) { - try { -- return AbstractContainerMenu.getRedstoneSignalFromBlockEntity(world.getBlockEntity(pos)); -+ return org.leavesmc.leaves.LeavesConfig.modify.oldMC.updater.cceUpdateSuppression ? -+ AbstractContainerMenu.getRedstoneSignalFromContainer((net.minecraft.world.Container) world.getBlockEntity(pos)) : -+ AbstractContainerMenu.getRedstoneSignalFromBlockEntity(world.getBlockEntity(pos)); - } catch (ClassCastException ex) { - throw new org.leavesmc.leaves.util.UpdateSuppressionException(pos, this); - } - } else { -- return AbstractContainerMenu.getRedstoneSignalFromBlockEntity(world.getBlockEntity(pos)); -+ return org.leavesmc.leaves.LeavesConfig.modify.oldMC.updater.cceUpdateSuppression ? -+ AbstractContainerMenu.getRedstoneSignalFromContainer((net.minecraft.world.Container) world.getBlockEntity(pos)) : -+ AbstractContainerMenu.getRedstoneSignalFromBlockEntity(world.getBlockEntity(pos)); - } -- // Leaves end - fix update suppression crash -+ // Leaves end - fix update suppression crash - and cce fix - } - - @Override diff --git a/patches/server/0098-Armor-stand-cant-kill-by-mob-projectile.patch b/patches/server/0098-Armor-stand-cant-kill-by-mob-projectile.patch deleted file mode 100644 index bea53f0b..00000000 --- a/patches/server/0098-Armor-stand-cant-kill-by-mob-projectile.patch +++ /dev/null @@ -1,25 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Fri, 29 Sep 2023 10:39:36 +0800 -Subject: [PATCH] Armor stand cant kill by mob projectile - - -diff --git a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -index 49e8a9d5df9b20bced385019f5e7fb622536213a..44c0b846ee038d683f76de46e667d8d8173e896f 100644 ---- a/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -+++ b/src/main/java/net/minecraft/world/entity/decoration/ArmorStand.java -@@ -532,6 +532,14 @@ public class ArmorStand extends LivingEntity { - boolean flag = source.is(DamageTypeTags.CAN_BREAK_ARMOR_STAND); - boolean flag1 = source.is(DamageTypeTags.ALWAYS_KILLS_ARMOR_STANDS); - -+ // Leaves start - Armor stand cant kill by mob projectile -+ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.armorStandCantKillByMobProjectile) { -+ if (!flag && !(source.getDirectEntity() instanceof net.minecraft.world.entity.projectile.AbstractArrow)) { -+ return false; -+ } -+ } -+ // Leaves end - Armor stand cant kill by mob projectile -+ - if (!flag && !flag1) { - return false; - } else { diff --git a/patches/server/0099-Make-Item-tick-vanilla.patch b/patches/server/0099-Make-Item-tick-vanilla.patch deleted file mode 100644 index 52425ca2..00000000 --- a/patches/server/0099-Make-Item-tick-vanilla.patch +++ /dev/null @@ -1,29 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 30 Oct 2023 10:43:44 +0800 -Subject: [PATCH] Make Item tick vanilla - - -diff --git a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -index 9e8a40f51337822ac55b5020778076c7466f5ef3..0d79396c4e65710e7b9f20c740cf2f8254ea120c 100644 ---- a/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -+++ b/src/main/java/net/minecraft/world/entity/item/ItemEntity.java -@@ -251,6 +251,9 @@ public class ItemEntity extends Entity implements TraceableEntity { - // Spigot start - copied from above - @Override - public void inactiveTick() { -+ // Leaves start - vanilla -+ this.tick(); -+ /* - // Paper start - remove anti tick skipping measures / wall time - copied from above - if (this.pickupDelay > 0 && this.pickupDelay != 32767) { - --this.pickupDelay; -@@ -269,6 +272,8 @@ public class ItemEntity extends Entity implements TraceableEntity { - // CraftBukkit end - this.discard(EntityRemoveEvent.Cause.DESPAWN); // CraftBukkit - add Bukkit remove cause - } -+ */ -+ // Leaves end - vanilla - } - // Spigot end - diff --git a/patches/server/0101-Crafter-1-gt-delay.patch b/patches/server/0101-Crafter-1-gt-delay.patch deleted file mode 100644 index 64722431..00000000 --- a/patches/server/0101-Crafter-1-gt-delay.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 12 Dec 2023 14:15:54 +0800 -Subject: [PATCH] Crafter 1 gt delay - - -diff --git a/src/main/java/net/minecraft/world/level/block/CrafterBlock.java b/src/main/java/net/minecraft/world/level/block/CrafterBlock.java -index 0e609b1e3abd50b415d8376dc550375a8a0251b6..37954c43b30c64d2d4748e1fd190784ed342e4c5 100644 ---- a/src/main/java/net/minecraft/world/level/block/CrafterBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/CrafterBlock.java -@@ -91,7 +91,7 @@ public class CrafterBlock extends BaseEntityBlock { - BlockEntity tileentity = world.getBlockEntity(pos); - - if (flag1 && !flag2) { -- world.scheduleTick(pos, (Block) this, 4); -+ world.scheduleTick(pos, (Block) this, !org.leavesmc.leaves.LeavesConfig.modify.oldMC.crafter1gt ? 4 : 1); // Leaves - crafter 1 gt delay - world.setBlock(pos, (BlockState) state.setValue(CrafterBlock.TRIGGERED, true), 2); - this.setBlockEntityTriggered(tileentity, true); - } else if (!flag1 && flag2) { -@@ -157,7 +157,7 @@ public class CrafterBlock extends BaseEntityBlock { - @Override - public void setPlacedBy(Level world, BlockPos pos, BlockState state, LivingEntity placer, ItemStack itemStack) { - if ((Boolean) state.getValue(CrafterBlock.TRIGGERED)) { -- world.scheduleTick(pos, (Block) this, 4); -+ world.scheduleTick(pos, (Block) this, !org.leavesmc.leaves.LeavesConfig.modify.oldMC.crafter1gt ? 4 : 1); // Leaves - crafter 1 gt delay - } - - } diff --git a/patches/server/0102-Linear-region-file-format.patch b/patches/server/0102-Linear-region-file-format.patch deleted file mode 100644 index b7fd7bae..00000000 --- a/patches/server/0102-Linear-region-file-format.patch +++ /dev/null @@ -1,887 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sun, 14 Jan 2024 22:22:57 +0800 -Subject: [PATCH] Linear region file format - -This patch is Powered by LinearPurpur(https://github.com/StupidCraft/LinearPurpur) - -diff --git a/build.gradle.kts b/build.gradle.kts -index 0add94373b7b38f271893b1c44a448051b845e8b..8559918f8f91e6137ac873b37af29aa3b6c0c4a0 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -43,6 +43,10 @@ dependencies { - alsoShade(log4jPlugins.output) - implementation("io.netty:netty-codec-haproxy:4.1.97.Final") // Paper - Add support for proxy protocol - // Paper end -+ // Leaves start - Linear format -+ implementation("com.github.luben:zstd-jni:1.5.6-3") -+ implementation("org.lz4:lz4-java:1.8.0") -+ // Leaves end - Linear format - implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") // Paper - remove exclusion - implementation("org.ow2.asm:asm-commons:9.7.1") - implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java -index a814512fcfb85312474ae2c2c21443843bf57831..c56812494d2cd270eb66bde1f0a8a9b4100777bd 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/ChunkSystemRegionFileStorage.java -+++ b/src/main/java/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 org.leavesmc.leaves.region.AbstractRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ); - -- public RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; -+ public org.leavesmc.leaves.region.AbstractRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException; - - public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite( - final int chunkX, final int chunkZ, final CompoundTag compound -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java -index 1acea58838f057ab87efd103cbecb6f5aeaef393..740cb0c60e6220ca20f3dd9b6f4063737c8adf06 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java -+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java -@@ -1462,7 +1462,7 @@ public final class MoonriseRegionFileIO { - - public static interface IORunnable { - -- public void run(final RegionFile regionFile) throws IOException; -+ public void run(final org.leavesmc.leaves.region.AbstractRegionFile regionFile) throws IOException; // Leaves - - } - } -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java -index 51c126735ace8fdde89ad97b5cab62f244212db0..054d03b0417077b30c34dbea1f3dc6b6de11b41b 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/storage/ChunkSystemChunkBuffer.java -+++ b/src/main/java/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 org.leavesmc.leaves.region.AbstractRegionFile regionFile) throws IOException; // Leaves - } -diff --git a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -index 622d0cbe023774d92d212f242b60b96317720835..c182e2d26a72ebd28636b50bbbc050e34fa600cc 100644 ---- a/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -+++ b/src/main/java/net/minecraft/util/worldupdate/WorldUpgrader.java -@@ -75,7 +75,7 @@ public class WorldUpgrader implements AutoCloseable { - volatile int skipped; - final Reference2FloatMap> progressMap = Reference2FloatMaps.synchronize(new Reference2FloatOpenHashMap()); - volatile Component status = Component.translatable("optimizeWorld.stage.counting"); -- static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.mca$"); -+ static final Pattern REGEX = Pattern.compile("^r\\.(-?[0-9]+)\\.(-?[0-9]+)\\.(linear | mca)$"); // Leaves - final DimensionDataStorage overworldDataStorage; - - public WorldUpgrader(LevelStorageSource.LevelStorageAccess session, DataFixer dataFixer, RegistryAccess dynamicRegistryManager, boolean eraseCache, boolean recreateRegionFiles) { -@@ -399,7 +399,7 @@ public class WorldUpgrader implements AutoCloseable { - - private static List getAllChunkPositions(RegionStorageInfo key, Path regionDirectory) { - File[] afile = regionDirectory.toFile().listFiles((file, s) -> { -- return s.endsWith(".mca"); -+ return s.endsWith(".mca") || s.endsWith(".linear"); // Leaves - }); - - if (afile == null) { -@@ -419,7 +419,7 @@ public class WorldUpgrader implements AutoCloseable { - List list1 = Lists.newArrayList(); - - try { -- RegionFile regionfile = new RegionFile(key, file.toPath(), regionDirectory, true); -+ org.leavesmc.leaves.region.AbstractRegionFile regionfile = org.leavesmc.leaves.region.AbstractRegionFileFactory.getAbstractRegionFile(key, file.toPath(), regionDirectory, true); // Leaves - - try { - for (int i1 = 0; i1 < 32; ++i1) { -@@ -482,7 +482,7 @@ public class WorldUpgrader implements AutoCloseable { - - protected abstract boolean tryProcessOnePosition(T storage, ChunkPos chunkPos, ResourceKey worldKey); - -- private void onFileFinished(RegionFile regionFile) { -+ private void onFileFinished(org.leavesmc.leaves.region.AbstractRegionFile regionFile) { // Leaves - if (WorldUpgrader.this.recreateRegionFiles) { - if (this.previousWriteFuture != null) { - this.previousWriteFuture.join(); -@@ -507,7 +507,7 @@ public class WorldUpgrader implements AutoCloseable { - } - } - -- static record FileToUpgrade(RegionFile file, List chunksToUpgrade) { -+ static record FileToUpgrade(org.leavesmc.leaves.region.AbstractRegionFile file, List chunksToUpgrade) { // Leaves - - } - -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -index 16f07007a0f73ec0c6f421c9b082518e87e8cc7b..b171304ad750a70944f48ddacb93836d3696af90 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFile.java -@@ -28,7 +28,7 @@ import net.minecraft.nbt.NbtIo; // Paper - import net.minecraft.world.level.ChunkPos; - 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, org.leavesmc.leaves.region.AbstractRegionFile { // Paper - rewrite chunk system // Leaves - - private static final Logger LOGGER = LogUtils.getLogger(); - private static final int SECTOR_BYTES = 4096; -@@ -129,7 +129,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - } - - // note: only call for CHUNK regionfiles -- boolean recalculateHeader() throws IOException { -+ public boolean recalculateHeader() throws IOException { // Leaves - make it public - if (!this.canRecalcHeader) { - return false; - } -@@ -810,7 +810,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - } - } - -- protected synchronized void write(ChunkPos pos, ByteBuffer buf) throws IOException { -+ public synchronized void write(ChunkPos pos, ByteBuffer buf) throws IOException { // Leaves - protected -> public - int i = RegionFile.getOffsetIndex(pos); - int j = this.offsets.get(i); - int k = RegionFile.getSectorNumber(j); -@@ -952,10 +952,10 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche - private static int getChunkIndex(int x, int z) { - return (x & 31) + (z & 31) * 32; - } -- synchronized boolean isOversized(int x, int z) { -+ public synchronized boolean isOversized(int x, int z) { // Leaves - 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 { // Leaves - final int offset = getChunkIndex(x, z); - boolean previous = this.oversized[offset] == 1; - this.oversized[offset] = (byte) (oversized ? 1 : 0); -@@ -994,7 +994,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 CompoundTag getOversizedData(int x, int z) throws IOException { -+ public synchronized CompoundTag getOversizedData(int x, int z) throws IOException { // Leaves - Path file = getOversizedFile(x, z); - try (DataInputStream out = new DataInputStream(new java.io.BufferedInputStream(new InflaterInputStream(Files.newInputStream(file))))) { - return NbtIo.read((java.io.DataInput) out); -@@ -1021,7 +1021,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 org.leavesmc.leaves.region.AbstractRegionFile regionFile) throws IOException { // Leaves - regionFile.write(this.pos, ByteBuffer.wrap(this.buf, 0, this.count)); - } - // Paper end - rewrite chunk system -diff --git a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -index e40665cead218502b44dd49051a53326ed94f061..a6f5d79b12a118bc9ed3d1bdba8a68dc10ed5be3 100644 ---- a/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -+++ b/src/main/java/net/minecraft/world/level/chunk/storage/RegionFileStorage.java -@@ -23,7 +23,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - - public static final String ANVIL_EXTENSION = ".mca"; - private static final int MAX_CACHE_SIZE = 256; -- public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap(); -+ public final Long2ObjectLinkedOpenHashMap regionCache = new Long2ObjectLinkedOpenHashMap(); // Leaves - private final RegionStorageInfo info; - private final Path folder; - private final boolean sync; -@@ -33,9 +33,15 @@ 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) + (org.leavesmc.leaves.LeavesConfig.region.format != org.leavesmc.leaves.region.RegionFileFormat.LINEAR ? ".mca" : ".linear"); // Leaves - } - -+ // Leaves start -+ private static String getOtherRegionFileName(final int chunkX, final int chunkZ) { -+ return "r." + (chunkX >> REGION_SHIFT) + "." + (chunkZ >> REGION_SHIFT) + (org.leavesmc.leaves.LeavesConfig.region.format == org.leavesmc.leaves.region.RegionFileFormat.LINEAR ? ".mca" : ".linear"); -+ } -+ // Leaves end -+ - private boolean doesRegionFilePossiblyExist(final long position) { - synchronized (this.nonExistingRegionFiles) { - if (this.nonExistingRegionFiles.contains(position)) { -@@ -68,15 +74,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 org.leavesmc.leaves.region.AbstractRegionFile moonrise$getRegionFileIfLoaded(final int chunkX, final int chunkZ) { // Leaves - return this.regionCache.getAndMoveToFirst(ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT)); - } - - @Override -- public synchronized final RegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { -+ public synchronized final org.leavesmc.leaves.region.AbstractRegionFile moonrise$getRegionFileIfExists(final int chunkX, final int chunkZ) throws IOException { // Leaves - final long key = ChunkPos.asLong(chunkX >> REGION_SHIFT, chunkZ >> REGION_SHIFT); - -- RegionFile ret = this.regionCache.getAndMoveToFirst(key); -+ org.leavesmc.leaves.region.AbstractRegionFile ret = this.regionCache.getAndMoveToFirst(key); // Leaves - if (ret != null) { - return ret; - } -@@ -88,19 +94,23 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - if (this.regionCache.size() >= io.papermc.paper.configuration.GlobalConfiguration.get().misc.regionFileCacheSize) { // Paper - this.regionCache.removeLast().close(); - } -- -- final Path regionPath = this.folder.resolve(getRegionFileName(chunkX, chunkZ)); -+ // Leaves start -+ Path regionPath = this.folder.resolve(getRegionFileName(chunkX, chunkZ)); - - if (!java.nio.file.Files.exists(regionPath)) { -- this.markNonExisting(key); -- return null; -+ regionPath = this.folder.resolve(getOtherRegionFileName(chunkX, chunkZ)); -+ if (!java.nio.file.Files.exists(regionPath)) { -+ this.markNonExisting(key); -+ return null; -+ } - } -+ // Leaves end - - this.createRegionFile(key); - - FileUtil.createDirectoriesSafe(this.folder); - -- ret = new RegionFile(this.info, regionPath, this.folder, this.sync); -+ ret = org.leavesmc.leaves.region.AbstractRegionFileFactory.getAbstractRegionFile(this.info, regionPath, this.folder, this.sync); // Leaves - - this.regionCache.putAndMoveToFirst(key, ret); - -@@ -119,7 +129,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - } - - final ChunkPos pos = new ChunkPos(chunkX, chunkZ); -- final RegionFile regionFile = this.getRegionFile(pos); -+ final org.leavesmc.leaves.region.AbstractRegionFile regionFile = this.getRegionFile(pos); // Leaves - - // 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) -@@ -153,7 +163,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 org.leavesmc.leaves.region.AbstractRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Leaves - if (regionFile != null) { - regionFile.clear(pos); - } // else: didn't exist -@@ -168,7 +178,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 org.leavesmc.leaves.region.AbstractRegionFile regionFile = this.moonrise$getRegionFileIfExists(chunkX, chunkZ); // Leaves - - final DataInputStream input = regionFile == null ? null : regionFile.getChunkDataInputStream(new ChunkPos(chunkX, chunkZ)); - -@@ -250,12 +260,12 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - } - - // Paper start - rewrite chunk system -- public RegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { -+ public org.leavesmc.leaves.region.AbstractRegionFile getRegionFile(ChunkPos chunkcoordintpair) throws IOException { // Leaves - return this.getRegionFile(chunkcoordintpair, false); - } - // Paper end - rewrite chunk system - -- public RegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public -+ public org.leavesmc.leaves.region.AbstractRegionFile getRegionFile(ChunkPos chunkcoordintpair, boolean existingOnly) throws IOException { // CraftBukkit // Paper - public // Leaves - // Paper start - rewrite chunk system - if (existingOnly) { - return this.moonrise$getRegionFileIfExists(chunkcoordintpair.x, chunkcoordintpair.z); -@@ -263,7 +273,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - synchronized (this) { - final long key = ChunkPos.asLong(chunkcoordintpair.x >> REGION_SHIFT, chunkcoordintpair.z >> REGION_SHIFT); - -- RegionFile ret = this.regionCache.getAndMoveToFirst(key); -+ org.leavesmc.leaves.region.AbstractRegionFile ret = this.regionCache.getAndMoveToFirst(key); // Leaves - if (ret != null) { - return ret; - } -@@ -278,7 +288,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - - FileUtil.createDirectoriesSafe(this.folder); - -- ret = new RegionFile(this.info, regionPath, this.folder, this.sync); -+ ret = org.leavesmc.leaves.region.AbstractRegionFileFactory.getAbstractRegionFile(this.info, regionPath, this.folder, this.sync); // Leaves - - this.regionCache.putAndMoveToFirst(key, ret); - -@@ -292,7 +302,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - org.apache.logging.log4j.LogManager.getLogger().fatal(msg + " (" + file.toString().replaceAll(".+[\\\\/]", "") + " - " + x + "," + z + ") Go clean it up to remove this message. /minecraft:tp " + (x<<4)+" 128 "+(z<<4) + " - 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."); - } - -- private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { -+ private static CompoundTag readOversizedChunk(org.leavesmc.leaves.region.AbstractRegionFile regionfile, ChunkPos chunkCoordinate) throws IOException { // Leaves - synchronized (regionfile) { - try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) { - CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x, chunkCoordinate.z); -@@ -327,7 +337,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - @Nullable - public CompoundTag read(ChunkPos pos) throws IOException { - // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing -- RegionFile regionfile = this.getRegionFile(pos, true); -+ org.leavesmc.leaves.region.AbstractRegionFile regionfile = this.getRegionFile(pos, true); // Paper // Leaves - if (regionfile == null) { - return null; - } -@@ -391,7 +401,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - - public void scanChunk(ChunkPos chunkPos, StreamTagVisitor scanner) throws IOException { - // CraftBukkit start - SPIGOT-5680: There's no good reason to preemptively create files on read, save that for writing -- RegionFile regionfile = this.getRegionFile(chunkPos, true); -+ org.leavesmc.leaves.region.AbstractRegionFile regionfile = this.getRegionFile(chunkPos, true); // Leaves - if (regionfile == null) { - return; - } -@@ -421,7 +431,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - } - - public void write(ChunkPos pos, @Nullable CompoundTag nbt) throws IOException { // Paper - rewrite chunk system - public -- RegionFile regionfile = this.getRegionFile(pos, nbt == null); // CraftBukkit // Paper - rewrite chunk system -+ org.leavesmc.leaves.region.AbstractRegionFile regionfile = this.getRegionFile(pos, nbt == null); // CraftBukkit // Paper - rewrite chunk system // Leaves - // Paper start - rewrite chunk system - if (regionfile == null) { - // if the RegionFile doesn't exist, no point in deleting from it -@@ -432,8 +442,33 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise - if (nbt == null) { - regionfile.clear(pos); - } else { -- DataOutputStream dataoutputstream = regionfile.getChunkDataOutputStream(pos); -+ // Leaves start - auto convert anvil to linear -+ DataOutputStream dataoutputstream; -+ -+ if (regionfile instanceof RegionFile && org.leavesmc.leaves.LeavesConfig.region.format == org.leavesmc.leaves.region.RegionFileFormat.LINEAR && org.leavesmc.leaves.LeavesConfig.region.linear.autoConvertAnvilToLinear) { -+ Path linearFilePath = Path.of(regionfile.getPath().toString().replaceAll(".mca", ".linear")); -+ try (org.leavesmc.leaves.region.LinearRegionFile linearRegionFile = new org.leavesmc.leaves.region.LinearRegionFile(linearFilePath, org.leavesmc.leaves.LeavesConfig.region.linear.compressionLevel)) { -+ DataInputStream regionDataInputStream = regionfile.getChunkDataInputStream(pos); -+ if (regionDataInputStream == null) { -+ return; // ? -+ } - -+ CompoundTag compoundTag = NbtIo.read(regionDataInputStream); -+ try (DataOutputStream linearDataOutputStream = linearRegionFile.getChunkDataOutputStream(pos)) { -+ NbtIo.write(compoundTag, linearDataOutputStream); -+ } -+ -+ linearRegionFile.flush(); -+ if(java.nio.file.Files.isRegularFile(regionfile.getPath())) { -+ java.nio.file.Files.delete(regionfile.getPath()); -+ } -+ -+ dataoutputstream = linearRegionFile.getChunkDataOutputStream(pos); -+ } -+ } else { -+ dataoutputstream = regionfile.getChunkDataOutputStream(pos); -+ } -+ // leaves end - auto convert anvil to linear - try { - NbtIo.write(nbt, (DataOutput) dataoutputstream); - regionfile.setOversized(pos.x, pos.z, false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone -@@ -465,7 +500,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 org.leavesmc.leaves.region.AbstractRegionFile regionFile : this.regionCache.values()) { // Leaves - try { - regionFile.close(); - } catch (final IOException ex) { -@@ -482,7 +517,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 org.leavesmc.leaves.region.AbstractRegionFile regionFile : this.regionCache.values()) { // Leaves - try { - regionFile.flush(); - } catch (final IOException ex) { -diff --git a/src/main/java/org/leavesmc/leaves/region/AbstractRegionFile.java b/src/main/java/org/leavesmc/leaves/region/AbstractRegionFile.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3f95d7ffa880c598242d38899f71174e4e264338 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/region/AbstractRegionFile.java -@@ -0,0 +1,41 @@ -+package org.leavesmc.leaves.region; -+ -+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 AbstractRegionFile extends ChunkSystemRegionFile { -+ -+ Path getPath(); -+ -+ DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException; -+ -+ DataOutputStream getChunkDataOutputStream(ChunkPos pos) throws IOException; -+ -+ CompoundTag getOversizedData(int x, int z) throws IOException; -+ -+ boolean recalculateHeader() throws IOException ; -+ -+ boolean hasChunk(ChunkPos pos); -+ -+ boolean doesChunkExist(ChunkPos pos); -+ -+ boolean isOversized(int x, int z); -+ -+ void flush() throws IOException; -+ -+ void close() throws IOException; -+ -+ void clear(ChunkPos pos) throws IOException; -+ -+ void setOversized(int x, int z, boolean oversized) throws IOException; -+ -+ void write(ChunkPos pos, ByteBuffer buf) throws IOException; -+} -+ -diff --git a/src/main/java/org/leavesmc/leaves/region/AbstractRegionFileFactory.java b/src/main/java/org/leavesmc/leaves/region/AbstractRegionFileFactory.java -new file mode 100644 -index 0000000000000000000000000000000000000000..8df8f28d15736c4e363e2abdec0a56d6ccb15861 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/region/AbstractRegionFileFactory.java -@@ -0,0 +1,24 @@ -+package org.leavesmc.leaves.region; -+ -+import net.minecraft.world.level.chunk.storage.RegionFile; -+import net.minecraft.world.level.chunk.storage.RegionFileVersion; -+import net.minecraft.world.level.chunk.storage.RegionStorageInfo; -+import org.leavesmc.leaves.LeavesConfig; -+ -+import java.io.IOException; -+import java.nio.file.Path; -+ -+public class AbstractRegionFileFactory { -+ -+ public static AbstractRegionFile getAbstractRegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync) throws IOException { -+ return getAbstractRegionFile(storageKey, directory, path, RegionFileVersion.getCompressionFormat(), dsync); -+ } -+ -+ public static AbstractRegionFile getAbstractRegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync) throws IOException { -+ if (path.toString().endsWith(".linear")) { -+ return new LinearRegionFile(path, LeavesConfig.region.linear.compressionLevel); -+ } else { -+ return new RegionFile(storageKey, path, directory, compressionFormat, dsync); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/region/LinearRegionFile.java b/src/main/java/org/leavesmc/leaves/region/LinearRegionFile.java -new file mode 100644 -index 0000000000000000000000000000000000000000..f903895b53eada8795a6ee9ab33cf30bf69c2c98 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/region/LinearRegionFile.java -@@ -0,0 +1,331 @@ -+package org.leavesmc.leaves.region; -+ -+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 java.io.BufferedOutputStream; -+import java.io.ByteArrayInputStream; -+import java.io.ByteArrayOutputStream; -+import java.io.DataInputStream; -+import java.io.DataOutputStream; -+import java.io.File; -+import java.io.FileInputStream; -+import java.io.FileOutputStream; -+import java.io.IOException; -+import java.io.InputStream; -+import java.nio.ByteBuffer; -+import java.nio.file.Files; -+import java.nio.file.Path; -+import java.nio.file.StandardCopyOption; -+import java.util.ArrayList; -+import java.util.Arrays; -+import java.util.List; -+import java.util.concurrent.atomic.AtomicBoolean; -+import javax.annotation.Nullable; -+ -+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 org.slf4j.Logger; -+ -+// Powered by LinearPurpur(https://github.com/StupidCraft/LinearPurpur) -+public class LinearRegionFile implements AbstractRegionFile, AutoCloseable { -+ -+ private static final long SUPERBLOCK = -4323716122432332390L; -+ private static final byte VERSION = 2; -+ private static final int HEADER_SIZE = 32; -+ private static final int FOOTER_SIZE = 8; -+ private static final Logger LOGGER = LogUtils.getLogger(); -+ private static final List SUPPORTED_VERSIONS = Arrays.asList((byte) 1, (byte) 2); -+ private static final LinearRegionFileFlusher linearRegionFileFlusher = new LinearRegionFileFlusher(); -+ private final byte[][] buffer = new byte[1024][]; -+ private final int[] bufferUncompressedSize = new int[1024]; -+ private final int[] chunkTimestamps = new int[1024]; -+ private final LZ4Compressor compressor; -+ private final LZ4FastDecompressor decompressor; -+ private final int compressionLevel; -+ private final AtomicBoolean markedToSave = new AtomicBoolean(false); -+ public boolean closed = false; -+ public Path path; -+ -+ public LinearRegionFile(Path file, int compression) throws IOException { -+ this.path = file; -+ this.compressionLevel = compression; -+ this.compressor = LZ4Factory.fastestInstance().fastCompressor(); -+ this.decompressor = LZ4Factory.fastestInstance().fastDecompressor(); -+ -+ File regionFile = new File(this.path.toString()); -+ -+ Arrays.fill(this.bufferUncompressedSize, 0); -+ -+ if (!regionFile.canRead()) { -+ return; -+ } -+ -+ try (FileInputStream fileStream = new FileInputStream(regionFile); -+ DataInputStream rawDataStream = new DataInputStream(fileStream)) { -+ -+ long superBlock = rawDataStream.readLong(); -+ if (superBlock != SUPERBLOCK) { -+ throw new RuntimeException("Invalid superblock: " + superBlock + " in " + file); -+ } -+ -+ byte version = rawDataStream.readByte(); -+ if (!SUPPORTED_VERSIONS.contains(version)) { -+ throw new RuntimeException("Invalid version: " + version + " in " + file); -+ } -+ -+ // Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused. -+ rawDataStream.skipBytes(11); -+ -+ int dataCount = rawDataStream.readInt(); -+ long fileLength = file.toFile().length(); -+ if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE) { -+ throw new IOException("Invalid file length: " + this.path + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE)); -+ } -+ -+ rawDataStream.skipBytes(8); // Skip data hash (Long): Unused. -+ -+ byte[] rawCompressed = new byte[dataCount]; -+ rawDataStream.readFully(rawCompressed, 0, dataCount); -+ -+ superBlock = rawDataStream.readLong(); -+ if (superBlock != SUPERBLOCK) { -+ throw new IOException("Footer superblock invalid " + this.path); -+ } -+ -+ try (DataInputStream dataStream = new DataInputStream(new ZstdInputStream(new ByteArrayInputStream(rawCompressed)))) { -+ int[] starts = new int[1024]; -+ for (int i = 0; i < 1024; i++) { -+ starts[i] = dataStream.readInt(); -+ dataStream.skipBytes(4); // Skip timestamps (Int): Unused. -+ } -+ -+ for (int i = 0; i < 1024; i++) { -+ if (starts[i] > 0) { -+ int size = starts[i]; -+ byte[] b = new byte[size]; -+ dataStream.readFully(b, 0, size); -+ -+ int maxCompressedLength = this.compressor.maxCompressedLength(size); -+ byte[] compressed = new byte[maxCompressedLength]; -+ int compressedLength = this.compressor.compress(b, 0, size, compressed, 0, maxCompressedLength); -+ b = new byte[compressedLength]; -+ System.arraycopy(compressed, 0, b, 0, compressedLength); -+ -+ this.buffer[i] = b; -+ this.bufferUncompressedSize[i] = size; -+ } -+ } -+ } -+ } -+ } -+ -+ private static int getChunkIndex(int x, int z) { -+ return (x & 31) + ((z & 31) << 5); -+ } -+ -+ private static int getTimestamp() { -+ return (int) (System.currentTimeMillis() / 1000L); -+ } -+ -+ public Path getPath() { -+ return this.path; -+ } -+ -+ public void flush() throws IOException { -+ if (isMarkedToSave()) flushWrapper(); // sync -+ } -+ -+ private void markToSave() { -+ linearRegionFileFlusher.scheduleSave(this); -+ markedToSave.set(true); -+ } -+ -+ public boolean isMarkedToSave() { -+ return markedToSave.getAndSet(false); -+ } -+ -+ public void flushWrapper() { -+ try { -+ save(); -+ } catch (IOException e) { -+ LOGGER.error("Failed to flush region file {}", path.toAbsolutePath(), e); -+ } -+ } -+ -+ public boolean doesChunkExist(ChunkPos pos) { -+ return false; -+ } -+ -+ private synchronized void save() throws IOException { -+ long timestamp = getTimestamp(); -+ short chunkCount = 0; -+ -+ File tempFile = new File(path.toString() + ".tmp"); -+ -+ try (FileOutputStream fileStream = new FileOutputStream(tempFile); -+ ByteArrayOutputStream zstdByteArray = new ByteArrayOutputStream(); -+ ZstdOutputStream zstdStream = new ZstdOutputStream(zstdByteArray, this.compressionLevel); -+ DataOutputStream zstdDataStream = new DataOutputStream(zstdStream); -+ DataOutputStream dataStream = new DataOutputStream(fileStream)) { -+ -+ dataStream.writeLong(SUPERBLOCK); -+ dataStream.writeByte(VERSION); -+ dataStream.writeLong(timestamp); -+ dataStream.writeByte(this.compressionLevel); -+ -+ ArrayList byteBuffers = new ArrayList<>(); -+ for (int i = 0; i < 1024; i++) { -+ if (this.bufferUncompressedSize[i] != 0) { -+ chunkCount += 1; -+ byte[] content = new byte[bufferUncompressedSize[i]]; -+ this.decompressor.decompress(buffer[i], 0, content, 0, bufferUncompressedSize[i]); -+ -+ byteBuffers.add(content); -+ } else { -+ byteBuffers.add(null); -+ } -+ } -+ for (int i = 0; i < 1024; i++) { -+ zstdDataStream.writeInt(this.bufferUncompressedSize[i]); // Write uncompressed size -+ zstdDataStream.writeInt(this.chunkTimestamps[i]); // Write timestamp -+ } -+ for (int i = 0; i < 1024; i++) { -+ if (byteBuffers.get(i) != null) { -+ zstdDataStream.write(byteBuffers.get(i), 0, byteBuffers.get(i).length); -+ } -+ } -+ zstdDataStream.close(); -+ -+ dataStream.writeShort(chunkCount); -+ -+ byte[] compressed = zstdByteArray.toByteArray(); -+ -+ dataStream.writeInt(compressed.length); -+ dataStream.writeLong(0); -+ -+ dataStream.write(compressed, 0, compressed.length); -+ dataStream.writeLong(SUPERBLOCK); -+ -+ dataStream.flush(); -+ fileStream.getFD().sync(); -+ fileStream.getChannel().force(true); // Ensure atomicity on Btrfs -+ } -+ Files.move(tempFile.toPath(), this.path, StandardCopyOption.REPLACE_EXISTING); -+ } -+ -+ public synchronized void write(ChunkPos pos, ByteBuffer buffer) { -+ try { -+ byte[] b = toByteArray(new ByteArrayInputStream(buffer.array())); -+ int uncompressedSize = b.length; -+ -+ 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.path); -+ } -+ markToSave(); -+ } -+ -+ public DataOutputStream getChunkDataOutputStream(ChunkPos pos) { -+ return new DataOutputStream(new BufferedOutputStream(new ChunkBuffer(pos))); -+ } -+ -+ 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) { -+ 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 void clear(ChunkPos pos) { -+ int i = getChunkIndex(pos.x, pos.z); -+ this.buffer[i] = null; -+ this.bufferUncompressedSize[i] = 0; -+ this.chunkTimestamps[i] = getTimestamp(); -+ markToSave(); -+ } -+ -+ public boolean hasChunk(ChunkPos pos) { -+ return this.bufferUncompressedSize[getChunkIndex(pos.x, pos.z)] > 0; -+ } -+ -+ public void close() throws IOException { -+ if (closed) { -+ return; -+ } -+ -+ closed = true; -+ flush(); // sync -+ } -+ -+ 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.path); -+ } -+ -+ @Override -+ public boolean recalculateHeader() { -+ return false; -+ } -+ -+ public boolean isOversized(int x, int z) { -+ return false; -+ } -+ -+ @Override -+ public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) { -+ DataOutputStream buff = getChunkDataOutputStream(pos); -+ return new MoonriseRegionFileIO.RegionDataController.WriteData( -+ data, MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE, -+ buff, LinearRegionFile::writeNothing -+ ); -+ } -+ -+ private static void writeNothing(AbstractRegionFile regionFile) { -+ } -+ -+ private class ChunkBuffer extends ByteArrayOutputStream { -+ private final ChunkPos pos; -+ -+ public ChunkBuffer(ChunkPos chunkcoordintpair) { -+ super(); -+ this.pos = chunkcoordintpair; -+ } -+ -+ public void close() { -+ ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count); -+ LinearRegionFile.this.write(this.pos, bytebuffer); -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/region/LinearRegionFileFlusher.java b/src/main/java/org/leavesmc/leaves/region/LinearRegionFileFlusher.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ab6346b678dd619bb7e2fa5e79bd82b7268485f1 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/region/LinearRegionFileFlusher.java -@@ -0,0 +1,56 @@ -+package org.leavesmc.leaves.region; -+ -+import com.google.common.util.concurrent.ThreadFactoryBuilder; -+ -+import java.util.Queue; -+import java.util.concurrent.ExecutorService; -+import java.util.concurrent.Executors; -+import java.util.concurrent.LinkedBlockingQueue; -+import java.util.concurrent.ScheduledExecutorService; -+import java.util.concurrent.TimeUnit; -+ -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+ -+// Powered by LinearPurpur(https://github.com/StupidCraft/LinearPurpur) -+public class LinearRegionFileFlusher { -+ -+ private final Queue savingQueue = new LinkedBlockingQueue<>(); -+ private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor( -+ new ThreadFactoryBuilder() -+ .setNameFormat("linear-flush-scheduler") -+ .build() -+ ); -+ private final ExecutorService executor = Executors.newFixedThreadPool( -+ LeavesConfig.region.linear.getLinearFlushThreads(), -+ new ThreadFactoryBuilder() -+ .setNameFormat("linear-flusher-%d") -+ .build() -+ ); -+ -+ public LinearRegionFileFlusher() { -+ LeavesLogger.LOGGER.info("Using " + LeavesConfig.region.linear.getLinearFlushThreads() + " threads for linear region flushing."); -+ scheduler.scheduleAtFixedRate(this::pollAndFlush, 0L, LeavesConfig.region.linear.flushFrequency, TimeUnit.SECONDS); -+ } -+ -+ public void scheduleSave(LinearRegionFile regionFile) { -+ if (savingQueue.contains(regionFile)) { -+ return; -+ } -+ savingQueue.add(regionFile); -+ } -+ -+ private void pollAndFlush() { -+ while (!savingQueue.isEmpty()) { -+ LinearRegionFile regionFile = savingQueue.poll(); -+ if (!regionFile.closed && regionFile.isMarkedToSave()) { -+ executor.execute(regionFile::flushWrapper); -+ } -+ } -+ } -+ -+ public void shutdown() { -+ executor.shutdown(); -+ scheduler.shutdown(); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/region/RegionFileFormat.java b/src/main/java/org/leavesmc/leaves/region/RegionFileFormat.java -new file mode 100644 -index 0000000000000000000000000000000000000000..2469211b0dfa42d87d855abc7b1303d2852d99a0 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/region/RegionFileFormat.java -@@ -0,0 +1,5 @@ -+package org.leavesmc.leaves.region; -+ -+public enum RegionFileFormat { -+ ANVIL, LINEAR -+} diff --git a/patches/server/0103-No-TNT-place-update.patch b/patches/server/0103-No-TNT-place-update.patch deleted file mode 100644 index cfdd9de6..00000000 --- a/patches/server/0103-No-TNT-place-update.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 18 Jan 2024 12:31:31 +0800 -Subject: [PATCH] No TNT place update - - -diff --git a/src/main/java/net/minecraft/world/level/block/TntBlock.java b/src/main/java/net/minecraft/world/level/block/TntBlock.java -index 2cbe2053dd5d0bcdcd89de69762c77b400b8697a..be5a34c87173f43969f97cf52f610dae663883de 100644 ---- a/src/main/java/net/minecraft/world/level/block/TntBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/TntBlock.java -@@ -51,7 +51,7 @@ public class TntBlock extends Block { - @Override - protected void onPlace(BlockState state, Level world, BlockPos pos, BlockState oldState, boolean notify) { - if (!oldState.is(state.getBlock())) { -- if (world.hasNeighborSignal(pos) && CraftEventFactory.callTNTPrimeEvent(world, pos, PrimeCause.REDSTONE, null, null)) { // CraftBukkit - TNTPrimeEvent -+ if (!org.leavesmc.leaves.LeavesConfig.modify.noTNTPlaceUpdate && world.hasNeighborSignal(pos) && CraftEventFactory.callTNTPrimeEvent(world, pos, PrimeCause.REDSTONE, null, null)) { // CraftBukkit - TNTPrimeEvent // Leaves - No tnt place update - // Paper start - TNTPrimeEvent - org.bukkit.block.Block tntBlock = org.bukkit.craftbukkit.block.CraftBlock.at(world, pos); - if (!new com.destroystokyo.paper.event.block.TNTPrimeEvent(tntBlock, com.destroystokyo.paper.event.block.TNTPrimeEvent.PrimeReason.REDSTONE, null).callEvent()) { diff --git a/patches/server/0104-Servux-Protocol.patch b/patches/server/0104-Servux-Protocol.patch deleted file mode 100644 index 996146c7..00000000 --- a/patches/server/0104-Servux-Protocol.patch +++ /dev/null @@ -1,917 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 13 Sep 2023 19:31:20 +0800 -Subject: [PATCH] Servux Protocol - - -diff --git a/src/main/java/net/minecraft/server/level/ServerLevel.java b/src/main/java/net/minecraft/server/level/ServerLevel.java -index 4ff490f7fcf574d0c42a2e1c2773ccf85a80fdb9..cf53a0f9a8c9827ec4f6a52a13c872f1b0f1e1f0 100644 ---- a/src/main/java/net/minecraft/server/level/ServerLevel.java -+++ b/src/main/java/net/minecraft/server/level/ServerLevel.java -@@ -2170,6 +2170,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe - } - - this.lastSpawnChunkRadius = i; -+ org.leavesmc.leaves.protocol.servux.ServuxStructuresProtocol.refreshSpawnMetadata = true; // Leaves - servux - } - - public LongSet getForcedChunks() { -diff --git a/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java b/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java -new file mode 100644 -index 0000000000000000000000000000000000000000..3a0e790f0d8e6866950601f9936984a83a4cdf2c ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java -@@ -0,0 +1,117 @@ -+package org.leavesmc.leaves.protocol.servux; -+ -+import io.netty.buffer.Unpooled; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.server.level.ServerPlayer; -+ -+import javax.annotation.Nullable; -+import java.util.HashMap; -+import java.util.Map; -+ -+// Powered by Servux(https://github.com/sakura-ryoko/servux) -+ -+/** -+ * Network packet splitter code from QuickCarpet by skyrising -+ * -+ * @author skyrising -+ *

-+ * Updated by Sakura to work with newer versions by changing the Reading Session keys, -+ * and using the HANDLER interface to send packets via the Payload system -+ *

-+ * Move to Leaves by violetc -+ */ -+public class PacketSplitter { -+ public static final int MAX_TOTAL_PER_PACKET_S2C = 1048576; -+ public static final int MAX_PAYLOAD_PER_PACKET_S2C = MAX_TOTAL_PER_PACKET_S2C - 5; -+ public static final int MAX_TOTAL_PER_PACKET_C2S = 32767; -+ public static final int MAX_PAYLOAD_PER_PACKET_C2S = MAX_TOTAL_PER_PACKET_C2S - 5; -+ public static final int DEFAULT_MAX_RECEIVE_SIZE_C2S = 1048576; -+ public static final int DEFAULT_MAX_RECEIVE_SIZE_S2C = 67108864; -+ -+ private static final Map READING_SESSIONS = new HashMap<>(); -+ -+ public static boolean send(IPacketSplitterHandler handler, FriendlyByteBuf packet, ServerPlayer player) { -+ return send(handler, packet, player, MAX_PAYLOAD_PER_PACKET_S2C); -+ } -+ -+ private static boolean send(IPacketSplitterHandler handler, FriendlyByteBuf packet, ServerPlayer player, int payloadLimit) { -+ int len = packet.writerIndex(); -+ -+ packet.resetReaderIndex(); -+ -+ for (int offset = 0; offset < len; offset += payloadLimit) { -+ int thisLen = Math.min(len - offset, payloadLimit); -+ FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer(thisLen)); -+ -+ buf.resetWriterIndex(); -+ -+ if (offset == 0) { -+ buf.writeVarInt(len); -+ } -+ -+ buf.writeBytes(packet, thisLen); -+ handler.encode(player, buf); -+ } -+ -+ packet.release(); -+ -+ return true; -+ } -+ -+ public static FriendlyByteBuf receive(long key, FriendlyByteBuf buf) { -+ return receive(key, buf, DEFAULT_MAX_RECEIVE_SIZE_S2C); -+ } -+ -+ @Nullable -+ private static FriendlyByteBuf receive(long key, FriendlyByteBuf buf, int maxLength) { -+ return READING_SESSIONS.computeIfAbsent(key, ReadingSession::new).receive(buf, maxLength); -+ } -+ -+ /** -+ * I had to fix the `Pair.of` key mappings, because they were removed from MC; -+ * So I made it into a pre-shared random session 'key' between client and server. -+ * Generated using 'long key = Random.create(Util.getMeasuringTimeMs()).nextLong();' -+ * - -+ * It can be shared to the receiving end via a separate packet; or it can just be -+ * generated randomly on the receiving end per an expected Reading Session. -+ * It needs to be stored and changed for every unique session. -+ */ -+ private static class ReadingSession { -+ private final long key; -+ private int expectedSize = -1; -+ private FriendlyByteBuf received; -+ -+ private ReadingSession(long key) { -+ this.key = key; -+ } -+ -+ @Nullable -+ private FriendlyByteBuf receive(FriendlyByteBuf data, int maxLength) { -+ data.readerIndex(0); -+ // data = PacketUtils.slice(data); -+ -+ if (this.expectedSize < 0) { -+ this.expectedSize = data.readVarInt(); -+ -+ if (this.expectedSize > maxLength) { -+ throw new IllegalArgumentException("Payload too large"); -+ } -+ -+ this.received = new FriendlyByteBuf(Unpooled.buffer(this.expectedSize)); -+ } -+ -+ this.received.writeBytes(data.readBytes(data.readableBytes())); -+ -+ if (this.received.writerIndex() >= this.expectedSize) { -+ READING_SESSIONS.remove(this.key); -+ return this.received; -+ } -+ -+ return null; -+ } -+ } -+ -+ public interface IPacketSplitterHandler { -+ void encode(ServerPlayer player, FriendlyByteBuf buf); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..36cc207c87f7dc56309f2683f3a97fe2f9fff661 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java -@@ -0,0 +1,278 @@ -+package org.leavesmc.leaves.protocol.servux; -+ -+import io.netty.buffer.Unpooled; -+import net.minecraft.Util; -+import net.minecraft.core.BlockPos; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.util.RandomSource; -+import net.minecraft.world.entity.Entity; -+import net.minecraft.world.level.block.entity.BlockEntity; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+import java.util.HashMap; -+import java.util.Map; -+import java.util.UUID; -+ -+// Powered by Servux(https://github.com/sakura-ryoko/servux) -+ -+@LeavesProtocol(namespace = "servux") -+public class ServuxEntityDataProtocol { -+ -+ public static final ResourceLocation CHANNEL = ServuxProtocol.id("entity_data"); -+ public static final int PROTOCOL_VERSION = 1; -+ -+ private static final Map readingSessionKeys = new HashMap<>(); -+ -+ @ProtocolHandler.PlayerJoin -+ public static void onPlayerLoggedIn(ServerPlayer player) { -+ if (!LeavesConfig.protocol.servux.entityProtocol) { -+ return; -+ } -+ -+ sendMetadata(player); -+ } -+ -+ @ProtocolHandler.PayloadReceiver(payload = EntityDataPayload.class, payloadId = "entity_data") -+ public static void onPacketReceive(ServerPlayer player, EntityDataPayload payload) { -+ if (!LeavesConfig.protocol.servux.entityProtocol) { -+ return; -+ } -+ -+ switch (payload.packetType) { -+ case PACKET_C2S_METADATA_REQUEST -> sendMetadata(player); -+ case PACKET_C2S_BLOCK_ENTITY_REQUEST -> onBlockEntityRequest(player, payload.pos); -+ case PACKET_C2S_ENTITY_REQUEST -> onEntityRequest(player, payload.entityId); -+ case PACKET_C2S_NBT_RESPONSE_DATA -> { -+ UUID uuid = player.getUUID(); -+ long readingSessionKey; -+ -+ if (!readingSessionKeys.containsKey(uuid)) { -+ readingSessionKey = RandomSource.create(Util.getMillis()).nextLong(); -+ readingSessionKeys.put(uuid, readingSessionKey); -+ } else { -+ readingSessionKey = readingSessionKeys.get(uuid); -+ } -+ -+ FriendlyByteBuf fullPacket = PacketSplitter.receive(readingSessionKey, payload.buffer); -+ -+ if (fullPacket != null) { -+ readingSessionKeys.remove(uuid); -+ LeavesLogger.LOGGER.warning("ServuxEntityDataProtocol,PACKET_C2S_NBT_RESPONSE_DATA NOT Implemented!"); -+ } -+ } -+ } -+ } -+ -+ public static void sendMetadata(ServerPlayer player) { -+ CompoundTag metadata = new CompoundTag(); -+ metadata.putString("name", "entity_data"); -+ metadata.putString("id", CHANNEL.toString()); -+ metadata.putInt("version", PROTOCOL_VERSION); -+ metadata.putString("servux", ServuxProtocol.SERVUX_STRING); -+ -+ EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_METADATA); -+ payload.nbt.merge(metadata); -+ sendPacket(player, payload); -+ } -+ -+ public static void onBlockEntityRequest(ServerPlayer player, BlockPos pos) { -+ MinecraftServer.getServer().execute(() -> { -+ BlockEntity be = player.serverLevel().getBlockEntity(pos); -+ CompoundTag nbt = be != null ? be.saveWithoutMetadata(player.registryAccess()) : new CompoundTag(); -+ -+ EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE); -+ payload.pos = pos.immutable(); -+ payload.nbt.merge(nbt); -+ sendPacket(player, payload); -+ }); -+ } -+ -+ public static void onEntityRequest(ServerPlayer player, int entityId) { -+ MinecraftServer.getServer().execute(() -> { -+ Entity entity = player.serverLevel().getEntity(entityId); -+ CompoundTag nbt = entity != null ? entity.saveWithoutId(new CompoundTag()) : new CompoundTag(); -+ -+ EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE); -+ payload.entityId = entityId; -+ payload.nbt.merge(nbt); -+ sendPacket(player, payload); -+ }); -+ } -+ -+ public static void sendPacket(ServerPlayer player, EntityDataPayload payload) { -+ if (!LeavesConfig.protocol.servux.entityProtocol) { -+ return; -+ } -+ -+ if (payload.packetType == EntityDataPayloadType.PACKET_S2C_NBT_RESPONSE_START) { -+ FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); -+ buffer.writeNbt(payload.nbt); -+ PacketSplitter.send(ServuxEntityDataProtocol::sendWithSplitter, buffer, player); -+ } else { -+ ProtocolUtils.sendPayloadPacket(player, payload); -+ } -+ } -+ -+ private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { -+ EntityDataPayload payload = new EntityDataPayload(EntityDataPayloadType.PACKET_S2C_NBT_RESPONSE_DATA); -+ payload.buffer = buf; -+ payload.nbt = new CompoundTag(); -+ sendPacket(player, payload); -+ } -+ -+ public enum EntityDataPayloadType { -+ PACKET_S2C_METADATA(1), -+ PACKET_C2S_METADATA_REQUEST(2), -+ PACKET_C2S_BLOCK_ENTITY_REQUEST(3), -+ PACKET_C2S_ENTITY_REQUEST(4), -+ PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE(5), -+ PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE(6), -+ // For Packet Splitter (Oversize Packets, S2C) -+ PACKET_S2C_NBT_RESPONSE_START(10), -+ PACKET_S2C_NBT_RESPONSE_DATA(11), -+ // For Packet Splitter (Oversize Packets, C2S) -+ PACKET_C2S_NBT_RESPONSE_START(12), -+ PACKET_C2S_NBT_RESPONSE_DATA(13); -+ -+ private static final class Helper { -+ static Map ID_TO_TYPE = new HashMap<>(); -+ } -+ -+ public final int type; -+ -+ EntityDataPayloadType(int type) { -+ this.type = type; -+ EntityDataPayloadType.Helper.ID_TO_TYPE.put(type, this); -+ } -+ -+ public static EntityDataPayloadType fromId(int id) { -+ return EntityDataPayloadType.Helper.ID_TO_TYPE.get(id); -+ } -+ } -+ -+ public static class EntityDataPayload implements LeavesCustomPayload { -+ -+ private final EntityDataPayloadType packetType; -+ private final int transactionId; -+ private int entityId; -+ private BlockPos pos; -+ private CompoundTag nbt; -+ private FriendlyByteBuf buffer; -+ -+ private EntityDataPayload(EntityDataPayloadType type) { -+ this.packetType = type; -+ this.transactionId = -1; -+ this.entityId = -1; -+ this.pos = BlockPos.ZERO; -+ this.nbt = new CompoundTag(); -+ this.clearPacket(); -+ } -+ -+ private void clearPacket() { -+ if (this.buffer != null) { -+ this.buffer.clear(); -+ this.buffer = new FriendlyByteBuf(Unpooled.buffer()); -+ } -+ } -+ -+ @New -+ public static EntityDataPayload decode(ResourceLocation location, FriendlyByteBuf buffer) { -+ EntityDataPayloadType type = EntityDataPayloadType.fromId(buffer.readVarInt()); -+ if (type == null) { -+ throw new IllegalStateException("invalid packet type received"); -+ } -+ -+ EntityDataPayload payload = new EntityDataPayload(type); -+ switch (type) { -+ case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { -+ buffer.readVarInt(); -+ payload.pos = buffer.readBlockPos().immutable(); -+ } -+ -+ case PACKET_C2S_ENTITY_REQUEST -> { -+ buffer.readVarInt(); -+ payload.entityId = buffer.readVarInt(); -+ } -+ -+ case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { -+ payload.pos = buffer.readBlockPos().immutable(); -+ CompoundTag nbt = buffer.readNbt(); -+ if (nbt != null) { -+ payload.nbt.merge(nbt); -+ } -+ } -+ -+ case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { -+ payload.entityId = buffer.readVarInt(); -+ CompoundTag nbt = buffer.readNbt(); -+ if (nbt != null) { -+ payload.nbt.merge(nbt); -+ } -+ } -+ -+ case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> { -+ payload.buffer = new FriendlyByteBuf(buffer.readBytes(buffer.readableBytes())); -+ payload.nbt = new CompoundTag(); -+ } -+ -+ case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> { -+ CompoundTag nbt = buffer.readNbt(); -+ if (nbt != null) { -+ payload.nbt.merge(nbt); -+ } -+ } -+ } -+ -+ return payload; -+ } -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ buf.writeVarInt(this.packetType.type); -+ -+ switch (this.packetType) { -+ case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { -+ buf.writeVarInt(this.transactionId); -+ buf.writeBlockPos(this.pos); -+ } -+ -+ case PACKET_C2S_ENTITY_REQUEST -> { -+ buf.writeVarInt(this.transactionId); -+ buf.writeVarInt(this.entityId); -+ } -+ -+ case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { -+ buf.writeBlockPos(this.pos); -+ buf.writeNbt(this.nbt); -+ } -+ -+ case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { -+ buf.writeVarInt(this.entityId); -+ buf.writeNbt(this.nbt); -+ } -+ -+ case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> { -+ buf.writeBytes(this.buffer.readBytes(this.buffer.readableBytes())); -+ } -+ -+ case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> { -+ buf.writeNbt(this.nbt); -+ } -+ } -+ } -+ -+ @Override -+ public ResourceLocation id() { -+ return CHANNEL; -+ } -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ea83d25b76f2e4814c82c1010ba4c0bca0758840 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java -@@ -0,0 +1,17 @@ -+package org.leavesmc.leaves.protocol.servux; -+ -+import net.minecraft.resources.ResourceLocation; -+import org.jetbrains.annotations.Contract; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+public class ServuxProtocol { -+ -+ public static final String PROTOCOL_ID = "servux"; -+ public static final String SERVUX_STRING = ProtocolUtils.buildProtocolVersion(PROTOCOL_ID); -+ -+ @Contract("_ -> new") -+ public static @NotNull ResourceLocation id(String path) { -+ return new ResourceLocation(PROTOCOL_ID, path); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java -new file mode 100644 -index 0000000000000000000000000000000000000000..fb5779d445d693e0a533a61b0a66d786f9b9cb0e ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java -@@ -0,0 +1,452 @@ -+package org.leavesmc.leaves.protocol.servux; -+ -+import io.netty.buffer.Unpooled; -+import it.unimi.dsi.fastutil.longs.LongIterator; -+import it.unimi.dsi.fastutil.longs.LongOpenHashSet; -+import it.unimi.dsi.fastutil.longs.LongSet; -+import net.minecraft.core.BlockPos; -+import net.minecraft.nbt.CompoundTag; -+import net.minecraft.nbt.ListTag; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.resources.ResourceLocation; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.server.level.ServerPlayer; -+import net.minecraft.world.level.ChunkPos; -+import net.minecraft.world.level.GameRules; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.chunk.ChunkAccess; -+import net.minecraft.world.level.chunk.LevelChunk; -+import net.minecraft.world.level.chunk.status.ChunkStatus; -+import net.minecraft.world.level.levelgen.structure.Structure; -+import net.minecraft.world.level.levelgen.structure.StructureStart; -+import net.minecraft.world.level.levelgen.structure.pieces.StructurePieceSerializationContext; -+import org.jetbrains.annotations.NotNull; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.LeavesLogger; -+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; -+import org.leavesmc.leaves.protocol.core.LeavesProtocol; -+import org.leavesmc.leaves.protocol.core.ProtocolHandler; -+import org.leavesmc.leaves.protocol.core.ProtocolUtils; -+ -+import java.util.HashMap; -+import java.util.HashSet; -+import java.util.Map; -+import java.util.Set; -+import java.util.UUID; -+import java.util.concurrent.ConcurrentHashMap; -+ -+// Powered by Servux(https://github.com/sakura-ryoko/servux) -+ -+@LeavesProtocol(namespace = "servux") -+public class ServuxStructuresProtocol { -+ -+ public static final int PROTOCOL_VERSION = 2; -+ -+ private static final int updateInterval = 40; -+ private static final int timeout = 30 * 20; -+ -+ public static boolean refreshSpawnMetadata = false; -+ private static int retainDistance; -+ -+ public static final ResourceLocation CHANNEL = ServuxProtocol.id("structures"); -+ -+ private static final Map players = new ConcurrentHashMap<>(); -+ private static final Map> timeouts = new HashMap<>(); -+ -+ @ProtocolHandler.PayloadReceiver(payload = StructuresPayload.class, payloadId = "structures") -+ public static void onPacketReceive(ServerPlayer player, StructuresPayload payload) { -+ if (!LeavesConfig.protocol.servux.structureProtocol) { -+ return; -+ } -+ -+ switch (payload.packetType()) { -+ case PACKET_C2S_STRUCTURES_REGISTER -> onPlayerSubscribed(player); -+ case PACKET_C2S_REQUEST_SPAWN_METADATA -> refreshSpawnMetadata(player); -+ case PACKET_C2S_STRUCTURES_UNREGISTER -> { -+ onPlayerLoggedOut(player); -+ refreshSpawnMetadata(player); -+ } -+ } -+ } -+ -+ @ProtocolHandler.PlayerJoin -+ public static void onPlayerLoggedIn(ServerPlayer player) { -+ if (!LeavesConfig.protocol.servux.structureProtocol) { -+ return; -+ } -+ -+ onPlayerSubscribed(player); -+ } -+ -+ @ProtocolHandler.PlayerLeave -+ public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { -+ if (!LeavesConfig.protocol.servux.structureProtocol) { -+ return; -+ } -+ -+ players.remove(player.getId()); -+ } -+ -+ @ProtocolHandler.Ticker -+ public static void tick() { -+ if (!LeavesConfig.protocol.servux.structureProtocol) { -+ return; -+ } -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ int tickCounter = server.getTickCount(); -+ if ((tickCounter % updateInterval) == 0) { -+ retainDistance = server.getPlayerList().getViewDistance() + 2; -+ for (ServerPlayer player : players.values()) { -+ if (refreshSpawnMetadata) { -+ refreshSpawnMetadata(player); -+ } -+ -+ // TODO DimensionChange -+ refreshTrackedChunks(player, tickCounter); -+ } -+ -+ if (refreshSpawnMetadata) { -+ refreshSpawnMetadata = false; -+ } -+ } -+ } -+ -+ public static void onStartedWatchingChunk(ServerPlayer player, LevelChunk chunk) { -+ if (!LeavesConfig.protocol.servux.structureProtocol) { -+ return; -+ } -+ -+ MinecraftServer server = player.getServer(); -+ -+ if (players.containsKey(player.getId()) && server != null) { -+ addChunkTimeoutIfHasReferences(player.getUUID(), chunk, server.getTickCount()); -+ } -+ } -+ -+ private static void addChunkTimeoutIfHasReferences(final UUID uuid, LevelChunk chunk, final int tickCounter) { -+ final ChunkPos pos = chunk.getPos(); -+ -+ if (chunkHasStructureReferences(pos.x, pos.z, chunk.getLevel())) { -+ final Map map = timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>()); -+ map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter - timeout)); -+ } -+ } -+ -+ private static boolean chunkHasStructureReferences(int chunkX, int chunkZ, Level world) { -+ if (!world.hasChunk(chunkX, chunkZ)) { -+ return false; -+ } -+ -+ ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); -+ -+ for (Map.Entry entry : chunk.getAllReferences().entrySet()) { -+ if (!entry.getValue().isEmpty()) { -+ return true; -+ } -+ } -+ -+ return false; -+ } -+ -+ -+ public static void onPlayerSubscribed(@NotNull ServerPlayer player) { -+ if (!players.containsKey(player.getId())) { -+ players.put(player.getId(), player); -+ } else { -+ LeavesLogger.LOGGER.warning(player.getScoreboardName() + " re-register servux:structures"); -+ } -+ -+ CompoundTag tag = new CompoundTag(); -+ tag.putString("name", "structure_bounding_boxes"); -+ tag.putString("id", CHANNEL.toString()); -+ tag.putInt("version", PROTOCOL_VERSION); -+ tag.putString("servux", ServuxProtocol.SERVUX_STRING); -+ tag.putInt("timeout", timeout); -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ BlockPos spawnPos = server.overworld().levelData.getSpawnPos(); -+ tag.putInt("spawnPosX", spawnPos.getX()); -+ tag.putInt("spawnPosY", spawnPos.getY()); -+ tag.putInt("spawnPosZ", spawnPos.getZ()); -+ tag.putInt("spawnChunkRadius", server.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); -+ -+ sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_METADATA, tag)); -+ initialSyncStructures(player, player.moonrise$getViewDistanceHolder().getViewDistances().sendViewDistance() + 2, server.getTickCount()); -+ } -+ -+ public static void refreshSpawnMetadata(ServerPlayer player) { -+ CompoundTag tag = new CompoundTag(); -+ tag.putString("id", CHANNEL.toString()); -+ tag.putString("servux", ServuxProtocol.SERVUX_STRING); -+ -+ MinecraftServer server = MinecraftServer.getServer(); -+ BlockPos spawnPos = server.overworld().levelData.getSpawnPos(); -+ tag.putInt("spawnPosX", spawnPos.getX()); -+ tag.putInt("spawnPosY", spawnPos.getY()); -+ tag.putInt("spawnPosZ", spawnPos.getZ()); -+ tag.putInt("spawnChunkRadius", server.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); -+ -+ sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_SPAWN_METADATA, tag)); -+ } -+ -+ public static void initialSyncStructures(ServerPlayer player, int chunkRadius, int tickCounter) { -+ UUID uuid = player.getUUID(); -+ ChunkPos center = player.getLastSectionPos().chunk(); -+ Map references = getStructureReferences(player.serverLevel(), center, chunkRadius); -+ -+ timeouts.remove(uuid); -+ -+ sendStructures(player, references, tickCounter); -+ } -+ -+ public static Map getStructureReferences(ServerLevel world, ChunkPos center, int chunkRadius) { -+ Map references = new HashMap<>(); -+ -+ for (int cx = center.x - chunkRadius; cx <= center.x + chunkRadius; ++cx) { -+ for (int cz = center.z - chunkRadius; cz <= center.z + chunkRadius; ++cz) { -+ getReferencesFromChunk(cx, cz, world, references); -+ } -+ } -+ -+ return references; -+ } -+ -+ public static void getReferencesFromChunk(int chunkX, int chunkZ, Level world, Map references) { -+ if (!world.hasChunk(chunkX, chunkZ)) { -+ return; -+ } -+ -+ ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); -+ -+ for (Map.Entry entry : chunk.getAllReferences().entrySet()) { -+ Structure feature = entry.getKey(); -+ LongSet startChunks = entry.getValue(); -+ -+ // TODO add an option && feature != StructureFeature.MINESHAFT (?) -+ if (!startChunks.isEmpty()) { -+ references.merge(feature, startChunks, (oldSet, entrySet) -> { -+ LongOpenHashSet newSet = new LongOpenHashSet(oldSet); -+ newSet.addAll(entrySet); -+ return newSet; -+ }); -+ } -+ } -+ } -+ -+ public static void sendStructures(ServerPlayer player, Map references, int tickCounter) { -+ ServerLevel world = player.serverLevel(); -+ Map starts = getStructureStarts(world, references); -+ -+ if (!starts.isEmpty()) { -+ addOrRefreshTimeouts(player.getUUID(), references, tickCounter); -+ -+ ListTag structureList = getStructureList(starts, world); -+ -+ if (players.containsKey(player.getId())) { -+ CompoundTag test = new CompoundTag(); -+ test.put("Structures", structureList.copy()); -+ sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA_START, test)); -+ } -+ } -+ } -+ -+ public static ListTag getStructureList(Map structures, ServerLevel world) { -+ ListTag list = new ListTag(); -+ StructurePieceSerializationContext ctx = StructurePieceSerializationContext.fromLevel(world); -+ -+ for (Map.Entry entry : structures.entrySet()) { -+ ChunkPos pos = entry.getKey(); -+ list.add(entry.getValue().createTag(ctx, pos)); -+ } -+ -+ return list; -+ } -+ -+ public static Map getStructureStarts(ServerLevel world, Map references) { -+ Map starts = new HashMap<>(); -+ -+ for (Map.Entry entry : references.entrySet()) { -+ Structure structure = entry.getKey(); -+ LongSet startChunks = entry.getValue(); -+ LongIterator iter = startChunks.iterator(); -+ -+ while (iter.hasNext()) { -+ ChunkPos pos = new ChunkPos(iter.nextLong()); -+ -+ if (!world.hasChunk(pos.x, pos.z)) { -+ continue; -+ } -+ -+ ChunkAccess chunk = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS, false); -+ StructureStart start = chunk.getStartForStructure(structure); -+ -+ if (start != null) { -+ starts.put(pos, start); -+ } -+ } -+ } -+ -+ return starts; -+ } -+ -+ public static void refreshTrackedChunks(ServerPlayer player, int tickCounter) { -+ UUID uuid = player.getUUID(); -+ Map map = timeouts.get(uuid); -+ -+ if (map != null) { -+ sendAndRefreshExpiredStructures(player, map, tickCounter); -+ } -+ } -+ -+ public static void sendAndRefreshExpiredStructures(ServerPlayer player, Map map, int tickCounter) { -+ Set positionsToUpdate = new HashSet<>(); -+ -+ for (Map.Entry entry : map.entrySet()) { -+ Timeout out = entry.getValue(); -+ -+ if (out.needsUpdate(tickCounter, timeout)) { -+ positionsToUpdate.add(entry.getKey()); -+ } -+ } -+ -+ if (!positionsToUpdate.isEmpty()) { -+ ServerLevel world = player.serverLevel(); -+ ChunkPos center = player.getLastSectionPos().chunk(); -+ Map references = new HashMap<>(); -+ -+ for (ChunkPos pos : positionsToUpdate) { -+ if (isOutOfRange(pos, center)) { -+ map.remove(pos); -+ } else { -+ getReferencesFromChunk(pos.x, pos.z, world, references); -+ -+ Timeout timeout = map.get(pos); -+ -+ if (timeout != null) { -+ timeout.setLastSync(tickCounter); -+ } -+ } -+ } -+ -+ if (!references.isEmpty()) { -+ sendStructures(player, references, tickCounter); -+ } -+ } -+ } -+ -+ protected static boolean isOutOfRange(ChunkPos pos, ChunkPos center) { -+ return Math.abs(pos.x - center.x) > retainDistance || Math.abs(pos.z - center.z) > retainDistance; -+ } -+ -+ public static void addOrRefreshTimeouts(final UUID uuid, final Map references, final int tickCounter) { -+ Map map = timeouts.computeIfAbsent(uuid, (u) -> new HashMap<>()); -+ -+ for (LongSet chunks : references.values()) { -+ for (Long chunkPosLong : chunks) { -+ final ChunkPos pos = new ChunkPos(chunkPosLong); -+ map.computeIfAbsent(pos, (p) -> new Timeout(tickCounter)).setLastSync(tickCounter); -+ } -+ } -+ } -+ -+ public enum StructuresPayloadType { -+ PACKET_S2C_METADATA(1), -+ PACKET_S2C_STRUCTURE_DATA(2), -+ PACKET_C2S_STRUCTURES_REGISTER(3), -+ PACKET_C2S_STRUCTURES_UNREGISTER(4), -+ PACKET_S2C_STRUCTURE_DATA_START(5), -+ PACKET_S2C_SPAWN_METADATA(10), -+ PACKET_C2S_REQUEST_SPAWN_METADATA(11); -+ -+ private static final class Helper { -+ static Map ID_TO_TYPE = new HashMap<>(); -+ } -+ -+ public final int type; -+ -+ StructuresPayloadType(int type) { -+ this.type = type; -+ Helper.ID_TO_TYPE.put(type, this); -+ } -+ -+ public static StructuresPayloadType fromId(int id) { -+ return Helper.ID_TO_TYPE.get(id); -+ } -+ } -+ -+ public record StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt, FriendlyByteBuf buffer) implements LeavesCustomPayload { -+ -+ public StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt) { -+ this(packetType, nbt, null); -+ } -+ -+ public StructuresPayload(StructuresPayloadType packetType, FriendlyByteBuf buffer) { -+ this(packetType, new CompoundTag(), buffer); -+ } -+ -+ @New -+ private static StructuresPayload decode(ResourceLocation id, FriendlyByteBuf buf) { -+ int i = buf.readVarInt(); -+ StructuresPayloadType type = StructuresPayloadType.fromId(i); -+ -+ if (type == null) { -+ throw new IllegalStateException("invalid packet type received"); -+ } else if (type.equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) { -+ return new StructuresPayload(type, new FriendlyByteBuf(buf.readBytes(buf.readableBytes()))); -+ } else { -+ return new StructuresPayload(type, buf.readNbt()); -+ } -+ } -+ -+ @Override -+ public void write(FriendlyByteBuf buf) { -+ buf.writeVarInt(this.packetType.type); -+ if (this.packetType.equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) { -+ buf.writeBytes(this.buffer.readBytes(this.buffer.readableBytes())); -+ } else { -+ buf.writeNbt(this.nbt); -+ } -+ } -+ -+ @Override -+ public ResourceLocation id() { -+ return CHANNEL; -+ } -+ } -+ -+ public static class Timeout { -+ private int lastSync; -+ -+ public Timeout(int currentTick) { -+ this.lastSync = currentTick; -+ } -+ -+ public boolean needsUpdate(int currentTick, int timeout) { -+ return currentTick - this.lastSync >= timeout; -+ } -+ -+ public void setLastSync(int tickCounter) { -+ this.lastSync = tickCounter; -+ } -+ } -+ -+ public static void sendPacket(ServerPlayer player, StructuresPayload payload) { -+ if (!LeavesConfig.protocol.servux.structureProtocol) { -+ return; -+ } -+ -+ if (payload.packetType() == StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA_START) { -+ FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); -+ buffer.writeNbt(payload.nbt()); -+ PacketSplitter.send(ServuxStructuresProtocol::sendWithSplitter, buffer, player); -+ } else { -+ ProtocolUtils.sendPayloadPacket(player, payload); -+ } -+ } -+ -+ private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { -+ sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA, buf)); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/region/LeavesHooks.java b/src/main/java/org/leavesmc/leaves/region/LeavesHooks.java -index 47b6241ba0a23a0579b8b046f1e45832117b6581..59fe70c95e157824e12d9ebbf02a5da8cd95a107 100644 ---- a/src/main/java/org/leavesmc/leaves/region/LeavesHooks.java -+++ b/src/main/java/org/leavesmc/leaves/region/LeavesHooks.java -@@ -15,5 +15,6 @@ public final class LeavesHooks extends PaperHooks { - @Override - public void onChunkWatch(ServerLevel world, LevelChunk chunk, ServerPlayer player) { - super.onChunkWatch(world, chunk, player); -+ org.leavesmc.leaves.protocol.servux.ServuxStructuresProtocol.onStartedWatchingChunk(player, chunk); // servux - } - } diff --git a/patches/server/0106-Renewable-deepslate.patch b/patches/server/0106-Renewable-deepslate.patch deleted file mode 100644 index 8a3da7ec..00000000 --- a/patches/server/0106-Renewable-deepslate.patch +++ /dev/null @@ -1,32 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 22 Jan 2024 11:15:53 +0800 -Subject: [PATCH] Renewable deepslate - - -diff --git a/src/main/java/net/minecraft/world/level/block/LiquidBlock.java b/src/main/java/net/minecraft/world/level/block/LiquidBlock.java -index a2d023ff011f71f80032f02430a53d6a08a23623..3efec7e75c9666182ce728f5d1ba2f645a5a2386 100644 ---- a/src/main/java/net/minecraft/world/level/block/LiquidBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/LiquidBlock.java -@@ -193,7 +193,7 @@ public class LiquidBlock extends Block implements BucketPickup { - BlockPos blockposition1 = pos.relative(enumdirection.getOpposite()); - - if (world.getFluidState(blockposition1).is(FluidTags.WATER)) { -- Block block = world.getFluidState(pos).isSource() ? Blocks.OBSIDIAN : Blocks.COBBLESTONE; -+ Block block = world.getFluidState(pos).isSource() ? Blocks.OBSIDIAN : (org.leavesmc.leaves.LeavesConfig.modify.renewableDeepslate && world.dimension() == Level.OVERWORLD && pos.getY() < 0 ? Blocks.COBBLED_DEEPSLATE : Blocks.COBBLESTONE); // Leaves - renewable deepslate - - // CraftBukkit start - if (org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world, pos, block.defaultBlockState())) { -diff --git a/src/main/java/net/minecraft/world/level/material/LavaFluid.java b/src/main/java/net/minecraft/world/level/material/LavaFluid.java -index 884db3e64cb22ed765beec8f11ea309fcf810207..1562a7a6a026115691bec2157fe4516db0b4a893 100644 ---- a/src/main/java/net/minecraft/world/level/material/LavaFluid.java -+++ b/src/main/java/net/minecraft/world/level/material/LavaFluid.java -@@ -212,7 +212,7 @@ public abstract class LavaFluid extends FlowingFluid { - if (this.is(FluidTags.LAVA) && fluid1.is(FluidTags.WATER)) { - if (state.getBlock() instanceof LiquidBlock) { - // CraftBukkit start -- if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world.getMinecraftWorld(), pos, Blocks.STONE.defaultBlockState(), 3)) { -+ if (!org.bukkit.craftbukkit.event.CraftEventFactory.handleBlockFormEvent(world.getMinecraftWorld(), pos, org.leavesmc.leaves.LeavesConfig.modify.renewableDeepslate && world.getMinecraftWorld().dimension() == Level.OVERWORLD && pos.getY() < 0 ? Blocks.DEEPSLATE.defaultBlockState() : Blocks.STONE.defaultBlockState(), 3)) { // Leaves - renewable deepslate - return; - } - // CraftBukkit end diff --git a/patches/server/0108-Renewable-coral.patch b/patches/server/0108-Renewable-coral.patch deleted file mode 100644 index 09c764be..00000000 --- a/patches/server/0108-Renewable-coral.patch +++ /dev/null @@ -1,148 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Tue, 23 Jan 2024 01:08:41 +0800 -Subject: [PATCH] Renewable coral - - -diff --git a/src/main/java/net/minecraft/world/level/block/CoralFanBlock.java b/src/main/java/net/minecraft/world/level/block/CoralFanBlock.java -index 09849438e2170d8b2fbc82ee7dfc5df28ce91cc9..ea788e532b6964bd16ac9297064657df775e2323 100644 ---- a/src/main/java/net/minecraft/world/level/block/CoralFanBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/CoralFanBlock.java -@@ -14,7 +14,7 @@ import net.minecraft.world.level.block.state.BlockState; - import net.minecraft.world.level.material.Fluid; - import net.minecraft.world.level.material.Fluids; - --public class CoralFanBlock extends BaseCoralFanBlock { -+public class CoralFanBlock extends BaseCoralFanBlock implements org.leavesmc.leaves.util.FertilizableCoral { // Leaves - renewable coral - - public static final MapCodec CODEC = RecordCodecBuilder.mapCodec((instance) -> { - return instance.group(CoralBlock.DEAD_CORAL_FIELD.forGetter((blockcoralfan) -> { -@@ -64,4 +64,11 @@ public class CoralFanBlock extends BaseCoralFanBlock { - return super.updateShape(state, world, tickView, pos, direction, neighborPos, neighborState, random); - } - } -+ -+ // Leaves start - renewable coral -+ @Override -+ public boolean isEnabled() { -+ return org.leavesmc.leaves.LeavesConfig.modify.renewableCoral == org.leavesmc.leaves.LeavesConfig.ModifyConfig.RenewableCoralType.EXPANDED; -+ } -+ // Leaves end - renewable coral - } -diff --git a/src/main/java/net/minecraft/world/level/block/CoralPlantBlock.java b/src/main/java/net/minecraft/world/level/block/CoralPlantBlock.java -index 5200ceb141795dd7d4d28d861996ce360adeae01..954b2b19bdc19c18c9cdb9a867d6d9407367713b 100644 ---- a/src/main/java/net/minecraft/world/level/block/CoralPlantBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/CoralPlantBlock.java -@@ -17,7 +17,7 @@ import net.minecraft.world.level.material.Fluids; - import net.minecraft.world.phys.shapes.CollisionContext; - import net.minecraft.world.phys.shapes.VoxelShape; - --public class CoralPlantBlock extends BaseCoralPlantTypeBlock { -+public class CoralPlantBlock extends BaseCoralPlantTypeBlock implements org.leavesmc.leaves.util.FertilizableCoral { // Leaves - renewable coral - - public static final MapCodec CODEC = RecordCodecBuilder.mapCodec((instance) -> { - return instance.group(CoralBlock.DEAD_CORAL_FIELD.forGetter((blockcoralplant) -> { -@@ -74,4 +74,12 @@ public class CoralPlantBlock extends BaseCoralPlantTypeBlock { - protected VoxelShape getShape(BlockState state, BlockGetter world, BlockPos pos, CollisionContext context) { - return CoralPlantBlock.SHAPE; - } -+ -+ // Leaves start - renewable coral -+ @Override -+ public boolean isEnabled() { -+ return org.leavesmc.leaves.LeavesConfig.modify.renewableCoral == org.leavesmc.leaves.LeavesConfig.ModifyConfig.RenewableCoralType.EXPANDED -+ || org.leavesmc.leaves.LeavesConfig.modify.renewableCoral == org.leavesmc.leaves.LeavesConfig.ModifyConfig.RenewableCoralType.TRUE; -+ } -+ // Leaves end - renewable coral - } -diff --git a/src/main/java/net/minecraft/world/level/levelgen/feature/CoralFeature.java b/src/main/java/net/minecraft/world/level/levelgen/feature/CoralFeature.java -index 7bc5ff8eb1174834dcc27363af4a5cef19017b3d..790686428b01127d0f94e044f0ec4bff4127f79e 100644 ---- a/src/main/java/net/minecraft/world/level/levelgen/feature/CoralFeature.java -+++ b/src/main/java/net/minecraft/world/level/levelgen/feature/CoralFeature.java -@@ -31,7 +31,7 @@ public abstract class CoralFeature extends Feature { - return !optional.isEmpty() && this.placeFeature(worldGenLevel, randomSource, blockPos, optional.get().defaultBlockState()); - } - -- protected abstract boolean placeFeature(LevelAccessor world, RandomSource random, BlockPos pos, BlockState state); -+ public abstract boolean placeFeature(LevelAccessor world, RandomSource random, BlockPos pos, BlockState state); // Leaves - protected -> public - - protected boolean placeCoralBlock(LevelAccessor world, RandomSource random, BlockPos pos, BlockState state) { - BlockPos blockPos = pos.above(); -diff --git a/src/main/java/org/leavesmc/leaves/util/FertilizableCoral.java b/src/main/java/org/leavesmc/leaves/util/FertilizableCoral.java -new file mode 100644 -index 0000000000000000000000000000000000000000..ec5aa65df54e2467d594a1885dbda850cee5a886 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/util/FertilizableCoral.java -@@ -0,0 +1,72 @@ -+package org.leavesmc.leaves.util; -+ -+import net.minecraft.core.BlockPos; -+import net.minecraft.core.Holder; -+import net.minecraft.core.HolderSet; -+import net.minecraft.core.registries.Registries; -+import net.minecraft.server.level.ServerLevel; -+import net.minecraft.tags.BlockTags; -+import net.minecraft.tags.FluidTags; -+import net.minecraft.util.RandomSource; -+import net.minecraft.world.level.Level; -+import net.minecraft.world.level.LevelReader; -+import net.minecraft.world.level.block.BaseCoralPlantTypeBlock; -+import net.minecraft.world.level.block.Block; -+import net.minecraft.world.level.block.Blocks; -+import net.minecraft.world.level.block.BonemealableBlock; -+import net.minecraft.world.level.block.state.BlockState; -+import net.minecraft.world.level.levelgen.feature.CoralClawFeature; -+import net.minecraft.world.level.levelgen.feature.CoralFeature; -+import net.minecraft.world.level.levelgen.feature.CoralMushroomFeature; -+import net.minecraft.world.level.levelgen.feature.CoralTreeFeature; -+import net.minecraft.world.level.levelgen.feature.configurations.NoneFeatureConfiguration; -+import net.minecraft.world.level.material.MapColor; -+import org.jetbrains.annotations.NotNull; -+ -+// Powered by fabric-carpet/src/main/java/carpet/helpers/FertilizableCoral.java -+public interface FertilizableCoral extends BonemealableBlock { -+ -+ boolean isEnabled(); -+ -+ @Override -+ default boolean isValidBonemealTarget(@NotNull LevelReader world, @NotNull BlockPos pos, @NotNull BlockState state) { -+ return isEnabled() && state.getValue(BaseCoralPlantTypeBlock.WATERLOGGED) && world.getFluidState(pos.above()).is(FluidTags.WATER); -+ } -+ -+ @Override -+ default boolean isBonemealSuccess(@NotNull Level world, RandomSource random, @NotNull BlockPos pos, @NotNull BlockState state) { -+ return random.nextFloat() < 0.15D; -+ } -+ -+ @Override -+ default void performBonemeal(@NotNull ServerLevel worldIn, RandomSource random, @NotNull BlockPos pos, @NotNull BlockState blockUnder) { -+ int variant = random.nextInt(3); -+ CoralFeature coral = switch (variant) { -+ case 0 -> new CoralClawFeature(NoneFeatureConfiguration.CODEC); -+ case 1 -> new CoralTreeFeature(NoneFeatureConfiguration.CODEC); -+ default -> new CoralMushroomFeature(NoneFeatureConfiguration.CODEC); -+ }; -+ -+ MapColor color = blockUnder.getMapColor(worldIn, pos); -+ BlockState properBlock = blockUnder; -+ HolderSet.Named coralBlocks = worldIn.registryAccess().lookupOrThrow(Registries.BLOCK).getOrThrow(BlockTags.CORAL_BLOCKS); -+ for (Holder block : coralBlocks) { -+ properBlock = block.value().defaultBlockState(); -+ if (properBlock.getMapColor(worldIn, pos) == color) { -+ break; -+ } -+ } -+ worldIn.setBlock(pos, Blocks.WATER.defaultBlockState(), Block.UPDATE_NONE); -+ -+ if (!coral.placeFeature(worldIn, random, pos, properBlock)) { -+ worldIn.setBlock(pos, blockUnder, 3); -+ } else { -+ if (worldIn.random.nextInt(10) == 0) { -+ BlockPos randomPos = pos.offset(worldIn.random.nextInt(16) - 8, worldIn.random.nextInt(8), worldIn.random.nextInt(16) - 8); -+ if (coralBlocks.contains(worldIn.getBlockState(randomPos).getBlockHolder())) { -+ worldIn.setBlock(randomPos, Blocks.WET_SPONGE.defaultBlockState(), Block.UPDATE_ALL); -+ } -+ } -+ } -+ } -+} diff --git a/patches/server/0109-Fast-resume.patch b/patches/server/0109-Fast-resume.patch deleted file mode 100644 index 208f0e93..00000000 --- a/patches/server/0109-Fast-resume.patch +++ /dev/null @@ -1,261 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 25 Jan 2024 01:16:49 +0800 -Subject: [PATCH] Fast resume - - -diff --git a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java -index 91a6f57f35fc1553159cca138a0619e703b2b014..32a6f6e0d05ab43e1d5b48c45e55119d3cd65321 100644 ---- a/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java -+++ b/src/main/java/ca/spottedleaf/moonrise/patches/chunk_system/scheduling/ChunkHolderManager.java -@@ -602,6 +602,49 @@ public final class ChunkHolderManager { - } - } - -+ // Leaves start - add custom ticket -+ public void addTicketAtLevelCustom(final Ticket ticket, final long chunk, final boolean lock) { -+ final long removeDelay = ticket.moonrise$getRemoveDelay(); -+ -+ final int chunkX = CoordinateUtils.getChunkX(chunk); -+ final int chunkZ = CoordinateUtils.getChunkZ(chunk); -+ -+ final ReentrantAreaLock.Node ticketLock = lock ? this.ticketLockArea.lock(chunkX, chunkZ) : null; -+ try { -+ final SortedArraySet> ticketsAtChunk = this.tickets.computeIfAbsent(chunk, (final long keyInMap) -> SortedArraySet.create(4)); -+ -+ final int levelBefore = getTicketLevelAt(ticketsAtChunk); -+ final Ticket current = (Ticket)((ChunkSystemSortedArraySet>)ticketsAtChunk).moonrise$replace(ticket); -+ final int levelAfter = getTicketLevelAt(ticketsAtChunk); -+ -+ if (current != ticket) { -+ final long oldRemoveDelay = ((ChunkSystemTicket) current).moonrise$getRemoveDelay(); -+ if (removeDelay != oldRemoveDelay) { -+ if (oldRemoveDelay != NO_TIMEOUT_MARKER && removeDelay == NO_TIMEOUT_MARKER) { -+ this.removeExpireCount(chunkX, chunkZ); -+ } else if (oldRemoveDelay == NO_TIMEOUT_MARKER) { -+ // since old != new, we have that NO_TIMEOUT_MARKER != new -+ this.addExpireCount(chunkX, chunkZ); -+ } -+ } -+ } else { -+ if (removeDelay != NO_TIMEOUT_MARKER) { -+ this.addExpireCount(chunkX, chunkZ); -+ } -+ } -+ -+ if (levelBefore != levelAfter) { -+ this.updateTicketLevel(chunk, levelAfter); -+ } -+ -+ } finally { -+ if (ticketLock != null) { -+ this.ticketLockArea.unlock(ticketLock); -+ } -+ } -+ } -+ // Leaves end - add custom ticket -+ - public boolean removeTicketAtLevel(final TicketType type, final ChunkPos chunkPos, final int level, final T identifier) { - return this.removeTicketAtLevel(type, CoordinateUtils.getChunkKey(chunkPos), level, identifier); - } -diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java -index 94c239ccf8d5ebca84810509abd13db1badfe008..1ca8771eb18c04d14c69b08554f0e697b29a0347 100644 ---- a/src/main/java/net/minecraft/server/MinecraftServer.java -+++ b/src/main/java/net/minecraft/server/MinecraftServer.java -@@ -766,6 +766,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop> NEED_SAVED = Set.of(TicketType.PLAYER, TicketType.PORTAL, RegionizedPlayerChunkLoader.PLAYER_TICKET); -+ -+ public static void tryToLoadTickets() { -+ if (!LeavesConfig.modify.fastResume) { -+ return; -+ } -+ -+ File file = MinecraftServer.getServer().getWorldPath(LevelResource.ROOT).resolve("chunk_tickets.leaves.json").toFile(); -+ if (file.isFile()) { -+ try (BufferedReader bfr = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { -+ JsonObject json = new Gson().fromJson(bfr, JsonObject.class); -+ loadSavedChunkTickets(json); -+ if (!file.delete()) { -+ throw new IOException(); -+ } -+ } catch (IOException e) { -+ LeavesLogger.LOGGER.severe("Failed to load saved chunk tickets file", e); -+ } -+ } -+ } -+ -+ public static void tryToSaveTickets() { -+ if (!LeavesConfig.modify.fastResume) { -+ return; -+ } -+ -+ File file = MinecraftServer.getServer().getWorldPath(LevelResource.ROOT).resolve("chunk_tickets.leaves.json").toFile(); -+ try (BufferedWriter bfw = Files.newBufferedWriter(file.toPath(), StandardCharsets.UTF_8)) { -+ bfw.write(new Gson().toJson(getSavedChunkTickets())); -+ } catch (IOException e) { -+ LeavesLogger.LOGGER.severe("Failed to save chunk tickets file", e); -+ } -+ } -+ -+ public static void loadSavedChunkTickets(JsonObject json) { -+ MinecraftServer server = MinecraftServer.getServer(); -+ for (String worldKey : json.keySet()) { -+ ResourceLocation dimensionKey = ResourceLocation.tryParse(worldKey); -+ if (dimensionKey == null) { -+ continue; -+ } -+ -+ ServerLevel level = server.getLevel(ResourceKey.create(Registries.DIMENSION, dimensionKey)); -+ if (level == null) { -+ continue; -+ } -+ -+ DistanceManager chunkDistanceManager = level.getChunkSource().chunkMap.distanceManager; -+ for (JsonElement chunkElement : json.get(worldKey).getAsJsonArray()) { -+ JsonObject chunkJson = (JsonObject) chunkElement; -+ long chunkKey = chunkJson.get("key").getAsLong(); -+ -+ for (JsonElement ticketElement : chunkJson.get("tickets").getAsJsonArray()) { -+ Ticket ticket = tickFormJson((JsonObject) ticketElement); -+ chunkDistanceManager.moonrise$getChunkHolderManager().addTicketAtLevelCustom(ticket, chunkKey, true); -+ } -+ } -+ } -+ } -+ -+ public static JsonObject getSavedChunkTickets() { -+ JsonObject json = new JsonObject(); -+ -+ for (ServerLevel level : MinecraftServer.getServer().getAllLevels()) { -+ JsonArray levelArray = new JsonArray(); -+ DistanceManager chunkDistanceManager = level.getChunkSource().chunkMap.distanceManager; -+ -+ for (Long2ObjectMap.Entry>> chunkTickets : chunkDistanceManager.moonrise$getChunkHolderManager().getTicketsCopy().long2ObjectEntrySet()) { -+ long chunkKey = chunkTickets.getLongKey(); -+ JsonArray ticketArray = new JsonArray(); -+ SortedArraySet> tickets = chunkTickets.getValue(); -+ -+ for (Ticket ticket : tickets) { -+ if (!NEED_SAVED.contains(ticket.getType())) { -+ continue; -+ } -+ -+ ticketArray.add(ticketToJson(ticket)); -+ } -+ -+ if (!ticketArray.isEmpty()) { -+ JsonObject chunkJson = new JsonObject(); -+ chunkJson.addProperty("key", chunkKey); -+ chunkJson.add("tickets", ticketArray); -+ levelArray.add(chunkJson); -+ } -+ } -+ -+ if (!levelArray.isEmpty()) { -+ json.add(level.dimension().location().toString(), levelArray); -+ } -+ } -+ -+ return json; -+ } -+ -+ private static JsonObject ticketToJson(Ticket ticket) { -+ JsonObject json = new JsonObject(); -+ json.addProperty("type", ticket.getType().toString()); -+ json.addProperty("ticketLevel", ticket.getTicketLevel()); -+ json.addProperty("removeDelay", ticket.moonrise$getRemoveDelay()); -+ if (ticket.key instanceof BlockPos pos) { -+ json.addProperty("key", pos.asLong()); -+ } else if (ticket.key instanceof ChunkPos pos) { -+ json.addProperty("key", pos.toLong()); -+ } else if (ticket.key instanceof Long l) { -+ json.addProperty("key", l); -+ } -+ return json; -+ } -+ -+ private static Ticket tickFormJson(JsonObject json) { -+ TicketType ticketType = null; -+ Object key = null; -+ switch (json.get("type").getAsString()) { -+ case "player" -> { -+ ticketType = TicketType.PLAYER; -+ key = new ChunkPos(json.get("key").getAsLong()); -+ } -+ case "portal" -> { -+ ticketType = TicketType.PORTAL; -+ key = BlockPos.of(json.get("key").getAsLong()); -+ } -+ case "chunk_system:player_ticket" -> { -+ ticketType = RegionizedPlayerChunkLoader.PLAYER_TICKET; -+ key = json.get("key").getAsLong(); -+ } -+ } -+ -+ if (ticketType == null) { -+ throw new IllegalArgumentException("Cant convert " + json.get("type").getAsString() + ", report it ???"); -+ } -+ -+ int ticketLevel = json.get("ticketLevel").getAsInt(); -+ long removeDelay = json.get("removeDelay").getAsLong(); -+ @SuppressWarnings("unchecked") -+ Ticket ticket = new Ticket<>((TicketType) ticketType, ticketLevel, (T) key); -+ ticket.moonrise$setRemoveDelay(removeDelay); -+ -+ return ticket; -+ } -+} diff --git a/patches/server/0112-Fix-falling-block-s-block-location.patch b/patches/server/0112-Fix-falling-block-s-block-location.patch deleted file mode 100644 index 45c0df32..00000000 --- a/patches/server/0112-Fix-falling-block-s-block-location.patch +++ /dev/null @@ -1,25 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Fri, 16 Feb 2024 19:50:03 +0800 -Subject: [PATCH] Fix falling block's block location - - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index c317035db4f7456988dd02b72729ec6ad7703a7c..7dfa1dc42224eafc2196a532a00dc606d35b8d89 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -5074,6 +5074,14 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - int j = Mth.floor(y); - int k = Mth.floor(z); - -+ // Leaves start - fix falling block location -+ if (this instanceof net.minecraft.world.entity.item.FallingBlockEntity) { -+ if (y < 0.0 && y + 1e-10 > 0.0) { -+ j = 0; -+ } -+ } -+ // Leaves end - fix falling block location -+ - if (i != this.blockPosition.getX() || j != this.blockPosition.getY() || k != this.blockPosition.getZ()) { - this.blockPosition = new BlockPos(i, j, k); - this.inBlockState = null; diff --git a/patches/server/0113-Bytebuf-API.patch b/patches/server/0113-Bytebuf-API.patch deleted file mode 100644 index f556c894..00000000 --- a/patches/server/0113-Bytebuf-API.patch +++ /dev/null @@ -1,649 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> -Date: Wed, 22 May 2024 10:12:22 +0800 -Subject: [PATCH] Bytebuf API - - -diff --git a/src/main/java/net/minecraft/network/chat/Component.java b/src/main/java/net/minecraft/network/chat/Component.java -index 5941800692ed06e17925ec6526ea38793a65de12..f77b2e12fb43039cf16a8511c9820859874c5c97 100644 ---- a/src/main/java/net/minecraft/network/chat/Component.java -+++ b/src/main/java/net/minecraft/network/chat/Component.java -@@ -302,7 +302,7 @@ public interface Component extends Message, FormattedText, Iterable { - return (MutableComponent) ComponentSerialization.CODEC.parse(registries.createSerializationContext(JsonOps.INSTANCE), json).getOrThrow(JsonParseException::new); - } - -- static JsonElement serialize(Component text, HolderLookup.Provider registries) { -+ public static JsonElement serialize(Component text, HolderLookup.Provider registries) { // Leaves - package -> public - return (JsonElement) ComponentSerialization.CODEC.encodeStart(registries.createSerializationContext(JsonOps.INSTANCE), text).getOrThrow(JsonParseException::new); - } - -diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java -index 13dea31c8232cdb08aff09627e84711d2ef0aa6b..44371c123e4cdd2d5257cfeaaf3139b20e785769 100644 ---- a/src/main/java/net/minecraft/server/players/PlayerList.java -+++ b/src/main/java/net/minecraft/server/players/PlayerList.java -@@ -471,6 +471,12 @@ public abstract class PlayerList { - return; - } - -+ // Leaves start - Bytebuf API -+ if (!(player instanceof ServerBot) && !(player instanceof ServerPhotographer)) { -+ this.cserver.getBytebufHandler().injectPlayer(player); -+ } -+ // Leaves end - Bytebuf API -+ - org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); // Leaves - protocol - - // Leaves start - bot support -diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -index ea4def367b645a442e7ee4fd8b1b2075b8604ccb..54d99e5b42082c306e0ac16b2b5ba9ff3c43a1b0 100644 ---- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java -+++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java -@@ -315,6 +315,7 @@ public final class CraftServer implements Server { - public final io.papermc.paper.SparksFly spark; // Paper - spark - private final org.leavesmc.leaves.entity.CraftBotManager botManager; // Leaves - private final org.leavesmc.leaves.entity.CraftPhotographerManager photographerManager = new org.leavesmc.leaves.entity.CraftPhotographerManager(); // Leaves -+ private final org.leavesmc.leaves.bytebuf.internal.InternalBytebufHandler internalBytebufHandler = new org.leavesmc.leaves.bytebuf.internal.InternalBytebufHandler(); // Leaves - - // Paper start - Folia region threading API - private final io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler regionizedScheduler = new io.papermc.paper.threadedregions.scheduler.FallbackRegionScheduler(); -@@ -3298,4 +3299,15 @@ public final class CraftServer implements Server { - return photographerManager; - } - // Leaves end - replay mod api -+ -+ // Leaves start - Bytebuf API -+ @Override -+ public org.leavesmc.leaves.bytebuf.BytebufManager getBytebufManager() { -+ return internalBytebufHandler.getManager(); -+ } -+ -+ public org.leavesmc.leaves.bytebuf.internal.InternalBytebufHandler getBytebufHandler() { -+ return internalBytebufHandler; -+ } -+ // Leaves end - Bytebuf API - } -diff --git a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -index d0132751fb057dc29e13ae3489beedb580225fa7..723db803297e06c053e160fd593dbed597eab82c 100644 ---- a/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -+++ b/src/main/java/org/bukkit/craftbukkit/entity/CraftPlayer.java -@@ -3580,4 +3580,16 @@ public class CraftPlayer extends CraftHumanEntity implements Player { - this.getHandle().connection.send(new net.minecraft.network.protocol.game.ClientboundEntityEventPacket(((CraftEntity) target).getHandle(), effect.getData())); - } - // Paper end - entity effect API -+ -+ // Leaves start - Bytebuf API -+ @Override -+ public void sendPacket(org.leavesmc.leaves.bytebuf.packet.Packet packet) { -+ this.server.getBytebufHandler().applyPacketToPlayer(this.getHandle(), packet); -+ } -+ -+ @Override -+ public void sendPacket(org.leavesmc.leaves.bytebuf.Bytebuf bytebuf, org.leavesmc.leaves.bytebuf.packet.PacketType type) { -+ this.server.getBytebufHandler().applyPacketToPlayer(this.getHandle(), new org.leavesmc.leaves.bytebuf.packet.Packet(type, bytebuf)); -+ } -+ // Leaves end - Bytebuf API - } -diff --git a/src/main/java/org/leavesmc/leaves/bytebuf/SimpleBytebufManager.java b/src/main/java/org/leavesmc/leaves/bytebuf/SimpleBytebufManager.java -new file mode 100644 -index 0000000000000000000000000000000000000000..443f7f6e0b8d40eaafb8009b3b7e405c6ec78d02 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bytebuf/SimpleBytebufManager.java -@@ -0,0 +1,35 @@ -+package org.leavesmc.leaves.bytebuf; -+ -+import io.netty.buffer.Unpooled; -+import org.bukkit.plugin.Plugin; -+import org.leavesmc.leaves.bytebuf.internal.InternalBytebufHandler; -+import org.leavesmc.leaves.bytebuf.packet.PacketListener; -+ -+public class SimpleBytebufManager implements BytebufManager { -+ -+ private final InternalBytebufHandler internal; -+ -+ public SimpleBytebufManager(InternalBytebufHandler internal) { -+ this.internal = internal; -+ } -+ -+ @Override -+ public void registerListener(Plugin plugin, PacketListener listener) { -+ internal.listenerMap.put(listener, plugin); -+ } -+ -+ @Override -+ public void unregisterListener(Plugin plugin, PacketListener listener) { -+ internal.listenerMap.remove(listener); -+ } -+ -+ @Override -+ public Bytebuf newBytebuf(int size) { -+ return new WrappedBytebuf(Unpooled.buffer(size)); -+ } -+ -+ @Override -+ public Bytebuf toBytebuf(byte[] bytes) { -+ return new WrappedBytebuf(Unpooled.wrappedBuffer(bytes)); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bytebuf/WrappedBytebuf.java b/src/main/java/org/leavesmc/leaves/bytebuf/WrappedBytebuf.java -new file mode 100644 -index 0000000000000000000000000000000000000000..9601eb5aa8cbcf1836cba7853af9ece2d93ec9c3 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bytebuf/WrappedBytebuf.java -@@ -0,0 +1,277 @@ -+package org.leavesmc.leaves.bytebuf; -+ -+import com.google.gson.JsonElement; -+import io.netty.buffer.ByteBuf; -+import net.minecraft.core.RegistryAccess; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.chat.Component; -+import net.minecraft.network.chat.ComponentSerialization; -+import net.minecraft.server.MinecraftServer; -+import org.bukkit.inventory.ItemStack; -+import org.bukkit.craftbukkit.inventory.CraftItemStack; -+ -+import java.util.List; -+import java.util.UUID; -+ -+public class WrappedBytebuf implements Bytebuf { -+ -+ private final FriendlyByteBuf buf; -+ private final RegistryFriendlyByteBuf registryBuf; -+ -+ public WrappedBytebuf(ByteBuf buf) { -+ if (buf instanceof RegistryFriendlyByteBuf) { -+ this.buf = (FriendlyByteBuf) buf; -+ this.registryBuf = (RegistryFriendlyByteBuf) buf; -+ } else { -+ this.buf = new FriendlyByteBuf(buf); -+ this.registryBuf = new RegistryFriendlyByteBuf(this.buf, MinecraftServer.getServer().registryAccess()); -+ } -+ } -+ -+ public RegistryFriendlyByteBuf getRegistryBuf() { -+ return registryBuf; -+ } -+ -+ @Override -+ public byte[] toArray() { -+ int length = buf.readableBytes(); -+ byte[] data = new byte[length]; -+ buf.getBytes(buf.readerIndex(), data); -+ return data; -+ } -+ -+ @Override -+ public Bytebuf skipBytes(int i) { -+ buf.skipBytes(i); -+ return this; -+ } -+ -+ @Override -+ public int readerIndex() { -+ return buf.readerIndex(); -+ } -+ -+ @Override -+ public Bytebuf readerIndex(int i) { -+ buf.readerIndex(i); -+ return this; -+ } -+ -+ @Override -+ public int writerIndex() { -+ return buf.writerIndex(); -+ } -+ -+ @Override -+ public Bytebuf writerIndex(int i) { -+ buf.writerIndex(i); -+ return this; -+ } -+ -+ @Override -+ public Bytebuf resetReaderIndex() { -+ buf.resetReaderIndex(); -+ return this; -+ } -+ -+ @Override -+ public Bytebuf resetWriterIndex() { -+ buf.resetWriterIndex(); -+ return this; -+ } -+ -+ @Override -+ public Bytebuf writeByte(int i) { -+ buf.writeByte(i); -+ return this; -+ } -+ -+ @Override -+ public byte readByte() { -+ return buf.readByte(); -+ } -+ -+ @Override -+ public Bytebuf writeBoolean(boolean b) { -+ buf.writeBoolean(b); -+ return this; -+ } -+ -+ @Override -+ public boolean readBoolean() { -+ return buf.readBoolean(); -+ } -+ -+ @Override -+ public Bytebuf writeFloat(float f) { -+ buf.writeFloat(f); -+ return this; -+ } -+ -+ @Override -+ public float readFloat() { -+ return buf.readFloat(); -+ } -+ -+ @Override -+ public Bytebuf writeDouble(double d) { -+ buf.writeDouble(d); -+ return this; -+ } -+ -+ @Override -+ public double readDouble() { -+ return buf.readDouble(); -+ } -+ -+ @Override -+ public Bytebuf writeShort(int i) { -+ buf.writeShort(i); -+ return this; -+ } -+ -+ @Override -+ public short readShort() { -+ return buf.readShort(); -+ } -+ -+ @Override -+ public Bytebuf writeInt(int i) { -+ buf.writeShort(i); -+ return this; -+ } -+ -+ @Override -+ public int readInt() { -+ return buf.readInt(); -+ } -+ -+ @Override -+ public Bytebuf writeLong(long i) { -+ buf.writeLong(i); -+ return this; -+ } -+ -+ @Override -+ public long readLong() { -+ return buf.readLong(); -+ } -+ -+ @Override -+ public Bytebuf writeVarInt(int i) { -+ this.buf.writeVarInt(i); -+ return this; -+ } -+ -+ @Override -+ public int readVarInt() { -+ return this.buf.readVarInt(); -+ } -+ -+ @Override -+ public Bytebuf writeVarLong(long i) { -+ this.buf.writeVarLong(i); -+ return this; -+ } -+ -+ @Override -+ public long readVarLong() { -+ return this.buf.readVarLong(); -+ } -+ -+ @Override -+ public Bytebuf writeUUID(UUID uuid) { -+ this.buf.writeUUID(uuid); -+ return this; -+ } -+ -+ @Override -+ public UUID readUUID() { -+ return this.buf.readUUID(); -+ } -+ -+ @Override -+ public Bytebuf writeEnum(Enum instance) { -+ this.buf.writeEnum(instance); -+ return this; -+ } -+ -+ @Override -+ public > T readEnum(Class enumClass) { -+ return this.buf.readEnum(enumClass); -+ } -+ -+ @Override -+ public Bytebuf writeUTFString(String utf) { -+ buf.writeUtf(utf); -+ return this; -+ } -+ -+ @Override -+ public String readUTFString() { -+ return buf.readUtf(); -+ } -+ -+ @Override -+ public Bytebuf writeComponentPlain(String str) { -+ ComponentSerialization.STREAM_CODEC.encode(new RegistryFriendlyByteBuf(this.buf, RegistryAccess.EMPTY), Component.literal(str)); -+ return this; -+ } -+ -+ @Override -+ public String readComponentPlain() { -+ return ComponentSerialization.STREAM_CODEC.decode(new RegistryFriendlyByteBuf(buf, RegistryAccess.EMPTY)).getString(); -+ } -+ -+ @Override -+ public Bytebuf writeComponentJson(JsonElement json) { -+ Component component = Component.Serializer.fromJson(json, RegistryAccess.EMPTY); -+ if (component == null) { -+ throw new IllegalArgumentException("Null can not be serialize to Minecraft chat component"); -+ } -+ ComponentSerialization.STREAM_CODEC.encode(new RegistryFriendlyByteBuf(buf, RegistryAccess.EMPTY), component); -+ return this; -+ } -+ -+ @Override -+ public JsonElement readComponentJson() { -+ return Component.Serializer.serialize(ComponentSerialization.STREAM_CODEC.decode(new RegistryFriendlyByteBuf(buf, RegistryAccess.EMPTY)), RegistryAccess.EMPTY); -+ } -+ -+ @Override -+ public Bytebuf writeItemStack(ItemStack itemStack) { -+ net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.unwrap(itemStack); -+ net.minecraft.world.item.ItemStack.OPTIONAL_STREAM_CODEC.encode(this.registryBuf, nmsItem); -+ return this; -+ } -+ -+ @Override -+ public ItemStack readItemStack() { -+ net.minecraft.world.item.ItemStack nmsItem = net.minecraft.world.item.ItemStack.OPTIONAL_STREAM_CODEC.decode(this.registryBuf); -+ return nmsItem.asBukkitMirror(); -+ } -+ -+ @Override -+ public Bytebuf writeItemStackList(List itemStacks) { -+ List nmsItemList = itemStacks.stream().map(CraftItemStack::unwrap).toList(); -+ net.minecraft.world.item.ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(this.registryBuf, nmsItemList); -+ return this; -+ } -+ -+ @Override -+ public List readItemStackList() { -+ List nmsItemList = net.minecraft.world.item.ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(this.registryBuf); -+ return nmsItemList.stream().map(net.minecraft.world.item.ItemStack::asBukkitMirror).toList(); -+ } -+ -+ @Override -+ public Bytebuf copy() { -+ return new WrappedBytebuf(this.buf.copy()); -+ } -+ -+ @Override -+ public boolean release() { -+ return this.buf.release(); -+ } -+} -diff --git a/src/main/java/org/leavesmc/leaves/bytebuf/internal/InternalBytebufHandler.java b/src/main/java/org/leavesmc/leaves/bytebuf/internal/InternalBytebufHandler.java -new file mode 100644 -index 0000000000000000000000000000000000000000..04dbb4cb7c3ea4979c33dba595208e22e6552b0a ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/bytebuf/internal/InternalBytebufHandler.java -@@ -0,0 +1,234 @@ -+package org.leavesmc.leaves.bytebuf.internal; -+ -+import com.google.common.cache.Cache; -+import com.google.common.cache.CacheBuilder; -+import com.google.common.collect.ImmutableMap; -+import io.netty.buffer.Unpooled; -+import io.netty.channel.ChannelDuplexHandler; -+import io.netty.channel.ChannelHandlerContext; -+import io.netty.channel.ChannelPromise; -+import net.minecraft.network.Connection; -+import net.minecraft.network.FriendlyByteBuf; -+import net.minecraft.network.RegistryFriendlyByteBuf; -+import net.minecraft.network.codec.StreamCodec; -+import net.minecraft.network.protocol.BundleDelimiterPacket; -+import net.minecraft.network.protocol.BundlePacket; -+import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; -+import net.minecraft.network.protocol.game.*; -+import net.minecraft.server.MinecraftServer; -+import net.minecraft.server.level.ServerPlayer; -+import org.bukkit.entity.Player; -+import org.bukkit.plugin.Plugin; -+import org.jetbrains.annotations.Nullable; -+import org.leavesmc.leaves.LeavesConfig; -+import org.leavesmc.leaves.bytebuf.BytebufManager; -+import org.leavesmc.leaves.bytebuf.SimpleBytebufManager; -+import org.leavesmc.leaves.bytebuf.WrappedBytebuf; -+import org.leavesmc.leaves.bytebuf.packet.Packet; -+import org.leavesmc.leaves.bytebuf.packet.PacketListener; -+import org.leavesmc.leaves.bytebuf.packet.PacketType; -+ -+import java.lang.reflect.Field; -+import java.util.HashMap; -+import java.util.Map; -+ -+import static org.leavesmc.leaves.bytebuf.packet.PacketType.*; -+ -+public class InternalBytebufHandler { -+ -+ private class PacketHandler extends ChannelDuplexHandler { -+ -+ private final static String handlerName = "leaves-bytebuf-handler"; -+ private final Player player; -+ -+ public PacketHandler(Player player) { -+ this.player = player; -+ } -+ -+ @Override -+ public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { -+ if (msg instanceof BundlePacket || msg instanceof BundleDelimiterPacket) { -+ super.channelRead(ctx, msg); -+ return; -+ } -+ -+ if (msg instanceof net.minecraft.network.protocol.Packet nmsPacket) { -+ try { -+ msg = callPacketInEvent(player, nmsPacket); -+ } catch (Exception e) { -+ MinecraftServer.LOGGER.error("Error on PacketInEvent.", e); -+ } -+ } -+ -+ if (msg != null) { -+ super.channelRead(ctx, msg); -+ } -+ } -+ -+ @Override -+ public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception { -+ if (msg instanceof BundlePacket || msg instanceof BundleDelimiterPacket) { -+ super.write(ctx, msg, promise); -+ return; -+ } -+ -+ if (msg instanceof net.minecraft.network.protocol.Packet nmsPacket) { -+ try { -+ msg = callPacketOutEvent(player, nmsPacket); -+ } catch (Exception e) { -+ MinecraftServer.LOGGER.error("Error on PacketInEvent.", e); -+ } -+ } -+ -+ if (msg != null) { -+ super.write(ctx, msg, promise); -+ } -+ } -+ } -+ -+ public final Map listenerMap = new HashMap<>(); -+ private final BytebufManager manager = new SimpleBytebufManager(this); -+ private final ImmutableMap type2CodecMap; -+ private final Cache, PacketType> resultCache = CacheBuilder.newBuilder().build(); -+ -+ public InternalBytebufHandler() { -+ ImmutableMap.Builder builder = ImmutableMap.builder(); -+ for (PacketType packet : PacketType.values()) { -+ Class packetClass; -+ try { -+ packetClass = Class.forName("net.minecraft.network.protocol.game." + packet.name() + "Packet"); -+ } catch (ClassNotFoundException e) { -+ try { -+ packetClass = Class.forName("net.minecraft.network.protocol.common." + packet.name() + "Packet"); -+ } catch (ClassNotFoundException e2) { -+ try { -+ packetClass = Class.forName("net.minecraft.network.protocol.ping." + packet.name() + "Packet"); -+ } catch (ClassNotFoundException ignored) { -+ continue; -+ } -+ } -+ } -+ try { -+ Field field = packetClass.getDeclaredField("STREAM_CODEC"); -+ builder.put(packet, (StreamCodec>) field.get(null)); -+ } catch (Exception ignored) { -+ } -+ } -+ -+ builder.put(ClientboundMoveEntityPos, ClientboundMoveEntityPacket.Pos.STREAM_CODEC); -+ builder.put(ClientboundMoveEntityPosRot, ClientboundMoveEntityPacket.PosRot.STREAM_CODEC); -+ builder.put(ClientboundMoveEntityRot, ClientboundMoveEntityPacket.Rot.STREAM_CODEC); -+ builder.put(ServerboundMovePlayerPos, ServerboundMovePlayerPacket.Pos.STREAM_CODEC); -+ builder.put(ServerboundMovePlayerPosRot, ServerboundMovePlayerPacket.PosRot.STREAM_CODEC); -+ builder.put(ServerboundMovePlayerRot, ServerboundMovePlayerPacket.Rot.STREAM_CODEC); -+ builder.put(ServerboundMovePlayerStatusOnly, ServerboundMovePlayerPacket.StatusOnly.STREAM_CODEC); -+ builder.put(ClientboundCustomPayload, ClientboundCustomPayloadPacket.GAMEPLAY_STREAM_CODEC); -+ -+ type2CodecMap = builder.build(); -+ } -+ -+ public void injectPlayer(ServerPlayer player) { -+ if (LeavesConfig.mics.leavesPacketEvent) { -+ player.connection.connection.channel.pipeline().addBefore("packet_handler", PacketHandler.handlerName, new PacketHandler(player.getBukkitEntity())); -+ } -+ } -+ -+ public BytebufManager getManager() { -+ return manager; -+ } -+ -+ public net.minecraft.network.protocol.Packet callPacketInEvent(Player player, net.minecraft.network.protocol.Packet nmsPacket) { -+ if (listenerMap.isEmpty()) { -+ return nmsPacket; -+ } -+ PacketType type = toEnumType(nmsPacket.type()); -+ if (type == null) { -+ return nmsPacket; -+ } -+ Packet packet = createBytebufPacket(type, nmsPacket); -+ for (PacketListener listener : listenerMap.keySet()) { -+ if (listenerMap.get(listener).isEnabled()) { -+ packet = listener.onPacketIn(player, packet); -+ packet.bytebuf().resetReaderIndex(); -+ } else { -+ listenerMap.remove(listener); -+ } -+ } -+ return createNMSPacket(packet); -+ } -+ -+ public net.minecraft.network.protocol.Packet callPacketOutEvent(Player player, net.minecraft.network.protocol.Packet nmsPacket) { -+ if (listenerMap.isEmpty()) { -+ return nmsPacket; -+ } -+ PacketType type = toEnumType(nmsPacket.type()); -+ if (type == null) { -+ return nmsPacket; -+ } -+ Packet packet = createBytebufPacket(type, nmsPacket); -+ for (PacketListener listener : listenerMap.keySet()) { -+ if (listenerMap.get(listener).isEnabled()) { -+ packet = listener.onPacketOut(player, packet); -+ packet.bytebuf().resetReaderIndex(); -+ } else { -+ listenerMap.remove(listener); -+ } -+ } -+ return createNMSPacket(packet); -+ } -+ -+ public void applyPacketToPlayer(ServerPlayer player, Packet packet) { -+ Connection sp = player.connection.connection; -+ sp.send(createNMSPacket(packet)); -+ } -+ -+ public net.minecraft.network.protocol.Packet createNMSPacket(Packet packet) { -+ StreamCodec> codec = type2CodecMap.get(packet.type()); -+ if (codec == null) { -+ throw new UnsupportedOperationException("This feature is not completely finished yet, packet type " + packet.type() + " is not supported temporary."); -+ } -+ return codec.decode(((WrappedBytebuf) packet.bytebuf()).getRegistryBuf()); -+ } -+ -+ @Nullable -+ private PacketType toEnumType(net.minecraft.network.protocol.PacketType type) { -+ try { -+ return this.resultCache.get(type, () -> { -+ StringBuilder builder = new StringBuilder(); -+ String bound = type.toString().split("/")[0]; -+ String name = type.toString().split(":")[1]; -+ builder.append(bound.substring(0, 1).toUpperCase()).append(bound.substring(1)); -+ boolean flag = true; -+ for (int i = 0; i < name.length(); i++) { -+ if (flag) { -+ builder.append(name.substring(i, i + 1).toUpperCase()); -+ flag = false; -+ continue; -+ } -+ if (name.charAt(i) == '_') { -+ flag = true; -+ } else { -+ builder.append(name.charAt(i)); -+ } -+ } -+ try { -+ return PacketType.valueOf(builder.toString()); -+ } catch (IllegalArgumentException e) { -+ throw new RuntimeException(e); -+ } -+ }); -+ } catch (Exception ignore) { -+ return null; -+ } -+ } -+ -+ public Packet createBytebufPacket(PacketType type, net.minecraft.network.protocol.Packet nmsPacket) { -+ RegistryFriendlyByteBuf buf = new RegistryFriendlyByteBuf(Unpooled.buffer(), MinecraftServer.getServer().registryAccess()); -+ StreamCodec> codec = type2CodecMap.get(type); -+ if (codec == null) { -+ throw new UnsupportedOperationException("This feature is not completely finished yet, packet type " + type + " is not supported temporary."); -+ } -+ codec.encode(buf, nmsPacket); -+ return new Packet(type, new WrappedBytebuf(buf)); -+ } -+} diff --git a/patches/server/0114-Allow-grindstone-overstacking.patch b/patches/server/0114-Allow-grindstone-overstacking.patch deleted file mode 100644 index 9316177f..00000000 --- a/patches/server/0114-Allow-grindstone-overstacking.patch +++ /dev/null @@ -1,28 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> -Date: Wed, 26 Jun 2024 17:59:56 +0800 -Subject: [PATCH] Allow grindstone overstacking - - -diff --git a/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java b/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java -index 5687f492fc76f699e2a388790ca5380d9b8c8d0a..53e79f2ca799b8de7d6410dbb3dafa1cb2eff7b6 100644 ---- a/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java -+++ b/src/main/java/net/minecraft/world/inventory/GrindstoneMenu.java -@@ -194,7 +194,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { - byte b0 = 1; - - if (!firstInput.isDamageableItem()) { -- if (firstInput.getMaxStackSize() < 2 || !ItemStack.matches(firstInput, secondInput)) { -+ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.allowGrindstoneOverstacking && firstInput.getMaxStackSize() < 2 || !ItemStack.matches(firstInput, secondInput)) { // Leaves - allowGrindstoneOverstaking - return ItemStack.EMPTY; - } - -@@ -278,7 +278,7 @@ public class GrindstoneMenu extends AbstractContainerMenu { - ItemStack itemstack3 = this.repairSlots.getItem(1); - - if (slot == 2) { -- if (!this.moveItemStackTo(itemstack1, 3, 39, true)) { -+ if (!this.moveItemStackTo(itemstack1, 3, 39, true, org.leavesmc.leaves.LeavesConfig.modify.oldMC.allowGrindstoneOverstacking)) { // Leaves - allowGrindstoneOverstacking: Disable stack check - return ItemStack.EMPTY; - } - diff --git a/patches/server/0115-Configurable-MC-67.patch b/patches/server/0115-Configurable-MC-67.patch deleted file mode 100644 index ea72d590..00000000 --- a/patches/server/0115-Configurable-MC-67.patch +++ /dev/null @@ -1,18 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Lumine1909 <133463833+Lumine1909@users.noreply.github.com> -Date: Wed, 26 Jun 2024 18:15:57 +0800 -Subject: [PATCH] Configurable MC-67 - - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 7dfa1dc42224eafc2196a532a00dc606d35b8d89..58906cda0432dc8d5988f83f19a5f2deac094566 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -4175,6 +4175,7 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - - public boolean canTeleport(Level from, Level to) { -+ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.allowEntityPortalWithPassenger && (this.isPassenger() || this.isVehicle())) return false; // Leaves - allowEntityPortalWithPassenger - if (!this.isAlive() || !this.valid) return false; // Paper - Fix item duplication and teleport issues - if (from.dimension() == Level.END && to.dimension() == Level.OVERWORLD) { - Iterator iterator = this.getPassengers().iterator(); diff --git a/patches/server/0116-Disable-end-gateway-portal-entity-ticking.patch b/patches/server/0116-Disable-end-gateway-portal-entity-ticking.patch deleted file mode 100644 index 9892ec6f..00000000 --- a/patches/server/0116-Disable-end-gateway-portal-entity-ticking.patch +++ /dev/null @@ -1,22 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Mon, 1 Jul 2024 22:09:33 +0800 -Subject: [PATCH] Disable end gateway portal entity ticking - - -diff --git a/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java b/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java -index 50d726874af316a264fa0c2589f5b04559dffb50..fd9c27cf12cca630b192f9cdf876b024157b6073 100644 ---- a/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/EndGatewayBlock.java -@@ -130,7 +130,10 @@ public class EndGatewayBlock extends BaseEntityBlock implements Portal { - } - // Leaves end - force void trade - -- return vec3d == null ? null : (entity instanceof ThrownEnderpearl ? new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Set.of(), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY) : new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), TeleportTransition.PLACE_PORTAL_TICKET, PlayerTeleportEvent.TeleportCause.END_GATEWAY)); // CraftBukkit -+ // Leaves start - Disable end gateway portal entity ticking -+ TeleportTransition.PostTeleportTransition postTeleportTransition = org.leavesmc.leaves.LeavesConfig.modify.oldMC.disableGatewayPortalEntityTicking ? TeleportTransition.DO_NOTHING : TeleportTransition.PLACE_PORTAL_TICKET; -+ return vec3d == null ? null : (entity instanceof ThrownEnderpearl ? new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Set.of(), postTeleportTransition, PlayerTeleportEvent.TeleportCause.END_GATEWAY) : new TeleportTransition(world, vec3d, Vec3.ZERO, 0.0F, 0.0F, Relative.union(Relative.DELTA, Relative.ROTATION), postTeleportTransition, PlayerTeleportEvent.TeleportCause.END_GATEWAY)); // CraftBukkit -+ // Leaves end - Disable end gateway portal entity ticking - } else { - return null; - } diff --git a/patches/server/0117-Disable-crystal-portal-proximity-check.patch b/patches/server/0117-Disable-crystal-portal-proximity-check.patch deleted file mode 100644 index 652016f6..00000000 --- a/patches/server/0117-Disable-crystal-portal-proximity-check.patch +++ /dev/null @@ -1,64 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Bluemangoo -Date: Fri, 19 Jul 2024 15:04:03 +0800 -Subject: [PATCH] Disable crystal-portal proximity check - - -diff --git a/src/main/java/net/minecraft/world/item/EndCrystalItem.java b/src/main/java/net/minecraft/world/item/EndCrystalItem.java -index b62db8c7c8c57e43869ee239ebf4b02f112355d9..b33ac3221ad9f91d466285a4b00b0e0c67000864 100644 ---- a/src/main/java/net/minecraft/world/item/EndCrystalItem.java -+++ b/src/main/java/net/minecraft/world/item/EndCrystalItem.java -@@ -30,7 +30,7 @@ public class EndCrystalItem extends Item { - if (!iblockdata.is(Blocks.OBSIDIAN) && !iblockdata.is(Blocks.BEDROCK)) { - return InteractionResult.FAIL; - } else { -- BlockPos blockposition1 = blockposition.above(); final BlockPos aboveBlockPosition = blockposition1; // Paper - OBFHELPER -+ BlockPos blockposition1 = blockposition.above(); // final BlockPos aboveBlockPosition = blockposition1; // Paper - OBFHELPER // Leaves - - if (!world.isEmptyBlock(blockposition1)) { - return InteractionResult.FAIL; -@@ -58,7 +58,8 @@ public class EndCrystalItem extends Item { - EndDragonFight enderdragonbattle = ((ServerLevel) world).getDragonFight(); - - if (enderdragonbattle != null) { -- enderdragonbattle.tryRespawn(aboveBlockPosition); // Paper - Perf: Do crystal-portal proximity check before entity lookup -+ // enderdragonbattle.tryRespawn(aboveBlockPosition); // Paper - Perf: Do crystal-portal proximity check before entity lookup -+ enderdragonbattle.tryRespawn(); // Leaves - } - } - -diff --git a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java -index e944a730503a9c50bcde819515a1d7e7f1ec59fd..65d338cc685bb3f5da23aa5bb8b632360cad66b3 100644 ---- a/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java -+++ b/src/main/java/net/minecraft/world/level/dimension/end/EndDragonFight.java -@@ -622,12 +622,14 @@ public class EndDragonFight { - } - - public boolean tryRespawn() { // CraftBukkit - return boolean -+ /* Leaves - // Paper start - Perf: Do crystal-portal proximity check before entity lookup - return this.tryRespawn(null); - } - - public boolean tryRespawn(@Nullable BlockPos placedEndCrystalPos) { // placedEndCrystalPos is null if the tryRespawn() call was not caused by a placed end crystal - // Paper end - Perf: Do crystal-portal proximity check before entity lookup -+ */ - if (this.dragonKilled && this.respawnStage == null) { - BlockPos blockposition = this.portalLocation; - -@@ -645,6 +647,7 @@ public class EndDragonFight { - blockposition = this.portalLocation; - } - -+ /* Leaves - // Paper start - Perf: Do crystal-portal proximity check before entity lookup - if (placedEndCrystalPos != null) { - // The end crystal must be 0 or 1 higher than the portal origin -@@ -660,6 +663,7 @@ public class EndDragonFight { - } - } - // Paper end - Perf: Do crystal-portal proximity check before entity lookup -+ */ - - List list = Lists.newArrayList(); - BlockPos blockposition1 = blockposition.above(1); diff --git a/patches/server/0119-Leaves-plugins.patch b/patches/server/0119-Leaves-plugins.patch deleted file mode 100644 index 14492889..00000000 --- a/patches/server/0119-Leaves-plugins.patch +++ /dev/null @@ -1,317 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: MC_XiaoHei -Date: Mon, 22 Jul 2024 09:05:56 +0000 -Subject: [PATCH] Leaves plugins - - -diff --git a/build.gradle.kts b/build.gradle.kts -index 8559918f8f91e6137ac873b37af29aa3b6c0c4a0..f2eb3b2eb2ad6a3c41f6ad84497fc65f22823f32 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -47,6 +47,7 @@ dependencies { - implementation("com.github.luben:zstd-jni:1.5.6-3") - implementation("org.lz4:lz4-java:1.8.0") - // Leaves end - Linear format -+ implementation("org.spongepowered:configurate-hocon:4.2.0-SNAPSHOT") // Leaves - leaves plugins - implementation("org.apache.logging.log4j:log4j-iostreams:2.22.1") // Paper - remove exclusion - implementation("org.ow2.asm:asm-commons:9.7.1") - implementation("org.spongepowered:configurate-yaml:4.2.0-SNAPSHOT") // Paper - config files -diff --git a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java -index f0fce4113fb07c64adbec029d177c236cbdcbae8..f3cb913f29e1aff46233af2f086d205a51ac582d 100644 ---- a/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java -+++ b/src/main/java/io/papermc/paper/command/PaperPluginsCommand.java -@@ -63,6 +63,7 @@ public class PaperPluginsCommand extends BukkitCommand { - - private static final Component LEGACY_PLUGIN_STAR = Component.text('*', TextColor.color(255, 212, 42)).hoverEvent(LEGACY_PLUGIN_INFO); - private static final Component INFO_ICON_START = Component.text("ℹ ", INFO_COLOR); -+ private static final Component LEAVES_HEADER = Component.text("Leaves Plugins:", TextColor.color(55, 209, 171)); // Leaves - leaves plugin - private static final Component PAPER_HEADER = Component.text("Paper Plugins:", TextColor.color(2, 136, 209)); - private static final Component BUKKIT_HEADER = Component.text("Bukkit Plugins:", TextColor.color(237, 129, 6)); - private static final Component PLUGIN_TICK = Component.text("- ", NamedTextColor.DARK_GRAY); -@@ -170,6 +171,8 @@ public class PaperPluginsCommand extends BukkitCommand { - - TreeMap> paperPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - TreeMap> spigotPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); -+ // Leaves start - leaves plugin -+ TreeMap> leavesPlugins = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); - - - for (PluginProvider provider : LaunchEntryPointHandler.INSTANCE.get(Entrypoint.PLUGIN).getRegisteredProviders()) { -@@ -178,14 +181,23 @@ public class PaperPluginsCommand extends BukkitCommand { - if (provider instanceof SpigotPluginProvider) { - spigotPlugins.put(configuration.getDisplayName(), provider); - } else if (provider instanceof PaperPluginParent.PaperServerPluginProvider) { -- paperPlugins.put(configuration.getDisplayName(), provider); -+ if(provider.getMeta() instanceof org.leavesmc.leaves.plugin.provider.configuration.LeavesPluginMeta) leavesPlugins.put(configuration.getDisplayName(), provider); -+ else paperPlugins.put(configuration.getDisplayName(), provider); - } - } - -- Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size()), NamedTextColor.WHITE); -+ Component infoMessage = Component.text("Server Plugins (%s):".formatted(paperPlugins.size() + spigotPlugins.size() + leavesPlugins.size()), NamedTextColor.WHITE); - //.append(INFO_ICON_START.hoverEvent(SERVER_PLUGIN_INFO)); TODO: Add docs - - sender.sendMessage(infoMessage); -+ if (!leavesPlugins.isEmpty()) { -+ sender.sendMessage(LEAVES_HEADER); -+ } -+ -+ for (Component component : formatProviders(leavesPlugins)) { -+ sender.sendMessage(component); -+ } -+ // Leaves end - leaves plugin - - if (!paperPlugins.isEmpty()) { - sender.sendMessage(PAPER_HEADER); -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..390625fbf54139b205a23b94d89a860fbb2c92d9 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 { // Leaves - leaves plugins - - - private static final TypeToken>> TYPE_TOKEN = new TypeToken<>() { -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 d3b3a8baca013909fa9c6204d964d7d7efeb2719..cdde16a4999fbf56c334c65e23d995b7a3604518 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 plugins - 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 8d0da6e46d4eb5eb05c3144510c4ef083559d0ec..72a69ed1d4cdeecd25bfa4fddc3ecc2b21550bad 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_CONF = "leaves-plugin.conf"; // Leaves - leaves plugins - 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 plugins -+ public static final PluginFileType LEAVES = new PluginFileType<>(LEAVES_PLUGIN_CONF, PaperPluginParent.LEAVES_FACTORY) { -+ @Override -+ protected void register(EntrypointHandler entrypointHandler, PaperPluginParent parent) { -+ PaperPluginParent.PaperBootstrapProvider bootstrapPluginProvider = null; -+ if (parent.shouldCreateBootstrap()) { -+ bootstrapPluginProvider = parent.createBootstrapProvider(); -+ entrypointHandler.register(Entrypoint.BOOTSTRAPPER, bootstrapPluginProvider); -+ } -+ entrypointHandler.register(Entrypoint.PLUGIN, parent.createPluginProvider(bootstrapPluginProvider)); -+ } -+ }; - -- private static final List> VALUES = List.of(PAPER, SPIGOT); -+ private static final List> VALUES = List.of(LEAVES, PAPER, SPIGOT); -+ // Leaves end - leaves plugins - - 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..ebde8a79143a5e314d5054f2d125d276eaa1e734 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 plugins - private final Path path; - private final JarFile jarFile; - private final PaperPluginMeta description; -diff --git a/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java b/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java -index 28857d0c9b53f2068d51b8f09ef40df7a2b97502..b4d2d7280237a9ad7df095e26773e01211201b84 100644 ---- a/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java -+++ b/src/main/java/io/papermc/paper/pluginremap/PluginRemapper.java -@@ -333,7 +333,13 @@ public final class PluginRemapper { - } - index.skip(inputFile); - return CompletableFuture.completedFuture(inputFile); -- } -+ } else if (ns == null && Files.exists(fs.getPath(PluginFileType.LEAVES_PLUGIN_CONF))) { // Leaves start - leaves plugins -+ if (DEBUG_LOGGING) { -+ LOGGER.info("Plugin '{}' is a Leaves plugin with no namespace specified.", inputFile); -+ } -+ index.skip(inputFile); -+ return CompletableFuture.completedFuture(inputFile); -+ } // Leaves end - leaves plugins - } - } catch (final IOException ex) { - return CompletableFuture.failedFuture(new RuntimeException("Failed to open plugin jar " + inputFile, ex)); -diff --git a/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java b/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java -new file mode 100644 -index 0000000000000000000000000000000000000000..35fe2f6a1785d525a46c4bc4d61c5043a056daef ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/plugin/provider/LeavesPluginProviderFactory.java -@@ -0,0 +1,57 @@ -+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 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) { -+ 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/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java b/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java -new file mode 100644 -index 0000000000000000000000000000000000000000..4eb3396fd82e3d32ecb4ee4af4ffa28ab86f34b7 ---- /dev/null -+++ b/src/main/java/org/leavesmc/leaves/plugin/provider/configuration/LeavesPluginMeta.java -@@ -0,0 +1,90 @@ -+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.spongepowered.configurate.CommentedConfigurationNode; -+import org.spongepowered.configurate.ConfigurateException; -+import org.spongepowered.configurate.hocon.HoconConfigurationLoader; -+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.List; -+import java.util.function.Predicate; -+ -+@ConfigSerializable -+public class LeavesPluginMeta extends PaperPluginMeta { -+ private List mixins; -+ static final ApiVersion MINIMUM = ApiVersion.getOrCreateVersion("1.21"); -+ -+ public static LeavesPluginMeta create(BufferedReader reader) throws ConfigurateException { -+ HoconConfigurationLoader loader = HoconConfigurationLoader.builder() -+ .prettyPrinting(true) -+ .emitComments(true) -+ .emitJsonCompatible(true) -+ .source(() -> reader) -+ .defaultOptions((options) -> -+ options.serializers((serializers) -> -+ serializers.register(new ScalarSerializer<>(ApiVersion.class) { -+ @Override -+ public ApiVersion deserialize(final Type type, final 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 Object serialize(final ApiVersion item, final 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(); -+ CommentedConfigurationNode node = loader.load(); -+ LegacyPaperMeta.migrate(node); -+ LeavesPluginMeta pluginConfiguration = node.require(LeavesPluginMeta.class); -+ -+ if (!node.node("author").virtual()) { -+ pluginConfiguration.authors = ImmutableList.builder() -+ .addAll(pluginConfiguration.authors) -+ .add(node.node("author").getString()) -+ .build(); -+ } -+ -+ return pluginConfiguration; -+ } -+ -+ public List getMixins() { -+ return mixins; -+ } -+} diff --git a/patches/server/0121-Fix-FallingBlockEntity-Duplicate.patch b/patches/server/0121-Fix-FallingBlockEntity-Duplicate.patch deleted file mode 100644 index 853dab95..00000000 --- a/patches/server/0121-Fix-FallingBlockEntity-Duplicate.patch +++ /dev/null @@ -1,19 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 31 Jul 2024 12:51:44 +0800 -Subject: [PATCH] Fix FallingBlockEntity Duplicate - - -diff --git a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java -index 06d9a519e64d4b8b8764b3ad7691ad93b5cee065..e067e826aae963e30dd9e12e57e9a63912165b0d 100644 ---- a/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java -+++ b/src/main/java/net/minecraft/world/entity/item/FallingBlockEntity.java -@@ -435,7 +435,7 @@ public class FallingBlockEntity extends Entity { - boolean flag = (resourcekey1 == Level.END || resourcekey == Level.END) && resourcekey1 != resourcekey; - Entity entity = super.teleport(teleportTarget); - -- this.forceTickAfterTeleportToDuplicate = entity != null && flag && io.papermc.paper.configuration.GlobalConfiguration.get().unsupportedSettings.allowUnsafeEndPortalTeleportation; // Paper -+ this.forceTickAfterTeleportToDuplicate = entity != null && flag; // Paper // Leaves - return entity; - } - } diff --git a/patches/server/0122-Old-BlockEntity-behaviour.patch b/patches/server/0122-Old-BlockEntity-behaviour.patch deleted file mode 100644 index d7393a08..00000000 --- a/patches/server/0122-Old-BlockEntity-behaviour.patch +++ /dev/null @@ -1,83 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Wed, 14 Aug 2024 01:48:14 +0800 -Subject: [PATCH] Old BlockEntity behaviour - - -diff --git a/src/main/java/net/minecraft/world/level/block/ChiseledBookShelfBlock.java b/src/main/java/net/minecraft/world/level/block/ChiseledBookShelfBlock.java -index 5a95ecd7e94652d2af3eb42ee9cf61913fb95a69..cadd3362e9993715cd47e5e5c6d7a3eb60d11d33 100644 ---- a/src/main/java/net/minecraft/world/level/block/ChiseledBookShelfBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/ChiseledBookShelfBlock.java -@@ -194,6 +194,13 @@ public class ChiseledBookShelfBlock extends BaseEntityBlock { - } - - chiseledBookShelfBlockEntity.clearContent(); -+ // Leaves start - behaviour 1.21.1- -+ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) { -+ bl = false; -+ world.updateNeighbourForOutputSignal(pos, this); -+ break label32; -+ } -+ // Leaves end - behaviour 1.21.1- - bl = true; - break label32; - } -diff --git a/src/main/java/net/minecraft/world/level/block/LecternBlock.java b/src/main/java/net/minecraft/world/level/block/LecternBlock.java -index 70f2e6278e2d970245ca5b46fbd9ffae4727b47b..cb1ef444efaaff0579154537db151d16efae357e 100644 ---- a/src/main/java/net/minecraft/world/level/block/LecternBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/LecternBlock.java -@@ -229,10 +229,11 @@ public class LecternBlock extends BaseEntityBlock { - this.popBook(state, world, pos); - } - -- super.onRemove(state, world, pos, newState, moved); -+ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, world, pos, newState, moved); // Leaves - behaviour 1.21.1- - if ((Boolean) state.getValue(LecternBlock.POWERED)) { - LecternBlock.updateBelow(world, pos, state); - } -+ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, world, pos, newState, moved); // Leaves - behaviour 1.21.1- - - } - } -diff --git a/src/main/java/net/minecraft/world/level/block/SculkSensorBlock.java b/src/main/java/net/minecraft/world/level/block/SculkSensorBlock.java -index 0ed449a188d98f87dbddd2d76009fed02a29ed25..e812d158cf56d4d1d67ea335433a23c7be0e4292 100644 ---- a/src/main/java/net/minecraft/world/level/block/SculkSensorBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/SculkSensorBlock.java -@@ -151,10 +151,11 @@ public class SculkSensorBlock extends BaseEntityBlock implements SimpleWaterlogg - @Override - protected void onRemove(BlockState state, Level world, BlockPos pos, BlockState newState, boolean moved) { - if (!state.is(newState.getBlock())) { -- super.onRemove(state, world, pos, newState, moved); -+ if (!org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, world, pos, newState, moved); // Leaves - behaviour 1.21.1- - if (SculkSensorBlock.getPhase(state) == SculkSensorPhase.ACTIVE) { - SculkSensorBlock.updateNeighbours(world, pos, state); - } -+ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, world, pos, newState, moved); // Leaves - behaviour 1.21.1- - - } - } -diff --git a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java -index b01918fc97926a3182c21145bb7411e7bc409d35..0646a861cc04ad29ba850f34cd6a629228f2c640 100644 ---- a/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/ShulkerBoxBlock.java -@@ -186,6 +186,7 @@ public class ShulkerBoxBlock extends BaseEntityBlock { - if (blockEntity instanceof ShulkerBoxBlockEntity) { - world.updateNeighbourForOutputSignal(pos, state.getBlock()); - } -+ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour) super.onRemove(state, world, pos, newState, moved); // Leaves - behaviour 1.21.1- - } - } - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java -index 1f929b467a0ece3143af58a657cf5983c07a8d51..e8ab0901393844c9d2ea3595a28306bfd8bd40fb 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/BlockEntity.java -@@ -67,7 +67,7 @@ public abstract class BlockEntity { - } - - public boolean isValidBlockState(BlockState state) { -- return this.type.isValid(state); -+ return org.leavesmc.leaves.LeavesConfig.modify.oldMC.oldBlockEntityBehaviour || this.type.isValid(state); // Leaves - behaviour 1.21.1- - } - - public static BlockPos getPosFromTag(CompoundTag nbt) { diff --git a/patches/server/0123-Revert-raid-changes.patch b/patches/server/0123-Revert-raid-changes.patch deleted file mode 100644 index b19b61a8..00000000 --- a/patches/server/0123-Revert-raid-changes.patch +++ /dev/null @@ -1,98 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: huanli233 <392352840@qq.com> -Date: Wed, 23 Oct 2024 23:10:48 +0800 -Subject: [PATCH] Revert raid changes - - -diff --git a/src/main/java/net/minecraft/world/effect/BadOmenMobEffect.java b/src/main/java/net/minecraft/world/effect/BadOmenMobEffect.java -index e33e8db5f24437081d2026e66c13256aa3893a15..07cdc52ad0aa3c246e1769c3dda3c17bb9a10e8b 100644 ---- a/src/main/java/net/minecraft/world/effect/BadOmenMobEffect.java -+++ b/src/main/java/net/minecraft/world/effect/BadOmenMobEffect.java -@@ -22,6 +22,11 @@ class BadOmenMobEffect extends MobEffect { - && !serverPlayer.isSpectator() - && world.getDifficulty() != Difficulty.PEACEFUL - && world.isVillage(serverPlayer.blockPosition())) { -+ // Leaves start - Revert raid changes -+ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.raid.allowBadOmenTriggerRaid) { -+ return world.getRaids().createOrExtendRaid(serverPlayer, serverPlayer.blockPosition()) != null; -+ } -+ // Leaves end - Revert raid changes - Raid raid = world.getRaidAt(serverPlayer.blockPosition()); - if (raid == null || raid.getRaidOmenLevel() < raid.getMaxRaidOmenLevel()) { - serverPlayer.addEffect(new MobEffectInstance(MobEffects.RAID_OMEN, 600, amplifier)); -diff --git a/src/main/java/net/minecraft/world/entity/raid/Raider.java b/src/main/java/net/minecraft/world/entity/raid/Raider.java -index 9b911254b24bc77930c518a9c61916983ba72e3c..124bfe881356fcee25e8d66c18c918a4c31bafa4 100644 ---- a/src/main/java/net/minecraft/world/entity/raid/Raider.java -+++ b/src/main/java/net/minecraft/world/entity/raid/Raider.java -@@ -17,8 +17,11 @@ import net.minecraft.network.syncher.EntityDataSerializers; - import net.minecraft.network.syncher.SynchedEntityData; - import net.minecraft.server.level.ServerLevel; - import net.minecraft.sounds.SoundEvent; -+import net.minecraft.util.Mth; - import net.minecraft.world.DifficultyInstance; - import net.minecraft.world.damagesource.DamageSource; -+import net.minecraft.world.effect.MobEffectInstance; -+import net.minecraft.world.effect.MobEffects; - import net.minecraft.world.entity.Entity; - import net.minecraft.world.entity.EntitySpawnReason; - import net.minecraft.world.entity.EntityType; -@@ -32,10 +35,13 @@ import net.minecraft.world.entity.ai.targeting.TargetingConditions; - import net.minecraft.world.entity.ai.util.DefaultRandomPos; - import net.minecraft.world.entity.ai.village.poi.PoiManager; - import net.minecraft.world.entity.ai.village.poi.PoiTypes; -+import net.minecraft.world.entity.animal.Wolf; - import net.minecraft.world.entity.item.ItemEntity; - import net.minecraft.world.entity.monster.AbstractIllager; - import net.minecraft.world.entity.monster.PatrollingMonster; -+import net.minecraft.world.entity.player.Player; - import net.minecraft.world.item.ItemStack; -+import net.minecraft.world.level.GameRules; - import net.minecraft.world.level.Level; - import net.minecraft.world.level.ServerLevelAccessor; - import net.minecraft.world.level.pathfinder.Path; -@@ -134,6 +140,45 @@ public abstract class Raider extends PatrollingMonster { - - raid.removeFromRaid(this, false); - } -+ -+ // Leaves start - Revert raid changes -+ if (this.level() instanceof ServerLevel serverLevel) { -+ if (org.leavesmc.leaves.LeavesConfig.modify.oldMC.raid.giveBadOmenWhenKillPatrolLeader && this.isPatrolLeader() && raid == null && serverLevel.getRaidAt(this.blockPosition()) == null) { -+ ItemStack itemstack = this.getItemBySlot(EquipmentSlot.HEAD); -+ Player entityhuman = null; -+ -+ if (entity instanceof Player) { -+ entityhuman = (Player) entity; -+ } else if (entity instanceof Wolf) { -+ Wolf entitywolf = (Wolf) entity; -+ LivingEntity entityliving = entitywolf.getOwner(); -+ -+ if (entitywolf.isTame() && entityliving instanceof Player) { -+ entityhuman = (Player) entityliving; -+ } -+ } -+ -+ if (!itemstack.isEmpty() && ItemStack.matches(itemstack, Raid.getOminousBannerInstance(this.registryAccess().lookupOrThrow(Registries.BANNER_PATTERN))) && entityhuman != null) { -+ MobEffectInstance mobeffect = entityhuman.getEffect(MobEffects.BAD_OMEN); -+ int i = 1; -+ -+ if (mobeffect != null) { -+ i += mobeffect.getAmplifier(); -+ entityhuman.removeEffectNoUpdate(MobEffects.BAD_OMEN); -+ } else { -+ --i; -+ } -+ -+ i = Mth.clamp(i, 0, 4); -+ MobEffectInstance mobeffect1 = new MobEffectInstance(MobEffects.BAD_OMEN, 120000, i, false, false, true); -+ -+ if (!serverLevel.getGameRules().getBoolean(GameRules.RULE_DISABLE_RAIDS)) { -+ entityhuman.addEffect(mobeffect1, org.bukkit.event.entity.EntityPotionEffectEvent.Cause.PATROL_CAPTAIN); // CraftBukkit -+ } -+ } -+ } -+ } -+ // Leaves end - Revert raid changes - } - - super.die(damageSource); diff --git a/patches/server/0126-Disable-vault-blacklist.patch b/patches/server/0126-Disable-vault-blacklist.patch deleted file mode 100644 index 4002c551..00000000 --- a/patches/server/0126-Disable-vault-blacklist.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: huanli233 <392352840@qq.com> -Date: Sun, 27 Oct 2024 15:57:37 +0800 -Subject: [PATCH] Disable vault blacklist - - -diff --git a/src/main/java/net/minecraft/world/level/block/entity/vault/VaultServerData.java b/src/main/java/net/minecraft/world/level/block/entity/vault/VaultServerData.java -index 30544783d959fbdf4b6fd53c10bd3806e718a066..4319e2108b59aa78ed00d88859ba742e663edbd3 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/vault/VaultServerData.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/vault/VaultServerData.java -@@ -57,11 +57,12 @@ public class VaultServerData { - } - - boolean hasRewardedPlayer(Player player) { -- return this.rewardedPlayers.contains(player.getUUID()); -+ return !org.leavesmc.leaves.LeavesConfig.modify.disableVaultBlacklist && this.rewardedPlayers.contains(player.getUUID()); // Leaves - disable vault blacklist - } - - @VisibleForTesting - public void addToRewardedPlayers(Player player) { -+ if (org.leavesmc.leaves.LeavesConfig.modify.disableVaultBlacklist) return; // Leaves - disable vault blacklist - this.rewardedPlayers.add(player.getUUID()); - if (this.rewardedPlayers.size() > 128) { - Iterator iterator = this.rewardedPlayers.iterator(); -diff --git a/src/main/java/net/minecraft/world/level/block/entity/vault/VaultSharedData.java b/src/main/java/net/minecraft/world/level/block/entity/vault/VaultSharedData.java -index f09ce717869741bb2f027ab1216fad9d86425683..a32c8920ef6c1ab4b6ec7a68b9ed10c37afdfbf3 100644 ---- a/src/main/java/net/minecraft/world/level/block/entity/vault/VaultSharedData.java -+++ b/src/main/java/net/minecraft/world/level/block/entity/vault/VaultSharedData.java -@@ -68,8 +68,8 @@ public class VaultSharedData { - Set set = config.playerDetector() - .detect(world, config.entitySelector(), pos, radius, false) - .stream() -- .filter(uuid -> !serverData.getRewardedPlayers().contains(uuid)) -- .collect(Collectors.toSet()); -+ .filter(uuid -> org.leavesmc.leaves.LeavesConfig.modify.disableVaultBlacklist || !serverData.getRewardedPlayers().contains(uuid)) -+ .collect(Collectors.toSet()); // Leaves - disable vault blacklist - if (!this.connectedPlayers.equals(set)) { - this.connectedPlayers = set; - this.markDirty(); diff --git a/patches/server/0127-TEMP-Fix-color-in-console.patch b/patches/server/0127-TEMP-Fix-color-in-console.patch deleted file mode 100644 index 29ad645e..00000000 --- a/patches/server/0127-TEMP-Fix-color-in-console.patch +++ /dev/null @@ -1,20 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Sat, 28 Dec 2024 02:44:49 +0800 -Subject: [PATCH] TEMP Fix color in console - -will remove in 1.21.4 - -diff --git a/build.gradle.kts b/build.gradle.kts -index f2eb3b2eb2ad6a3c41f6ad84497fc65f22823f32..b07ccf91b3f6d967c5617dba54b6d38823039e6d 100644 ---- a/build.gradle.kts -+++ b/build.gradle.kts -@@ -30,7 +30,7 @@ dependencies { - 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 - implementation("net.minecrell:terminalconsoleappender:1.3.0") -- implementation("net.kyori:adventure-text-serializer-ansi:4.17.0") // Keep in sync with adventureVersion from Paper-API build file -+ implementation("net.kyori:adventure-text-serializer-ansi:4.18.0") // Keep in sync with adventureVersion from Paper-API build file // Leaves - up 4.18.0 to fix color - /* - Required to add the missing Log4j2Plugins.dat file from log4j-core - which has been removed by Mojang. Without it, log4j has to classload diff --git a/patches/server/0128-Fix-EntityPortalExitEvent-logic.patch b/patches/server/0128-Fix-EntityPortalExitEvent-logic.patch deleted file mode 100644 index 3b16775d..00000000 --- a/patches/server/0128-Fix-EntityPortalExitEvent-logic.patch +++ /dev/null @@ -1,44 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 2 Jan 2025 22:01:17 +0800 -Subject: [PATCH] Fix EntityPortalExitEvent logic - - -diff --git a/src/main/java/net/minecraft/world/entity/Entity.java b/src/main/java/net/minecraft/world/entity/Entity.java -index 58906cda0432dc8d5988f83f19a5f2deac094566..bfbb894f73251ec241451bee831ebf7973cb4293 100644 ---- a/src/main/java/net/minecraft/world/entity/Entity.java -+++ b/src/main/java/net/minecraft/world/entity/Entity.java -@@ -3962,19 +3962,29 @@ public abstract class Entity implements SyncedDataHolder, Nameable, EntityAccess - } - if (this.portalProcess != null) { // if in a portal - CraftEntity bukkitEntity = this.getBukkitEntity(); -+ org.bukkit.util.Vector after = org.bukkit.craftbukkit.util.CraftVector.toBukkit(velocity); // Leaves - fix - org.bukkit.event.entity.EntityPortalExitEvent event = new org.bukkit.event.entity.EntityPortalExitEvent( - bukkitEntity, - bukkitEntity.getLocation(), to.clone(), -- bukkitEntity.getVelocity(), org.bukkit.craftbukkit.util.CraftVector.toBukkit(velocity) -+ bukkitEntity.getVelocity(), after // Leaves - fix - ); - event.callEvent(); - -+ // Leaves start - fix - // Only change the target if actually needed, since we reset relative flags -- if (!event.isCancelled() && event.getTo() != null && (!event.getTo().equals(event.getFrom()) || !event.getAfter().equals(event.getBefore()))) { -- to = event.getTo().clone(); -- velocity = org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getAfter()); -+ if (event.isCancelled() || (!to.equals(event.getTo()) || !after.equals(event.getAfter()))) { -+ if (!event.isCancelled()) { -+ if (event.getTo() != null) { -+ to = event.getTo().clone(); -+ } -+ velocity = org.bukkit.craftbukkit.util.CraftVector.toNMS(event.getAfter()); -+ } else { -+ to = event.getFrom().clone(); -+ velocity = Vec3.ZERO; -+ } - teleportTarget = new TeleportTransition(((CraftWorld) to.getWorld()).getHandle(), CraftLocation.toVec3D(to), velocity, to.getYaw(), to.getPitch(), teleportTarget.missingRespawnBlock(), teleportTarget.asPassenger(), Set.of(), teleportTarget.postTeleportTransition(), teleportTarget.cause()); - } -+ // Leaves end - fix - } - if (this.isRemoved()) { - return null; diff --git a/patches/server/0129-Fix-CraftPortalEvent-logic.patch b/patches/server/0129-Fix-CraftPortalEvent-logic.patch deleted file mode 100644 index d7936f4e..00000000 --- a/patches/server/0129-Fix-CraftPortalEvent-logic.patch +++ /dev/null @@ -1,26 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: violetc <58360096+s-yh-china@users.noreply.github.com> -Date: Thu, 2 Jan 2025 22:01:36 +0800 -Subject: [PATCH] Fix CraftPortalEvent logic - - -diff --git a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java -index 4aa14f975e1ceedf3d4a427e0daefb58b12fcafe..90ef0c5f53741516f9d9d0c2fead3bbbb930f72a 100644 ---- a/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java -+++ b/src/main/java/net/minecraft/world/level/block/EndPortalBlock.java -@@ -124,13 +124,13 @@ public class EndPortalBlock extends BaseEntityBlock implements Portal { - } - - // CraftBukkit start -- CraftPortalEvent event = entity.callPortalEvent(entity, CraftLocation.toBukkit(vec3d, worldserver1.getWorld(), f, entity.getXRot()), PlayerTeleportEvent.TeleportCause.END_PORTAL, 0, 0); -+ CraftPortalEvent event = entity.callPortalEvent(entity, CraftLocation.toBukkit(vec3d, worldserver1.getWorld(), f, 0.0F), PlayerTeleportEvent.TeleportCause.END_PORTAL, 0, 0); // Leaves - fix - if (event == null) { - return null; - } - Location to = event.getTo(); - -- return new TeleportTransition(((CraftWorld) to.getWorld()).getHandle(), CraftLocation.toVec3D(to), entity.getDeltaMovement(), to.getYaw(), to.getPitch(), set, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), PlayerTeleportEvent.TeleportCause.END_PORTAL); -+ return new TeleportTransition(((CraftWorld) to.getWorld()).getHandle(), CraftLocation.toVec3D(to), Vec3.ZERO, to.getYaw(), to.getPitch(), set, TeleportTransition.PLAY_PORTAL_SOUND.then(TeleportTransition.PLACE_PORTAL_TICKET), PlayerTeleportEvent.TeleportCause.END_PORTAL); // Leaves - fix - // CraftBukkit end - } - } diff --git a/scripts/GetReleaseInfo.sh b/scripts/GetReleaseInfo.sh index 222649a4..37beb79a 100644 --- a/scripts/GetReleaseInfo.sh +++ b/scripts/GetReleaseInfo.sh @@ -30,7 +30,7 @@ make_latest=$([ "$preVersion" = "true" ] && echo "false" || echo "true") rm -f $discordmes rm -f $releaseinfo -mv build/libs/Leaves-leavesclip-"$gradleVersion"-mojmap.jar "$jarName" +mv leaves-server/build/libs/leaves-leavesclip-"$gradleVersion"-mojmap.jar "$jarName" { echo "name=$leavesid" echo "tag=$tagid" diff --git a/scripts/RemoveOldVersionTags.sh b/scripts/RemoveOldVersionTags.sh deleted file mode 100644 index 52c37623..00000000 --- a/scripts/RemoveOldVersionTags.sh +++ /dev/null @@ -1,19 +0,0 @@ -if [ $# != 1 ]; then - echo need input version - exit 1 -fi - -version=$1 -lastestTag=$(git describe --tags --abbrev=0 --match "$version-*") - -echo "$(git tag --list "$version-*")" >> tags.temp - -while read line -do - if [ $line != $lastestTag ]; then - git push origin :refs/tags/$line - #echo $line - fi -done < tags.temp - -rm tags.temp diff --git a/settings.gradle.kts b/settings.gradle.kts index 98147135..bf4a5bba 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -7,9 +7,9 @@ pluginManagement { } plugins { - id("org.gradle.toolchains.foojay-resolver-convention") version("0.8.0") + id("org.gradle.toolchains.foojay-resolver-convention") version("0.9.0") } rootProject.name = "Leaves" -include("leaves-api", "leaves-server", "paper-api-generator") +include("leaves-api", "leaves-server")