From d09469e84be085e11f1c8b82797aa3aeaf7eea3d Mon Sep 17 00:00:00 2001 From: Helvetica Volubi <88063803+Suisuroru@users.noreply.github.com> Date: Mon, 4 Aug 2025 15:02:58 +0800 Subject: [PATCH] servux protocol & unapply old replaceable by mushrooms [(#38)(#40)](#43) * feat: add a config to allow to disable some check for operator * feat: leaves servux protocol * fix: upload missing files * Update Upstream(Luminol) * fix: upload missing files - 2 * fix: reuse tickCount * fix: fix * fix: adjust folia * add leaves license * add infinite support --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 + .github/ISSUE_TEMPLATE/feature-request.yml | 2 + .github/workflows/build.yml | 18 +- .../0009-Leaves-Leaves-Protocol-Core.patch | 198 ++++++++ ...=> 0010-Leaves-Disable-packet-limit.patch} | 2 +- ... => 0011-Leaves-Item-overstack-util.patch} | 2 +- ... 0012-Leaves-Redstone-Shears-Wrench.patch} | 0 .../0014-Leaves-Servux-Protocol.patch | 36 ++ ...rarily-fix-endermite-spawn-position.patch} | 0 ...patch => 0016-Paper-Fix-quick-craft.patch} | 0 ...rpur-Barrels-and-enderchests-6-rows.patch} | 4 +- .../0018-Old-replaceable-by-mushrooms.patch | 51 -- ...tch => 0018-Spawn-invulnerable-time.patch} | 0 ...019-Old-Explosion-Damage-Calculator.patch} | 0 ...ch => 0020-Old-zombie-reinforcement.patch} | 0 ...-support.patch => 0021-I18n-support.patch} | 0 ...22-Disable-some-check-for-operators.patch} | 0 ...urpur-Barrels-and-enderchests-6-rows.patch | 5 +- .../0003-Leaves-Leaves-Protocol-Core.patch | 29 ++ .../config/modules/misc/OldFeatureConfig.java | 3 - .../modules/misc/SurvuxProtocolConfig.java | 46 ++ .../modules/optimizations/NetworkConfig.java | 2 +- .../config/modules/removed/RemovedConfig.java | 1 + .../leaves/protocol/core/Context.java | 9 + .../protocol/core/IdentifierSelector.java | 11 + .../protocol/core/LeavesCustomPayload.java | 30 ++ .../leaves/protocol/core/LeavesProtocol.java | 21 + .../protocol/core/LeavesProtocolManager.java | 383 +++++++++++++++ .../leaves/protocol/core/ProtocolHandler.java | 85 ++++ .../leaves/protocol/core/ProtocolUtils.java | 71 +++ .../core/invoker/AbstractInvokerHolder.java | 75 +++ .../invoker/BytebufReceiverInvokerHolder.java | 18 + .../core/invoker/EmptyInvokerHolder.java | 15 + .../core/invoker/InitInvokerHolder.java | 16 + .../MinecraftRegisterInvokerHolder.java | 18 + .../invoker/PayloadReceiverInvokerHolder.java | 18 + .../core/invoker/PlayerInvokerHolder.java | 16 + .../protocol/servux/PacketSplitter.java | 134 ++++++ .../servux/ServuxEntityDataProtocol.java | 278 +++++++++++ .../servux/ServuxHudDataProtocol.java | 363 ++++++++++++++ .../protocol/servux/ServuxProtocol.java | 36 ++ .../servux/ServuxStructuresProtocol.java | 419 +++++++++++++++++ .../litematics/LitematicaSchematic.java | 281 +++++++++++ .../servux/litematics/SchematicMetadata.java | 62 +++ .../litematics/ServuxLitematicsProtocol.java | 442 ++++++++++++++++++ .../container/LitematicaBitArray.java | 104 +++++ .../LitematicaBlockStateContainer.java | 118 +++++ .../LitematicaBlockStatePalette.java | 62 +++ .../LitematicaBlockStatePaletteHashMap.java | 90 ++++ .../LitematicaBlockStatePaletteLinear.java | 91 ++++ .../placement/SchematicPlacement.java | 296 ++++++++++++ .../placement/SubRegionPlacement.java | 55 +++ .../servux/litematics/selection/Box.java | 36 ++ .../servux/litematics/utils/EntityUtils.java | 111 +++++ .../servux/litematics/utils/FileType.java | 28 ++ .../litematics/utils/Int2ObjectBiMap.java | 211 +++++++++ .../litematics/utils/IntBoundingBox.java | 43 ++ .../servux/litematics/utils/NbtUtils.java | 92 ++++ .../litematics/utils/PositionUtils.java | 149 ++++++ .../litematics/utils/ReplaceBehavior.java | 40 ++ .../servux/litematics/utils/Schema.java | 168 +++++++ .../utils/SchematicPlacingUtils.java | 440 +++++++++++++++++ .../protocol/servux/logger/DataLogger.java | 170 +++++++ .../servux/logger/data/MobCapData.java | 50 ++ .../protocol/servux/logger/data/TickData.java | 35 ++ .../leaves/util/ItemOverstackUtils.java | 17 + .../org/leavesmc/leaves/util/TagFactory.java | 46 ++ .../org/leavesmc/leaves/util/TagUtil.java | 88 ++++ scripts/SetENV.sh | 8 +- 69 files changed, 5678 insertions(+), 72 deletions(-) create mode 100644 lophine-server/minecraft-patches/features/0009-Leaves-Leaves-Protocol-Core.patch rename lophine-server/minecraft-patches/features/{0009-Leaves-Disable-packet-limit.patch => 0010-Leaves-Disable-packet-limit.patch} (98%) rename lophine-server/minecraft-patches/features/{0010-Leaves-Item-overstack-util.patch => 0011-Leaves-Item-overstack-util.patch} (99%) rename lophine-server/minecraft-patches/features/{0011-Leaves-Redstone-Shears-Wrench.patch => 0012-Leaves-Redstone-Shears-Wrench.patch} (100%) create mode 100644 lophine-server/minecraft-patches/features/0014-Leaves-Servux-Protocol.patch rename lophine-server/minecraft-patches/features/{0012-Leaves-Temporarily-fix-endermite-spawn-position.patch => 0015-Leaves-Temporarily-fix-endermite-spawn-position.patch} (100%) rename lophine-server/minecraft-patches/features/{0014-Paper-Fix-quick-craft.patch => 0016-Paper-Fix-quick-craft.patch} (100%) rename lophine-server/minecraft-patches/features/{0015-Purpur-Barrels-and-enderchests-6-rows.patch => 0017-Purpur-Barrels-and-enderchests-6-rows.patch} (98%) delete mode 100644 lophine-server/minecraft-patches/features/0018-Old-replaceable-by-mushrooms.patch rename lophine-server/minecraft-patches/features/{0016-Spawn-invulnerable-time.patch => 0018-Spawn-invulnerable-time.patch} (100%) rename lophine-server/minecraft-patches/features/{0017-Old-Explosion-Damage-Calculator.patch => 0019-Old-Explosion-Damage-Calculator.patch} (100%) rename lophine-server/minecraft-patches/features/{0019-Old-zombie-reinforcement.patch => 0020-Old-zombie-reinforcement.patch} (100%) rename lophine-server/minecraft-patches/features/{0020-I18n-support.patch => 0021-I18n-support.patch} (100%) rename lophine-server/minecraft-patches/features/{0021-Disable-some-check-for-operators.patch => 0022-Disable-some-check-for-operators.patch} (100%) create mode 100644 lophine-server/paper-patches/features/0003-Leaves-Leaves-Protocol-Core.patch create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/misc/SurvuxProtocolConfig.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/Context.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/IdentifierSelector.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/AbstractInvokerHolder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/BytebufReceiverInvokerHolder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/EmptyInvokerHolder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/InitInvokerHolder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/MinecraftRegisterInvokerHolder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PayloadReceiverInvokerHolder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PlayerInvokerHolder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/LitematicaSchematic.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/SchematicMetadata.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/ServuxLitematicsProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBitArray.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStateContainer.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePalette.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteHashMap.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteLinear.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SchematicPlacement.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SubRegionPlacement.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/selection/Box.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/EntityUtils.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/FileType.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Int2ObjectBiMap.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/IntBoundingBox.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/NbtUtils.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/PositionUtils.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/ReplaceBehavior.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Schema.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/SchematicPlacingUtils.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/DataLogger.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/data/MobCapData.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/data/TickData.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/util/TagFactory.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/util/TagUtil.java diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 166185c..0e441a3 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -9,6 +9,8 @@ body: - type: markdown attributes: value: | + 在提问之前,请您先阅读 [提问的智慧](https://lug.ustc.edu.cn/wiki/doc/smart-questions/) ,我们不会为任何无意义的问题作回复。 + Please read [smart questions](http://www.catb.org/~esr/faqs/smart-questions.html) before asking questions, we won't respond to any meaningless questions. 请详细描述您所遇到的Bug。确保您**真的**遇到了**服务端**Bug,而非原版特性或客户端Bug。 Please describe the bug you encountered in detail. Make sure you're **actually** hitting a **server-side** bug, not a vanilla feature or client-side bug.
如果您确定的话,就按照下面的模板填充信息,并提交反馈。 diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 0f44fca..4f96978 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -11,6 +11,8 @@ body: value: | 感谢您为 Lophine 提交新的需求!请尽可能详细地描述,以便我们可以更轻松地考虑和审查请求。 Thank you for filling out a feature request for Lophine! Please be as detailed as possible so that we may consider and review the request easier.
+ 在提问之前,请您先阅读 [提问的智慧](https://lug.ustc.edu.cn/wiki/doc/smart-questions/) ,我们不会为任何无意义的问题作回复。 + Please read [smart questions](http://www.catb.org/~esr/faqs/smart-questions.html) before asking questions, we won't respond to any meaningless questions. 请搜索所有问题以避免重复的功能请求。如果您期望的功能已存在,请回复如果您有什么要补充的。 We ask that you search all the issues to avoid a duplicate feature request. If one exists, please reply if you have anything to add.
在请求新功能之前,请确保您使用的是最新版本并且您请求的功能不在 Lophine 中。 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2e47cfd..ef334c7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -39,14 +39,6 @@ jobs: - name: CreateMojmapPaperclipJar run: ./gradlew --refresh-dependencies createMojmapPaperclipJar - - name: Publish to repo - if: github.event_name != 'pull_request' && false # do not push to any repo - continue-on-error: true - run: ./gradlew --refresh-dependencies generateDevelopmentBundle publish -PpublishDevBundle=true - env: - PRIVATE_MAVEN_REPO_PASSWORD: ${{ secrets.PRIVATE_MAVEN_REPO_PASSWORD }} - PRIVATE_MAVEN_REPO_USERNAME: ${{ secrets.PRIVATE_MAVEN_REPO_USERNAME }} - - name: Upload Artifact uses: actions/upload-artifact@v4 with: @@ -57,8 +49,16 @@ jobs: if: github.event_name != 'pull_request' run: sh scripts/SetENV.sh + - name: Publish to repo + if: github.event_name != 'pull_request' && env.flag_release == 'true' && false # do not push to any repo + continue-on-error: true + run: ./gradlew --refresh-dependencies generateDevelopmentBundle publish -PpublishDevBundle=true + env: + PRIVATE_MAVEN_REPO_PASSWORD: ${{ secrets.PRIVATE_MAVEN_REPO_PASSWORD }} + PRIVATE_MAVEN_REPO_USERNAME: ${{ secrets.PRIVATE_MAVEN_REPO_USERNAME }} + - name: Create Release - if: github.event_name != 'pull_request' + if: github.event_name != 'pull_request' && env.flag_release == 'true' uses: ncipollo/release-action@v1 with: tag: ${{ env.tag }} diff --git a/lophine-server/minecraft-patches/features/0009-Leaves-Leaves-Protocol-Core.patch b/lophine-server/minecraft-patches/features/0009-Leaves-Leaves-Protocol-Core.patch new file mode 100644 index 0000000..da41e60 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0009-Leaves-Leaves-Protocol-Core.patch @@ -0,0 +1,198 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 3 Aug 2025 15:24:01 +0800 +Subject: [PATCH] Leaves: Leaves Protocol Core + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/ea91106ae57fc4cc14e2e0225009cf9919072f7f/leaves-server/minecraft-patches/features/0004-Leaves-Protocol-Core.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/io/papermc/paper/threadedregions/RegionizedServer.java b/io/papermc/paper/threadedregions/RegionizedServer.java +index 8405157ec35a443e238fa6866772c839621d73d7..4edb6e6e6287f232f7eef45c747554264ec05319 100644 +--- a/io/papermc/paper/threadedregions/RegionizedServer.java ++++ b/io/papermc/paper/threadedregions/RegionizedServer.java +@@ -334,6 +334,8 @@ public final class RegionizedServer { + } + // Luminol end - Add a config to enable tick command + ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleTick(tickCount); // Leaves - protocol ++ + // tick connections + this.tickConnections(); + +@@ -443,6 +445,8 @@ public final class RegionizedServer { + + world.updateTickData(); + ++ MinecraftServer.getServer().handleTickCount(tickCount); // Lophine - reuse tick count ++ + world.moonrise$getChunkTaskScheduler().chunkHolderManager.processTicketUpdates(); // required to eventually process ticket updates + } + +diff --git a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java +index fb263fa1f30a7dfcb7ec2656abfb38e5fe88eac9..56fd1ed7ccaf96e7eedea60fbdbf7f934939d563 100644 +--- a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java ++++ b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java +@@ -40,13 +40,22 @@ 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) { ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.encode(buffer, payload); ++ 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 e8c9e739de35313c0212bab97a202fa362fd0887..c990e80f16d03967c9bb6fc5f48f145c01948209 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -305,6 +305,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop CompletableFuture submit(java.util.function.Supplier task) { +@@ -2223,9 +2224,16 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop pluginMessagerChannels; + // Paper end - retain certain values ++ public final GameProfile profile; // Leaves - protocol core + + public ServerCommonPacketListenerImpl(MinecraftServer server, Connection connection, CommonListenerCookie cookie) { + this.server = server; +@@ -70,6 +71,7 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + this.pluginMessagerChannels = cookie.channels(); + this.keepAlive = cookie.keepAlive(); + // Paper end ++ this.profile = cookie.gameProfile(); // Leaves - protocol core + } + + // Paper start - configuration phase API +@@ -160,6 +162,18 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + + @Override + public void handleCustomPayload(ServerboundCustomPayloadPacket packet) { ++ // Leaves start - protocol ++ if (packet.payload() instanceof org.leavesmc.leaves.protocol.core.LeavesCustomPayload leavesPayload) { ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePayload(org.leavesmc.leaves.protocol.core.ProtocolUtils.createSelector(this), leavesPayload); ++ return; ++ } ++ if (packet.payload() instanceof net.minecraft.network.protocol.common.custom.DiscardedPayload(net.minecraft.resources.ResourceLocation id, byte[] data)) { ++ if (org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleBytebuf(org.leavesmc.leaves.protocol.core.ProtocolUtils.createSelector(this), id, io.netty.buffer.Unpooled.wrappedBuffer(data))) { ++ return; ++ } ++ } ++ // Leaves end - protocol ++ + // Paper start + if (!(packet.payload() instanceof final net.minecraft.network.protocol.common.custom.DiscardedPayload discardedPayload)) { + return; +@@ -219,10 +233,11 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + final String channel = new String(data, from, length, java.nio.charset.StandardCharsets.US_ASCII); + if (register) { + bridge.addChannel(channel); ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleMinecraftRegister(channel, org.leavesmc.leaves.protocol.core.ProtocolUtils.createSelector(this)); // Leaves - protocol + } else { + bridge.removeChannel(channel); + } +- // Paper end ++ // Paper end + } + + @Override +@@ -385,9 +400,9 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + net.minecraft.server.level.ServerPlayer player = serverGamePacketListener.player; + org.bukkit.event.player.PlayerKickEvent.Cause cause = disconnectionDetails.disconnectionReason().orElseThrow().game().orElse(org.bukkit.event.player.PlayerKickEvent.Cause.UNKNOWN); + org.bukkit.event.player.PlayerKickEvent event = new org.bukkit.event.player.PlayerKickEvent( +- player.getBukkitEntity(), +- io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionDetails.reason()), +- rawLeaveMessage, cause ++ player.getBukkitEntity(), ++ io.papermc.paper.adventure.PaperAdventure.asAdventure(disconnectionDetails.reason()), ++ rawLeaveMessage, cause + + ); + +@@ -420,10 +435,10 @@ public abstract class ServerCommonPacketListenerImpl implements ServerCommonPack + + private void disconnect0(DisconnectionDetails disconnectionDetails) { + this.connection +- .send( +- new ClientboundDisconnectPacket(disconnectionDetails.reason()), +- PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails)) +- ); ++ .send( ++ new ClientboundDisconnectPacket(disconnectionDetails.reason()), ++ PacketSendListener.thenRun(() -> this.connection.disconnect(disconnectionDetails)) ++ ); + this.onDisconnect(disconnectionDetails); + this.connection.setReadOnly(); + // CraftBukkit - Don't wait +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 5c21fc0c749973d17df690fe7e2eab597b198131..8aea58c7797940929944973380b6ab67f177fa56 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -423,6 +423,8 @@ public abstract class PlayerList { + //return; // Folia - region threading - must still allow the player to connect, as we must add to chunk map before handling disconnect + } + ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handlePlayerJoin(player); ++ + final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); + + if (jm != null && !jm.equals(net.kyori.adventure.text.Component.empty())) { // Paper - Adventure +@@ -605,6 +607,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 @Nullable 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.level(); + player.awardStat(Stats.LEAVE_GAME); +@@ -1522,6 +1525,7 @@ public abstract class PlayerList { + serverPlayer.connection.send(clientboundUpdateRecipesPacket); + serverPlayer.getRecipeBook().sendInitialRecipeBook(serverPlayer); + } ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleDataPackReload(); // Leaves - protocol core + } + + public boolean isAllowCommandsForAllPlayers() { diff --git a/lophine-server/minecraft-patches/features/0009-Leaves-Disable-packet-limit.patch b/lophine-server/minecraft-patches/features/0010-Leaves-Disable-packet-limit.patch similarity index 98% rename from lophine-server/minecraft-patches/features/0009-Leaves-Disable-packet-limit.patch rename to lophine-server/minecraft-patches/features/0010-Leaves-Disable-packet-limit.patch index 4ded975..6fb5289 100644 --- a/lophine-server/minecraft-patches/features/0009-Leaves-Disable-packet-limit.patch +++ b/lophine-server/minecraft-patches/features/0010-Leaves-Disable-packet-limit.patch @@ -23,7 +23,7 @@ index 6dc5f3e88a3886b03b99d59b317085fdc3e2fa9a..e556bcaf2a5863073a047947f5962179 synchronized (PACKET_LIMIT_LOCK) { if (this.allPacketCounts != null) { diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 68f0023c4234585a8385a830ed410883c8bd1549..206b4a532eba02b2a1f6f3b832970bc4c94fc45b 100644 +index 68f0023c4234585a8385a830ed410883c8bd1549..d017cfc5b97552388d8f9ff80ca31b0972b80a2e 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -856,7 +856,7 @@ public class ServerGamePacketListenerImpl diff --git a/lophine-server/minecraft-patches/features/0010-Leaves-Item-overstack-util.patch b/lophine-server/minecraft-patches/features/0011-Leaves-Item-overstack-util.patch similarity index 99% rename from lophine-server/minecraft-patches/features/0010-Leaves-Item-overstack-util.patch rename to lophine-server/minecraft-patches/features/0011-Leaves-Item-overstack-util.patch index 71b417a..93bf7aa 100644 --- a/lophine-server/minecraft-patches/features/0010-Leaves-Item-overstack-util.patch +++ b/lophine-server/minecraft-patches/features/0011-Leaves-Item-overstack-util.patch @@ -41,7 +41,7 @@ index 5a1c21b05545a03fbb00cf734605049870d3eecb..a0459bc6b2241e7fca74bfbfce058882 if (count > i) { source.sendFailure(Component.translatable("commands.give.failed.toomanyitems", i, itemStack.getDisplayName())); diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java -index 206b4a532eba02b2a1f6f3b832970bc4c94fc45b..39b5422af26fce5f1d5437ed35676b511bf13241 100644 +index d017cfc5b97552388d8f9ff80ca31b0972b80a2e..a684f5edf420d9655046de872c54e0c1a89103cf 100644 --- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -3078,7 +3078,7 @@ public class ServerGamePacketListenerImpl diff --git a/lophine-server/minecraft-patches/features/0011-Leaves-Redstone-Shears-Wrench.patch b/lophine-server/minecraft-patches/features/0012-Leaves-Redstone-Shears-Wrench.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0011-Leaves-Redstone-Shears-Wrench.patch rename to lophine-server/minecraft-patches/features/0012-Leaves-Redstone-Shears-Wrench.patch diff --git a/lophine-server/minecraft-patches/features/0014-Leaves-Servux-Protocol.patch b/lophine-server/minecraft-patches/features/0014-Leaves-Servux-Protocol.patch new file mode 100644 index 0000000..ce2f0f3 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0014-Leaves-Servux-Protocol.patch @@ -0,0 +1,36 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 3 Aug 2025 15:19:23 +0800 +Subject: [PATCH] Leaves: Servux Protocol + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/ea91106ae57fc4cc14e2e0225009cf9919072f7f/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/server/ServerTickRateManager.java b/net/minecraft/server/ServerTickRateManager.java +index d0d90a25a10bbecfffceee1992af88c60d14fd87..187e48e78e69c80b25710f193813335ec643957f 100644 +--- a/net/minecraft/server/ServerTickRateManager.java ++++ b/net/minecraft/server/ServerTickRateManager.java +@@ -134,4 +134,10 @@ public class ServerTickRateManager extends TickRateManager { + player.connection.send(ClientboundTickingStatePacket.from(this)); + player.connection.send(ClientboundTickingStepPacket.from(this)); + } ++ ++ // Leaves start - servux ++ public long getRemainingSprintTicks() { ++ return remainingSprintTicks; ++ } ++ // Leaves end - servux + } +diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java +index 87377bba2ebdc0eba5b7b212d971994c35ca0b62..94d0c7bd2dfc2620a67035ce9084e9da219e6509 100644 +--- a/net/minecraft/server/level/ServerLevel.java ++++ b/net/minecraft/server/level/ServerLevel.java +@@ -2368,6 +2368,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe + } + + this.lastSpawnChunkRadius = i; ++ org.leavesmc.leaves.protocol.servux.ServuxHudDataProtocol.refreshSpawnMetadata = true; // Leaves - servux + } + + public LongSet getForceLoadedChunks() { diff --git a/lophine-server/minecraft-patches/features/0012-Leaves-Temporarily-fix-endermite-spawn-position.patch b/lophine-server/minecraft-patches/features/0015-Leaves-Temporarily-fix-endermite-spawn-position.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0012-Leaves-Temporarily-fix-endermite-spawn-position.patch rename to lophine-server/minecraft-patches/features/0015-Leaves-Temporarily-fix-endermite-spawn-position.patch diff --git a/lophine-server/minecraft-patches/features/0014-Paper-Fix-quick-craft.patch b/lophine-server/minecraft-patches/features/0016-Paper-Fix-quick-craft.patch similarity index 100% rename from lophine-server/minecraft-patches/features/0014-Paper-Fix-quick-craft.patch rename to lophine-server/minecraft-patches/features/0016-Paper-Fix-quick-craft.patch diff --git a/lophine-server/minecraft-patches/features/0015-Purpur-Barrels-and-enderchests-6-rows.patch b/lophine-server/minecraft-patches/features/0017-Purpur-Barrels-and-enderchests-6-rows.patch similarity index 98% rename from lophine-server/minecraft-patches/features/0015-Purpur-Barrels-and-enderchests-6-rows.patch rename to lophine-server/minecraft-patches/features/0017-Purpur-Barrels-and-enderchests-6-rows.patch index f85048b..b8f0a36 100644 --- a/lophine-server/minecraft-patches/features/0015-Purpur-Barrels-and-enderchests-6-rows.patch +++ b/lophine-server/minecraft-patches/features/0017-Purpur-Barrels-and-enderchests-6-rows.patch @@ -8,10 +8,10 @@ As part of: Purpur (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18 Licensed under: MIT (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/LICENSE) diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java -index 5c21fc0c749973d17df690fe7e2eab597b198131..f01e3147cb49ee37d8e33d4146aa1a72cf38014f 100644 +index 8aea58c7797940929944973380b6ab67f177fa56..9c4e5789f6881ca300cc183294e50b03dd03f6c6 100644 --- a/net/minecraft/server/players/PlayerList.java +++ b/net/minecraft/server/players/PlayerList.java -@@ -1086,6 +1086,10 @@ public abstract class PlayerList { +@@ -1089,6 +1089,10 @@ public abstract class PlayerList { player.getBukkitEntity().recalculatePermissions(); // CraftBukkit this.server.getCommands().sendCommands(player); } // Paper - Add sendOpLevel API diff --git a/lophine-server/minecraft-patches/features/0018-Old-replaceable-by-mushrooms.patch b/lophine-server/minecraft-patches/features/0018-Old-replaceable-by-mushrooms.patch deleted file mode 100644 index 26c36b7..0000000 --- a/lophine-server/minecraft-patches/features/0018-Old-replaceable-by-mushrooms.patch +++ /dev/null @@ -1,51 +0,0 @@ -From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 -From: Helvetica Volubi -Date: Wed, 18 Jun 2025 14:24:54 +0800 -Subject: [PATCH] Old replaceable by mushrooms - - -diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java -index 93d722d437121e605cee0e168ac17ee9bef0610e..cb2a2fbd17157726b285665fdca0ab9663e0d1de 100644 ---- a/net/minecraft/world/level/Level.java -+++ b/net/minecraft/world/level/Level.java -@@ -1111,7 +1111,7 @@ public abstract class Level implements LevelAccessor, UUIDLookup, AutoCl - if (worldData.captureTreeGeneration) { // Folia - region threading - // Paper start - Protect Bedrock and End Portal/Frames from being destroyed - BlockState type = getBlockState(pos); -- if (!type.isDestroyable()) return false; -+ if (!type.isDestroyable() && !(fun.bm.lophine.config.modules.misc.OldFeatureConfig.oldReplaceableByMushrooms && flags == 3)) return false; - // Paper end - Protect Bedrock and End Portal/Frames from being destroyed - CraftBlockState blockstate = worldData.capturedBlockStates.get(pos); // Folia - region threading - if (blockstate == null) { -diff --git a/net/minecraft/world/level/block/state/BlockBehaviour.java b/net/minecraft/world/level/block/state/BlockBehaviour.java -index 834e27ef2f7b342b074ff9e1e390e02f3ca1c399..f5765e3bbf9e735d6f959e1bf6ee1846ee11b4e9 100644 ---- a/net/minecraft/world/level/block/state/BlockBehaviour.java -+++ b/net/minecraft/world/level/block/state/BlockBehaviour.java -@@ -770,6 +770,14 @@ public abstract class BlockBehaviour implements FeatureElement { - return this.solidRender; - } - -+ public boolean mushroomCheck(BlockState state) { -+ BlockState blockState = state.asState(); -+ if (blockState.canOcclude()) { -+ return !Block.isShapeFullBlock(blockState.getOcclusionShape()); -+ } -+ return true; -+ } -+ - public final boolean canOcclude() { // Paper - Perf: Final for inlining - return this.canOcclude; - } -diff --git a/net/minecraft/world/level/levelgen/feature/AbstractHugeMushroomFeature.java b/net/minecraft/world/level/levelgen/feature/AbstractHugeMushroomFeature.java -index 3a37d66ab3c27c9abd60f35ef3bf3a93f8d7c3cd..2aad65d76cdd0800ac593a5bb3ce84ad1925eab0 100644 ---- a/net/minecraft/world/level/levelgen/feature/AbstractHugeMushroomFeature.java -+++ b/net/minecraft/world/level/levelgen/feature/AbstractHugeMushroomFeature.java -@@ -26,7 +26,7 @@ public abstract class AbstractHugeMushroomFeature extends Feature Date: Thu, 20 Feb 2025 01:00:29 +0800 -Subject: [PATCH] Purpur-Barrels-and-enderchests-6-rows +Subject: [PATCH] Purpur: Barrels and enderchests 6 rows +Co-authored by: William Blake Galbreath +As part of: Purpur (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/purpur-server/paper-patches/features/0003-Barrels-and-enderchests-6-rows.patch) +Licensed under: MIT (https://github.com/PurpurMC/Purpur/blob/09f547de09fc5d886f18f6d99ff389289766ec9d/LICENSE) diff --git a/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java b/src/main/java/org/bukkit/craftbukkit/inventory/CraftContainer.java index 2f41a92465b9da28e026297cc3528898bb1c8412..fb5300ec1bd19b3dfd6a2b4106440b9f73c48205 100644 diff --git a/lophine-server/paper-patches/features/0003-Leaves-Leaves-Protocol-Core.patch b/lophine-server/paper-patches/features/0003-Leaves-Leaves-Protocol-Core.patch new file mode 100644 index 0000000..5662284 --- /dev/null +++ b/lophine-server/paper-patches/features/0003-Leaves-Leaves-Protocol-Core.patch @@ -0,0 +1,29 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Helvetica Volubi +Date: Sun, 3 Aug 2025 15:57:08 +0800 +Subject: [PATCH] Leaves: Leaves Protocol Core + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves/blob/ea91106ae57fc4cc14e2e0225009cf9919072f7f/leaves-server/paper-patches/features/0004-Leaves-Protocol-Core.patch) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 44b7ac045c028f3bc940d78f158079e20582f4b8..bdcd25b98fa8189af874e4715db7d4440752756e 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -496,6 +496,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 + if (false) this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark // Luminol - Force disable builtin spark ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.init(); // Leaves - protocol + } + + public boolean getCommandBlockOverride(String command) { +@@ -1112,6 +1113,7 @@ public final class CraftServer implements Server { + if (false) this.spark.registerCommandBeforePlugins(this); // Paper - spark // Luminol - Force disable builtin spark + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleServerReload(); // Leaves - protocol + + int pollCount = 0; + diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/misc/OldFeatureConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/misc/OldFeatureConfig.java index 7c7575b..6b1e8c7 100644 --- a/lophine-server/src/main/java/fun/bm/lophine/config/modules/misc/OldFeatureConfig.java +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/misc/OldFeatureConfig.java @@ -11,9 +11,6 @@ public class OldFeatureConfig implements IConfigModule { @ConfigInfo(baseName = "old_zombie_reinforcement") public static boolean oldZombieReinforcement = false; - @ConfigInfo(baseName = "old_replaceable_by_mushrooms") - public static boolean oldReplaceableByMushrooms = false; - @ConfigInfo(baseName = "old_explosion_damage_calculator") public static boolean oldExplosionDamageCalculator = false; diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/misc/SurvuxProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/misc/SurvuxProtocolConfig.java new file mode 100644 index 0000000..d2099f9 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/misc/SurvuxProtocolConfig.java @@ -0,0 +1,46 @@ +package fun.bm.lophine.config.modules.misc; + +import me.earthme.luminol.config.EnumConfigCategory; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigInfo; + +import java.util.List; + +public class SurvuxProtocolConfig implements IConfigModule { + @ConfigInfo(baseName = "entity-protocol") + public static boolean entityProtocol = false; + + @ConfigInfo(baseName = "hud-logger-protocol") + public static boolean hudLoggerProtocol = false; + + @ConfigInfo(baseName = "hud-metadata-protocol") + public static boolean hudMetadataProtocol = false; + + @ConfigInfo(baseName = "hud-metadata-share-seed") + public static boolean hudMetadataShareSeed = false; + + @ConfigInfo(baseName = "structure-protocol") + public static boolean structureProtocol = false; + + @ConfigInfo(baseName = "hud-enabled-loggers") + public static List hudEnabledLoggers = List.of("tps", "mob_caps"); + + @ConfigInfo(baseName = "hud-update-interval") + public static int hudUpdateInterval = 1; + + @ConfigInfo(baseName = "litematics-enabled") + public static boolean litematicsEnabled = false; + + @ConfigInfo(baseName = "litematics-max-nbt-size") + public static int litematicsMaxNbtSize = 2097152; + + @Override + public EnumConfigCategory getCategory() { + return EnumConfigCategory.MISC; + } + + @Override + public String getBaseName() { + return "servux-protocol"; + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/optimizations/NetworkConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/optimizations/NetworkConfig.java index f217c77..3fa2c8b 100644 --- a/lophine-server/src/main/java/fun/bm/lophine/config/modules/optimizations/NetworkConfig.java +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/optimizations/NetworkConfig.java @@ -6,7 +6,7 @@ import me.earthme.luminol.config.flags.ConfigInfo; public class NetworkConfig implements IConfigModule { @ConfigInfo(baseName = "unlimit-packet", comments = """ - Disable packet limit""") + Disable packet limit""") public static boolean disablePacketLimit = false; @Override diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/removed/RemovedConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/removed/RemovedConfig.java index 6b3b656..3bdad8c 100644 --- a/lophine-server/src/main/java/fun/bm/lophine/config/modules/removed/RemovedConfig.java +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/removed/RemovedConfig.java @@ -6,6 +6,7 @@ import me.earthme.luminol.config.flags.ConfigInfo; import me.earthme.luminol.config.flags.TransformedConfig; public class RemovedConfig implements IConfigModule { + @TransformedConfig(name = "old_replaceable_by_mushrooms", category = {"misc", "old-feature"}, transform = false) @TransformedConfig(name = "old_nether_portal_collision", category = {"misc", "old-feature"}, transform = false) @TransformedConfig(name = "better_shulker_box", category = {"misc", "container_expansion"}, transform = false) @ConfigInfo(baseName = "removed", comments = diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/Context.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/Context.java new file mode 100644 index 0000000..ca17fdb --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/Context.java @@ -0,0 +1,9 @@ +package org.leavesmc.leaves.protocol.core; + +import com.mojang.authlib.GameProfile; +import net.minecraft.network.Connection; +import org.jetbrains.annotations.NotNull; + +public record Context(@NotNull GameProfile profile, Connection connection) { + +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/IdentifierSelector.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/IdentifierSelector.java new file mode 100644 index 0000000..3fe9d89 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/IdentifierSelector.java @@ -0,0 +1,11 @@ +package org.leavesmc.leaves.protocol.core; + +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Nullable; + +public record IdentifierSelector(@Nullable Context context, @Nullable ServerPlayer player) { + + public Object select(ProtocolHandler.Stage stage) { + return stage == ProtocolHandler.Stage.CONFIGURATION ? context : player; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java new file mode 100644 index 0000000..59e4aff --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java @@ -0,0 +1,30 @@ +package org.leavesmc.leaves.protocol.core; + +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 { + + Type LEAVES_TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath("leaves", "custom_payload")); + + @Override + default @NotNull Type type() { + return LEAVES_TYPE; + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface ID { + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface Codec { + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java new file mode 100644 index 0000000..8479a3f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java @@ -0,0 +1,21 @@ +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 interface LeavesProtocol { + + boolean isActive(); + + default int tickerInterval(String tickerID) { + return 1; + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface Register { + String namespace(); + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java new file mode 100644 index 0000000..05386eb --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java @@ -0,0 +1,383 @@ +package org.leavesmc.leaves.protocol.core; + +import com.mojang.logging.LogUtils; +import io.netty.buffer.ByteBuf; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.leavesmc.leaves.protocol.core.invoker.*; +import org.slf4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +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.*; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class LeavesProtocolManager { + private static final Logger LOGGER = LogUtils.getClassLogger(); + + private static final Map, PayloadReceiverInvokerHolder> PAYLOAD_RECEIVERS = new HashMap<>(); + private static final Map, ResourceLocation> IDS = new HashMap<>(); + private static final Map, StreamCodec> CODECS = new HashMap<>(); + private static final Map> ID2CODEC = new HashMap<>(); + + private static final Map STRICT_BYTEBUF_RECEIVERS = new HashMap<>(); + private static final Map NAMESPACED_BYTEBUF_RECEIVERS = new HashMap<>(); + private static final List GENERIC_BYTEBUF_RECEIVERS = new ArrayList<>(); + + private static final Map STRICT_MINECRAFT_REGISTER = new HashMap<>(); + private static final Map NAMESPACED_MINECRAFT_REGISTER = new HashMap<>(); + private static final List WILD_MINECRAFT_REGISTER = new ArrayList<>(); + + 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 List> RELOAD_DATAPACK = new ArrayList<>(); + + @SuppressWarnings("unchecked") + public static void init() { + for (Class clazz : getClasses("org.leavesmc.leaves.protocol")) { + if (LeavesCustomPayload.class.isAssignableFrom(clazz) && !clazz.equals(LeavesCustomPayload.class)) { + for (Field field : clazz.getDeclaredFields()) { + field.setAccessible(true); + if (!Modifier.isStatic(field.getModifiers())) { + continue; + } + try { + final LeavesCustomPayload.ID id = field.getAnnotation(LeavesCustomPayload.ID.class); + if (id != null && field.getType().equals(ResourceLocation.class)) { + IDS.put((Class) clazz, (ResourceLocation) field.get(null)); + } + final LeavesCustomPayload.Codec codec = field.getAnnotation(LeavesCustomPayload.Codec.class); + if (codec != null && field.getType().equals(StreamCodec.class)) { + CODECS.put((Class) clazz, (StreamCodec) field.get(null)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + continue; + } + + final LeavesProtocol.Register register = clazz.getAnnotation(LeavesProtocol.Register.class); + if (register == null) { + continue; + } + LeavesProtocol protocol; + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + protocol = (LeavesProtocol) constructor.newInstance(); + } catch (Throwable throwable) { + LOGGER.error("Failed to load class {}. {}", clazz.getName(), throwable); + return; + } + + for (final Method method : clazz.getDeclaredMethods()) { + if (method.isBridge() || method.isSynthetic()) { + continue; + } + method.setAccessible(true); + + final ProtocolHandler.Init init = method.getAnnotation(ProtocolHandler.Init.class); + if (init != null) { + InitInvokerHolder holder = new InitInvokerHolder(protocol, method, init); + try { + holder.invoke(); + } catch (RuntimeException exception) { + LOGGER.error("Failed to invoke init method {} in {}, {}: {}", method.getName(), clazz.getName(), exception.getCause(), exception.getMessage()); + } + continue; + } + + final ProtocolHandler.PayloadReceiver payloadReceiver = method.getAnnotation(ProtocolHandler.PayloadReceiver.class); + if (payloadReceiver != null) { + PAYLOAD_RECEIVERS.put(payloadReceiver.payload(), new PayloadReceiverInvokerHolder(protocol, method, payloadReceiver)); + continue; + } + + final ProtocolHandler.BytebufReceiver bytebufReceiver = method.getAnnotation(ProtocolHandler.BytebufReceiver.class); + if (bytebufReceiver != null) { + String key = bytebufReceiver.key(); + BytebufReceiverInvokerHolder holder = new BytebufReceiverInvokerHolder(protocol, method, bytebufReceiver); + if (bytebufReceiver.onlyNamespace()) { + NAMESPACED_BYTEBUF_RECEIVERS.put(key.isEmpty() ? register.namespace() : key, holder); + } else { + if (key.isEmpty()) { + GENERIC_BYTEBUF_RECEIVERS.add(holder); + } else { + if (key.contains(":")) { + STRICT_BYTEBUF_RECEIVERS.put(key, holder); + } else { + STRICT_BYTEBUF_RECEIVERS.put(register.namespace() + ":" + key, holder); + } + } + } + continue; + } + + final ProtocolHandler.Ticker ticker = method.getAnnotation(ProtocolHandler.Ticker.class); + if (ticker != null) { + TICKERS.add(new EmptyInvokerHolder<>(protocol, method, ticker)); + continue; + } + + final ProtocolHandler.PlayerJoin playerJoin = method.getAnnotation(ProtocolHandler.PlayerJoin.class); + if (playerJoin != null) { + PLAYER_JOIN.add(new PlayerInvokerHolder<>(protocol, method, playerJoin)); + continue; + } + + final ProtocolHandler.PlayerLeave playerLeave = method.getAnnotation(ProtocolHandler.PlayerLeave.class); + if (playerLeave != null) { + PLAYER_LEAVE.add(new PlayerInvokerHolder<>(protocol, method, playerLeave)); + continue; + } + + final ProtocolHandler.ReloadServer reloadServer = method.getAnnotation(ProtocolHandler.ReloadServer.class); + if (reloadServer != null) { + RELOAD_SERVER.add(new EmptyInvokerHolder<>(protocol, method, reloadServer)); + continue; + } + + final ProtocolHandler.ReloadDataPack reloadDataPack = method.getAnnotation(ProtocolHandler.ReloadDataPack.class); + if (reloadDataPack != null) { + RELOAD_DATAPACK.add(new EmptyInvokerHolder<>(protocol, method, reloadDataPack)); + continue; + } + + final ProtocolHandler.MinecraftRegister minecraftRegister = method.getAnnotation(ProtocolHandler.MinecraftRegister.class); + if (minecraftRegister != null) { + String key = minecraftRegister.key(); + MinecraftRegisterInvokerHolder holder = new MinecraftRegisterInvokerHolder(protocol, method, minecraftRegister); + if (minecraftRegister.onlyNamespace()) { + NAMESPACED_MINECRAFT_REGISTER.put(key.isEmpty() ? register.namespace() : key, holder); + } else { + if (key.isEmpty()) { + WILD_MINECRAFT_REGISTER.add(holder); + } else { + if (key.contains(":")) { + STRICT_MINECRAFT_REGISTER.put(key, holder); + } else { + STRICT_MINECRAFT_REGISTER.put(register.namespace() + ":" + key, holder); + } + } + } + } + } + } + for (var idInfo : IDS.entrySet()) { + var codec = CODECS.get(idInfo.getKey()); + if (codec == null) { + throw new IllegalArgumentException("Payload " + idInfo.getKey() + " is not configured correctly"); + } + ID2CODEC.put(idInfo.getValue(), codec); + } + } + + public static LeavesCustomPayload decode(ResourceLocation location, FriendlyByteBuf buf) { + var codec = ID2CODEC.get(location); + if (codec == null) { + return null; + } + return codec.decode(ProtocolUtils.decorate(buf)); + } + + public static void encode(FriendlyByteBuf buf, LeavesCustomPayload payload) { + var location = IDS.get(payload.getClass()); + var codec = CODECS.get(payload.getClass()); + if (location == null || codec == null) { + throw new IllegalArgumentException("Payload " + payload.getClass() + " is not configured correctly " + location + " " + codec); + } + buf.writeResourceLocation(location); + codec.encode(ProtocolUtils.decorate(buf), payload); + } + + public static void handlePayload(IdentifierSelector selector, LeavesCustomPayload payload) { + PayloadReceiverInvokerHolder holder; + if ((holder = PAYLOAD_RECEIVERS.get(payload.getClass())) != null) { + holder.invoke(selector, payload); + } + } + + public static boolean handleBytebuf(IdentifierSelector selector, ResourceLocation location, ByteBuf buf) { + RegistryFriendlyByteBuf buf1 = ProtocolUtils.decorate(buf); + BytebufReceiverInvokerHolder holder; + if ((holder = STRICT_BYTEBUF_RECEIVERS.get(location.toString())) != null) { + holder.invoke(selector, buf1); + return true; + } + if ((holder = NAMESPACED_BYTEBUF_RECEIVERS.get(location.getNamespace())) != null) { + if (holder.invoke(selector, buf1)) { + return true; + } + } + for (var holder1 : GENERIC_BYTEBUF_RECEIVERS) { + if (holder1.invoke(selector, buf1)) { + return true; + } + } + return false; + } + + public static void handleTick(long tickCount) { + for (var tickerInfo : TICKERS) { + if (tickCount % tickerInfo.owner().tickerInterval(tickerInfo.handler().tickerId()) == 0) { + tickerInfo.invoke(); + } + } + } + + public static void handlePlayerJoin(ServerPlayer player) { + sendKnownId(player); + for (var join : PLAYER_JOIN) { + join.invoke(player); + } + } + + public static void handlePlayerLeave(ServerPlayer player) { + for (var leave : PLAYER_LEAVE) { + leave.invoke(player); + } + } + + public static void handleServerReload() { + for (var reload : RELOAD_SERVER) { + reload.invoke(); + } + } + + public static void handleDataPackReload() { + for (var reload : RELOAD_DATAPACK) { + reload.invoke(); + } + } + + public static void handleMinecraftRegister(String channelId, IdentifierSelector selector) { + ResourceLocation location = ResourceLocation.tryParse(channelId); + if (location == null) { + return; + } + + for (var wildHolder : WILD_MINECRAFT_REGISTER) { + wildHolder.invoke(selector, location); + } + + MinecraftRegisterInvokerHolder holder; + if ((holder = STRICT_MINECRAFT_REGISTER.get(location.toString())) != null) { + holder.invoke(selector, location); + } + if ((holder = NAMESPACED_MINECRAFT_REGISTER.get(location.getNamespace())) != null) { + holder.invoke(selector, location); + } + } + + private static void sendKnownId(ServerPlayer player) { + Set set = new HashSet<>(); + PAYLOAD_RECEIVERS.forEach((clazz, holder) -> { + if (holder.owner().isActive()) { + set.add(IDS.get(clazz).toString()); + } + }); + STRICT_BYTEBUF_RECEIVERS.forEach((key, holder) -> { + if (holder.owner().isActive()) { + set.add(key); + } + }); + ProtocolUtils.sendBytebufPacket(player, ResourceLocation.fromNamespaceAndPath("minecraft", "register"), buf -> { + for (String channel : set) { + buf.writeBytes(channel.getBytes(StandardCharsets.US_ASCII)); + buf.writeByte(0); + } + buf.writerIndex(Math.max(buf.writerIndex() - 1, 0)); + }); + } + + 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.warn("Failed to load jar file, {}: {}", exception.getCause(), exception.getMessage()); + } + } + } + } catch (IOException exception) { + LOGGER.warn("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.warn("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.warn("Failed to load class {}, {}: {}", className, exception.getCause(), exception.getMessage()); + } + } + } + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java new file mode 100644 index 0000000..5aefe65 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java @@ -0,0 +1,85 @@ +package org.leavesmc.leaves.protocol.core; + +import net.minecraft.server.level.ServerPlayer; + +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(); + + Stage stage() default Stage.GAME; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface BytebufReceiver { + String key() default ""; + + boolean onlyNamespace() default false; + + Stage stage() default Stage.GAME; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface Ticker { + String tickerId() default ""; + } + + @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 key() default ""; + + boolean onlyNamespace() default false; + + Stage stage() default Stage.CONFIGURATION; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ReloadDataPack { + } + + public enum Stage { + CONFIGURATION(Context.class), + GAME(ServerPlayer.class); + + private final Class identifier; + + Stage(Class identifier) { + this.identifier = identifier; + } + + public Class identifier() { + return identifier; + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java new file mode 100644 index 0000000..963c3b4 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java @@ -0,0 +1,71 @@ +package org.leavesmc.leaves.protocol.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.papermc.paper.ServerBuildInfo; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.network.protocol.common.custom.DiscardedPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.server.network.ServerCommonPacketListenerImpl; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class ProtocolUtils { + + private static final Function bufDecorator = buf -> buf instanceof RegistryFriendlyByteBuf registry ? registry : new RegistryFriendlyByteBuf(buf, MinecraftServer.getServer().registryAccess()); + private static final byte[] EMPTY = new byte[0]; + + public static String buildProtocolVersion(String protocol) { + return protocol + "-lophine-" + ServerBuildInfo.buildInfo().asString(ServerBuildInfo.StringRepresentation.VERSION_SIMPLE); + } + + public static void sendEmptyPacket(ServerPlayer player, ResourceLocation id) { + player.connection.send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, EMPTY))); + } + + public static void sendBytebufPacket(@NotNull ServerPlayer player, ResourceLocation id, Consumer consumer) { + RegistryFriendlyByteBuf buf = decorate(Unpooled.buffer()); + consumer.accept(buf); + player.connection.send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, ByteBufUtil.getBytes(buf)))); + } + + public static void sendPayloadPacket(ServerPlayer player, CustomPacketPayload payload) { + player.connection.send(new ClientboundCustomPayloadPacket(payload)); + } + + public static void sendEmptyPacket(Context context, ResourceLocation id) { + context.connection().send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, EMPTY))); + } + + public static void sendBytebufPacket(@NotNull Context context, ResourceLocation id, Consumer consumer) { + RegistryFriendlyByteBuf buf = decorate(Unpooled.buffer()); + consumer.accept(buf); + context.connection().send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, ByteBufUtil.getBytes(buf)))); + } + + public static void sendPayloadPacket(Context context, CustomPacketPayload payload) { + context.connection().send(new ClientboundCustomPayloadPacket(payload)); + } + + public static RegistryFriendlyByteBuf decorate(ByteBuf buf) { + return bufDecorator.apply(buf); + } + + public static IdentifierSelector createSelector(ServerCommonPacketListenerImpl common) { + ServerPlayer player = common instanceof ServerGamePacketListenerImpl game ? game.getPlayer() : null; + return new IdentifierSelector(new Context(common.profile, common.connection), player); + } + + public static ByteBuf wrapNullable(byte @Nullable [] data) { + return data == null ? Unpooled.wrappedBuffer(EMPTY) : Unpooled.wrappedBuffer(data); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/AbstractInvokerHolder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/AbstractInvokerHolder.java new file mode 100644 index 0000000..23d0065 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/AbstractInvokerHolder.java @@ -0,0 +1,75 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +public abstract class AbstractInvokerHolder { + + protected final LeavesProtocol owner; + protected final Method invoker; + protected final T handler; + protected final Class returnType; + protected final Class[] parameterTypes; + protected final boolean isStatic; + + protected AbstractInvokerHolder(LeavesProtocol owner, Method invoker, T handler, @Nullable Class returnType, @NotNull Class... parameterTypes) { + this.owner = owner; + this.invoker = invoker; + this.handler = handler; + this.returnType = returnType; + this.parameterTypes = parameterTypes; + this.isStatic = Modifier.isStatic(invoker.getModifiers()); + + validateMethodSignature(); + } + + protected void validateMethodSignature() { + if (returnType != null && !returnType.isAssignableFrom(invoker.getReturnType())) { + throw new IllegalArgumentException("Return type mismatch in " + owner.getClass().getName() + "#" + invoker.getName() + + ": expected " + returnType.getName() + " but found " + invoker.getReturnType().getName()); + } + + Class[] methodParamTypes = invoker.getParameterTypes(); + if (methodParamTypes.length != parameterTypes.length) { + throw new IllegalArgumentException("Parameter count mismatch in " + owner.getClass().getName() + "#" + invoker.getName() + + ": expected " + parameterTypes.length + " but found " + methodParamTypes.length); + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (!parameterTypes[i].isAssignableFrom(methodParamTypes[i])) { + throw new IllegalArgumentException("Parameter type mismatch in " + owner.getClass().getName() + "#" + invoker.getName() + + " at index " + i + ": expected " + parameterTypes[i].getName() + " but found " + methodParamTypes[i].getName()); + } + } + } + + public LeavesProtocol owner() { + return owner; + } + + public T handler() { + return handler; + } + + protected Object invoke0(boolean force, Object... args) { + if (!force && !owner.isActive()) { + return null; + } + try { + if (isStatic) { + return invoker.invoke(null, args); + } else { + return invoker.invoke(owner, args); + } + } catch (InvocationTargetException e) { + throw new RuntimeException(e.getCause()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/BytebufReceiverInvokerHolder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/BytebufReceiverInvokerHolder.java new file mode 100644 index 0000000..958ba57 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/BytebufReceiverInvokerHolder.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import net.minecraft.network.FriendlyByteBuf; +import org.leavesmc.leaves.protocol.core.IdentifierSelector; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; + +import java.lang.reflect.Method; + +public class BytebufReceiverInvokerHolder extends AbstractInvokerHolder { + public BytebufReceiverInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.BytebufReceiver handler) { + super(owner, invoker, handler, null, handler.stage().identifier(), FriendlyByteBuf.class); + } + + public boolean invoke(IdentifierSelector selector, FriendlyByteBuf buf) { + return invoke0(false, selector.select(handler.stage()), buf) instanceof Boolean b && b; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/EmptyInvokerHolder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/EmptyInvokerHolder.java new file mode 100644 index 0000000..d6e18d3 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/EmptyInvokerHolder.java @@ -0,0 +1,15 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import org.leavesmc.leaves.protocol.core.LeavesProtocol; + +import java.lang.reflect.Method; + +public class EmptyInvokerHolder extends AbstractInvokerHolder { + public EmptyInvokerHolder(LeavesProtocol owner, Method invoker, T handler) { + super(owner, invoker, handler, null); + } + + public void invoke() { + invoke0(false); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/InitInvokerHolder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/InitInvokerHolder.java new file mode 100644 index 0000000..128aaf8 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/InitInvokerHolder.java @@ -0,0 +1,16 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; + +import java.lang.reflect.Method; + +public class InitInvokerHolder extends AbstractInvokerHolder { + public InitInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.Init handler) { + super(owner, invoker, handler, null); + } + + public void invoke() { + invoke0(true); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/MinecraftRegisterInvokerHolder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/MinecraftRegisterInvokerHolder.java new file mode 100644 index 0000000..5960042 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/MinecraftRegisterInvokerHolder.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import net.minecraft.resources.ResourceLocation; +import org.leavesmc.leaves.protocol.core.IdentifierSelector; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; + +import java.lang.reflect.Method; + +public class MinecraftRegisterInvokerHolder extends AbstractInvokerHolder { + public MinecraftRegisterInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.MinecraftRegister handler) { + super(owner, invoker, handler, null, handler.stage().identifier(), ResourceLocation.class); + } + + public void invoke(IdentifierSelector selector, ResourceLocation id) { + invoke0(false, selector.select(handler.stage()), id); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PayloadReceiverInvokerHolder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PayloadReceiverInvokerHolder.java new file mode 100644 index 0000000..2801bff --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PayloadReceiverInvokerHolder.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import org.leavesmc.leaves.protocol.core.IdentifierSelector; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; + +import java.lang.reflect.Method; + +public class PayloadReceiverInvokerHolder extends AbstractInvokerHolder { + public PayloadReceiverInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.PayloadReceiver handler) { + super(owner, invoker, handler, null, handler.stage().identifier(), handler.payload()); + } + + public void invoke(IdentifierSelector selector, LeavesCustomPayload payload) { + invoke0(false, selector.select(handler.stage()), payload); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PlayerInvokerHolder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PlayerInvokerHolder.java new file mode 100644 index 0000000..5ed2c2d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PlayerInvokerHolder.java @@ -0,0 +1,16 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import net.minecraft.server.level.ServerPlayer; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; + +import java.lang.reflect.Method; + +public class PlayerInvokerHolder extends AbstractInvokerHolder { + public PlayerInvokerHolder(LeavesProtocol owner, Method invoker, T handler) { + super(owner, invoker, handler, null, ServerPlayer.class); + } + + public void invoke(ServerPlayer player) { + invoke0(false, player); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java new file mode 100644 index 0000000..38cf7e0 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/PacketSplitter.java @@ -0,0 +1,134 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.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); + } + + public interface IPacketSplitterHandler { + void encode(ServerPlayer player, FriendlyByteBuf buf); + } + + /** + * 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; + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java new file mode 100644 index 0000000..af87ab4 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxEntityDataProtocol.java @@ -0,0 +1,278 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux; + +import com.mojang.logging.LogUtils; +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.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +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.bukkit.Bukkit; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.util.TagUtil; +import org.slf4j.Logger; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +// Powered by Servux(https://github.com/sakura-ryoko/servux) + +@LeavesProtocol.Register(namespace = "servux") +public class ServuxEntityDataProtocol implements LeavesProtocol { + private static final Logger LOGGER = LogUtils.getClassLogger(); + + public static final int PROTOCOL_VERSION = 1; + + private static final Map readingSessionKeys = new HashMap<>(); + + @ProtocolHandler.PlayerJoin + public static void onPlayerJoin(ServerPlayer player) { + sendMetadata(player); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLeave(ServerPlayer player) { + readingSessionKeys.remove(player.getUUID()); + } + + @ProtocolHandler.PayloadReceiver(payload = EntityDataPayload.class) + public static void onPacketReceive(ServerPlayer player, EntityDataPayload payload) { + 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); + LOGGER.warn("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", EntityDataPayload.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) { + Bukkit.getGlobalRegionScheduler().run(MinecraftInternalPlugin.INSTANCE, (task) -> { + BlockEntity be = player.level().getBlockEntity(pos); + CompoundTag nbt = be != null ? be.saveWithFullMetadata(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) { + Bukkit.getGlobalRegionScheduler().run(MinecraftInternalPlugin.INSTANCE, (task) -> { + Entity entity = player.level().getEntity(entityId); + CompoundTag nbt = TagUtil.saveEntityWithoutId(entity); + + 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 (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); + } + + @Override + public boolean isActive() { + return fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.entityProtocol; + } + + 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); + + public final int type; + + EntityDataPayloadType(int type) { + this.type = type; + Helper.ID_TO_TYPE.put(type, this); + } + + public static EntityDataPayloadType fromId(int id) { + return Helper.ID_TO_TYPE.get(id); + } + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + } + + public static class EntityDataPayload implements LeavesCustomPayload { + + @ID + public static final ResourceLocation CHANNEL = ServuxProtocol.id("entity_data"); + + @Codec + public static final StreamCodec CODEC = StreamCodec.of( + (buf, payload) -> { + buf.writeVarInt(payload.packetType.type); + switch (payload.packetType) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + buf.writeVarInt(payload.transactionId); + buf.writeBlockPos(payload.pos); + } + case PACKET_C2S_ENTITY_REQUEST -> { + buf.writeVarInt(payload.transactionId); + buf.writeVarInt(payload.entityId); + } + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + buf.writeBlockPos(payload.pos); + buf.writeNbt(payload.nbt); + } + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + buf.writeVarInt(payload.entityId); + buf.writeNbt(payload.nbt); + } + case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> + buf.writeBytes(payload.buffer.copy()); + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> buf.writeNbt(payload.nbt); + } + }, + buf -> { + EntityDataPayloadType type = EntityDataPayloadType.fromId(buf.readVarInt()); + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } + EntityDataPayload payload = new EntityDataPayload(type); + switch (type) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + buf.readVarInt(); + payload.pos = buf.readBlockPos().immutable(); + } + case PACKET_C2S_ENTITY_REQUEST -> { + buf.readVarInt(); + payload.entityId = buf.readVarInt(); + } + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + payload.pos = buf.readBlockPos().immutable(); + CompoundTag nbt = buf.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + payload.entityId = buf.readVarInt(); + CompoundTag nbt = buf.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> { + payload.buffer = new FriendlyByteBuf(buf.readBytes(buf.readableBytes())); + payload.nbt = new CompoundTag(); + } + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> { + CompoundTag nbt = buf.readNbt(); + if (nbt != null) { + payload.nbt.merge(nbt); + } + } + } + return payload; + } + ); + + 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()); + } + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java new file mode 100644 index 0000000..be22193 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java @@ -0,0 +1,363 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux; + +import com.mojang.serialization.DataResult; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.codec.StreamCodec; +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.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.GameRules; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.servux.logger.DataLogger; + +import javax.annotation.Nonnull; +import java.util.*; + +@LeavesProtocol.Register(namespace = "servux") +public class ServuxHudDataProtocol implements LeavesProtocol { + + public static final int PROTOCOL_VERSION = 2; + + private static final List players = new ArrayList<>(); + private static final int updateInterval = 80; + + private static final HashMap> loggerPlayers = new HashMap<>(); + private static final HashMap> LOGGERS = new HashMap<>(); + private static final HashMap DATA = new HashMap<>(); + + public static boolean refreshSpawnMetadata = false; + + @ProtocolHandler.Init + private static void initializeLoggers() { + if (!LOGGERS.isEmpty()) { + return; + } + for (DataLogger.Type type : DataLogger.Type.VALUES) { + DataLogger entry = type.init(); + if (entry != null) { + LOGGERS.put(type, entry); + } + } + } + + @ProtocolHandler.PlayerJoin + private static void onPlayerJoin(ServerPlayer player) { + sendHudMetadata(player); + } + + @ProtocolHandler.PlayerLeave + private static void onPlayerLeave(ServerPlayer player) { + players.remove(player); + loggerPlayers.remove(player); + } + + @ProtocolHandler.PayloadReceiver(payload = HudDataPayload.class) + public static void onPacketReceive(ServerPlayer player, HudDataPayload payload) { + switch (payload.packetType) { + case PACKET_C2S_METADATA_REQUEST -> { + players.add(player); + sendHudMetadata(player); + } + case PACKET_C2S_SPAWN_DATA_REQUEST -> refreshSpawnMetadata(player); + case PACKET_C2S_RECIPE_MANAGER_REQUEST -> refreshRecipeManager(player); + case PACKET_C2S_DATA_LOGGER_REQUEST -> refreshLoggers(player, payload.nbt); + } + } + + public static void sendHudMetadata(ServerPlayer player) { + CompoundTag metadata = new CompoundTag(); + metadata.putString("name", "hud_metadata"); + metadata.putString("id", HudDataPayload.CHANNEL.toString()); + metadata.putInt("version", PROTOCOL_VERSION); + metadata.putString("servux", ServuxProtocol.SERVUX_STRING); + if (fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.hudLoggerProtocol) { + CompoundTag nbt = new CompoundTag(); + for (DataLogger.Type type : DataLogger.Type.VALUES) { + nbt.putBoolean(type.getSerializedName(), isLoggerTypeEnabled(type)); + } + metadata.put("Loggers", nbt); + } + putWorldData(metadata); + + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_METADATA, metadata)); + } + + public static void refreshSpawnMetadata(ServerPlayer player) { + CompoundTag metadata = new CompoundTag(); + metadata.putString("id", HudDataPayload.CHANNEL.toString()); + metadata.putString("servux", ServuxProtocol.SERVUX_STRING); + putWorldData(metadata); + + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_SPAWN_DATA, metadata)); + } + + public static void refreshRecipeManager(ServerPlayer player) { + Collection> recipes = MinecraftServer.getServer().getRecipeManager().getRecipes(); + CompoundTag nbt = new CompoundTag(); + ListTag list = new ListTag(); + + recipes.forEach((recipeEntry -> { + DataResult dr = Recipe.CODEC.encodeStart(NbtOps.INSTANCE, recipeEntry.value()); + + if (dr.result().isPresent()) { + CompoundTag entry = new CompoundTag(); + entry.putString("id_reg", recipeEntry.id().registry().toString()); + entry.putString("id_value", recipeEntry.id().location().toString()); + entry.put("recipe", dr.result().get()); + list.add(entry); + } + })); + + nbt.put("RecipeManager", list); + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_START, nbt)); + } + + public static void refreshWeatherData(ServerPlayer player) { + ServerLevel level = MinecraftServer.getServer().overworld(); + if (!level.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE)) { + return; + } + + CompoundTag nbt = new CompoundTag(); + nbt.putString("id", HudDataPayload.CHANNEL.toString()); + nbt.putString("servux", ServuxProtocol.SERVUX_STRING); + + if (level.serverLevelData.isRaining() && level.serverLevelData.getRainTime() > -1) { + nbt.putInt("SetRaining", level.serverLevelData.getRainTime()); + nbt.putBoolean("isRaining", true); + } else { + nbt.putBoolean("isRaining", false); + } + + if (level.serverLevelData.isThundering() && level.serverLevelData.getThunderTime() > -1) { + nbt.putInt("SetThundering", level.serverLevelData.getThunderTime()); + nbt.putBoolean("isThundering", true); + } else { + nbt.putBoolean("isThundering", false); + } + + if (level.serverLevelData.getClearWeatherTime() > -1) { + nbt.putInt("SetClear", level.serverLevelData.getClearWeatherTime()); + } + + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_WEATHER_TICK, nbt)); + } + + private static void putWorldData(@NotNull CompoundTag metadata) { + ServerLevel level = MinecraftServer.getServer().overworld(); + BlockPos spawnPos = level.levelData.getSpawnPos(); + metadata.putInt("spawnPosX", spawnPos.getX()); + metadata.putInt("spawnPosY", spawnPos.getY()); + metadata.putInt("spawnPosZ", spawnPos.getZ()); + metadata.putInt("spawnChunkRadius", level.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); + + if (fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.hudMetadataShareSeed) { + metadata.putLong("worldSeed", level.getSeed()); + } + } + + public static void refreshLoggers(ServerPlayer player, @Nonnull CompoundTag nbt) { + if (!player.getBukkitEntity().hasPermission("servux.provider.hud_data.logger")) { + player.sendSystemMessage(Component.translatable("servux.hud_data.error.insufficient_for_loggers", "any")); + return; + } + + if (nbt.isEmpty()) { + return; + } + + List list = new ArrayList<>(); + + for (String key : nbt.keySet()) { + DataLogger.Type type = DataLogger.Type.fromStringStatic(key); + + if (type != null && nbt.getBooleanOr(key, false)) { + list.add(type); + } + } + if (!list.isEmpty()) { + loggerPlayers.put(player, list); + } else { + loggerPlayers.remove(player); + } + } + + private static boolean isLoggerTypeEnabled(DataLogger.Type type) { + return fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.hudEnabledLoggers.contains(type.getSerializedName()); + } + + @ProtocolHandler.Ticker + public void protocolTick() { + for (ServerPlayer player : players) { + if (refreshSpawnMetadata) { + refreshSpawnMetadata(player); + } + refreshWeatherData(player); + } + refreshSpawnMetadata = false; + } + + @ProtocolHandler.Ticker(tickerId = "logger") + public void loggerTick() { + if (!fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.hudLoggerProtocol) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + + if (server.getTickCount() % fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.hudUpdateInterval == 0) { + DATA.clear(); + LOGGERS.forEach((type, logger) -> { + if (!isLoggerTypeEnabled(type)) { + return; + } + DATA.put(type, logger.getResult(server)); + }); + } + + for (ServerPlayer player : loggerPlayers.keySet()) { + List list = loggerPlayers.get(player); + if (list.isEmpty()) { + return; + } + + CompoundTag nbt = new CompoundTag(); + for (DataLogger.Type type : list) { + if (DATA.containsKey(type)) { + nbt.put(type.getSerializedName(), DATA.get(type)); + } + } + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_DATA_LOGGER_TICK, nbt)); + } + } + + @Override + public boolean isActive() { + return fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.hudMetadataProtocol; + } + + @Override + public int tickerInterval(String tickerID) { + return tickerID.equals("logger") ? 1 : updateInterval; + } + + public static void sendPacket(ServerPlayer player, HudDataPayload payload) { + if (payload.packetType == HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_START) { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + buffer.writeNbt(payload.nbt); + PacketSplitter.send(ServuxHudDataProtocol::sendWithSplitter, buffer, player); + } else { + ProtocolUtils.sendPayloadPacket(player, payload); + } + } + + private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_DATA, buf)); + } + + public enum HudDataPayloadType { + PACKET_S2C_METADATA(1), + PACKET_C2S_METADATA_REQUEST(2), + PACKET_S2C_SPAWN_DATA(3), + PACKET_C2S_SPAWN_DATA_REQUEST(4), + PACKET_S2C_WEATHER_TICK(5), + PACKET_C2S_RECIPE_MANAGER_REQUEST(6), + PACKET_S2C_DATA_LOGGER_TICK(7), + PACKET_C2S_DATA_LOGGER_REQUEST(8), + // For Packet Splitter (Oversize Packets, S2C) + PACKET_S2C_NBT_RESPONSE_START(10), + PACKET_S2C_NBT_RESPONSE_DATA(11); + + public final int type; + + HudDataPayloadType(int type) { + this.type = type; + Helper.ID_TO_TYPE.put(type, this); + } + + public static HudDataPayloadType fromId(int id) { + return Helper.ID_TO_TYPE.get(id); + } + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + } + + public record HudDataPayload(HudDataPayloadType packetType, CompoundTag nbt, + FriendlyByteBuf buffer) implements LeavesCustomPayload { + + @ID + public static final ResourceLocation CHANNEL = ServuxProtocol.id("hud_metadata"); + + @Codec + public static final StreamCodec CODEC = StreamCodec.of( + (buf, payload) -> { + buf.writeVarInt(payload.packetType().type); + switch (payload.packetType()) { + case PACKET_S2C_NBT_RESPONSE_DATA -> buf.writeBytes(payload.buffer().copy()); + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA, PACKET_C2S_SPAWN_DATA_REQUEST, + PACKET_S2C_SPAWN_DATA, PACKET_S2C_WEATHER_TICK, PACKET_C2S_RECIPE_MANAGER_REQUEST, + PACKET_C2S_DATA_LOGGER_REQUEST, PACKET_S2C_DATA_LOGGER_TICK -> buf.writeNbt(payload.nbt()); + } + }, + buf -> { + HudDataPayloadType type = HudDataPayloadType.fromId(buf.readVarInt()); + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } + switch (type) { + case PACKET_S2C_NBT_RESPONSE_DATA -> { + return new HudDataPayload(type, new FriendlyByteBuf(buf.readBytes(buf.readableBytes()))); + } + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA, PACKET_C2S_SPAWN_DATA_REQUEST, + PACKET_S2C_SPAWN_DATA, PACKET_S2C_WEATHER_TICK, + PACKET_C2S_RECIPE_MANAGER_REQUEST, PACKET_C2S_DATA_LOGGER_REQUEST, + PACKET_S2C_DATA_LOGGER_TICK -> { + return new HudDataPayload(type, buf.readNbt()); + } + } + throw new IllegalStateException("invalid packet type received"); + } + ); + + public HudDataPayload(HudDataPayloadType type, CompoundTag nbt) { + this(type, nbt, null); + } + + public HudDataPayload(HudDataPayloadType type, FriendlyByteBuf buffer) { + this(type, new CompoundTag(), buffer); + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java new file mode 100644 index 0000000..23c2a9a --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxProtocol.java @@ -0,0 +1,36 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Contract; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ServuxProtocol { + + public static final String PROTOCOL_ID = "servux"; + public static final Logger LOGGER = LoggerFactory.getLogger(PROTOCOL_ID.toUpperCase()); + 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/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java new file mode 100644 index 0000000..5ad24f8 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java @@ -0,0 +1,419 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux; + +import com.mojang.logging.LogUtils; +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.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +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.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.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.slf4j.Logger; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +// Powered by Servux(https://github.com/sakura-ryoko/servux) + +@LeavesProtocol.Register(namespace = "servux") +public class ServuxStructuresProtocol implements LeavesProtocol { + private static final Logger LOGGER = LogUtils.getClassLogger(); + + public static final int PROTOCOL_VERSION = 2; + private static final int updateInterval = 40; + private static final int timeout = 30 * 20; + private static final Map players = new ConcurrentHashMap<>(); + private static final Map> timeouts = new HashMap<>(); + private static int retainDistance; + + @ProtocolHandler.PlayerJoin + public static void onPlayerJoin(ServerPlayer player) { + sendMetaData(player); + } + + @ProtocolHandler.PayloadReceiver(payload = StructuresPayload.class) + public static void onPacketReceive(ServerPlayer player, StructuresPayload payload) { + switch (payload.packetType()) { + case PACKET_C2S_STRUCTURES_REGISTER -> onPlayerSubscribed(player); + case PACKET_C2S_REQUEST_SPAWN_METADATA -> ServuxHudDataProtocol.refreshSpawnMetadata(player); // move to + case PACKET_C2S_STRUCTURES_UNREGISTER -> onPlayerLoggedOut(player); + } + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + players.remove(player.getId()); + timeouts.remove(player.getUUID()); + } + + @ProtocolHandler.Ticker + public static void tick() { + MinecraftServer server = MinecraftServer.getServer(); + int tickCounter = server.getTickCount(); + retainDistance = server.getPlayerList().getViewDistance() + 2; + for (ServerPlayer player : players.values()) { + // TODO DimensionChange + refreshTrackedChunks(player, tickCounter); + } + } + + public static void onStartedWatchingChunk(ServerPlayer player, LevelChunk chunk) { + 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, @NotNull Level world) { + if (!world.hasChunk(chunkX, chunkZ)) { + return false; + } + + ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); + + if (chunk != null) { + 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 { + LOGGER.warn("{} re-register servux:structures", player.getScoreboardName()); + } + + MinecraftServer server = MinecraftServer.getServer(); + sendMetaData(player); + initialSyncStructures(player, player.moonrise$getViewDistanceHolder().getViewDistances().sendViewDistance() + 2, server.getTickCount()); + } + + private static void sendMetaData(ServerPlayer player) { + CompoundTag tag = new CompoundTag(); + tag.putString("name", "structure_bounding_boxes"); + tag.putString("id", StructuresPayload.CHANNEL.toString()); + tag.putInt("version", PROTOCOL_VERSION); + tag.putString("servux", ServuxProtocol.SERVUX_STRING); + tag.putInt("timeout", timeout); + + sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_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.level(), 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); + + if (chunk != null) { + 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.level(); + 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 = null; + if (chunk != null) { + 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.level(); + 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 static void sendPacket(ServerPlayer player, StructuresPayload payload) { + 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)); + } + + @Override + public int tickerInterval(String tickerID) { + return updateInterval; + } + + @Override + public boolean isActive() { + return fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.structureProtocol; + } + + 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); + + 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); + } + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + } + + public record StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt, + FriendlyByteBuf buffer) implements LeavesCustomPayload { + + @ID + public static final ResourceLocation CHANNEL = ServuxProtocol.id("structures"); + + @Codec + public static final StreamCodec CODEC = StreamCodec.of( + (buf, payload) -> { + buf.writeVarInt(payload.packetType().type); + if (payload.packetType().equals(StructuresPayloadType.PACKET_S2C_STRUCTURE_DATA)) { + buf.writeBytes(payload.buffer().readBytes(payload.buffer().readableBytes())); + } else { + buf.writeNbt(payload.nbt()); + } + }, + 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()); + } + } + ); + + public StructuresPayload(StructuresPayloadType packetType, CompoundTag nbt) { + this(packetType, nbt, null); + } + + public StructuresPayload(StructuresPayloadType packetType, FriendlyByteBuf buffer) { + this(packetType, new CompoundTag(), buffer); + } + } + + 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; + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/LitematicaSchematic.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/LitematicaSchematic.java new file mode 100644 index 0000000..55e47c6 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/LitematicaSchematic.java @@ -0,0 +1,281 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.SharedConstants; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.LongArrayTag; +import net.minecraft.nbt.Tag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.Mth; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.ScheduledTick; +import net.minecraft.world.ticks.TickPriority; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Unmodifiable; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; +import org.leavesmc.leaves.protocol.servux.litematics.container.LitematicaBlockStateContainer; +import org.leavesmc.leaves.protocol.servux.litematics.utils.FileType; +import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils; +import org.leavesmc.leaves.protocol.servux.litematics.utils.PositionUtils; + +import java.util.*; + +public record LitematicaSchematic(Map subRegions, SchematicMetadata metadata) { + + public static final int MINECRAFT_DATA_VERSION = SharedConstants.getProtocolVersion(); + public static final int SCHEMATIC_VERSION = 7; + + @NotNull + @Contract("_ -> new") + public static LitematicaSchematic readFromNBT(@NotNull CompoundTag nbt) { + if (nbt.contains("Version")) { + final int version = nbt.getIntOr("Version", -1); + final int minecraftDataVersion = nbt.contains("MinecraftDataVersion") ? nbt.getInt("MinecraftDataVersion").orElseThrow() : SharedConstants.getProtocolVersion(); + + if (version >= 1 && version <= SCHEMATIC_VERSION) { + SchematicMetadata metadata = SchematicMetadata.readFromNBT(nbt.getCompoundOrEmpty("Metadata"), version, minecraftDataVersion, FileType.LITEMATICA_SCHEMATIC); + Map subRegions = readSubRegionsFromNBT(nbt.getCompoundOrEmpty("Regions"), version, minecraftDataVersion); + return new LitematicaSchematic(subRegions, metadata); + } else { + throw new RuntimeException("Unsupported or future schematic version"); + } + } else { + throw new RuntimeException("The schematic doesn't have version information, and can't be safely loaded!"); + } + } + + @NotNull + private static Map readSubRegionsFromNBT(@NotNull CompoundTag tag, int version, int minecraftDataVersion) { + Map subRegions = new HashMap<>(); + for (String regionName : tag.keySet()) { + Tag region = tag.get(regionName); + if (region == null || region.getId() != Tag.TAG_COMPOUND) { + throw new RuntimeException("Unknown region: " + regionName); + } + subRegions.put(regionName, SubRegion.readFromNBT(tag.getCompoundOrEmpty(regionName), version, minecraftDataVersion)); + } + return subRegions; + } + + @NotNull + @Unmodifiable + public Map getAreaSizes() { + ImmutableMap.Builder builder = ImmutableMap.builderWithExpectedSize(subRegions.size()); + for (Map.Entry entry : subRegions.entrySet()) { + builder.put(entry.getKey(), entry.getValue().size()); + } + return builder.build(); + } + + @NotNull + public SubRegion getSubRegion(String name) { + return Objects.requireNonNull(subRegions.get(name)); + } + + public record SubRegion( + LitematicaBlockStateContainer blockContainers, + Map tileEntities, + Map> pendingBlockTicks, + Map> pendingFluidTicks, + List entities, + BlockPos position, + BlockPos size + ) { + @NotNull + @Contract("_, _, _ -> new") + public static SubRegion readFromNBT(@NotNull CompoundTag regionTag, int version, int minecraftDataVersion) { + BlockPos position = NbtUtils.readBlockPos(regionTag.getCompoundOrEmpty("Position")); + BlockPos size = NbtUtils.readBlockPos(regionTag.getCompoundOrEmpty("Size")); + if (position == null || size == null) { + throw new IllegalArgumentException("Invalid region"); + } + + Map tileEntities; + List entities; + if (version >= 2) { + tileEntities = readTileEntitiesFromNBT(regionTag.getListOrEmpty("TileEntities")); + entities = readEntitiesFromNBT(regionTag.getListOrEmpty("Entities")); + } else { + tileEntities = readTileEntitiesFromNBT_v1(regionTag.getListOrEmpty("TileEntities")); + entities = readEntitiesFromNBT_v1(regionTag.getListOrEmpty("Entities")); + } + + Map> pendingBlockTicks = null; + if (version >= 3) { + pendingBlockTicks = readPendingTicksFromNBT(regionTag.getListOrEmpty("PendingBlockTicks"), BuiltInRegistries.BLOCK, "Block", Blocks.AIR); + } + + Map> pendingFluidTicks = null; + if (version >= 5) { + pendingFluidTicks = readPendingTicksFromNBT(regionTag.getListOrEmpty("PendingFluidTicks"), BuiltInRegistries.FLUID, "Fluid", Fluids.EMPTY); + } + + LitematicaBlockStateContainer blockContainers = null; + Tag blockState = regionTag.get("BlockStates"); + if (blockState != null && blockState.getId() == Tag.TAG_LONG_ARRAY) { + ListTag palette = regionTag.getListOrEmpty("BlockStatePalette"); + long[] blockStateArr = ((LongArrayTag) blockState).getAsLongArray(); + BlockPos posEndRel = PositionUtils.getRelativeEndPositionFromAreaSize(size).offset(position); + BlockPos posMin = PositionUtils.getMinCorner(position, posEndRel); + BlockPos posMax = PositionUtils.getMaxCorner(position, posEndRel); + BlockPos containerSize = posMax.subtract(posMin).offset(1, 1, 1); + blockContainers = LitematicaBlockStateContainer.createFrom(palette, blockStateArr, containerSize); + if (minecraftDataVersion < MINECRAFT_DATA_VERSION) { + ServuxProtocol.LOGGER.warn("Cannot process minecraft data version: {}", minecraftDataVersion); + } + } + + return new SubRegion(blockContainers, tileEntities, pendingBlockTicks, pendingFluidTicks, entities, position, size); + } + + private static List readEntitiesFromNBT(ListTag tagList) { + List entityList = new ArrayList<>(); + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag entityData = tagList.getCompoundOrEmpty(i); + Vec3 posVec = NbtUtils.readEntityPositionFromTag(entityData); + + if (posVec != null && !entityData.isEmpty()) { + entityList.add(new EntityInfo(posVec, entityData)); + } + } + + return entityList; + } + + private static Map readTileEntitiesFromNBT(ListTag tagList) { + Map tileMap = new HashMap<>(); + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompoundOrEmpty(i); + BlockPos pos = NbtUtils.readBlockPos(tag); + + if (pos != null && !tag.isEmpty()) { + tileMap.put(pos, tag); + } + } + + return tileMap; + } + + private static Map> readPendingTicksFromNBT(ListTag tagList, Registry registry, String tagName, T emptyValue) { + Map> tickMap = new HashMap<>(); + final int size = tagList.size(); + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompoundOrEmpty(i); + + // XXX these were accidentally saved as longs in version 3 + if (!tag.contains("Time")) { + continue; + } + T target; + ResourceLocation resourceLocation = ResourceLocation.tryParse(tag.getStringOr(tagName, "")); + if (resourceLocation == null) { + continue; + } + Optional> tReference = registry.get(resourceLocation); + if (tReference.isEmpty()) { + continue; + } + target = tReference.get().value(); + if (target == emptyValue) { + continue; + } + BlockPos pos = new BlockPos(tag.getIntOr("x", 0), tag.getIntOr("y", 0), tag.getIntOr("z", 0)); + TickPriority priority = TickPriority.byValue(tag.getIntOr("Priority", 0)); + int scheduledTime = tag.getIntOr("Time", 0); + long subTick = tag.getLongOr("SubTick", 0L); + tickMap.put(pos, new ScheduledTick<>(target, pos, scheduledTime, priority, subTick)); + } + + return tickMap; + } + + private static List readEntitiesFromNBT_v1(ListTag tagList) { + List entityList = new ArrayList<>(); + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompoundOrEmpty(i); + Vec3 posVec = NbtUtils.readVec3(tag); + CompoundTag entityData = tag.getCompoundOrEmpty("EntityData"); + + if (posVec != null && !entityData.isEmpty()) { + // Update the correct position to the TileEntity NBT, where it is stored in version 2 + NbtUtils.writeEntityPositionToTag(posVec, entityData); + entityList.add(new EntityInfo(posVec, entityData)); + } + } + + return entityList; + } + + private static Map readTileEntitiesFromNBT_v1(ListTag tagList) { + Map tileMap = new HashMap<>(); + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompoundOrEmpty(i); + CompoundTag tileNbt = tag.getCompoundOrEmpty("TileNBT"); + + // Note: This within-schematic relative position is not inside the tile tag! + BlockPos pos = NbtUtils.readBlockPos(tag); + + if (pos != null && !tileNbt.isEmpty()) { + // Update the correct position to the entity NBT, where it is stored in version 2 + NbtUtils.writeBlockPosToTag(pos, tileNbt); + tileMap.put(pos, tileNbt); + } + } + + return tileMap; + } + } + + public record EntityInfo(Vec3 posVec, CompoundTag nbt) { + public EntityInfo(Vec3 posVec, CompoundTag nbt) { + this.posVec = posVec; + + if (nbt.contains("SleepingX")) { + nbt.putInt("SleepingX", Mth.floor(posVec.x)); + } + if (nbt.contains("SleepingY")) { + nbt.putInt("SleepingY", Mth.floor(posVec.y)); + } + if (nbt.contains("SleepingZ")) { + nbt.putInt("SleepingZ", Mth.floor(posVec.z)); + } + + this.nbt = nbt; + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/SchematicMetadata.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/SchematicMetadata.java new file mode 100644 index 0000000..ebf5f9d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/SchematicMetadata.java @@ -0,0 +1,62 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics; + +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.servux.litematics.utils.FileType; +import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils; +import org.leavesmc.leaves.protocol.servux.litematics.utils.Schema; + +import javax.annotation.Nullable; +import java.util.Objects; + +public record SchematicMetadata( + String name, + String author, + String description, + Vec3i enclosingSize, + long timeCreated, + long timeModified, + int minecraftDataVersion, + int schematicVersion, + Schema schema, + FileType type, + int regionCount, + long totalVolume, + long totalBlocks, + @Nullable int[] thumbnailPixelData +) { + @NotNull + @Contract("_, _, _, _ -> new") + public static SchematicMetadata readFromNBT(@NotNull CompoundTag nbt, int version, int minecraftDataVersion, FileType fileType) { + String name = nbt.getStringOr("Name", "?"); + String author = nbt.getStringOr("Author", "?"); + String description = nbt.getStringOr("Description", ""); + int regionCount = nbt.getIntOr("RegionCount", -1); + long timeCreated = nbt.getLongOr("TimeCreated", -1L); + long timeModified = nbt.getLongOr("TimeModified", -1L); + long totalVolume = nbt.getIntOr("TotalVolume", -1); + long totalBlocks = nbt.getIntOr("TotalBlocks", -1); + Vec3i enclosingSize = Objects.requireNonNullElse(NbtUtils.readVec3iFromTag(nbt.getCompoundOrEmpty("EnclosingSize")), Vec3i.ZERO); + int[] thumbnailPixelData = nbt.getIntArray("PreviewImageData").orElse(null); + return new SchematicMetadata(name, author, description, enclosingSize, timeCreated, timeModified, minecraftDataVersion, version, Schema.getSchemaByDataVersion(minecraftDataVersion), fileType, regionCount, totalVolume, totalBlocks, thumbnailPixelData); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/ServuxLitematicsProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/ServuxLitematicsProtocol.java new file mode 100644 index 0000000..53d4e6d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/ServuxLitematicsProtocol.java @@ -0,0 +1,442 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics; + +import io.netty.buffer.Unpooled; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtAccounter; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +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.EntityType; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import org.bukkit.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.servux.PacketSplitter; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SchematicPlacement; +import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils; +import org.leavesmc.leaves.protocol.servux.litematics.utils.ReplaceBehavior; +import org.leavesmc.leaves.util.TagUtil; + +import java.util.*; + +@LeavesProtocol.Register(namespace = "servux") +public class ServuxLitematicsProtocol implements LeavesProtocol { + + public static final int PROTOCOL_VERSION = 1; + + private static final CompoundTag metadata = new CompoundTag(); + private static final Map playerSession = new HashMap<>(); + + @ProtocolHandler.Init + public static void init() { + metadata.putString("name", "litematic_data"); + metadata.putString("id", ServuxLitematicaPayload.CHANNEL.toString()); + metadata.putInt("version", PROTOCOL_VERSION); + metadata.putString("servux", ServuxProtocol.SERVUX_STRING); + } + + public static boolean hasPermission(ServerPlayer player) { + return player.getBukkitEntity().hasPermission("leaves.protocol.litematics"); + } + + public static void sendMetaData(ServerPlayer player) { + ServuxLitematicaPayload send = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_METADATA); + send.nbt.merge(metadata); + encodeServerData(player, send); + } + + public static void encodeServerData(ServerPlayer player, @NotNull ServuxLitematicaPayload packet) { + if (packet.packetType.equals(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_START)) { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + buffer.writeVarInt(packet.getTransactionId()); + buffer.writeNbt(packet.getCompound()); + PacketSplitter.send(ServuxLitematicsProtocol::sendWithSplitter, buffer, player); + } else { + ProtocolUtils.sendPayloadPacket(player, packet); + } + } + + private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { + ServuxLitematicaPayload payload = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_DATA); + payload.buffer = buf; + payload.nbt = new CompoundTag(); + encodeServerData(player, payload); + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerJoin(ServerPlayer player) { + sendMetaData(player); + } + + @ProtocolHandler.PayloadReceiver(payload = ServuxLitematicaPayload.class) + public static void onPacketReceive(ServerPlayer player, ServuxLitematicaPayload payload) { + if (!hasPermission(player)) { + return; + } + + switch (payload.packetType) { + case PACKET_C2S_METADATA_REQUEST -> sendMetaData(player); + + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> onBlockEntityRequest(player, payload.getPos()); + + case PACKET_C2S_ENTITY_REQUEST -> onEntityRequest(player, payload.getEntityId()); + + + case PACKET_C2S_BULK_ENTITY_NBT_REQUEST -> + onBulkEntityRequest(player, payload.getChunkPos(), payload.getCompound()); + + case PACKET_C2S_NBT_RESPONSE_DATA -> { + ServuxProtocol.LOGGER.debug("nbt response data"); + UUID uuid = player.getUUID(); + Long session = playerSession.getOrDefault(uuid, new Random().nextLong()); + playerSession.put(uuid, session); + FriendlyByteBuf fullPacket = PacketSplitter.receive(session, payload.getBuffer()); + if (fullPacket == null) { + ServuxProtocol.LOGGER.debug("packet is none"); + return; + } + playerSession.remove(uuid); + fullPacket.readVarInt(); + Tag tag = FriendlyByteBuf.readNbt(fullPacket, new NbtAccounter(fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.litematicsMaxNbtSize == -1 ? Long.MAX_VALUE : fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.litematicsMaxNbtSize, 512)); + if (!(tag instanceof CompoundTag)) { + ServuxProtocol.LOGGER.error("cannot read nbt tag from packet"); + return; + } + handleClientPasteRequest(player, (CompoundTag) tag); + } + } + } + + public static void onBlockEntityRequest(ServerPlayer player, BlockPos pos) { + if (!hasPermission(player)) { + return; + } + BlockEntity be = player.level().getBlockEntity(pos); + CompoundTag tag = be != null ? be.saveWithFullMetadata(MinecraftServer.getServer().registryAccess()) : new CompoundTag(); + ServuxLitematicaPayload payload = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE); + payload.pos = pos; + payload.nbt = tag; + encodeServerData(player, payload); + } + + public static void onEntityRequest(ServerPlayer player, int entityId) { + if (!hasPermission(player)) { + return; + } + Entity entity = player.level().getEntity(entityId); + if (entity == null) { + return; + } + CompoundTag tag = new CompoundTag(); + ServuxLitematicaPayload payload = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE); + payload.entityId = entityId; + if (entity instanceof net.minecraft.world.entity.player.Player) { + ResourceLocation loc = EntityType.getKey(entity.getType()); + tag = TagUtil.saveEntity(entity); + tag.putString("id", loc.toString()); + payload.nbt = tag; + encodeServerData(player, payload); + } else if (TagUtil.saveEntityAsPassenger(entity, tag)) { + payload.nbt = tag; + encodeServerData(player, payload); + } + } + + public static void onBulkEntityRequest(ServerPlayer player, ChunkPos chunkPos, CompoundTag req) { + if (req == null || req.isEmpty()) { + return; + } + + ServerLevel world = player.level(); + ChunkAccess chunk = world.getChunk(chunkPos.x, chunkPos.z, ChunkStatus.FULL, false); + + if (chunk == null) { + return; + } + if (!req.contains("Task") || req.getStringOr("Task", "").equals("BulkEntityRequest")) { + ServuxProtocol.LOGGER.debug("litematic_data: Sending Bulk NBT Data for ChunkPos [{}] to player {}", chunkPos, player.getName().getString()); + + long timeStart = System.currentTimeMillis(); + ListTag tileList = new ListTag(); + ListTag entityList = new ListTag(); + int minY = req.getIntOr("minY", -64); + int maxY = req.getIntOr("maxY", 319); + BlockPos pos1 = new BlockPos(chunkPos.getMinBlockX(), minY, chunkPos.getMinBlockZ()); + BlockPos pos2 = new BlockPos(chunkPos.getMaxBlockX(), maxY, chunkPos.getMaxBlockZ()); + AABB bb = AABB.encapsulatingFullBlocks(pos1, pos2); + Set teSet = chunk.getBlockEntitiesPos(); + List entities = world.getEntitiesOfClass(Entity.class, bb, e -> !(e instanceof Player)); + for (BlockPos tePos : teSet) { + if ((tePos.getX() < chunkPos.getMinBlockX() || tePos.getX() > chunkPos.getMaxBlockX()) || + (tePos.getZ() < chunkPos.getMinBlockZ() || tePos.getZ() > chunkPos.getMaxBlockZ()) || + (tePos.getY() < minY || tePos.getY() > maxY)) { + continue; + } + + BlockEntity be = world.getBlockEntity(tePos); + CompoundTag beTag = TagUtil.saveTileWithId(be); + tileList.add(beTag); + } + + for (Entity entity : entities) { + CompoundTag entTag = new CompoundTag(); + + if (TagUtil.saveEntity(entity, entTag)) { + Vec3 posVec = new Vec3(entity.getX() - pos1.getX(), entity.getY() - pos1.getY(), entity.getZ() - pos1.getZ()); + NbtUtils.writeEntityPositionToTag(posVec, entTag); + entTag.putInt("entityId", entity.getId()); + entityList.add(entTag); + } + } + + CompoundTag output = new CompoundTag(); + output.putString("Task", "BulkEntityReply"); + output.put("TileEntities", tileList); + output.put("Entities", entityList); + output.putInt("chunkX", chunkPos.x); + output.putInt("chunkZ", chunkPos.z); + ServuxProtocol.LOGGER.debug("process bulk entity used: {}ms", System.currentTimeMillis() - timeStart); + + ServuxLitematicaPayload send = new ServuxLitematicaPayload(ServuxLitematicaPayloadType.PACKET_S2C_NBT_RESPONSE_START); + send.nbt.merge(output); + encodeServerData(player, send); + } + } + + public static void handleClientPasteRequest(ServerPlayer player, @NotNull CompoundTag tags) { + if (!hasPermission(player)) { + player.getBukkitEntity().sendActionBar(Component.text("Insufficient Permissions for the Litematic paste operation", NamedTextColor.RED)); + return; + } + if (!player.isCreative()) { + player.getBukkitEntity().sendActionBar(Component.text("Creative Mode is required for the Litematic paste operation", NamedTextColor.RED)); + return; + } + + if (tags.getStringOr("Task", "").equals("LitematicaPaste")) { + ServuxProtocol.LOGGER.debug("litematic_data: Servux Paste request from player {}", player.getName().getString()); + ServerLevel serverLevel = player.level(); + long timeStart = System.currentTimeMillis(); + SchematicPlacement placement = SchematicPlacement.createFromNbt(tags); + ReplaceBehavior replaceMode = ReplaceBehavior.fromStringStatic(tags.getStringOr("ReplaceMode", ReplaceBehavior.NONE.name())); + io.papermc.paper.threadedregions.RegionizedServer.getInstance().taskQueue.queueTickTaskQueue( + player.level(), + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkX(player.position()), + ca.spottedleaf.moonrise.common.util.CoordinateUtils.getChunkZ(player.position()), + () -> { + placement.pasteTo(serverLevel, replaceMode); + long timeElapsed = System.currentTimeMillis() - timeStart; + player.getBukkitEntity().sendActionBar( + Component.text("Pasted ") + .append(Component.text(placement.getName(), NamedTextColor.AQUA)) + .append(Component.text(" to world ")) + .append(Component.text(serverLevel.serverLevelData.getLevelName(), NamedTextColor.LIGHT_PURPLE)) + .append(Component.text(" in ")) + .append(Component.text(timeElapsed, NamedTextColor.GREEN)) + .append(Component.text("ms")) + ); + }); + } + } + + @Override + public boolean isActive() { + return fun.bm.lophine.config.modules.misc.SurvuxProtocolConfig.litematicsEnabled; + } + + public enum ServuxLitematicaPayloadType { + 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), + PACKET_C2S_BULK_ENTITY_NBT_REQUEST(7), + // 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); + + public final int type; + + ServuxLitematicaPayloadType(int type) { + this.type = type; + Helper.ID_TO_TYPE.put(type, this); + } + + public static ServuxLitematicaPayloadType fromId(int id) { + return Helper.ID_TO_TYPE.get(id); + } + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + } + + public static class ServuxLitematicaPayload implements LeavesCustomPayload { + + @ID + public static final ResourceLocation CHANNEL = ServuxProtocol.id("litematics"); + + @Codec + public static final StreamCodec CODEC = StreamCodec.of( + (buf, payload) -> { + buf.writeVarInt(payload.packetType.type); + switch (payload.packetType) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + buf.writeVarInt(payload.transactionId); + buf.writeBlockPos(payload.pos); + } + case PACKET_C2S_ENTITY_REQUEST -> { + buf.writeVarInt(payload.transactionId); + buf.writeVarInt(payload.entityId); + } + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + buf.writeBlockPos(payload.pos); + buf.writeNbt(payload.nbt); + } + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + buf.writeVarInt(payload.entityId); + buf.writeNbt(payload.nbt); + } + case PACKET_C2S_BULK_ENTITY_NBT_REQUEST -> { + buf.writeChunkPos(payload.chunkPos); + buf.writeNbt(payload.nbt); + } + case PACKET_S2C_NBT_RESPONSE_DATA, PACKET_C2S_NBT_RESPONSE_DATA -> + buf.writeBytes(payload.buffer.readBytes(payload.buffer.readableBytes())); + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> buf.writeNbt(payload.nbt); + default -> ServuxProtocol.LOGGER.error("ServuxLitematicaPacket#toPacket: Unknown packet type!"); + } + }, + buf -> { + ServuxLitematicaPayloadType type = ServuxLitematicaPayloadType.fromId(buf.readVarInt()); + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } + ServuxLitematicaPayload payload = new ServuxLitematicaPayload(type); + switch (type) { + case PACKET_C2S_BLOCK_ENTITY_REQUEST -> { + buf.readVarInt(); + payload.pos = buf.readBlockPos().immutable(); + } + case PACKET_C2S_ENTITY_REQUEST -> { + buf.readVarInt(); + payload.entityId = buf.readVarInt(); + } + case PACKET_S2C_BLOCK_NBT_RESPONSE_SIMPLE -> { + payload.pos = buf.readBlockPos().immutable(); + payload.nbt = buf.readNbt(); + } + case PACKET_S2C_ENTITY_NBT_RESPONSE_SIMPLE -> { + payload.entityId = buf.readVarInt(); + payload.nbt = buf.readNbt(); + } + case PACKET_C2S_BULK_ENTITY_NBT_REQUEST -> { + payload.chunkPos = buf.readChunkPos(); + payload.nbt = buf.readNbt(); + } + case PACKET_C2S_NBT_RESPONSE_DATA, PACKET_S2C_NBT_RESPONSE_DATA -> + payload.buffer = new FriendlyByteBuf(buf.readBytes(buf.readableBytes())); + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA -> payload.nbt = buf.readNbt(); + } + return payload; + } + ); + + public static final int PROTOCOL_VERSION = 1; + private final ServuxLitematicaPayloadType packetType; + private final int transactionId; + private int entityId; + private BlockPos pos; + private CompoundTag nbt; + private ChunkPos chunkPos; + private FriendlyByteBuf buffer; + + private ServuxLitematicaPayload(ServuxLitematicaPayloadType type) { + this.packetType = type; + this.transactionId = -1; + this.entityId = -1; + this.pos = BlockPos.ZERO; + this.chunkPos = ChunkPos.ZERO; + this.nbt = new CompoundTag(); + } + + public int getVersion() { + return PROTOCOL_VERSION; + } + + public int getTransactionId() { + return this.transactionId; + } + + public int getEntityId() { + return this.entityId; + } + + public BlockPos getPos() { + return this.pos; + } + + public CompoundTag getCompound() { + return this.nbt; + } + + public ChunkPos getChunkPos() { + return this.chunkPos; + } + + public FriendlyByteBuf getBuffer() { + return this.buffer; + } + + public boolean hasBuffer() { + return this.buffer != null && this.buffer.isReadable(); + } + + public boolean hasNbt() { + return this.nbt != null && !this.nbt.isEmpty(); + } + + public boolean isEmpty() { + return !this.hasBuffer() && !this.hasNbt(); + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBitArray.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBitArray.java new file mode 100644 index 0000000..e503792 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBitArray.java @@ -0,0 +1,104 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import org.apache.commons.lang3.Validate; + +import javax.annotation.Nullable; +import java.util.Objects; + +public class LitematicaBitArray { + /** + * The long array that is used to store the data for this BitArray. + */ + private final long[] longArray; + /** + * Number of bits a single entry takes up + */ + private final int bitsPerEntry; + /** + * The maximum value for a single entry. This also works as a bitmask for a single entry. + * For instance, if bitsPerEntry were 5, this value would be 31 (ie, {@code 0b00011111}). + */ + private final long maxEntryValue; + /** + * Number of entries in this array (not the length of the long array that internally backs this array) + */ + private final long arraySize; + + public LitematicaBitArray(int bitsPerEntryIn, long arraySizeIn) { + this(bitsPerEntryIn, arraySizeIn, null); + } + + public LitematicaBitArray(int bitsPerEntryIn, long arraySizeIn, @Nullable long[] longArrayIn) { + Validate.inclusiveBetween(1L, 32L, bitsPerEntryIn); + this.arraySize = arraySizeIn; + this.bitsPerEntry = bitsPerEntryIn; + this.maxEntryValue = (1L << bitsPerEntryIn) - 1L; + + this.longArray = Objects.requireNonNullElseGet(longArrayIn, () -> new long[(int) (roundUp(arraySizeIn * bitsPerEntryIn, 64L) / 64L)]); + } + + public static long roundUp(long value, long interval) { + if (interval == 0L) { + return 0L; + } else if (value == 0L) { + return interval; + } else { + if (value < 0L) { + interval *= -1L; + } + + long remainder = value % interval; + + return remainder == 0L ? value : value + interval - remainder; + } + } + + public void setAt(long index, int value) { + long startOffset = index * (long) this.bitsPerEntry; + int startArrIndex = (int) (startOffset >> 6); // startOffset / 64 + int endArrIndex = (int) (((index + 1L) * (long) this.bitsPerEntry - 1L) >> 6); + int startBitOffset = (int) (startOffset & 0x3F); // startOffset % 64 + this.longArray[startArrIndex] = this.longArray[startArrIndex] & ~(this.maxEntryValue << startBitOffset) | ((long) value & this.maxEntryValue) << startBitOffset; + + if (startArrIndex != endArrIndex) { + int endOffset = 64 - startBitOffset; + int j1 = this.bitsPerEntry - endOffset; + this.longArray[endArrIndex] = this.longArray[endArrIndex] >>> j1 << j1 | ((long) value & this.maxEntryValue) >> endOffset; + } + } + + public int getAt(long index) { + long startOffset = index * (long) this.bitsPerEntry; + int startArrIndex = (int) (startOffset >> 6); // startOffset / 64 + int endArrIndex = (int) (((index + 1L) * (long) this.bitsPerEntry - 1L) >> 6); + int startBitOffset = (int) (startOffset & 0x3F); // startOffset % 64 + + if (startArrIndex == endArrIndex) { + return (int) (this.longArray[startArrIndex] >>> startBitOffset & this.maxEntryValue); + } else { + int endOffset = 64 - startBitOffset; + return (int) ((this.longArray[startArrIndex] >>> startBitOffset | this.longArray[endArrIndex] << endOffset) & this.maxEntryValue); + } + } + + public long size() { + return this.arraySize; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStateContainer.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStateContainer.java new file mode 100644 index 0000000..7a7093c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStateContainer.java @@ -0,0 +1,118 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; + +public class LitematicaBlockStateContainer { + + public static final BlockState AIR_BLOCK_STATE = Blocks.AIR.defaultBlockState(); + protected final Vec3i size; + protected final int sizeX; + protected final int sizeY; + protected final int sizeZ; + protected final int sizeLayer; + protected final long totalVolume; + protected LitematicaBitArray storage; + protected LitematicaBlockStatePalette palette; + protected int bits; + + public LitematicaBlockStateContainer(int sizeX, int sizeY, int sizeZ, int bits, @Nullable long[] backingLongArray) { + this.sizeX = sizeX; + this.sizeY = sizeY; + this.sizeZ = sizeZ; + this.sizeLayer = sizeX * sizeZ; + this.totalVolume = (long) this.sizeX * (long) this.sizeY * (long) this.sizeZ; + this.size = new Vec3i(this.sizeX, this.sizeY, this.sizeZ); + + this.setBits(bits, backingLongArray); + } + + public static LitematicaBlockStateContainer createFrom(ListTag palette, long[] blockStates, BlockPos size) { + int bits = Math.max(2, Integer.SIZE - Integer.numberOfLeadingZeros(palette.size() - 1)); + LitematicaBlockStateContainer container = new LitematicaBlockStateContainer(size.getX(), size.getY(), size.getZ(), bits, blockStates); + container.palette.readFromNBT(palette); + return container; + } + + public Vec3i getSize() { + return this.size; + } + + public LitematicaBitArray getArray() { + return this.storage; + } + + public BlockState get(int x, int y, int z) { + BlockState state = this.palette.getBlockState(this.storage.getAt(this.getIndex(x, y, z))); + return state == null ? AIR_BLOCK_STATE : state; + } + + protected int getIndex(int x, int y, int z) { + return (y * this.sizeLayer) + z * this.sizeX + x; + } + + protected void setBits(int bitsIn, @Nullable long[] backingLongArray) { + if (bitsIn != this.bits) { + this.bits = bitsIn; + + if (this.bits <= 4) { + this.bits = Math.max(2, this.bits); + this.palette = new LitematicaBlockStatePaletteLinear(this.bits, this); + } else { + this.palette = new LitematicaBlockStatePaletteHashMap(this.bits, this); + } + + this.palette.idFor(AIR_BLOCK_STATE); + + if (backingLongArray != null) { + this.storage = new LitematicaBitArray(this.bits, this.totalVolume, backingLongArray); + } else { + this.storage = new LitematicaBitArray(this.bits, this.totalVolume); + } + } + } + + public int onResize(int bits, BlockState state) { + LitematicaBitArray oldStorage = this.storage; + LitematicaBlockStatePalette oldPalette = this.palette; + final long storageLength = oldStorage.size(); + + this.setBits(bits, null); + + LitematicaBitArray newStorage = this.storage; + + for (long index = 0; index < storageLength; ++index) { + newStorage.setAt(index, oldStorage.getAt(index)); + } + + this.palette.readFromNBT(oldPalette.writeToNBT()); + + return this.palette.idFor(state); + } + + public LitematicaBlockStatePalette getPalette() { + return this.palette; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePalette.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePalette.java new file mode 100644 index 0000000..ed737ea --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePalette.java @@ -0,0 +1,62 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.server.MinecraftServer; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; + +public interface LitematicaBlockStatePalette { + /** + * Gets the palette id for the given block state and adds + * the state to the palette if it doesn't exist there yet. + */ + int idFor(BlockState state); + + /** + * Gets the block state by the palette id. + */ + @Nullable + BlockState getBlockState(int indexKey); + + void requestNewId(BlockState state); + + default void readFromNBT(ListTag tagList) { + Registry lookup = MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.BLOCK); + + final int size = tagList.size(); + + for (int i = 0; i < size; ++i) { + CompoundTag tag = tagList.getCompound(i).orElseThrow(); + BlockState state = NbtUtils.readBlockState(lookup, tag); + + if (i > 0 || state != LitematicaBlockStateContainer.AIR_BLOCK_STATE) { + this.requestNewId(state); + } + } + } + + ListTag writeToNBT(); +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteHashMap.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteHashMap.java new file mode 100644 index 0000000..e6131f3 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteHashMap.java @@ -0,0 +1,90 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.world.level.block.state.BlockState; +import org.leavesmc.leaves.protocol.servux.litematics.utils.Int2ObjectBiMap; + +import javax.annotation.Nullable; + +public class LitematicaBlockStatePaletteHashMap implements LitematicaBlockStatePalette { + + private final Int2ObjectBiMap statePaletteMap; + private final LitematicaBlockStateContainer blockStateContainer; + private final int bits; + + public LitematicaBlockStatePaletteHashMap(int bitsIn, LitematicaBlockStateContainer blockStateContainer) { + this.bits = bitsIn; + this.blockStateContainer = blockStateContainer; + this.statePaletteMap = Int2ObjectBiMap.create(1 << bitsIn); + } + + @Override + public int idFor(BlockState state) { + int i = this.statePaletteMap.getRawId(state); + + if (i == -1) { + i = this.statePaletteMap.add(state); + + if (i >= (1 << this.bits)) { + i = this.blockStateContainer.onResize(this.bits + 1, state); + } + } + + return i; + } + + @Override + @Nullable + public BlockState getBlockState(int indexKey) { + return this.statePaletteMap.get(indexKey); + } + + public void requestNewId(BlockState state) { + final int origId = this.statePaletteMap.add(state); + + if (origId >= (1 << this.bits)) { + int newId = this.blockStateContainer.onResize(this.bits + 1, LitematicaBlockStateContainer.AIR_BLOCK_STATE); + + if (newId <= origId) { + this.statePaletteMap.add(state); + } + } + } + + @Override + public ListTag writeToNBT() { + ListTag tagList = new ListTag(); + + for (int id = 0; id < this.statePaletteMap.size(); ++id) { + BlockState state = this.statePaletteMap.get(id); + + if (state == null) { + state = LitematicaBlockStateContainer.AIR_BLOCK_STATE; + } + + CompoundTag tag = NbtUtils.writeBlockState(state); + tagList.add(tag); + } + + return tagList; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteLinear.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteLinear.java new file mode 100644 index 0000000..cb1f1ec --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/container/LitematicaBlockStatePaletteLinear.java @@ -0,0 +1,91 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.container; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtUtils; +import net.minecraft.world.level.block.state.BlockState; + +import javax.annotation.Nullable; + +public class LitematicaBlockStatePaletteLinear implements LitematicaBlockStatePalette { + + private final BlockState[] states; + private final LitematicaBlockStateContainer blockStateContainer; + private final int bits; + private int currentSize; + + public LitematicaBlockStatePaletteLinear(int bitsIn, LitematicaBlockStateContainer blockStateContainer) { + this.states = new BlockState[1 << bitsIn]; + this.bits = bitsIn; + this.blockStateContainer = blockStateContainer; + } + + @Override + public int idFor(BlockState state) { + for (int i = 0; i < this.currentSize; ++i) { + if (this.states[i] == state) { + return i; + } + } + + final int size = this.currentSize; + + if (size < this.states.length) { + this.states[size] = state; + ++this.currentSize; + return size; + } else { + return this.blockStateContainer.onResize(this.bits + 1, state); + } + } + + @Override + @Nullable + public BlockState getBlockState(int indexKey) { + return indexKey >= 0 && indexKey < this.currentSize ? this.states[indexKey] : null; + } + + public void requestNewId(BlockState state) { + final int size = this.currentSize; + + if (size < this.states.length) { + this.states[size] = state; + ++this.currentSize; + } + } + + @Override + public ListTag writeToNBT() { + ListTag tagList = new ListTag(); + + for (int id = 0; id < this.currentSize; ++id) { + BlockState state = this.states[id]; + + if (state == null) { + state = LitematicaBlockStateContainer.AIR_BLOCK_STATE; + } + + CompoundTag tag = NbtUtils.writeBlockState(state); + tagList.add(tag); + } + + return tagList; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SchematicPlacement.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SchematicPlacement.java new file mode 100644 index 0000000..e08b9ab --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SchematicPlacement.java @@ -0,0 +1,296 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.placement; + +import com.google.common.collect.ImmutableMap; +import net.minecraft.core.BlockBox; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.AABB; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; +import org.leavesmc.leaves.protocol.servux.litematics.LitematicaSchematic; +import org.leavesmc.leaves.protocol.servux.litematics.selection.Box; +import org.leavesmc.leaves.protocol.servux.litematics.utils.*; + +import javax.annotation.Nullable; +import java.util.*; +import java.util.stream.Stream; + +public class SchematicPlacement { + + private final Map relativeSubRegionPlacements = new HashMap<>(); + private final LitematicaSchematic schematic; + private final BlockPos origin; + private final String name; + private Rotation rotation = Rotation.NONE; + private Mirror mirror = Mirror.NONE; + + private SchematicPlacement(LitematicaSchematic schematic, BlockPos origin, String name) { + this.schematic = schematic; + this.origin = origin; + this.name = name; + } + + public static SchematicPlacement createFromNbt(CompoundTag tags) { + SchematicPlacement placement = new SchematicPlacement( + LitematicaSchematic.readFromNBT(tags.getCompoundOrEmpty("Schematics")), + NbtUtils.readBlockPosFromArrayTag(tags, "Origin"), + tags.getStringOr("Name", "") + ); + placement.mirror = Mirror.values()[tags.getIntOr("Mirror", 0)]; + placement.rotation = Rotation.values()[tags.getIntOr("Rotation", 0)]; + for (String name : tags.getCompoundOrEmpty("SubRegions").keySet()) { + CompoundTag compound = tags.getCompoundOrEmpty("SubRegions").getCompoundOrEmpty(name); + var sub = new SubRegionPlacement( + compound.getStringOr("Name", "?"), + NbtUtils.readBlockPosFromArrayTag(compound, "Pos"), + Rotation.values()[compound.getIntOr("Rotation", 0)], + Mirror.values()[compound.getIntOr("Mirror", 0)], + compound.getBooleanOr("Enabled", true), + compound.getBooleanOr("IgnoreEntities", false) + ); + placement.relativeSubRegionPlacements.put(name, sub); + } + return placement; + } + + public static IntBoundingBox getBoundsWithinChunkForBox(Box box, int chunkX, int chunkZ) { + final int chunkXMin = chunkX << 4; + final int chunkZMin = chunkZ << 4; + final int chunkXMax = chunkXMin + 15; + final int chunkZMax = chunkZMin + 15; + BlockPos boxPos1 = box.pos1(); + BlockPos boxPos2 = box.pos2(); + if (boxPos1 == null || boxPos2 == null) { + return null; + } + + int x1 = boxPos1.getX(); + int x2 = boxPos2.getX(); + int y1 = boxPos1.getY(); + int y2 = boxPos2.getY(); + int z1 = boxPos1.getZ(); + int z2 = boxPos2.getZ(); + final int boxXMin = Math.min(x1, x2); + final int boxZMin = Math.min(z1, z2); + final int boxXMax = Math.max(x1, x2); + final int boxZMax = Math.max(z1, z2); + + boolean notOverlapping = boxXMin > chunkXMax || boxZMin > chunkZMax || boxXMax < chunkXMin || boxZMax < chunkZMin; + + if (!notOverlapping) { + final int xMin = Math.max(chunkXMin, boxXMin); + final int yMin = Math.min(y1, y2); + final int zMin = Math.max(chunkZMin, boxZMin); + final int xMax = Math.min(chunkXMax, boxXMax); + final int yMax = Math.max(y1, y2); + final int zMax = Math.min(chunkZMax, boxZMax); + + return new IntBoundingBox(xMin, yMin, zMin, xMax, yMax, zMax); + } + + return null; + } + + public boolean ignoreEntities() { + return false; + } + + public String getName() { + return this.name; + } + + public LitematicaSchematic getSchematic() { + return schematic; + } + + public BlockPos getOrigin() { + return origin; + } + + public Rotation getRotation() { + return rotation; + } + + public Mirror getMirror() { + return mirror; + } + + @Nullable + public SubRegionPlacement getRelativeSubRegionPlacement(String areaName) { + return this.relativeSubRegionPlacements.get(areaName); + } + + public ImmutableMap getSubRegionBoxes(SubRegionPlacement.RequiredEnabled required) { + ImmutableMap.Builder builder = ImmutableMap.builder(); + Map areaSizes = this.schematic.getAreaSizes(); + + for (Map.Entry entry : this.relativeSubRegionPlacements.entrySet()) { + String name = entry.getKey(); + BlockPos areaSize = areaSizes.get(name); + + if (areaSize == null) { + ServuxProtocol.LOGGER.warn("SchematicPlacement.getSubRegionBoxes(): Size for sub-region '{}' not found in the schematic '{}'", name, this.schematic.metadata().name()); + continue; + } + + SubRegionPlacement placement = entry.getValue(); + + if (placement.matchesRequirement(required)) { + putBoxPosIntoBuilder(builder, name, areaSize, placement); + } + } + + return builder.build(); + } + + private void putBoxPosIntoBuilder(ImmutableMap.Builder builder, String name, BlockPos areaSize, SubRegionPlacement placement) { + BlockPos boxOriginRelative = placement.pos(); + + BlockPos boxOriginAbsolute = PositionUtils.getTransformedBlockPos(boxOriginRelative, this.mirror, this.rotation).offset(this.origin); + BlockPos pos2 = PositionUtils.getRelativeEndPositionFromAreaSize(areaSize); + pos2 = PositionUtils.getTransformedBlockPos(pos2, this.mirror, this.rotation); + pos2 = PositionUtils.getTransformedBlockPos(pos2, placement.mirror(), placement.rotation()).offset(boxOriginAbsolute); + + builder.put(name, new Box(boxOriginAbsolute, pos2, name)); + } + + public ImmutableMap getSubRegionBoxFor(String regionName, SubRegionPlacement.RequiredEnabled required) { + SubRegionPlacement placement = this.relativeSubRegionPlacements.get(regionName); + if (placement == null) { + return ImmutableMap.of(); + } + ImmutableMap.Builder builder = ImmutableMap.builder(); + Map areaSizes = this.schematic.getAreaSizes(); + + if (placement.matchesRequirement(required)) { + BlockPos areaSize = areaSizes.get(regionName); + + if (areaSize != null) { + putBoxPosIntoBuilder(builder, regionName, areaSize, placement); + } else { + ServuxProtocol.LOGGER.warn("SchematicPlacement.getSubRegionBoxFor(): Size for sub-region '{}' not found in the schematic '{}'", regionName, this.schematic.metadata().name()); + } + } + + return builder.build(); + } + + public Set getRegionsTouchingChunk(int chunkX, int chunkZ) { + ImmutableMap map = this.getSubRegionBoxes(SubRegionPlacement.RequiredEnabled.PLACEMENT_ENABLED); + final int chunkXMin = chunkX << 4; + final int chunkZMin = chunkZ << 4; + final int chunkXMax = chunkXMin + 15; + final int chunkZMax = chunkZMin + 15; + Set set = new HashSet<>(); + + for (Map.Entry entry : map.entrySet()) { + Box box = entry.getValue(); + BlockPos boxPos1 = box.pos1(); + BlockPos boxPos2 = box.pos2(); + if (boxPos1 == null || boxPos2 == null) { + continue; + } + + int x1 = boxPos1.getX(); + int x2 = boxPos2.getX(); + int z1 = boxPos1.getZ(); + int z2 = boxPos2.getZ(); + final int boxXMin = Math.min(x1, x2); + final int boxZMin = Math.min(z1, z2); + final int boxXMax = Math.max(x1, x2); + final int boxZMax = Math.max(z1, z2); + + boolean notOverlapping = boxXMin > chunkXMax || boxZMin > chunkZMax || boxXMax < chunkXMin || boxZMax < chunkZMin; + + if (!notOverlapping) { + set.add(entry.getKey()); + } + } + + return set; + } + + @Nullable + public IntBoundingBox getBoxWithinChunkForRegion(String regionName, int chunkX, int chunkZ) { + Box box = this.getSubRegionBoxFor(regionName, SubRegionPlacement.RequiredEnabled.PLACEMENT_ENABLED).get(regionName); + return box != null ? getBoundsWithinChunkForBox(box, chunkX, chunkZ) : null; + } + + private Box getEnclosingBox() { + ImmutableMap boxes = this.getSubRegionBoxes(SubRegionPlacement.RequiredEnabled.ANY); + BlockPos pos1 = null; + BlockPos pos2 = null; + + for (Box box : boxes.values()) { + BlockPos tmp; + BlockPos boxPos1 = box.pos1(); + BlockPos boxPos2 = box.pos2(); + if (boxPos1 == null || boxPos2 == null) { + continue; + } + tmp = PositionUtils.getMinCorner(boxPos1, boxPos2); + + if (pos1 == null) { + pos1 = tmp; + } else if (tmp.getX() < pos1.getX() || tmp.getY() < pos1.getY() || tmp.getZ() < pos1.getZ()) { + pos1 = PositionUtils.getMinCorner(tmp, pos1); + } + + tmp = PositionUtils.getMaxCorner(boxPos1, boxPos2); + + if (pos2 == null) { + pos2 = tmp; + } else if (tmp.getX() > pos2.getX() || tmp.getY() > pos2.getY() || tmp.getZ() > pos2.getZ()) { + pos2 = PositionUtils.getMaxCorner(tmp, pos2); + } + } + + if (pos1 != null) { + return new Box(pos1, pos2, "Enclosing Box (Servux)"); + } + + return null; + } + + public Stream streamChunkPos(@NotNull BlockBox box) { + AABB aabb = box.aabb(); + int i = SectionPos.blockToSectionCoord(aabb.minX); + int j = SectionPos.blockToSectionCoord(aabb.minZ); + int k = SectionPos.blockToSectionCoord(aabb.maxX); + int l = SectionPos.blockToSectionCoord(aabb.maxZ); + return ChunkPos.rangeClosed(new ChunkPos(i, j), new ChunkPos(k, l)); + } + + public void pasteTo(ServerLevel serverWorld, ReplaceBehavior replaceBehavior) { + Box enclosingBox = this.getEnclosingBox(); + if (enclosingBox == null || enclosingBox.pos1() == null || enclosingBox.pos2() == null) { + ServuxProtocol.LOGGER.error("receiver a null enclosing box"); + return; + } + streamChunkPos(Objects.requireNonNull(enclosingBox.toVanilla())).forEach(chunkPos -> + SchematicPlacingUtils.placeToWorldWithinChunk(serverWorld, chunkPos, this, replaceBehavior, false) + ); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SubRegionPlacement.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SubRegionPlacement.java new file mode 100644 index 0000000..eecd046 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/placement/SubRegionPlacement.java @@ -0,0 +1,55 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.placement; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; + +public record SubRegionPlacement( + String name, + BlockPos pos, + Rotation rotation, + Mirror mirror, + boolean enabled, + boolean ignoreEntities +) { + public SubRegionPlacement(String name, BlockPos pos) { + this(name, pos, Rotation.NONE, Mirror.NONE, true, false); + } + + public boolean matchesRequirement(RequiredEnabled required) { + if (required == RequiredEnabled.ANY) { + return true; + } + + if (required == RequiredEnabled.PLACEMENT_ENABLED) { + return this.enabled(); + } + + ServuxProtocol.LOGGER.warn("RequiredEnabled.RENDERING_ENABLED is not supported on server side!"); + return false; + } + + public enum RequiredEnabled { + ANY, + PLACEMENT_ENABLED, + RENDERING_ENABLED + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/selection/Box.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/selection/Box.java new file mode 100644 index 0000000..eaf79c9 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/selection/Box.java @@ -0,0 +1,36 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.selection; + +import net.minecraft.core.BlockBox; +import net.minecraft.core.BlockPos; +import org.jetbrains.annotations.Nullable; + +public record Box(@Nullable BlockPos pos1, @Nullable BlockPos pos2, String name) { + public Box() { + this(BlockPos.ZERO, BlockPos.ZERO, "Unnamed"); + } + + @Nullable + public BlockBox toVanilla() { + if (pos1 != null && pos2 != null) { + return new BlockBox(pos1, pos2); + } + return null; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/EntityUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/EntityUtils.java new file mode 100644 index 0000000..1a1c46f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/EntityUtils.java @@ -0,0 +1,111 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import com.google.common.collect.ImmutableList; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntitySpawnReason; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.Level; +import org.leavesmc.leaves.util.TagFactory; + +import javax.annotation.Nullable; +import java.util.Optional; +import java.util.UUID; + +public class EntityUtils { + + @Nullable + private static Entity createEntityFromNBTSingle(CompoundTag nbt, Level world) { + try { + Optional optional = EntityType.create(TagFactory.input(nbt), world, EntitySpawnReason.LOAD); + + if (optional.isPresent()) { + Entity entity = optional.get(); + entity.setUUID(UUID.randomUUID()); + return entity; + } + } catch (Exception ignore) { + } + + return null; + } + + /** + * Note: This does NOT spawn any of the entities in the world! + * + * @param nbt () + * @param world () + * @return () + */ + @Nullable + public static Entity createEntityAndPassengersFromNBT(CompoundTag nbt, Level world) { + Entity entity = createEntityFromNBTSingle(nbt, world); + + if (entity == null) { + return null; + } + if (nbt.contains("Passengers")) { + ListTag tagList = nbt.getListOrEmpty("Passengers"); + + for (int i = 0; i < tagList.size(); ++i) { + Entity passenger = createEntityAndPassengersFromNBT(tagList.getCompoundOrEmpty(i), world); + + if (passenger != null) { + passenger.startRiding(entity, true); + } + } + } + + return entity; + } + + public static void spawnEntityAndPassengersInWorld(Entity entity, Level world) { + ImmutableList passengers = entity.passengers; + if (world.addFreshEntity(entity) && !passengers.isEmpty()) { + for (Entity passenger : passengers) { + passenger.snapTo( + entity.getX(), + entity.getY() + entity.getPassengerRidingPosition(passenger).y(), + entity.getZ(), + passenger.getYRot(), passenger.getXRot() + ); + setEntityRotations(passenger, passenger.getYRot(), passenger.getXRot()); + spawnEntityAndPassengersInWorld(passenger, world); + } + } + } + + public static void setEntityRotations(Entity entity, float yaw, float pitch) { + entity.setYRot(yaw); + entity.yRotO = yaw; + + entity.setXRot(pitch); + entity.xRotO = pitch; + + if (entity instanceof LivingEntity livingBase) { + livingBase.yHeadRot = yaw; + livingBase.yBodyRot = yaw; + livingBase.yHeadRotO = yaw; + livingBase.yBodyRotO = yaw; + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/FileType.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/FileType.java new file mode 100644 index 0000000..8e4e4ab --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/FileType.java @@ -0,0 +1,28 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +public enum FileType { + INVALID, + UNKNOWN, + JSON, + LITEMATICA_SCHEMATIC, + SCHEMATICA_SCHEMATIC, + SPONGE_SCHEMATIC, + VANILLA_STRUCTURE +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Int2ObjectBiMap.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Int2ObjectBiMap.java new file mode 100644 index 0000000..0cb170c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Int2ObjectBiMap.java @@ -0,0 +1,211 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import com.google.common.base.Predicates; +import com.google.common.collect.Iterators; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Arrays; +import java.util.Iterator; + +public class Int2ObjectBiMap implements Iterable { + + private static final Object EMPTY = null; + private K[] values; + private int[] ids; + private K[] idToValues; + private int nextId; + private int size; + + @SuppressWarnings("unchecked") + private Int2ObjectBiMap(int size) { + this.values = (K[]) (new Object[size]); + this.ids = new int[size]; + this.idToValues = (K[]) (new Object[size]); + } + + private Int2ObjectBiMap(K[] values, int[] ids, K[] idToValues, int nextId, int size) { + this.values = values; + this.ids = ids; + this.idToValues = idToValues; + this.nextId = nextId; + this.size = size; + } + + public static Int2ObjectBiMap create(int expectedSize) { + return new Int2ObjectBiMap<>((int) ((float) expectedSize / 0.8F)); + } + + private static int idealHash(int value) { + value ^= value >>> 16; + value *= -2048144789; + value ^= value >>> 13; + value *= -1028477387; + return value ^ value >>> 16; + } + + public int getRawId(@Nullable K value) { + return this.getIdFromIndex(this.findIndex(value, this.getIdealIndex(value))); + } + + @Nullable + public K get(int index) { + return index >= 0 && index < this.idToValues.length ? this.idToValues[index] : null; + } + + private int getIdFromIndex(int index) { + return index == -1 ? -1 : this.ids[index]; + } + + public boolean contains(K value) { + return this.getRawId(value) != -1; + } + + public boolean containsKey(int index) { + return this.get(index) != null; + } + + public int add(K value) { + int i = this.nextId(); + this.put(value, i); + return i; + } + + private int nextId() { + while (this.nextId < this.idToValues.length && this.idToValues[this.nextId] != null) { + this.nextId++; + } + + return this.nextId; + } + + private void resize(int newSize) { + K[] objects = this.values; + int[] is = this.ids; + Int2ObjectBiMap int2ObjectBiMap = new Int2ObjectBiMap<>(newSize); + + for (int i = 0; i < objects.length; i++) { + if (objects[i] != null) { + int2ObjectBiMap.put(objects[i], is[i]); + } + } + + this.values = int2ObjectBiMap.values; + this.ids = int2ObjectBiMap.ids; + this.idToValues = int2ObjectBiMap.idToValues; + this.nextId = int2ObjectBiMap.nextId; + this.size = int2ObjectBiMap.size; + } + + public void put(K value, int id) { + int i = Math.max(id, this.size + 1); + if ((float) i >= (float) this.values.length * 0.8F) { + int j = this.values.length << 1; + + while (j < id) { + j <<= 1; + } + + this.resize(j); + } + + int j = this.findFree(this.getIdealIndex(value)); + this.values[j] = value; + this.ids[j] = id; + this.idToValues[id] = value; + this.size++; + if (id == this.nextId) { + this.nextId++; + } + } + + private int getIdealIndex(@Nullable K value) { + + return (idealHash(System.identityHashCode(value)) & 2147483647) % this.values.length; + } + + private int findIndex(@Nullable K value, int id) { + for (int i = id; i < this.values.length; i++) { + if (this.values[i] == value) { + return i; + } + + if (this.values[i] == EMPTY) { + return -1; + } + } + + for (int i = 0; i < id; i++) { + if (this.values[i] == value) { + return i; + } + + if (this.values[i] == EMPTY) { + return -1; + } + } + + return -1; + } + + private int findFree(int size) { + for (int i = size; i < this.values.length; i++) { + if (this.values[i] == EMPTY) { + return i; + } + } + + for (int ix = 0; ix < size; ix++) { + if (this.values[ix] == EMPTY) { + return ix; + } + } + + throw new RuntimeException("Overflowed :("); + } + + public @NotNull Iterator iterator() { + return Iterators.filter(Iterators.forArray(this.idToValues), Predicates.notNull()); + } + + public void clear() { + Arrays.fill(this.values, null); + Arrays.fill(this.idToValues, null); + this.nextId = 0; + this.size = 0; + } + + public int size() { + return this.size; + } + + public Int2ObjectBiMap copy() { + return new Int2ObjectBiMap<>(this.values.clone(), this.ids.clone(), this.idToValues.clone(), this.nextId, this.size); + } + + public K getOrThrow(int index) { + K object = this.get(index); + if (object == null) { + throw new IllegalArgumentException("No value with id " + index); + } else { + return object; + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/IntBoundingBox.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/IntBoundingBox.java new file mode 100644 index 0000000..f2dbe30 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/IntBoundingBox.java @@ -0,0 +1,43 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import net.minecraft.core.Vec3i; + +public record IntBoundingBox(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + + public boolean containsPos(Vec3i pos) { + return pos.getX() >= this.minX && pos.getX() <= this.maxX && pos.getZ() >= this.minZ && pos.getZ() <= this.maxZ && pos.getY() >= this.minY && pos.getY() <= this.maxY; + } + + public boolean intersects(IntBoundingBox box) { + return this.maxX >= box.minX && this.minX <= box.maxX && this.maxZ >= box.minZ && this.minZ <= box.maxZ && this.maxY >= box.minY && this.minY <= box.maxY; + } + + public IntBoundingBox expand(int amount) { + return this.expand(amount, amount, amount); + } + + public IntBoundingBox expand(int x, int y, int z) { + return new IntBoundingBox(this.minX - x, this.minY - y, this.minZ - z, this.maxX + x, this.maxY + y, this.maxZ + z); + } + + public IntBoundingBox shrink(int x, int y, int z) { + return this.expand(-x, -y, -z); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/NbtUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/NbtUtils.java new file mode 100644 index 0000000..7824a2f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/NbtUtils.java @@ -0,0 +1,92 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class NbtUtils { + + public static void writeBlockPosToTag(Vec3i pos, CompoundTag tag) { + tag.putInt("x", pos.getX()); + tag.putInt("y", pos.getY()); + tag.putInt("z", pos.getZ()); + } + + @Nullable + public static BlockPos readBlockPos(@Nullable CompoundTag tag) { + if (tag != null && tag.contains("x") && tag.contains("y") && tag.contains("z")) { + return new BlockPos(tag.getIntOr("x", 0), tag.getIntOr("y", 0), tag.getIntOr("z", 0)); + } + + return null; + } + + public static void writeEntityPositionToTag(Vec3 pos, CompoundTag tag) { + ListTag posList = new ListTag(); + + posList.add(DoubleTag.valueOf(pos.x)); + posList.add(DoubleTag.valueOf(pos.y)); + posList.add(DoubleTag.valueOf(pos.z)); + tag.put("Pos", posList); + } + + @Nullable + public static Vec3 readVec3(@Nullable CompoundTag tag) { + if (tag != null && tag.contains("dx") && tag.contains("dy") && tag.contains("dz")) { + return new Vec3(tag.getDoubleOr("dx", 0.0), tag.getDoubleOr("dy", 0.0), tag.getDoubleOr("dz", 0.0)); + } + return null; + } + + @Nullable + public static Vec3 readEntityPositionFromTag(@Nullable CompoundTag tag) { + if (tag == null || !tag.contains("Pos")) { + return null; + } + ListTag tagList = tag.getListOrEmpty("Pos"); + if (tagList.size() != 3) { + return null; + } + return new Vec3(tagList.getDoubleOr(0, 0.0), tagList.getDoubleOr(1, 0.0), tagList.getDoubleOr(2, 0.0)); + } + + @Nullable + public static Vec3i readVec3iFromTag(@Nullable CompoundTag tag) { + if (tag != null && tag.contains("x") && tag.contains("y") && tag.contains("z")) { + return new Vec3i(tag.getIntOr("x", 0), tag.getIntOr("y", 0), tag.getIntOr("z", 0)); + } + return null; + } + + public static BlockPos readBlockPosFromArrayTag(@NotNull CompoundTag tag, String tagName) { + if (tag.contains(tagName)) { + int[] pos = tag.getIntArray(tagName).orElse(new int[0]); + if (pos.length == 3) { + return new BlockPos(pos[0], pos[1], pos[2]); + } + } + return null; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/PositionUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/PositionUtils.java new file mode 100644 index 0000000..dd0aa66 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/PositionUtils.java @@ -0,0 +1,149 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.phys.Vec3; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SchematicPlacement; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SubRegionPlacement; + +public class PositionUtils { + + public static Direction rotateYCounterclockwise(Direction direction) { + Direction var10000; + switch (direction.ordinal()) { + case 2 -> var10000 = Direction.WEST; + case 3 -> var10000 = Direction.EAST; + case 4 -> var10000 = Direction.SOUTH; + case 5 -> var10000 = Direction.NORTH; + default -> throw new IllegalStateException("Unable to get CCW facing of " + direction); + } + + return var10000; + } + + public static BlockPos getMinCorner(BlockPos pos1, BlockPos pos2) { + return new BlockPos(Math.min(pos1.getX(), pos2.getX()), Math.min(pos1.getY(), pos2.getY()), Math.min(pos1.getZ(), pos2.getZ())); + } + + public static BlockPos getMaxCorner(BlockPos pos1, BlockPos pos2) { + return new BlockPos(Math.max(pos1.getX(), pos2.getX()), Math.max(pos1.getY(), pos2.getY()), Math.max(pos1.getZ(), pos2.getZ())); + } + + public static BlockPos getTransformedPlacementPosition(BlockPos posWithinSub, SchematicPlacement schematicPlacement, SubRegionPlacement placement) { + BlockPos pos = posWithinSub; + pos = getTransformedBlockPos(pos, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + pos = getTransformedBlockPos(pos, placement.mirror(), placement.rotation()); + return pos; + } + + public static BlockPos getRelativeEndPositionFromAreaSize(Vec3i size) { + int x = size.getX(); + int y = size.getY(); + int z = size.getZ(); + + x = x >= 0 ? x - 1 : x + 1; + y = y >= 0 ? y - 1 : y + 1; + z = z >= 0 ? z - 1 : z + 1; + + return new BlockPos(x, y, z); + } + + /** + * Mirrors and then rotates the given position around the origin + */ + public static BlockPos getTransformedBlockPos(BlockPos pos, Mirror mirror, Rotation rotation) { + int x = pos.getX(); + int y = pos.getY(); + int z = pos.getZ(); + boolean isMirrored = true; + + switch (mirror) { + case LEFT_RIGHT -> z = -z; // LEFT_RIGHT is essentially NORTH_SOUTH + case FRONT_BACK -> x = -x; // FRONT_BACK is essentially EAST_WEST + default -> isMirrored = false; + } + + return switch (rotation) { + case CLOCKWISE_90 -> new BlockPos(-z, y, x); + case COUNTERCLOCKWISE_90 -> new BlockPos(z, y, -x); + case CLOCKWISE_180 -> new BlockPos(-x, y, -z); + default -> isMirrored ? new BlockPos(x, y, z) : pos; + }; + } + + public static BlockPos getReverseTransformedBlockPos(BlockPos pos, Mirror mirror, Rotation rotation) { + int x = pos.getX(); + int y = pos.getY(); + int z = pos.getZ(); + boolean isRotated = true; + int tmp = x; + + switch (rotation) { + case CLOCKWISE_90 -> { + x = z; + z = -tmp; + } + case COUNTERCLOCKWISE_90 -> { + x = -z; + z = tmp; + } + case CLOCKWISE_180 -> { + x = -x; + z = -z; + } + default -> isRotated = false; + } + + switch (mirror) { + case LEFT_RIGHT -> z = -z; // LEFT_RIGHT is essentially NORTH_SOUTH + case FRONT_BACK -> x = -x; // FRONT_BACK is essentially EAST_WEST + default -> { + if (!isRotated) { + return pos; + } + } + } + + return new BlockPos(x, y, z); + } + + public static Vec3 getTransformedPosition(Vec3 originalPos, Mirror mirror, Rotation rotation) { + double x = originalPos.x; + double y = originalPos.y; + double z = originalPos.z; + boolean transformed = true; + + switch (mirror) { + case LEFT_RIGHT -> z = 1.0D - z; + case FRONT_BACK -> x = 1.0D - x; + default -> transformed = false; + } + + return switch (rotation) { + case COUNTERCLOCKWISE_90 -> new Vec3(z, y, 1.0D - x); + case CLOCKWISE_90 -> new Vec3(1.0D - z, y, x); + case CLOCKWISE_180 -> new Vec3(1.0D - x, y, 1.0D - z); + default -> transformed ? new Vec3(x, y, z) : originalPos; + }; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/ReplaceBehavior.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/ReplaceBehavior.java new file mode 100644 index 0000000..1f632dd --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/ReplaceBehavior.java @@ -0,0 +1,40 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +public enum ReplaceBehavior { + NONE("none"), + ALL("all"), + WITH_NON_AIR("with_non_air"); + + private final String configString; + + ReplaceBehavior(String configString) { + this.configString = configString; + } + + public static ReplaceBehavior fromStringStatic(String name) { + for (ReplaceBehavior val : ReplaceBehavior.values()) { + if (val.configString.equalsIgnoreCase(name)) { + return val; + } + } + + return ReplaceBehavior.NONE; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Schema.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Schema.java new file mode 100644 index 0000000..8bdeb3d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/Schema.java @@ -0,0 +1,168 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import javax.annotation.Nullable; + +public enum Schema { + // TODO --> Add Schema Versions to this as versions get released + // Minecraft Data Versions + SCHEMA_FUTURE(9999, "FUTURE"), + SCHEMA_1_21_05(4325, "1.21.5"), + SCHEMA_25W10A(4319, "25w10a"), + SCHEMA_25W03A(4304, "25w03a"), // Entity Data Components ( https://www.minecraft.net/en-us/article/minecraft-snapshot-25w03a ) + SCHEMA_25W02A(4298, "25w02a"), + SCHEMA_1_21_04(4189, "1.21.4"), + SCHEMA_24W46A(4178, "24w46a"), + SCHEMA_24W44A(4174, "24w44a"), + SCHEMA_1_21_03(4082, "1.21.3"), + SCHEMA_1_21_02(4080, "1.21.2"), + SCHEMA_24W40A(4072, "24w40a"), + SCHEMA_24W37A(4065, "24w37a"), + SCHEMA_24W35A(4062, "24w35a"), + SCHEMA_24W33A(4058, "24w33a"), + SCHEMA_1_21_01(3955, "1.21.1"), + SCHEMA_1_21_00(3953, "1.21"), + SCHEMA_24W21A(3946, "24w21a"), + SCHEMA_24W18A(3940, "24w18a"), + SCHEMA_1_20_05(3837, "1.20.5"), + SCHEMA_24W14A(3827, "24w14a"), + SCHEMA_24W13A(3826, "24w13a"), + SCHEMA_24W12A(3824, "24w12a"), + SCHEMA_24W10A(3821, "24w10a"), + SCHEMA_24W09A(3819, "24w09a"), // Data Components ( https://minecraft.wiki/w/Data_component_format ) + SCHEMA_24W07A(3817, "24w07a"), + SCHEMA_24W03A(3804, "24w03a"), + SCHEMA_23W51A(3801, "23w51a"), + SCHEMA_1_20_04(3700, "1.20.4"), + SCHEMA_23W46A(3691, "23w46a"), + SCHEMA_23W43B(3687, "23w43b"), + SCHEMA_23W40A(3679, "23w40a"), + SCHEMA_1_20_02(3578, "1.20.2"), + SCHEMA_23W35A(3571, "23w35a"), + SCHEMA_23W31A(3567, "23w31a"), + SCHEMA_1_20_01(3465, "1.20.1"), + SCHEMA_1_20_00(3463, "1.20"), + SCHEMA_23W18A(3453, "23w18a"), + SCHEMA_23W16A(3449, "23w16a"), + SCHEMA_23W12A(3442, "23w12a"), + SCHEMA_1_19_04(3337, "1.19.4"), + SCHEMA_1_19_03(3218, "1.19.3"), + SCHEMA_1_19_02(3120, "1.19.2"), + SCHEMA_1_19_01(3117, "1.19.1"), + SCHEMA_1_19_00(3105, "1.19"), + SCHEMA_22W19A(3096, "22w19a"), + SCHEMA_22W16A(3091, "22w16a"), + SCHEMA_22W11A(3080, "22w11a"), + SCHEMA_1_18_02(2975, "1.18.2"), + SCHEMA_1_18_01(2865, "1.18.1"), + SCHEMA_1_18_00(2860, "1.18"), + SCHEMA_21W44A(2845, "21w44a"), + SCHEMA_21W41A(2839, "21w41a"), + SCHEMA_21W37A(2834, "21w37a"), + SCHEMA_1_17_01(2730, "1.17.1"), + SCHEMA_1_17_00(2724, "1.17"), + SCHEMA_21W20A(2715, "21w20a"), + SCHEMA_21W15A(2709, "21w15a"), + SCHEMA_21W10A(2699, "21w10a"), + SCHEMA_21W05A(2690, "21w05a"), + SCHEMA_20W49A(2685, "20w49a"), + SCHEMA_20W45A(2681, "20w45a"), + SCHEMA_1_16_05(2586, "1.16.5"), + SCHEMA_1_16_04(2584, "1.16.4"), + SCHEMA_1_16_03(2580, "1.16.3"), + SCHEMA_1_16_02(2578, "1.16.2"), + SCHEMA_1_16_01(2567, "1.16.1"), + SCHEMA_1_16_00(2566, "1.16"), + SCHEMA_20W22A(2555, "20w22a"), + SCHEMA_20W15A(2525, "20w15a"), + SCHEMA_20W06A(2504, "20w06a"), + SCHEMA_1_15_02(2230, "1.15.2"), + SCHEMA_1_15_01(2227, "1.15.1"), + SCHEMA_1_15_00(2225, "1.15"), + SCHEMA_19W46B(2217, "19w46b"), + SCHEMA_19W40A(2208, "19w40a"), + SCHEMA_19W34A(2200, "19w34a"), + SCHEMA_1_14_04(1976, "1.14.4"), + SCHEMA_1_14_03(1968, "1.14.3"), + SCHEMA_1_14_02(1963, "1.14.2"), + SCHEMA_1_14_01(1957, "1.14.1"), + SCHEMA_1_14_00(1952, "1.14"), + SCHEMA_19W14B(1945, "19w14b"), + SCHEMA_19W08B(1934, "19w08b"), + SCHEMA_18W50A(1919, "18w50a"), + SCHEMA_18W43A(1901, "18w43a"), + SCHEMA_1_13_02(1631, "1.13.2"), + SCHEMA_1_13_01(1628, "1.13.1"), + SCHEMA_1_13_00(1519, "1.13"), + SCHEMA_18W22C(1499, "18w22c"), + SCHEMA_18W14B(1481, "18w14b"), + SCHEMA_18W07C(1469, "18w07c"), + SCHEMA_17W50A(1457, "17w50a"), + SCHEMA_17W47A(1451, "17w47a"), // The Flattening ( https://minecraft.wiki/w/Java_Edition_1.13/Flattening ) + SCHEMA_17W46A(1449, "17w46a"), + SCHEMA_17W43A(1444, "17w43a"), + SCHEMA_1_12_02(1343, "1.12.2"), + SCHEMA_1_12_01(1241, "1.12.1"), + SCHEMA_1_12_00(1139, "1.12"), + SCHEMA_1_11_02(922, "1.11.2"), + SCHEMA_1_11_00(819, "1.11"), + SCHEMA_1_10_02(512, "1.10.2"), + SCHEMA_1_10_00(510, "1.10"), + SCHEMA_1_09_04(184, "1.9.4"), + SCHEMA_1_09_00(169, "1.9"), + SCHEMA_15W32A(100, "15w32a"); + + private final int schemaId; + private final String str; + + Schema(int id, String ver) { + this.schemaId = id; + this.str = ver; + } + + /** + * Returns the Schema of the closest dataVersion, or below it. + * + * @param dataVersion (Schema ID) + * @return (Schema | null) + */ + public static @Nullable Schema getSchemaByDataVersion(int dataVersion) { + for (Schema schema : Schema.values()) { + if (schema.getDataVersion() <= dataVersion) { + return schema; + } + } + + return null; + } + + public int getDataVersion() { + return this.schemaId; + } + + public String getString() { + return this.str; + } + + @Override + public String toString() { + return "MC: " + this.getString() + " [Schema: " + this.getDataVersion() + "]"; + } +} + diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/SchematicPlacingUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/SchematicPlacingUtils.java new file mode 100644 index 0000000..bb5728a --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/litematics/utils/SchematicPlacingUtils.java @@ -0,0 +1,440 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.litematics.utils; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.Container; +import net.minecraft.world.entity.Display; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.decoration.ItemFrame; +import net.minecraft.world.entity.decoration.Painting; +import net.minecraft.world.level.ChunkPos; +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.Mirror; +import net.minecraft.world.level.block.Rotation; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.Fluid; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.ticks.LevelTicks; +import net.minecraft.world.ticks.ScheduledTick; +import org.leavesmc.leaves.protocol.servux.ServuxProtocol; +import org.leavesmc.leaves.protocol.servux.litematics.LitematicaSchematic; +import org.leavesmc.leaves.protocol.servux.litematics.container.LitematicaBlockStateContainer; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SchematicPlacement; +import org.leavesmc.leaves.protocol.servux.litematics.placement.SubRegionPlacement; +import org.leavesmc.leaves.util.TagUtil; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class SchematicPlacingUtils { + public static void placeToWorldWithinChunk( + Level world, + ChunkPos chunkPos, + SchematicPlacement schematicPlacement, + ReplaceBehavior replace, + boolean notifyNeighbors + ) { + LitematicaSchematic schematic = schematicPlacement.getSchematic(); + Set regionsTouchingChunk = schematicPlacement.getRegionsTouchingChunk(chunkPos.x, chunkPos.z); + BlockPos origin = schematicPlacement.getOrigin(); + + for (String regionName : regionsTouchingChunk) { + LitematicaSchematic.SubRegion subRegion = schematic.getSubRegion(regionName); + LitematicaBlockStateContainer container = subRegion.blockContainers(); + + if (container == null) { + continue; + } + + SubRegionPlacement placement = schematicPlacement.getRelativeSubRegionPlacement(regionName); + if (placement == null) { + ServuxProtocol.LOGGER.error("receiver a null placement for region: {}", regionName); + continue; + } + if (!placement.enabled()) { + continue; + } + Map blockEntityMap = subRegion.tileEntities(); + Map> scheduledBlockTicks = subRegion.pendingBlockTicks(); + Map> scheduledFluidTicks = subRegion.pendingFluidTicks(); + + if (!placeBlocksWithinChunk(world, chunkPos, regionName, container, blockEntityMap, + origin, schematicPlacement, placement, scheduledBlockTicks, scheduledFluidTicks, replace, notifyNeighbors)) { + ServuxProtocol.LOGGER.warn("Invalid/missing schematic data in schematic '{}' for sub-region '{}'", schematic.metadata().name(), regionName); + } + + List entityList = subRegion.entities(); + + if (!schematicPlacement.ignoreEntities() && + !placement.ignoreEntities() && entityList != null) { + placeEntitiesToWorldWithinChunk(world, chunkPos, entityList, origin, schematicPlacement, placement); + } + } + } + + public static boolean placeBlocksWithinChunk( + Level world, ChunkPos chunkPos, String regionName, + LitematicaBlockStateContainer container, + Map blockEntityMap, + BlockPos origin, + SchematicPlacement schematicPlacement, + SubRegionPlacement placement, + @Nullable Map> scheduledBlockTicks, + @Nullable Map> scheduledFluidTicks, + ReplaceBehavior replace, boolean notifyNeighbors + ) { + IntBoundingBox bounds = schematicPlacement.getBoxWithinChunkForRegion(regionName, chunkPos.x, chunkPos.z); + Vec3i regionSize = schematicPlacement.getSchematic().getSubRegion(regionName).size(); + + if (bounds == null || container == null || blockEntityMap == null || regionSize == null) { + return false; + } + + BlockPos regionPos = placement.pos(); + + // These are the untransformed relative positions + BlockPos posEndRel = (new BlockPos(PositionUtils.getRelativeEndPositionFromAreaSize(regionSize))).offset(regionPos); + BlockPos posMinRel = PositionUtils.getMinCorner(regionPos, posEndRel); + + // The transformed sub-region origin position + BlockPos regionPosTransformed = PositionUtils.getTransformedBlockPos(regionPos, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + + // The relative offset of the affected region's corners, to the sub-region's origin corner + BlockPos boxMinRel = new BlockPos(bounds.minX() - origin.getX() - regionPosTransformed.getX(), 0, bounds.minZ() - origin.getZ() - regionPosTransformed.getZ()); + BlockPos boxMaxRel = new BlockPos(bounds.maxX() - origin.getX() - regionPosTransformed.getX(), 0, bounds.maxZ() - origin.getZ() - regionPosTransformed.getZ()); + + // Reverse transform that relative offset, to get the untransformed orientation's offsets + boxMinRel = PositionUtils.getReverseTransformedBlockPos(boxMinRel, placement.mirror(), placement.rotation()); + boxMaxRel = PositionUtils.getReverseTransformedBlockPos(boxMaxRel, placement.mirror(), placement.rotation()); + + boxMinRel = PositionUtils.getReverseTransformedBlockPos(boxMinRel, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + boxMaxRel = PositionUtils.getReverseTransformedBlockPos(boxMaxRel, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + + // Get the offset relative to the sub-region's minimum corner, instead of the origin corner (which can be at any corner) + boxMinRel = boxMinRel.subtract(posMinRel.subtract(regionPos)); + boxMaxRel = boxMaxRel.subtract(posMinRel.subtract(regionPos)); + + BlockPos posMin = PositionUtils.getMinCorner(boxMinRel, boxMaxRel); + BlockPos posMax = PositionUtils.getMaxCorner(boxMinRel, boxMaxRel); + + final int startX = posMin.getX(); + final int startZ = posMin.getZ(); + final int endX = posMax.getX(); + final int endZ = posMax.getZ(); + final int startY = 0; + final int endY = Math.abs(regionSize.getY()) - 1; + BlockPos.MutableBlockPos posMutable = new BlockPos.MutableBlockPos(); + if (startX < 0 || startZ < 0 || endX >= container.getSize().getX() || endZ >= container.getSize().getZ()) { + return false; + } + + final Rotation rotationCombined = schematicPlacement.getRotation().getRotated(placement.rotation()); + final Mirror mirrorMain = schematicPlacement.getMirror(); + final BlockState barrier = Blocks.BARRIER.defaultBlockState(); + Mirror mirrorSub = placement.mirror(); + final boolean ignoreInventories = false; + + if (mirrorSub != Mirror.NONE && + (schematicPlacement.getRotation() == Rotation.CLOCKWISE_90 || + schematicPlacement.getRotation() == Rotation.COUNTERCLOCKWISE_90)) { + mirrorSub = mirrorSub == Mirror.FRONT_BACK ? Mirror.LEFT_RIGHT : Mirror.FRONT_BACK; + } + + final int posMinRelMinusRegX = posMinRel.getX() - regionPos.getX(); + final int posMinRelMinusRegY = posMinRel.getY() - regionPos.getY(); + final int posMinRelMinusRegZ = posMinRel.getZ() - regionPos.getZ(); + + for (int y = startY; y <= endY; ++y) { + for (int z = startZ; z <= endZ; ++z) { + for (int x = startX; x <= endX; ++x) { + BlockState state = container.get(x, y, z); + + if (state.getBlock() == Blocks.STRUCTURE_VOID) { + continue; + } + + posMutable.set(x, y, z); + CompoundTag teNBT = blockEntityMap.get(posMutable); + + posMutable.set(posMinRelMinusRegX + x, + posMinRelMinusRegY + y, + posMinRelMinusRegZ + z); + + BlockPos pos = PositionUtils.getTransformedPlacementPosition(posMutable, schematicPlacement, placement); + pos = pos.offset(regionPosTransformed).offset(origin); + + BlockState stateOld = world.getBlockState(pos); + + if ((replace == ReplaceBehavior.NONE && !stateOld.isAir()) || + (replace == ReplaceBehavior.WITH_NON_AIR && state.isAir())) { + continue; + } + + if (mirrorMain != Mirror.NONE) { + state = state.mirror(mirrorMain); + } + if (mirrorSub != Mirror.NONE) { + state = state.mirror(mirrorSub); + } + if (rotationCombined != Rotation.NONE) { + state = state.rotate(rotationCombined); + } + + BlockEntity te = world.getBlockEntity(pos); + + if (te != null) { + if (te instanceof Container) { + ((Container) te).clearContent(); + } + + world.setBlock(pos, barrier, 2 | 16 | (notifyNeighbors ? 0 : 512)); + } + + if (world.setBlock(pos, state, 2 | 16 | (notifyNeighbors ? 0 : 512)) && teNBT != null) { + te = world.getBlockEntity(pos); + + if (te == null) { + continue; + } + teNBT = teNBT.copy(); + NbtUtils.writeBlockPosToTag(pos, teNBT); + + try { + TagUtil.loadTileWithComponents(te, teNBT); + } catch (Exception e) { + ServuxProtocol.LOGGER.warn("Failed to load BlockEntity data for {} @ {}", state, pos); + } + } + } + } + } + ServerLevel serverWorld = (ServerLevel) world; + IntBoundingBox box = new IntBoundingBox(startX, startY, startZ, endX, endY, endZ); + + if (scheduledBlockTicks != null && !scheduledBlockTicks.isEmpty()) { + LevelTicks scheduler = serverWorld.getBlockTicks(); + + for (Map.Entry> entry : scheduledBlockTicks.entrySet()) { + BlockPos pos = entry.getKey(); + + if (!box.containsPos(pos)) { + continue; + } + posMutable.set(posMinRelMinusRegX + pos.getX(), + posMinRelMinusRegY + pos.getY(), + posMinRelMinusRegZ + pos.getZ()); + + pos = PositionUtils.getTransformedPlacementPosition(posMutable, schematicPlacement, placement); + pos = pos.offset(regionPosTransformed).offset(origin); + ScheduledTick tick = entry.getValue(); + + if (world.getBlockState(pos).getBlock() == tick.type()) { + scheduler.schedule(new ScheduledTick<>(tick.type(), pos, tick.triggerTick(), tick.priority(), tick.subTickOrder())); + } + } + } + + if (scheduledFluidTicks != null && !scheduledFluidTicks.isEmpty()) { + LevelTicks scheduler = serverWorld.getFluidTicks(); + + for (Map.Entry> entry : scheduledFluidTicks.entrySet()) { + BlockPos pos = entry.getKey(); + + if (!box.containsPos(pos)) { + continue; + } + posMutable.set(posMinRelMinusRegX + pos.getX(), + posMinRelMinusRegY + pos.getY(), + posMinRelMinusRegZ + pos.getZ()); + + pos = PositionUtils.getTransformedPlacementPosition(posMutable, schematicPlacement, placement); + pos = pos.offset(regionPosTransformed).offset(origin); + ScheduledTick tick = entry.getValue(); + + if (world.getBlockState(pos).getFluidState().getType() == tick.type()) { + scheduler.schedule(new ScheduledTick<>(tick.type(), pos, tick.triggerTick(), tick.priority(), tick.subTickOrder())); + } + } + } + + if (!notifyNeighbors) { + return true; + } + for (int y = startY; y <= endY; ++y) { + for (int z = startZ; z <= endZ; ++z) { + for (int x = startX; x <= endX; ++x) { + posMutable.set(posMinRelMinusRegX + x, + posMinRelMinusRegY + y, + posMinRelMinusRegZ + z); + BlockPos pos = PositionUtils.getTransformedPlacementPosition(posMutable, schematicPlacement, placement); + pos = pos.offset(regionPosTransformed).offset(origin); + world.updateNeighborsAt(pos, world.getBlockState(pos).getBlock()); + } + } + } + + return true; + } + + public static void placeEntitiesToWorldWithinChunk( + Level world, ChunkPos chunkPos, + List entityList, + BlockPos origin, + SchematicPlacement schematicPlacement, + SubRegionPlacement placement + ) { + BlockPos regionPos = placement.pos(); + + if (entityList == null) { + return; + } + + BlockPos regionPosRelTransformed = PositionUtils.getTransformedBlockPos(regionPos, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + final int offX = regionPosRelTransformed.getX() + origin.getX(); + final int offY = regionPosRelTransformed.getY() + origin.getY(); + final int offZ = regionPosRelTransformed.getZ() + origin.getZ(); + final double minX = (chunkPos.x << 4); + final double minZ = (chunkPos.z << 4); + final double maxX = (chunkPos.x << 4) + 16; + final double maxZ = (chunkPos.z << 4) + 16; + + final Rotation rotationCombined = schematicPlacement.getRotation().getRotated(placement.rotation()); + final Mirror mirrorMain = schematicPlacement.getMirror(); + Mirror mirrorSub = placement.mirror(); + + if (mirrorSub != Mirror.NONE && + (schematicPlacement.getRotation() == Rotation.CLOCKWISE_90 || + schematicPlacement.getRotation() == Rotation.COUNTERCLOCKWISE_90)) { + mirrorSub = mirrorSub == Mirror.FRONT_BACK ? Mirror.LEFT_RIGHT : Mirror.FRONT_BACK; + } + + for (LitematicaSchematic.EntityInfo info : entityList) { + Vec3 pos = info.posVec(); + pos = PositionUtils.getTransformedPosition(pos, schematicPlacement.getMirror(), schematicPlacement.getRotation()); + pos = PositionUtils.getTransformedPosition(pos, placement.mirror(), placement.rotation()); + double x = pos.x + offX; + double y = pos.y + offY; + double z = pos.z + offZ; + float[] origRot = new float[2]; + + if (!(x >= minX && x < maxX && z >= minZ && z < maxZ)) { + continue; + } + CompoundTag tag = info.nbt().copy(); + String id = tag.getStringOr("id", ""); + + // Avoid warning about invalid hanging position. + // Note that this position isn't technically correct, but it only needs to be within 16 blocks + // of the entity position to avoid the warning. + if (id.equals("minecraft:glow_item_frame") || + id.equals("minecraft:item_frame") || + id.equals("minecraft:leash_knot") || + id.equals("minecraft:painting")) { + Vec3 p = NbtUtils.readEntityPositionFromTag(tag); + + if (p == null) { + p = new Vec3(x, y, z); + NbtUtils.writeEntityPositionToTag(p, tag); + } + + tag.store("block_pos", BlockPos.CODEC, new BlockPos((int) p.x, (int) p.y, (int) p.z)); + } + + ListTag rotation = tag.getListOrEmpty("Rotation"); + origRot[0] = rotation.getFloatOr(0, 0F); + origRot[1] = rotation.getFloatOr(1, 0F); + + Entity entity = EntityUtils.createEntityAndPassengersFromNBT(tag, world); + + if (entity == null) { + continue; + } + rotateEntity(entity, x, y, z, rotationCombined, mirrorMain, mirrorSub); + + // Update the sleeping position to the current position + if (entity instanceof LivingEntity living && living.isSleeping()) { + living.setSleepingPos(BlockPos.containing(x, y, z)); + } + + // Hack fix to fix the painting position offsets. + // The vanilla code will end up moving the position by one in two of the orientations, + // because it sets the hanging position to the given position (floored) + // and then it offsets the position from the hanging position + // by 0.5 or 1.0 blocks depending on the painting size. + if (entity instanceof Painting paintingEntity) { + Direction right = PositionUtils.rotateYCounterclockwise(paintingEntity.getDirection()); + + if ((paintingEntity.getVariant().value().width() % 2) == 0 && + right.getAxisDirection() == Direction.AxisDirection.POSITIVE) { + x -= 1.0 * right.getStepX(); + z -= 1.0 * right.getStepZ(); + } + + if ((paintingEntity.getVariant().value().height() % 2) == 0) { + y -= 1.0; + } + + entity.teleportTo(x, y, z); + } + if (entity instanceof ItemFrame frameEntity) { + if (frameEntity.getYRot() != origRot[0] && (frameEntity.getXRot() == 90.0F || frameEntity.getXRot() == -90.0F)) { + // Fix Yaw only if Pitch is +/- 90.0F (Floor, Ceiling mounted) + frameEntity.setYRot(origRot[0]); + } + } + + EntityUtils.spawnEntityAndPassengersInWorld(entity, world); + + if (entity instanceof Display) { + entity.tick(); // Required to set the full data for rendering + } + } + } + + public static void rotateEntity( + Entity entity, double x, double y, double z, + Rotation rotationCombined, Mirror mirrorMain, Mirror mirrorSub + ) { + float rotationYaw = entity.getYRot(); + + if (mirrorMain != Mirror.NONE) { + rotationYaw = entity.mirror(mirrorMain); + } + if (mirrorSub != Mirror.NONE) { + rotationYaw = entity.mirror(mirrorSub); + } + if (rotationCombined != Rotation.NONE) { + rotationYaw += entity.getYRot() - entity.rotate(rotationCombined); + } + + entity.snapTo(x, y, z, rotationYaw, entity.getXRot()); + EntityUtils.setEntityRotations(entity, rotationYaw, entity.getXRot()); + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/DataLogger.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/DataLogger.java new file mode 100644 index 0000000..363e56c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/DataLogger.java @@ -0,0 +1,170 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.logger; + +import com.google.common.collect.ImmutableList; +import com.mojang.serialization.Codec; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerTickRateManager; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.StringRepresentable; +import net.minecraft.world.entity.MobCategory; +import net.minecraft.world.level.NaturalSpawner; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.servux.logger.data.MobCapData; +import org.leavesmc.leaves.protocol.servux.logger.data.TickData; + +import java.util.ArrayList; +import java.util.function.Function; + +public abstract class DataLogger { + + private final Type type; + + public DataLogger(Type type) { + this.type = type; + } + + public Type getType() { + return this.type; + } + + public abstract T getResult(MinecraftServer server); + + public enum Type implements StringRepresentable { + TPS("tps", Tps::new, Tps.CODEC), + MOB_CAPS("mob_caps", MobCaps::new, MobCaps.CODEC); + + public static final EnumCodec CODEC = StringRepresentable.fromEnum(Type::values); + public static final ImmutableList VALUES = ImmutableList.copyOf(values()); + + private final String name; + private final Function> factory; + private final Codec codec; + + Type(String name, Function> factory, Codec codec) { + this.name = name; + this.factory = factory; + this.codec = codec; + } + + public static @Nullable Type fromStringStatic(String name) { + for (Type type : VALUES) { + if (type.name.equalsIgnoreCase(name)) { + return type; + } + } + + return null; + } + + public Function> getFactory() { + return factory; + } + + public @Nullable DataLogger init() { + return this.factory.apply(this); + } + + public Codec codec() { + return this.codec; + } + + @Override + public @NotNull String getSerializedName() { + return name; + } + } + + public static class Tps extends DataLogger { + + public static final Codec CODEC = CompoundTag.CODEC; + + public Tps(Type type) { + super(type); + } + + @Override + public CompoundTag getResult(MinecraftServer server) { + try { + return (CompoundTag) TickData.CODEC.encodeStart(server.registryAccess().createSerializationContext(NbtOps.INSTANCE), this.build(server)).getOrThrow(); + } catch (Exception e) { + return new CompoundTag(); + } + } + + private TickData build(MinecraftServer server) { + ServerTickRateManager tickManager = server.tickRateManager(); + boolean frozen = tickManager.isFrozen(); + boolean sprinting = tickManager.isSprinting(); + final double mspt = server.tickTimes5s.getAverage(); + double tps = 1000.0D / Math.max(sprinting ? 0.0 : tickManager.millisecondsPerTick(), mspt); + + if (frozen) { + tps = 0.0d; + } + + return new TickData( + mspt, tps, + tickManager.getRemainingSprintTicks(), + frozen, sprinting, + tickManager.isSteppingForward() + ); + } + } + + public static class MobCaps extends DataLogger { + + public static final Codec CODEC = CompoundTag.CODEC; + + public MobCaps(Type type) { + super(type); + } + + @Override + public CompoundTag getResult(MinecraftServer server) { + CompoundTag nbt = new CompoundTag(); + + for (ServerLevel world : server.getAllLevels()) { + NaturalSpawner.SpawnState info = world.getChunkSource().getLastSpawnState(); + if (info == null) { + continue; + } + + int chunks = info.getSpawnableChunkCount(); + Object2IntMap counts = info.getMobCategoryCounts(); + MobCapData mobCapData = new MobCapData(new ArrayList<>(), world.getGameTime()); + for (MobCategory category : MobCategory.values()) { + mobCapData.data().add(new MobCapData.Cap(counts.getOrDefault(category, 0), NaturalSpawner.globalLimitForCategory(world, category, chunks))); + } + + try { + nbt.put(world.dimension().location().toString(), MobCapData.CODEC.encodeStart(world.registryAccess().createSerializationContext(NbtOps.INSTANCE), mobCapData).getPartialOrThrow()); + } catch (Exception ignored) { + } + } + + return nbt; + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/data/MobCapData.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/data/MobCapData.java new file mode 100644 index 0000000..3444991 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/data/MobCapData.java @@ -0,0 +1,50 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.logger.data; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.PrimitiveCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import net.minecraft.world.entity.MobCategory; + +import java.util.List; + +public record MobCapData(List data, long worldTick) { + public static final int CAP_COUNT = MobCategory.values().length; + + private static MobCapData of(int count, List data, long worldTick) { + return new MobCapData(data, worldTick); + } + + public static final Codec CODEC = RecordCodecBuilder.create( + (inst) -> inst.group( + PrimitiveCodec.INT.fieldOf("cap_count").forGetter($ -> CAP_COUNT), + Codec.list(Cap.CODEC).fieldOf("cap_data").forGetter(MobCapData::data), + PrimitiveCodec.LONG.fieldOf("WorldTick").forGetter(MobCapData::worldTick) + ).apply(inst, MobCapData::of) + ); + + public record Cap(int current, int cap) { + public static Codec CODEC = RecordCodecBuilder.create( + (inst) -> inst.group( + PrimitiveCodec.INT.fieldOf("current").forGetter(Cap::current), + PrimitiveCodec.INT.fieldOf("cap").forGetter(Cap::cap) + ).apply(inst, Cap::new) + ); + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/data/TickData.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/data/TickData.java new file mode 100644 index 0000000..61f1ba9 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/servux/logger/data/TickData.java @@ -0,0 +1,35 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.servux.logger.data; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.PrimitiveCodec; +import com.mojang.serialization.codecs.RecordCodecBuilder; + +public record TickData(double mspt, double tps, long sprintTicks, boolean frozen, boolean sprinting, boolean stepping) { + public static Codec CODEC = RecordCodecBuilder.create( + (inst) -> inst.group( + PrimitiveCodec.DOUBLE.fieldOf("mspt").forGetter(TickData::mspt), + PrimitiveCodec.DOUBLE.fieldOf("tps").forGetter(TickData::tps), + PrimitiveCodec.LONG.fieldOf("sprintTicks").forGetter(TickData::sprintTicks), + PrimitiveCodec.BOOL.fieldOf("frozen").forGetter(TickData::frozen), + PrimitiveCodec.BOOL.fieldOf("sprinting").forGetter(TickData::sprinting), + PrimitiveCodec.BOOL.fieldOf("stepping").forGetter(TickData::stepping) + ).apply(inst, TickData::new) + ); +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java b/lophine-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java index eb86eae..be7efba 100644 --- a/lophine-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java +++ b/lophine-server/src/main/java/org/leavesmc/leaves/util/ItemOverstackUtils.java @@ -1,3 +1,20 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + package org.leavesmc.leaves.util; import fun.bm.lophine.config.modules.misc.ContainerExpansionConfig; diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/util/TagFactory.java b/lophine-server/src/main/java/org/leavesmc/leaves/util/TagFactory.java new file mode 100644 index 0000000..e33fc43 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/util/TagFactory.java @@ -0,0 +1,46 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.util; + +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ProblemReporter; +import net.minecraft.world.level.storage.TagValueInput; +import net.minecraft.world.level.storage.TagValueOutput; + +public class TagFactory { + + private static final RegistryAccess.Frozen registryAccess = MinecraftServer.getServer().registryAccess(); + + public static TagValueOutput output() { + return TagValueOutput.createWithContext(ProblemReporter.DISCARDING, registryAccess); + } + + public static TagValueOutput output(CompoundTag tag) { + return TagValueOutput.createWrappingWithContext(ProblemReporter.DISCARDING, registryAccess, tag); + } + + public static TagValueInput input() { + return input(new CompoundTag()); + } + + public static TagValueInput input(CompoundTag tag) { + return (TagValueInput) TagValueInput.create(ProblemReporter.DISCARDING, registryAccess, tag); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/util/TagUtil.java b/lophine-server/src/main/java/org/leavesmc/leaves/util/TagUtil.java new file mode 100644 index 0000000..a13232c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/util/TagUtil.java @@ -0,0 +1,88 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.util; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.storage.TagValueInput; +import net.minecraft.world.level.storage.TagValueOutput; +import org.jetbrains.annotations.Nullable; + +public class TagUtil { + + public static CompoundTag saveEntity(@Nullable Entity entity) { + if (entity == null) { + return new CompoundTag(); + } + TagValueOutput output = TagFactory.output(); + entity.save(output); + return output.buildResult(); + } + + public static boolean saveEntity(Entity entity, CompoundTag tag) { + if (entity == null) { + return false; + } + TagValueOutput output = TagFactory.output(tag); + return entity.save(output); + } + + public static CompoundTag saveEntityWithoutId(Entity entity) { + if (entity == null) { + return new CompoundTag(); + } + TagValueOutput output = TagFactory.output(); + entity.saveWithoutId(output); + return output.buildResult(); + } + + public static CompoundTag saveTileWithId(@Nullable BlockEntity entity) { + if (entity == null) { + return new CompoundTag(); + } + TagValueOutput output = TagFactory.output(); + entity.saveWithId(output); + return output.buildResult(); + } + + public static boolean saveEntityAsPassenger(@Nullable Entity entity, CompoundTag tag) { + if (entity == null) { + return false; + } + TagValueOutput output = TagFactory.output(tag); + return entity.saveAsPassenger(output); + } + + public static void loadEntity(Entity entity, CompoundTag tag) { + if (entity == null) { + return; + } + TagValueInput input = TagFactory.input(tag); + entity.load(input); + } + + public static void loadTileWithComponents(BlockEntity entity, CompoundTag tag) { + if (entity == null) { + return; + } + TagValueInput input = TagFactory.input(tag); + entity.loadWithComponents(input); + } + +} diff --git a/scripts/SetENV.sh b/scripts/SetENV.sh index 1574c66..b44ae6b 100644 --- a/scripts/SetENV.sh +++ b/scripts/SetENV.sh @@ -13,14 +13,15 @@ release_tag="$mcversion-$commitid" jarName="$project_id-$mcversion-paperclip.jar" jarName_dir="lophine-server/build/libs/$jarName" +flag_release=false pre=false -make_latest=false if [ "$release" = "1" ]; then pre=true - make_latest=false + flag_release=true + make_latest=true elif [ "$release" = "2" ]; then - pre=false + flag_release=true make_latest=true fi @@ -35,4 +36,5 @@ echo "pre=$pre" >> $GITHUB_ENV echo "tag=$release_tag" >> $GITHUB_ENV echo "jar=$jarName" >> $GITHUB_ENV echo "jar_dir=$jarName_dir" >> $GITHUB_ENV +echo "flag_release=$flag_release" >> $GITHUB_ENV echo "make_latest=$make_latest" >> $GITHUB_ENV