From 4ca94280ae1a12542757a16f3dd81347f8f03145 Mon Sep 17 00:00:00 2001 From: xiaoxijun Date: Mon, 20 Oct 2025 00:06:40 +0800 Subject: [PATCH] Add support for more mod protocols (#73) * Add Syncmatica protocols * Add AppleSkin protocols * Add BBOR Protocol * Add Jade Protocol * Add Xaero Map Protocol * Add Support REI protocol * Fixed an issue where the configuration file was not loading correctly. * move config file directory * add license & refactor code * update leaves fix https://github.com/LeavesMC/Leaves/issues/747#event-19986621829 * Update Luminol * fix up jade protocol * fix up rei --------- Co-authored-by: Helvetica Volubi Co-authored-by: Helvetica Volubi <88063803+Suisuroru@users.noreply.github.com> --- ...yer.patch => 0023-Leaves-Fakeplayer.patch} | 2 +- .../0025-Leaves-Syncmatica-Protocol.patch | 27 ++ .../features/0026-Leaves-BBOR-Protocol.patch | 25 + .../features/0027-Leaves-Jade-Protocol.patch | 114 +++++ .../0028-Leaves-Xaero-Map-Protocol.patch | 21 + .../0029-Leaves-Support-REI-protocol.patch | 43 ++ .../0004-Leaves-Leaves-Fakeplayer.patch | 2 +- .../protocol/AppleSkinProtocolConfig.java | 16 + .../function/protocol/BBORProtocolConfig.java | 13 + .../function/protocol/JadeProtocolConfig.java | 13 + .../protocol/REIServerProtocolConfig.java | 13 + .../{ => protocol}/ServuxProtocolConfig.java | 14 +- .../protocol/SyncmaticaProtocolConfig.java | 25 + .../protocol/XaeroMapProtocolConfig.java | 17 + .../leaves/protocol/AppleSkinProtocol.java | 141 ++++++ .../leaves/protocol/BBORProtocol.java | 244 ++++++++++ .../leaves/protocol/XaeroMapProtocol.java | 64 +++ .../leaves/protocol/jade/JadeProtocol.java | 266 +++++++++++ .../protocol/jade/accessor/Accessor.java | 39 ++ .../protocol/jade/accessor/AccessorImpl.java | 77 ++++ .../protocol/jade/accessor/BlockAccessor.java | 61 +++ .../jade/accessor/BlockAccessorImpl.java | 160 +++++++ .../jade/accessor/EntityAccessor.java | 59 +++ .../jade/accessor/EntityAccessorImpl.java | 129 ++++++ .../jade/payload/ClientHandshakePayload.java | 36 ++ .../jade/payload/ReceiveDataPayload.java | 37 ++ .../jade/payload/RequestBlockPayload.java | 53 +++ .../jade/payload/RequestEntityPayload.java | 54 +++ .../jade/payload/ServerHandshakePayload.java | 59 +++ .../protocol/jade/provider/IJadeProvider.java | 29 ++ .../jade/provider/IServerDataProvider.java | 25 + .../provider/IServerExtensionProvider.java | 27 ++ .../ItemStorageExtensionProvider.java | 165 +++++++ .../jade/provider/ItemStorageProvider.java | 104 +++++ .../provider/StreamServerDataProvider.java | 40 ++ .../jade/provider/block/BeehiveProvider.java | 51 +++ .../provider/block/BlockNameProvider.java | 77 ++++ .../provider/block/BrewingStandProvider.java | 60 +++ .../jade/provider/block/CampfireProvider.java | 72 +++ .../block/ChiseledBookshelfProvider.java | 61 +++ .../provider/block/CommandBlockProvider.java | 57 +++ .../jade/provider/block/FurnaceProvider.java | 68 +++ .../provider/block/HopperLockProvider.java | 54 +++ .../jade/provider/block/JukeboxProvider.java | 49 ++ .../jade/provider/block/LecternProvider.java | 50 ++ .../block/MobSpawnerCooldownProvider.java | 59 +++ .../jade/provider/block/RedstoneProvider.java | 53 +++ .../provider/entity/AnimalOwnerProvider.java | 65 +++ .../provider/entity/MobBreedingProvider.java | 61 +++ .../provider/entity/MobGrowthProvider.java | 60 +++ .../entity/NextEntityDropProvider.java | 59 +++ .../provider/entity/PetArmorProvider.java | 52 +++ .../entity/StatusEffectsProvider.java | 62 +++ .../entity/ZombieVillagerProvider.java | 51 +++ .../protocol/jade/tool/ShearsToolHandler.java | 54 +++ .../leaves/protocol/jade/util/CommonUtil.java | 92 ++++ .../protocol/jade/util/HierarchyLookup.java | 147 ++++++ .../protocol/jade/util/IHierarchyLookup.java | 84 ++++ .../protocol/jade/util/ItemCollector.java | 138 ++++++ .../protocol/jade/util/ItemIterator.java | 119 +++++ .../leaves/protocol/jade/util/JadeCodec.java | 77 ++++ .../jade/util/LootTableMineableCollector.java | 126 +++++ .../jade/util/PairHierarchyLookup.java | 134 ++++++ .../protocol/jade/util/PriorityStore.java | 57 +++ .../leaves/protocol/jade/util/ViewGroup.java | 75 +++ .../jade/util/WrappedHierarchyLookup.java | 124 +++++ .../protocol/rei/PacketTransformer.java | 163 +++++++ .../protocol/rei/REIServerProtocol.java | 432 ++++++++++++++++++ .../protocol/rei/display/BlastingDisplay.java | 35 ++ .../protocol/rei/display/CampfireDisplay.java | 35 ++ .../protocol/rei/display/CookingDisplay.java | 84 ++++ .../protocol/rei/display/CraftingDisplay.java | 38 ++ .../protocol/rei/display/CustomDisplay.java | 88 ++++ .../leaves/protocol/rei/display/Display.java | 297 ++++++++++++ .../protocol/rei/display/ShapedDisplay.java | 112 +++++ .../rei/display/ShapelessDisplay.java | 94 ++++ .../protocol/rei/display/SmeltingDisplay.java | 35 ++ .../protocol/rei/display/SmithingDisplay.java | 141 ++++++ .../protocol/rei/display/SmokingDisplay.java | 35 ++ .../rei/display/StoneCuttingDisplay.java | 77 ++++ .../rei/ingredient/EntryIngredient.java | 116 +++++ .../rei/payload/DisplaySyncPayload.java | 93 ++++ .../rei/transfer/InputSlotCrafter.java | 176 +++++++ .../rei/transfer/ItemRecipeFinder.java | 126 +++++ .../rei/transfer/NewInputSlotCrafter.java | 84 ++++ .../protocol/rei/transfer/RecipeFinder.java | 409 +++++++++++++++++ .../slot/PlayerInventorySlotAccessor.java | 56 +++ .../rei/transfer/slot/SlotAccessor.java | 43 ++ .../transfer/slot/VanillaSlotAccessor.java | 65 +++ .../servux/ServuxEntityDataProtocol.java | 2 +- .../servux/ServuxHudDataProtocol.java | 2 +- .../servux/ServuxStructuresProtocol.java | 2 +- .../litematics/ServuxLitematicsProtocol.java | 2 +- .../placement/SchematicPlacement.java | 2 +- .../syncmatica/CommunicationManager.java | 396 ++++++++++++++++ .../leaves/protocol/syncmatica/Feature.java | 40 ++ .../protocol/syncmatica/FeatureSet.java | 81 ++++ .../protocol/syncmatica/FileStorage.java | 97 ++++ .../syncmatica/LocalLitematicState.java | 41 ++ .../protocol/syncmatica/MessageType.java | 25 + .../protocol/syncmatica/PacketType.java | 47 ++ .../protocol/syncmatica/PlayerIdentifier.java | 54 +++ .../syncmatica/PlayerIdentifierProvider.java | 63 +++ .../protocol/syncmatica/ServerPlacement.java | 191 ++++++++ .../protocol/syncmatica/ServerPosition.java | 68 +++ .../protocol/syncmatica/SubRegionData.java | 107 +++++ .../SubRegionPlacementModification.java | 82 ++++ .../protocol/syncmatica/SyncmaticManager.java | 121 +++++ .../syncmatica/SyncmaticaPayload.java | 35 ++ .../syncmatica/SyncmaticaProtocol.java | 151 ++++++ .../syncmatica/exchange/AbstractExchange.java | 83 ++++ .../syncmatica/exchange/DownloadExchange.java | 141 ++++++ .../syncmatica/exchange/Exchange.java | 37 ++ .../syncmatica/exchange/ExchangeTarget.java | 56 +++ .../syncmatica/exchange/FeatureExchange.java | 65 +++ .../exchange/ModifyExchangeServer.java | 95 ++++ .../syncmatica/exchange/UploadExchange.java | 114 +++++ .../exchange/VersionHandshakeServer.java | 79 ++++ 118 files changed, 9666 insertions(+), 9 deletions(-) rename lophine-server/minecraft-patches/features/{0023-Leaves-Leaves-Fakeplayer.patch => 0023-Leaves-Fakeplayer.patch} (99%) create mode 100644 lophine-server/minecraft-patches/features/0025-Leaves-Syncmatica-Protocol.patch create mode 100644 lophine-server/minecraft-patches/features/0026-Leaves-BBOR-Protocol.patch create mode 100644 lophine-server/minecraft-patches/features/0027-Leaves-Jade-Protocol.patch create mode 100644 lophine-server/minecraft-patches/features/0028-Leaves-Xaero-Map-Protocol.patch create mode 100644 lophine-server/minecraft-patches/features/0029-Leaves-Support-REI-protocol.patch create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/AppleSkinProtocolConfig.java create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/BBORProtocolConfig.java create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/JadeProtocolConfig.java create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/REIServerProtocolConfig.java rename lophine-server/src/main/java/fun/bm/lophine/config/modules/function/{ => protocol}/ServuxProtocolConfig.java (76%) create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/SyncmaticaProtocolConfig.java create mode 100644 lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/XaeroMapProtocolConfig.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockNameProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/BlastingDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CampfireDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CookingDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CraftingDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CustomDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/Display.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapedDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapelessDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmeltingDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmithingDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmokingDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/StoneCuttingDisplay.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/ingredient/EntryIngredient.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/DisplaySyncPayload.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/InputSlotCrafter.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/ItemRecipeFinder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/NewInputSlotCrafter.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/RecipeFinder.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/PlayerInventorySlotAccessor.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/SlotAccessor.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/VanillaSlotAccessor.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java create mode 100644 lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java diff --git a/lophine-server/minecraft-patches/features/0023-Leaves-Leaves-Fakeplayer.patch b/lophine-server/minecraft-patches/features/0023-Leaves-Fakeplayer.patch similarity index 99% rename from lophine-server/minecraft-patches/features/0023-Leaves-Leaves-Fakeplayer.patch rename to lophine-server/minecraft-patches/features/0023-Leaves-Fakeplayer.patch index 8a57f5f..fa58812 100644 --- a/lophine-server/minecraft-patches/features/0023-Leaves-Leaves-Fakeplayer.patch +++ b/lophine-server/minecraft-patches/features/0023-Leaves-Fakeplayer.patch @@ -1,7 +1,7 @@ From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: Bacteriawa Date: Sat, 30 Aug 2025 17:16:32 +0800 -Subject: [PATCH] Leaves: Leaves Fakeplayer +Subject: [PATCH] Leaves: Fakeplayer Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> As a part of : Leaves (https://github.com/LeavesMC/Leaves) diff --git a/lophine-server/minecraft-patches/features/0025-Leaves-Syncmatica-Protocol.patch b/lophine-server/minecraft-patches/features/0025-Leaves-Syncmatica-Protocol.patch new file mode 100644 index 0000000..40ce463 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0025-Leaves-Syncmatica-Protocol.patch @@ -0,0 +1,27 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 16:51:01 +0800 +Subject: [PATCH] Leaves: Syncmatica Protocol + +This patch is Powered by Syncmatica(https://github.com/End-Tech/syncmatica) +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index e17f9629369f8653b484b4cc71de4874b5faa4d6..9be4bc9e78c22c12ba594b16dac6901023f0fd47 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -330,9 +330,12 @@ public class ServerGamePacketListenerImpl + this.signedMessageDecoder = SignedMessageChain.Decoder.unsigned(player.getUUID(), server::enforceSecureProfile); + this.chatMessageChain = new FutureChain(server.chatExecutor); // CraftBukkit - async chat + this.tickEndEvent = new io.papermc.paper.event.packet.ClientTickEndEvent(player.getBukkitEntity()); // Paper - add client tick end event ++ this.exchangeTarget = new org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget(this); // Leaves - Syncmatica Protocol + this.playerGameConnection = new io.papermc.paper.connection.PaperPlayerGameConnection(this); // Paper + } + ++ public final org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget exchangeTarget; // Leaves - Syncmatica Protocol ++ + // Paper start - configuration phase API + @Override + public io.papermc.paper.connection.PlayerCommonConnection getApiConnection() { diff --git a/lophine-server/minecraft-patches/features/0026-Leaves-BBOR-Protocol.patch b/lophine-server/minecraft-patches/features/0026-Leaves-BBOR-Protocol.patch new file mode 100644 index 0000000..01bd0ce --- /dev/null +++ b/lophine-server/minecraft-patches/features/0026-Leaves-BBOR-Protocol.patch @@ -0,0 +1,25 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 13:03:42 +0800 +Subject: [PATCH] Leaves: BBOR Protocol + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/world/level/chunk/LevelChunk.java b/net/minecraft/world/level/chunk/LevelChunk.java +index 29788fedd54b8411c9c8872b4d83b037874fb5f0..24af6821694768058dc0146220877ab70066b5f9 100644 +--- a/net/minecraft/world/level/chunk/LevelChunk.java ++++ b/net/minecraft/world/level/chunk/LevelChunk.java +@@ -754,6 +754,11 @@ public class LevelChunk extends ChunkAccess implements ca.spottedleaf.moonrise.p + + public void setLoaded(boolean loaded) { + this.loaded = loaded; ++ // Leaves start - bbor ++ if (loaded) { ++ org.leavesmc.leaves.protocol.BBORProtocol.onChunkLoaded(this); ++ } ++ // Leaves end - bbor + } + + public Level getLevel() { diff --git a/lophine-server/minecraft-patches/features/0027-Leaves-Jade-Protocol.patch b/lophine-server/minecraft-patches/features/0027-Leaves-Jade-Protocol.patch new file mode 100644 index 0000000..c8dead4 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0027-Leaves-Jade-Protocol.patch @@ -0,0 +1,114 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Mon, 3 Feb 2025 13:33:19 +0800 +Subject: [PATCH] Leaves: Jade Protocol + +This patch is Powered by Jade(https://github.com/Snownee/Jade) +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/world/entity/animal/armadillo/Armadillo.java b/net/minecraft/world/entity/animal/armadillo/Armadillo.java +index c1798db2972c8f2a343cf6e16fd9354ff212d906..a8d617b16ab2b2c0cdb289a0aa05fa171940cd7e 100644 +--- a/net/minecraft/world/entity/animal/armadillo/Armadillo.java ++++ b/net/minecraft/world/entity/animal/armadillo/Armadillo.java +@@ -63,7 +63,7 @@ public class Armadillo extends Animal { + public final AnimationState rollOutAnimationState = new AnimationState(); + public final AnimationState rollUpAnimationState = new AnimationState(); + public final AnimationState peekAnimationState = new AnimationState(); +- private int scuteTime; ++ public int scuteTime; // Leaves - private -> public + private boolean peekReceivedClient = false; + + public Armadillo(EntityType entityType, Level level) { +diff --git a/net/minecraft/world/entity/animal/frog/Tadpole.java b/net/minecraft/world/entity/animal/frog/Tadpole.java +index 17f58246849ed407821a987b200cc765eb7943f9..ac27df3ba0ce9bbdf2f32ea87171fbb9407008d6 100644 +--- a/net/minecraft/world/entity/animal/frog/Tadpole.java ++++ b/net/minecraft/world/entity/animal/frog/Tadpole.java +@@ -254,7 +254,7 @@ public class Tadpole extends AbstractFish { + } + } + +- private int getTicksLeftUntilAdult() { ++ public int getTicksLeftUntilAdult() { // Leaves - private -> public + return Math.max(0, ticksToBeFrog - this.age); + } + +diff --git a/net/minecraft/world/level/storage/loot/LootPool.java b/net/minecraft/world/level/storage/loot/LootPool.java +index 6901e629d941e22e64d83eed4e8cfee3165a96a1..fdc26c8d8c82c20534c57af2a0281b99998cc9f6 100644 +--- a/net/minecraft/world/level/storage/loot/LootPool.java ++++ b/net/minecraft/world/level/storage/loot/LootPool.java +@@ -37,7 +37,7 @@ public class LootPool { + ) + .apply(instance, LootPool::new) + ); +- private final List entries; ++ public final List entries; // Leaves - private -> public + private final List conditions; + private final Predicate compositeCondition; + private final List functions; +diff --git a/net/minecraft/world/level/storage/loot/LootTable.java b/net/minecraft/world/level/storage/loot/LootTable.java +index 8612cdf7161f8ddff60a6478cc901318b8f958ba..07a962d647baa99b0e1bf3898a07cc914e91397e 100644 +--- a/net/minecraft/world/level/storage/loot/LootTable.java ++++ b/net/minecraft/world/level/storage/loot/LootTable.java +@@ -50,7 +50,7 @@ public class LootTable { + public static final LootTable EMPTY = new LootTable(LootContextParamSets.EMPTY, Optional.empty(), List.of(), List.of()); + private final ContextKeySet paramSet; + private final Optional randomSequence; +- private final List pools; ++ public final List pools; // Leaves - private -> public + private final List functions; + private final BiFunction compositeFunction; + public org.bukkit.craftbukkit.CraftLootTable craftLootTable; // CraftBukkit +diff --git a/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java b/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java +index eeaa49e9f70a18b5d39493aeff73f31b05ac2faa..8cd0403d7873c4c37caef75935b06b056c3d951d 100644 +--- a/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java ++++ b/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java +@@ -16,7 +16,7 @@ public abstract class CompositeEntryBase extends LootPoolEntryContainer { + return "Empty children list"; + } + }; +- protected final List children; ++ public final List children; // Leaves - private -> public + private final ComposableEntryContainer composedChildren; + + protected CompositeEntryBase(List children, List conditions) { +diff --git a/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java b/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java +index 65e27bce9e59ef97bc8b914d646fba924d0f0877..a49bdcdf37b351436e0ba6d7865f10827c4e6ab4 100644 +--- a/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java ++++ b/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java +@@ -14,7 +14,7 @@ import net.minecraft.world.level.storage.loot.predicates.ConditionUserBuilder; + import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; + + public abstract class LootPoolEntryContainer implements ComposableEntryContainer { +- protected final List conditions; ++ public final List conditions; // Leaves - private -> public + private final Predicate compositeCondition; + + protected LootPoolEntryContainer(List conditions) { +diff --git a/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java b/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java +index 141026601cd9a4561426b85fd1f8e7dc0544fbd7..a5d7ebb93c147bf0f806ac3c9b2dc4b878573944 100644 +--- a/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java ++++ b/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java +@@ -29,7 +29,7 @@ public class NestedLootTable extends LootPoolSingletonContainer { + return "->{inline}"; + } + }; +- private final Either, LootTable> contents; ++ public final Either, LootTable> contents; // Leaves - private -> public + + private NestedLootTable( + Either, LootTable> contents, int weight, int quality, List conditions, List functions +diff --git a/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java b/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java +index bae72197acc929c7ed3e964f156115d728eb2176..8f3094f42f3366a1313d70c0b27fbe5632b2082a 100644 +--- a/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java ++++ b/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java +@@ -12,7 +12,7 @@ import net.minecraft.world.level.storage.loot.LootContext; + import net.minecraft.world.level.storage.loot.ValidationContext; + + public abstract class CompositeLootItemCondition implements LootItemCondition { +- protected final List terms; ++ public final List terms; // Leaves - private -> public + private final Predicate composedPredicate; + + protected CompositeLootItemCondition(List terms, Predicate composedPredicate) { diff --git a/lophine-server/minecraft-patches/features/0028-Leaves-Xaero-Map-Protocol.patch b/lophine-server/minecraft-patches/features/0028-Leaves-Xaero-Map-Protocol.patch new file mode 100644 index 0000000..143cd17 --- /dev/null +++ b/lophine-server/minecraft-patches/features/0028-Leaves-Xaero-Map-Protocol.patch @@ -0,0 +1,21 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Fri, 7 Feb 2025 14:23:43 +0800 +Subject: [PATCH] Leaves: Xaero Map Protocol + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index 908ec29a6e285122d1a8a43dc547b74af2697442..1bf1731e43278b71423fee5d92401c48d6dabaca 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -1247,6 +1247,7 @@ public abstract class PlayerList { + player.connection.send(new ClientboundInitializeBorderPacket(worldBorder)); + player.connection.send(new ClientboundSetTimePacket(level.getGameTime(), level.getDayTime(), level.getGameRules().getBoolean(GameRules.RULE_DAYLIGHT))); + player.connection.send(new ClientboundSetDefaultSpawnPositionPacket(level.getSharedSpawnPos(), level.getSharedSpawnAngle())); ++ org.leavesmc.leaves.protocol.XaeroMapProtocol.onSendWorldInfo(player); // Leaves - xaero map protocol + if (level.isRaining()) { + // CraftBukkit start - handle player weather + // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F)); diff --git a/lophine-server/minecraft-patches/features/0029-Leaves-Support-REI-protocol.patch b/lophine-server/minecraft-patches/features/0029-Leaves-Support-REI-protocol.patch new file mode 100644 index 0000000..4b067ae --- /dev/null +++ b/lophine-server/minecraft-patches/features/0029-Leaves-Support-REI-protocol.patch @@ -0,0 +1,43 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: violetc <58360096+s-yh-china@users.noreply.github.com> +Date: Thu, 27 Mar 2025 13:04:35 +0800 +Subject: [PATCH] Leaves: Support REI protocol + +Co-authored by: violetc <58360096+s-yh-china@users.noreply.github.com> +As a part of : Leaves (https://github.com/LeavesMC/Leaves) +Licensed under: GPL-3.0 (https://www.gnu.org/licenses/gpl-3.0.html) + +diff --git a/net/minecraft/world/item/crafting/SmithingTransformRecipe.java b/net/minecraft/world/item/crafting/SmithingTransformRecipe.java +index 9bc0a9c3577d63a0ad5489bfd4c07d5006245c5f..bb2e891539b7eb49dc7925630695149aa531f672 100644 +--- a/net/minecraft/world/item/crafting/SmithingTransformRecipe.java ++++ b/net/minecraft/world/item/crafting/SmithingTransformRecipe.java +@@ -83,6 +83,12 @@ public class SmithingTransformRecipe implements SmithingRecipe { + ); + } + ++ // Leaves start - REI ++ public SlotDisplay getResult() { ++ return this.result.display(); ++ } ++ // Leaves end - REI ++ + // CraftBukkit start + @Override + public org.bukkit.inventory.Recipe toBukkitRecipe(org.bukkit.NamespacedKey id) { +diff --git a/net/minecraft/world/item/crafting/SmithingTrimRecipe.java b/net/minecraft/world/item/crafting/SmithingTrimRecipe.java +index c324896afc2ee28ebb4d426ce4a469ee847ce24d..d5b26ff7be916e07e1162536cb2ddf5674c82f46 100644 +--- a/net/minecraft/world/item/crafting/SmithingTrimRecipe.java ++++ b/net/minecraft/world/item/crafting/SmithingTrimRecipe.java +@@ -85,6 +85,12 @@ public class SmithingTrimRecipe implements SmithingRecipe { + return Optional.of(this.addition); + } + ++ // Leaves start ++ public Holder pattern() { ++ return pattern; ++ } ++ // Leaves end ++ + @Override + public RecipeSerializer getSerializer() { + return RecipeSerializer.SMITHING_TRIM; diff --git a/lophine-server/paper-patches/features/0004-Leaves-Leaves-Fakeplayer.patch b/lophine-server/paper-patches/features/0004-Leaves-Leaves-Fakeplayer.patch index 2fa0678..f830bf0 100644 --- a/lophine-server/paper-patches/features/0004-Leaves-Leaves-Fakeplayer.patch +++ b/lophine-server/paper-patches/features/0004-Leaves-Leaves-Fakeplayer.patch @@ -161,7 +161,7 @@ index 852e1ffef6a022caad7c8eff34091e50112a2290..8eb5d014d9ed688ffebaffb4ce0bb408 if (entity instanceof EnderDragonPart complexPart) { if (complexPart.parentMob instanceof EnderDragon) { diff --git a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java -index e040f9a7c9451fbe547dda56952410047a99ddc0..05753de195bd7f4ac2952183662a8cc9ec8f4ba0 100644 +index 317837a2a9b3b511b5cac4df4db248d4652816dc..a8253d1a3129c8258e9ead7c430e1bcf82f735d7 100644 --- a/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java +++ b/src/main/java/org/bukkit/craftbukkit/event/CraftEventFactory.java @@ -897,7 +897,11 @@ public class CraftEventFactory { diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/AppleSkinProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/AppleSkinProtocolConfig.java new file mode 100644 index 0000000..3ae5b15 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/AppleSkinProtocolConfig.java @@ -0,0 +1,16 @@ +package fun.bm.lophine.config.modules.function.protocol; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "appleskin", directory = {"protocol"}) +public class AppleSkinProtocolConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Enable AppleSkin protocol support""") + public static boolean enabled = false; + @ConfigInfo(name = "sync-tick-interval", comments = """ + Set AppleSkin Synchronization Frequency (Unit: Game Ticks)""") + public static int syncTickInterval = 20; +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/BBORProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/BBORProtocolConfig.java new file mode 100644 index 0000000..7cf434e --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/BBORProtocolConfig.java @@ -0,0 +1,13 @@ +package fun.bm.lophine.config.modules.function.protocol; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "bbor", directory = {"protocol"}) +public class BBORProtocolConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Enable BBOR protocol support""") + public static boolean enabled = false; +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/JadeProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/JadeProtocolConfig.java new file mode 100644 index 0000000..9ad915e --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/JadeProtocolConfig.java @@ -0,0 +1,13 @@ +package fun.bm.lophine.config.modules.function.protocol; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "jade", directory = {"protocol"}) +public class JadeProtocolConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Enable Jade protocol support""") + public static boolean enabled = false; +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/REIServerProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/REIServerProtocolConfig.java new file mode 100644 index 0000000..f2345b4 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/REIServerProtocolConfig.java @@ -0,0 +1,13 @@ +package fun.bm.lophine.config.modules.function.protocol; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "rei", directory = {"protocol"}) +public class REIServerProtocolConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Enable Roughly Enough Items protocol support""") + public static boolean enabled = false; +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/ServuxProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/ServuxProtocolConfig.java similarity index 76% rename from lophine-server/src/main/java/fun/bm/lophine/config/modules/function/ServuxProtocolConfig.java rename to lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/ServuxProtocolConfig.java index e412175..481d40c 100644 --- a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/ServuxProtocolConfig.java +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/ServuxProtocolConfig.java @@ -1,4 +1,4 @@ -package fun.bm.lophine.config.modules.function; +package fun.bm.lophine.config.modules.function.protocol; import me.earthme.luminol.config.IConfigModule; import me.earthme.luminol.config.flags.CommandSuggestions; @@ -9,54 +9,64 @@ import me.earthme.luminol.enums.EnumConfigCategory; import java.util.List; -@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "servux-protocol") +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "servux", directory = {"protocol"}) public class ServuxProtocolConfig implements IConfigModule { + @TransformedConfig(name = "entity-protocol", directory = {"function", "servux-protocol", "data"}) @TransformedConfig(name = "entity-protocol", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "entity-protocol", directory = {"misc", "survux-protocol"}) @ConfigInfo(name = "entity-protocol", directory = {"data"}) public static boolean entityProtocol = false; + @TransformedConfig(name = "hud-logger-protocol", directory = {"function", "servux-protocol"}) @TransformedConfig(name = "hud-logger-protocol", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "hud-logger-protocol", directory = {"misc", "survux-protocol"}) @ConfigInfo(name = "hud-logger-protocol") public static boolean hudLoggerProtocol = false; + @TransformedConfig(name = "hud-metadata-protocol", directory = {"function", "servux-protocol"}) @TransformedConfig(name = "hud-metadata-protocol", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "hud-metadata-protocol", directory = {"misc", "survux-protocol"}) @ConfigInfo(name = "hud-metadata-protocol") public static boolean hudMetadataProtocol = false; + @TransformedConfig(name = "hud-metadata-share-seed", directory = {"function", "servux-protocol"}) @TransformedConfig(name = "hud-metadata-share-seed", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "hud-metadata-share-seed", directory = {"misc", "survux-protocol"}) @ConfigInfo(name = "hud-metadata-share-seed") public static boolean hudMetadataShareSeed = false; + @TransformedConfig(name = "structure-protocol", directory = {"function", "servux-protocol"}) @TransformedConfig(name = "structure-protocol", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "structure-protocol", directory = {"misc", "survux-protocol"}) @ConfigInfo(name = "structure-protocol") public static boolean structureProtocol = false; + @TransformedConfig(name = "hud-enabled-loggers", directory = {"function", "servux-protocol"}) @TransformedConfig(name = "hud-enabled-loggers", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "hud-enabled-loggers", directory = {"misc", "survux-protocol"}) @ConfigInfo(name = "hud-enabled-loggers") public static List hudEnabledLoggers = List.of("tps", "mob_caps"); + @TransformedConfig(name = "hud-update-interval", directory = {"function", "servux-protocol"}) @TransformedConfig(name = "hud-update-interval", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "hud-update-interval", directory = {"misc", "survux-protocol"}) @ConfigInfo(name = "hud-update-interval") public static int hudUpdateInterval = 1; + @TransformedConfig(name = "litematics-enabled", directory = {"function", "servux-protocol", "litematics"}) @TransformedConfig(name = "litematics-enabled", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "litematics-enabled", directory = {"misc", "survux-protocol"}) @ConfigInfo(name = "litematics-enabled", directory = {"litematics"}) public static boolean litematicsEnabled = false; + @TransformedConfig(name = "litematics-max-nbt-size", directory = {"function", "servux-protocol", "litematics"}) @TransformedConfig(name = "litematics-max-nbt-size", directory = {"function", "survux-protocol"}) @TransformedConfig(name = "litematics-max-nbt-size", directory = {"misc", "survux-protocol"}) @CommandSuggestions(suggest = {"-1", "2097152"}) @ConfigInfo(name = "litematics-max-nbt-size", directory = {"litematics"}) public static int litematicsMaxNbtSize = 2097152; + @TransformedConfig(name = "litematics-print-max-delay-ticks", directory = {"function", "servux-protocol", "litematics"}) @TransformedConfig(name = "litematics-print-max-delay-ticks", directory = {"function", "survux-protocol"}) @CommandSuggestions(suggest = {"-1", "1200"}) @ConfigInfo(name = "litematics-print-max-delay-ticks", directory = {"litematics"}, comments = "The max delay ticks for printing litematics, -1 to disable") diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/SyncmaticaProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/SyncmaticaProtocolConfig.java new file mode 100644 index 0000000..4d13df6 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/SyncmaticaProtocolConfig.java @@ -0,0 +1,25 @@ +package fun.bm.lophine.config.modules.function.protocol; + +import com.electronwill.nightconfig.core.file.CommentedFileConfig; +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "syncmatica", directory = {"protocol"}) +public class SyncmaticaProtocolConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Enable Syncmatica protocol support""") + public static boolean enabled = false; + @ConfigInfo(name = "useQuota", comments = """ + Is there a limit on the size of projection files?""") + public static boolean useQuota = false; + @ConfigInfo(name = "quota-Limit", comments = """ + Maximum Projection File Size (in bytes)""") + public static int quotaLimit = 40000000; + + public void onLoaded(CommentedFileConfig configInstance) { + SyncmaticaProtocol.init(enabled); + } +} diff --git a/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/XaeroMapProtocolConfig.java b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/XaeroMapProtocolConfig.java new file mode 100644 index 0000000..04e42d5 --- /dev/null +++ b/lophine-server/src/main/java/fun/bm/lophine/config/modules/function/protocol/XaeroMapProtocolConfig.java @@ -0,0 +1,17 @@ +package fun.bm.lophine.config.modules.function.protocol; + +import me.earthme.luminol.config.IConfigModule; +import me.earthme.luminol.config.flags.ConfigClassInfo; +import me.earthme.luminol.config.flags.ConfigInfo; +import me.earthme.luminol.enums.EnumConfigCategory; + +import java.util.Random; + +@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "xaero-map", directory = {"protocol"}) +public class XaeroMapProtocolConfig implements IConfigModule { + @ConfigInfo(name = "enabled", comments = """ + Enable Xaero World Map Protocol Support""") + public static boolean enabled = false; + @ConfigInfo(name = "xaeroMapServerID") + public static int xaeroMapServerID = new Random().nextInt(); +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java new file mode 100644 index 0000000..bc8d44c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java @@ -0,0 +1,141 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol; + +import fun.bm.lophine.config.modules.function.protocol.AppleSkinProtocolConfig; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.food.FoodData; +import net.minecraft.world.level.GameRules; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.Context; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.*; + +@LeavesProtocol.Register(namespace = "appleskin") +public class AppleSkinProtocol implements LeavesProtocol { + + public static final String PROTOCOL_ID = "appleskin"; + + private static final ResourceLocation SATURATION_KEY = id("saturation"); + private static final ResourceLocation EXHAUSTION_KEY = id("exhaustion"); + private static final ResourceLocation NATURAL_REGENERATION_KEY = id("natural_regeneration"); + + private static final float MINIMUM_EXHAUSTION_CHANGE_THRESHOLD = 0.01F; + + private static final Map previousSaturationLevels = new HashMap<>(); + private static final Map previousExhaustionLevels = new HashMap<>(); + private static final Map previousNaturalRegeneration = new HashMap<>(); + + private static final Map> subscribedChannels = new HashMap<>(); + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerLoggedIn(@NotNull ServerPlayer player) { + resetPlayerData(player); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + subscribedChannels.remove(player.getUUID()); + resetPlayerData(player); + } + + @ProtocolHandler.MinecraftRegister(onlyNamespace = true) + public static void onPlayerSubscribed(@NotNull Context context, ResourceLocation id) { + subscribedChannels.computeIfAbsent(context.profile().getId(), k -> new HashSet<>()).add(id.getPath()); + } + + @ProtocolHandler.Ticker + public static void tick() { + for (Map.Entry> entry : subscribedChannels.entrySet()) { + ServerPlayer player = MinecraftServer.getServer().getPlayerList().getPlayer(entry.getKey()); + if (player == null) { + continue; + } + + FoodData data = player.getFoodData(); + for (String channel : entry.getValue()) { + switch (channel) { + case "saturation" -> { + float saturation = data.getSaturationLevel(); + Float previousSaturation = previousSaturationLevels.get(player); + if (previousSaturation == null || saturation != previousSaturation) { + ProtocolUtils.sendBytebufPacket(player, SATURATION_KEY, buf -> buf.writeFloat(saturation)); + previousSaturationLevels.put(player, saturation); + } + } + + case "exhaustion" -> { + float exhaustion = data.exhaustionLevel; + Float previousExhaustion = previousExhaustionLevels.get(player); + if (previousExhaustion == null || Math.abs(exhaustion - previousExhaustion) >= MINIMUM_EXHAUSTION_CHANGE_THRESHOLD) { + ProtocolUtils.sendBytebufPacket(player, EXHAUSTION_KEY, buf -> buf.writeFloat(exhaustion)); + previousExhaustionLevels.put(player, exhaustion); + } + } + + case "natural_regeneration" -> { + boolean regeneration = player.level().getGameRules().getBoolean(GameRules.RULE_NATURAL_REGENERATION); + Boolean previousRegeneration = previousNaturalRegeneration.get(player); + if (previousRegeneration == null || regeneration != previousRegeneration) { + ProtocolUtils.sendBytebufPacket(player, NATURAL_REGENERATION_KEY, buf -> buf.writeBoolean(regeneration)); + previousNaturalRegeneration.put(player, regeneration); + } + } + } + } + } + } + + @ProtocolHandler.ReloadServer + public static void onServerReload() { + disableAllPlayer(); + } + + public static void disableAllPlayer() { + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + onPlayerLoggedOut(player); + } + } + + private static void resetPlayerData(@NotNull ServerPlayer player) { + previousExhaustionLevels.remove(player); + previousSaturationLevels.remove(player); + previousNaturalRegeneration.remove(player); + } + + @Override + public int tickerInterval(String tickerID) { + return AppleSkinProtocolConfig.syncTickInterval; + } + + @Override + public boolean isActive() { + return AppleSkinProtocolConfig.enabled; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java new file mode 100644 index 0000000..05d8c93 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/BBORProtocol.java @@ -0,0 +1,244 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol; + +import fun.bm.lophine.config.modules.function.protocol.BBORProtocolConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Registry; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.levelgen.structure.BoundingBox; +import net.minecraft.world.level.levelgen.structure.Structure; +import net.minecraft.world.level.levelgen.structure.StructurePiece; +import net.minecraft.world.level.levelgen.structure.StructureStart; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + +@LeavesProtocol.Register(namespace = "bbor") +public class BBORProtocol implements LeavesProtocol { + + public static final String PROTOCOL_ID = "bbor"; + + // send + private static final ResourceLocation INITIALIZE_CLIENT = id("initialize"); + private static final ResourceLocation ADD_BOUNDING_BOX = id("add_bounding_box_v2"); + private static final ResourceLocation STRUCTURE_LIST_SYNC = id("structure_list_sync_v1"); + // call + private static final Map players = new ConcurrentHashMap<>(); + private static final Map> playerBoundingBoxesCache = new HashMap<>(); + private static final Map>> dimensionCache = new ConcurrentHashMap<>(); + + private static boolean initialized = false; + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @ProtocolHandler.Ticker + public static void tick() { + for (var playerEntry : players.entrySet()) { + sendBoundingToPlayer(playerEntry.getKey(), playerEntry.getValue()); + } + } + + @ProtocolHandler.ReloadServer + public static void onServerReload() { + if (BBORProtocolConfig.enabled) { + initAllPlayer(); + } else { + loggedOutAllPlayer(); + } + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerLoggedIn(@NotNull ServerPlayer player) { + ServerLevel overworld = MinecraftServer.getServer().overworld(); + ProtocolUtils.sendBytebufPacket(player, INITIALIZE_CLIENT, buf -> { + buf.writeLong(overworld.getSeed()); + buf.writeInt(overworld.levelData.getSpawnPos().getX()); + buf.writeInt(overworld.levelData.getSpawnPos().getZ()); + }); + sendStructureList(player); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + players.remove(player.getId()); + playerBoundingBoxesCache.remove(player.getId()); + } + + @ProtocolHandler.BytebufReceiver(key = "subscribe") + public static void onPlayerSubscribed(@NotNull ServerPlayer player, FriendlyByteBuf buf) { + players.put(player.getId(), player); + sendBoundingToPlayer(player.getId(), player); + } + + @ProtocolHandler.ReloadDataPack + public static void onDataPackReload() { + players.values().forEach(BBORProtocol::sendStructureList); + } + + public static void onChunkLoaded(@NotNull LevelChunk chunk) { + Map structures = new HashMap<>(); + final Registry structureFeatureRegistry = chunk.getLevel().registryAccess().lookupOrThrow(Registries.STRUCTURE); + for (var es : chunk.getAllStarts().entrySet()) { + final var optional = structureFeatureRegistry.getResourceKey(es.getKey()); + optional.ifPresent(key -> structures.put(key.location().toString(), es.getValue())); + } + if (!structures.isEmpty()) { + onStructuresLoaded(chunk.getLevel().dimension().location(), structures); + } + } + + public static void onStructuresLoaded(@NotNull ResourceLocation dimensionID, @NotNull Map structures) { + Map> cache = getOrCreateCache(dimensionID); + for (var entry : structures.entrySet()) { + StructureStart structureStart = entry.getValue(); + if (structureStart == null) { + return; + } + + String type = "structure:" + entry.getKey(); + BoundingBox bb = structureStart.getBoundingBox(); + BBoundingBox boundingBox = buildStructure(bb, type); + if (cache.containsKey(boundingBox)) { + return; + } + + Set structureBoundingBoxes = new HashSet<>(); + for (StructurePiece structureComponent : structureStart.getPieces()) { + structureBoundingBoxes.add(buildStructure(structureComponent.getBoundingBox(), type)); + } + cache.put(boundingBox, structureBoundingBoxes); + } + } + + private static @NotNull BBoundingBox buildStructure(@NotNull BoundingBox bb, String type) { + BlockPos min = new BlockPos(bb.minX(), bb.minY(), bb.minZ()); + BlockPos max = new BlockPos(bb.maxX(), bb.maxY(), bb.maxZ()); + return new BBoundingBox(type, min, max); + } + + private static void sendStructureList(@NotNull ServerPlayer player) { + final Registry structureRegistry = MinecraftServer.getServer().registryAccess().lookupOrThrow(Registries.STRUCTURE); + final Set structureIds = structureRegistry.entrySet().stream() + .map(e -> e.getKey().location().toString()).collect(Collectors.toSet()); + ProtocolUtils.sendBytebufPacket(player, STRUCTURE_LIST_SYNC, buf -> { + buf.writeVarInt(structureIds.size()); + structureIds.forEach(buf::writeUtf); + }); + } + + private static void sendBoundingToPlayer(int id, ServerPlayer player) { + for (var entry : dimensionCache.entrySet()) { + if (entry.getValue() == null) { + return; + } + + Set playerBoundingBoxes = playerBoundingBoxesCache.computeIfAbsent(id, k -> new HashSet<>()); + Map> boundingBoxMap = entry.getValue(); + for (BBoundingBox key : boundingBoxMap.keySet()) { + if (playerBoundingBoxes.contains(key)) { + continue; + } + + Set boundingBoxes = boundingBoxMap.get(key); + ProtocolUtils.sendBytebufPacket(player, ADD_BOUNDING_BOX, buf -> { + buf.writeResourceLocation(entry.getKey()); + key.serialize(buf); + if (boundingBoxes != null && boundingBoxes.size() > 1) { + for (BBoundingBox box : boundingBoxes) { + box.serialize(buf); + } + } + }); + playerBoundingBoxes.add(key); + } + } + } + + public static void initAllPlayer() { + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) { + onPlayerLoggedIn(player); + } + initialized = true; + } + + public static void loggedOutAllPlayer() { + players.clear(); + playerBoundingBoxesCache.clear(); + for (var cache : dimensionCache.values()) { + cache.clear(); + } + dimensionCache.clear(); + } + + private static Map> getOrCreateCache(ResourceLocation dimensionId) { + return dimensionCache.computeIfAbsent(dimensionId, dt -> new ConcurrentHashMap<>()); + } + + @Override + public boolean isActive() { + boolean active = BBORProtocolConfig.enabled; + if (!active && initialized) { + initialized = false; + loggedOutAllPlayer(); + } + return active; + } + + private record BBoundingBox(String type, BlockPos min, BlockPos max) { + + private static int combineHashCodes(int @NotNull ... hashCodes) { + final int prime = 31; + int result = 0; + for (int hashCode : hashCodes) { + result = prime * result + hashCode; + } + return result; + } + + public void serialize(@NotNull FriendlyByteBuf buf) { + buf.writeChar('S'); + buf.writeInt(type.hashCode()); + buf.writeVarInt(min.getX()).writeVarInt(min.getY()).writeVarInt(min.getZ()); + buf.writeVarInt(max.getX()).writeVarInt(max.getY()).writeVarInt(max.getZ()); + } + + @Override + public int hashCode() { + return combineHashCodes(min.hashCode(), max.hashCode()); + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java new file mode 100644 index 0000000..c379fa2 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java @@ -0,0 +1,64 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol; + +import fun.bm.lophine.config.modules.function.protocol.XaeroMapProtocolConfig; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +@LeavesProtocol.Register(namespace = "xaerominimap_or_xaeroworldmap_i_dont_care") +public class XaeroMapProtocol implements LeavesProtocol { + + public static final String PROTOCOL_ID_MINI = "xaerominimap"; + public static final String PROTOCOL_ID_WORLD = "xaeroworldmap"; + + private static final ResourceLocation MINIMAP_KEY = idMini("main"); + private static final ResourceLocation WORLDMAP_KEY = idWorld("main"); + + @Contract("_ -> new") + public static ResourceLocation idMini(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID_MINI, path); + } + + @Contract("_ -> new") + public static ResourceLocation idWorld(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID_WORLD, path); + } + + public static void onSendWorldInfo(@NotNull ServerPlayer player) { + if (XaeroMapProtocolConfig.enabled) { + ProtocolUtils.sendBytebufPacket(player, MINIMAP_KEY, buf -> { + buf.writeByte(0); + buf.writeInt(XaeroMapProtocolConfig.xaeroMapServerID); + }); + ProtocolUtils.sendBytebufPacket(player, WORLDMAP_KEY, buf -> { + buf.writeByte(0); + buf.writeInt(XaeroMapProtocolConfig.xaeroMapServerID); + }); + } + } + + @Override + public boolean isActive() { + return XaeroMapProtocolConfig.enabled; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java new file mode 100644 index 0000000..32e582f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java @@ -0,0 +1,266 @@ +/* + * 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.jade; + +import com.mojang.logging.LogUtils; +import fun.bm.lophine.config.modules.function.protocol.JadeProtocolConfig; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.Chicken; +import net.minecraft.world.entity.animal.allay.Allay; +import net.minecraft.world.entity.animal.armadillo.Armadillo; +import net.minecraft.world.entity.animal.frog.Tadpole; +import net.minecraft.world.entity.animal.sniffer.Sniffer; +import net.minecraft.world.entity.monster.ZombieVillager; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.CampfireBlock; +import net.minecraft.world.level.block.entity.*; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.payload.*; +import org.leavesmc.leaves.protocol.jade.provider.*; +import org.leavesmc.leaves.protocol.jade.provider.block.*; +import org.leavesmc.leaves.protocol.jade.provider.entity.*; +import org.leavesmc.leaves.protocol.jade.util.*; +import org.leavesmc.leaves.protocol.servux.litematics.utils.NbtUtils; +import org.slf4j.Logger; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +@LeavesProtocol.Register(namespace = "jade") +public class JadeProtocol implements LeavesProtocol { + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final String PROTOCOL_ID = "jade"; + public static final String PROTOCOL_VERSION = "8"; + public static final HierarchyLookup> entityDataProviders = new HierarchyLookup<>(Entity.class); + public static final PairHierarchyLookup> blockDataProviders = new PairHierarchyLookup<>(new HierarchyLookup<>(Block.class), new HierarchyLookup<>(BlockEntity.class)); + public static final WrappedHierarchyLookup> itemStorageProviders = WrappedHierarchyLookup.forAccessor(); + private static final Set enabledPlayers = new HashSet<>(); + + public static PriorityStore priorities; + private static List shearableBlocks = null; + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + @Contract("_ -> new") + public static @NotNull ResourceLocation mc_id(String path) { + return ResourceLocation.withDefaultNamespace(path); + } + + @ProtocolHandler.Init + public static void init() { + priorities = new PriorityStore<>(IJadeProvider::getDefaultPriority, IJadeProvider::getUid); + + // core plugin + blockDataProviders.register(BlockEntity.class, BlockNameProvider.INSTANCE); + + // universal plugin + entityDataProviders.register(Entity.class, ItemStorageProvider.getEntity()); + blockDataProviders.register(Block.class, ItemStorageProvider.getBlock()); + + itemStorageProviders.register(Object.class, ItemStorageExtensionProvider.INSTANCE); + itemStorageProviders.register(Block.class, ItemStorageExtensionProvider.INSTANCE); + + // vanilla plugin + entityDataProviders.register(Entity.class, AnimalOwnerProvider.INSTANCE); + entityDataProviders.register(LivingEntity.class, StatusEffectsProvider.INSTANCE); + entityDataProviders.register(AgeableMob.class, MobGrowthProvider.INSTANCE); + entityDataProviders.register(Tadpole.class, MobGrowthProvider.INSTANCE); + entityDataProviders.register(Animal.class, MobBreedingProvider.INSTANCE); + entityDataProviders.register(Allay.class, MobBreedingProvider.INSTANCE); + entityDataProviders.register(Mob.class, PetArmorProvider.INSTANCE); + + entityDataProviders.register(Chicken.class, NextEntityDropProvider.INSTANCE); + entityDataProviders.register(Armadillo.class, NextEntityDropProvider.INSTANCE); + entityDataProviders.register(Sniffer.class, NextEntityDropProvider.INSTANCE); + + entityDataProviders.register(ZombieVillager.class, ZombieVillagerProvider.INSTANCE); + + blockDataProviders.register(BrewingStandBlockEntity.class, BrewingStandProvider.INSTANCE); + blockDataProviders.register(BeehiveBlockEntity.class, BeehiveProvider.INSTANCE); + blockDataProviders.register(CommandBlockEntity.class, CommandBlockProvider.INSTANCE); + blockDataProviders.register(JukeboxBlockEntity.class, JukeboxProvider.INSTANCE); + blockDataProviders.register(LecternBlockEntity.class, LecternProvider.INSTANCE); + + blockDataProviders.register(ComparatorBlockEntity.class, RedstoneProvider.INSTANCE); + blockDataProviders.register(HopperBlockEntity.class, HopperLockProvider.INSTANCE); + blockDataProviders.register(CalibratedSculkSensorBlockEntity.class, RedstoneProvider.INSTANCE); + + blockDataProviders.register(AbstractFurnaceBlockEntity.class, FurnaceProvider.INSTANCE); + blockDataProviders.register(ChiseledBookShelfBlockEntity.class, ChiseledBookshelfProvider.INSTANCE); + blockDataProviders.register(TrialSpawnerBlockEntity.class, MobSpawnerCooldownProvider.INSTANCE); + + itemStorageProviders.register(CampfireBlock.class, CampfireProvider.INSTANCE); + + blockDataProviders.idMapped(); + entityDataProviders.idMapped(); + + blockDataProviders.loadComplete(priorities); + entityDataProviders.loadComplete(priorities); + itemStorageProviders.loadComplete(priorities); + + rebuildShearableBlocks(); + } + + @ProtocolHandler.PayloadReceiver(payload = ClientHandshakePayload.class) + public static void clientHandshake(ServerPlayer player, ClientHandshakePayload payload) { + if (!payload.protocolVersion().equals(PROTOCOL_VERSION)) { + player.sendSystemMessage(Component.literal("You are using a different version of Jade than the server. Please update Jade or report to the server operator").withColor(0xff0000)); + return; + } + ProtocolUtils.sendPayloadPacket(player, new ServerHandshakePayload(Collections.emptyMap(), shearableBlocks, blockDataProviders.mappedIds(), entityDataProviders.mappedIds())); + enabledPlayers.add(player); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLeave(ServerPlayer player) { + enabledPlayers.remove(player); + } + + @ProtocolHandler.PayloadReceiver(payload = RequestEntityPayload.class) + public static void requestEntityData(ServerPlayer player, RequestEntityPayload payload) { + player.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + EntityAccessor accessor = payload.data().unpack(player); + if (accessor == null) { + return; + } + + Entity entity = accessor.getEntity(); + double maxDistance = Mth.square(player.entityInteractionRange() + 21); + if (entity == null || player.distanceToSqr(entity) > maxDistance) { + return; + } + + List> providers = entityDataProviders.get(entity); + if (providers.isEmpty()) { + return; + } + + CompoundTag tag = new CompoundTag(); + for (IServerDataProvider provider : providers) { + if (!payload.dataProviders().contains(provider)) { + continue; + } + try { + provider.appendServerData(tag, accessor); + } catch (Exception e) { + LOGGER.warn("Error while saving data for entity " + entity); + } + } + tag.putInt("EntityId", entity.getId()); + + ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag)); + }, null, 1L); + } + + @ProtocolHandler.PayloadReceiver(payload = RequestBlockPayload.class) + public static void requestBlockData(ServerPlayer player, RequestBlockPayload payload) { + player.getBukkitEntity().taskScheduler.schedule((LivingEntity nmsEntity) -> { + BlockAccessor accessor = payload.data().unpack(player); + if (accessor == null) { + return; + } + + BlockPos pos = accessor.getPosition(); + Block block = accessor.getBlock(); + BlockEntity blockEntity = accessor.getBlockEntity(); + double maxDistance = Mth.square(player.blockInteractionRange() + 21); + if (pos.distSqr(player.blockPosition()) > maxDistance || !accessor.getLevel().isLoaded(pos)) { + return; + } + + List> providers; + if (blockEntity != null) { + providers = blockDataProviders.getMerged(block, blockEntity); + } else { + providers = blockDataProviders.first.get(block); + } + + if (providers.isEmpty()) { + return; + } + + CompoundTag tag = new CompoundTag(); + for (IServerDataProvider provider : providers) { + if (!payload.dataProviders().contains(provider)) { + continue; + } + try { + provider.appendServerData(tag, accessor); + } catch (Exception e) { + LOGGER.warn("Error while saving data for block " + accessor.getBlockState()); + } + } + NbtUtils.writeBlockPosToTag(pos, tag); + tag.putString("BlockId", BuiltInRegistries.BLOCK.getKey(block).toString()); + + ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag)); + }, null, 1L); + } + + @ProtocolHandler.ReloadServer + public static void onServerReload() { + rebuildShearableBlocks(); + for (ServerPlayer player : enabledPlayers) { + ProtocolUtils.sendPayloadPacket(player, new ServerHandshakePayload(Collections.emptyMap(), shearableBlocks, blockDataProviders.mappedIds(), entityDataProviders.mappedIds())); + } + } + + private static void rebuildShearableBlocks() { + try { + shearableBlocks = Collections.unmodifiableList(LootTableMineableCollector.execute( + MinecraftServer.getServer().reloadableRegistries().lookup().lookupOrThrow(Registries.LOOT_TABLE), + Items.SHEARS.getDefaultInstance() + )); + } catch (Throwable ignore) { + shearableBlocks = List.of(); + LOGGER.warn("Failed to collect shearable blocks"); + } + } + + @Override + public boolean isActive() { + return JadeProtocolConfig.enabled; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java new file mode 100644 index 0000000..e8a9d7d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java @@ -0,0 +1,39 @@ +/* + * 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.jade.accessor; + +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamEncoder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.HitResult; +import org.jetbrains.annotations.Nullable; + +public interface Accessor { + ServerLevel getLevel(); + + Player getPlayer(); + + Tag encodeAsNbt(StreamEncoder codec, D value); + + T getHitResult(); + + @Nullable + Object getTarget(); +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java new file mode 100644 index 0000000..058ecbe --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java @@ -0,0 +1,77 @@ +/* + * 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.jade.accessor; + +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamEncoder; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.HitResult; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.function.Supplier; + +public abstract class AccessorImpl implements Accessor { + + private final ServerLevel level; + private final Player player; + private final Supplier hit; + protected boolean verify; + private RegistryFriendlyByteBuf buffer; + + public AccessorImpl(ServerLevel level, Player player, Supplier hit) { + this.level = level; + this.player = player; + this.hit = hit; + } + + @Override + public ServerLevel getLevel() { + return level; + } + + @Override + public Player getPlayer() { + return player; + } + + private RegistryFriendlyByteBuf buffer() { + if (buffer == null) { + buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), level.registryAccess()); + } + buffer.clear(); + return buffer; + } + + @Override + public Tag encodeAsNbt(StreamEncoder streamCodec, D value) { + RegistryFriendlyByteBuf buffer = buffer(); + streamCodec.encode(buffer, value); + ByteArrayTag tag = new ByteArrayTag(ArrayUtils.subarray(buffer.array(), 0, buffer.readableBytes())); + buffer.clear(); + return tag; + } + + @Override + public T getHitResult() { + return hit.get(); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java new file mode 100644 index 0000000..e5bdbe6 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java @@ -0,0 +1,61 @@ +/* + * 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.jade.accessor; + +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Supplier; + +public interface BlockAccessor extends Accessor { + + Block getBlock(); + + BlockState getBlockState(); + + BlockEntity getBlockEntity(); + + BlockPos getPosition(); + + @ApiStatus.NonExtendable + interface Builder { + Builder level(ServerLevel level); + + Builder player(Player player); + + Builder hit(BlockHitResult hit); + + Builder blockState(BlockState state); + + default Builder blockEntity(BlockEntity blockEntity) { + return blockEntity(() -> blockEntity); + } + + Builder blockEntity(Supplier blockEntity); + + Builder from(BlockAccessor accessor); + + BlockAccessor build(); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java new file mode 100644 index 0000000..425603e --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java @@ -0,0 +1,160 @@ +/* + * 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.jade.accessor; + +import com.google.common.base.Suppliers; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.Nullable; + +import java.util.function.Supplier; + +/** + * Class to get information of block target and context. + */ +public class BlockAccessorImpl extends AccessorImpl implements BlockAccessor { + + private final BlockState blockState; + @Nullable + private final Supplier blockEntity; + + private BlockAccessorImpl(Builder builder) { + super(builder.level, builder.player, Suppliers.ofInstance(builder.hit)); + blockState = builder.blockState; + blockEntity = builder.blockEntity; + } + + @Override + public Block getBlock() { + return getBlockState().getBlock(); + } + + @Override + public BlockState getBlockState() { + return blockState; + } + + @Override + public BlockEntity getBlockEntity() { + return blockEntity == null ? null : blockEntity.get(); + } + + @Override + public BlockPos getPosition() { + return getHitResult().getBlockPos(); + } + + @Nullable + @Override + public Object getTarget() { + return getBlockEntity(); + } + + public static class Builder implements BlockAccessor.Builder { + private ServerLevel level; + private Player player; + private BlockHitResult hit; + private BlockState blockState = Blocks.AIR.defaultBlockState(); + private Supplier blockEntity; + + @Override + public Builder level(ServerLevel level) { + this.level = level; + return this; + } + + @Override + public Builder player(Player player) { + this.player = player; + return this; + } + + @Override + public Builder hit(BlockHitResult hit) { + this.hit = hit; + return this; + } + + @Override + public Builder blockState(BlockState blockState) { + this.blockState = blockState; + return this; + } + + @Override + public Builder blockEntity(Supplier blockEntity) { + this.blockEntity = blockEntity; + return this; + } + + @Override + public Builder from(BlockAccessor accessor) { + level = accessor.getLevel(); + player = accessor.getPlayer(); + hit = accessor.getHitResult(); + blockEntity = accessor::getBlockEntity; + blockState = accessor.getBlockState(); + return this; + } + + @Override + public BlockAccessor build() { + return new BlockAccessorImpl(this); + } + } + + public record SyncData(boolean showDetails, BlockHitResult hit, BlockState blockState, ItemStack fakeBlock) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, + SyncData::showDetails, + StreamCodec.of(FriendlyByteBuf::writeBlockHitResult, FriendlyByteBuf::readBlockHitResult), + SyncData::hit, + ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY), + SyncData::blockState, + ItemStack.OPTIONAL_STREAM_CODEC, + SyncData::fakeBlock, + SyncData::new + ); + + public BlockAccessor unpack(ServerPlayer player) { + Supplier blockEntity = null; + if (blockState.hasBlockEntity()) { + blockEntity = Suppliers.memoize(() -> player.level().getBlockEntity(hit.getBlockPos())); + } + return new Builder() + .level(player.level()) + .player(player) + .hit(hit) + .blockState(blockState) + .blockEntity(blockEntity) + .build(); + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java new file mode 100644 index 0000000..e4f89f4 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java @@ -0,0 +1,59 @@ +/* + * 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.jade.accessor; + +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.EntityHitResult; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Supplier; + +public interface EntityAccessor extends Accessor { + + Entity getEntity(); + + /** + * For part entity like ender dragon's, getEntity() will return the parent entity. + */ + Entity getRawEntity(); + + @ApiStatus.NonExtendable + interface Builder { + Builder level(ServerLevel level); + + Builder player(Player player); + + default Builder hit(EntityHitResult hit) { + return hit(() -> hit); + } + + Builder hit(Supplier hit); + + default Builder entity(Entity entity) { + return entity(() -> entity); + } + + Builder entity(Supplier entity); + + Builder from(EntityAccessor accessor); + + EntityAccessor build(); + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java new file mode 100644 index 0000000..6b1ccb6 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java @@ -0,0 +1,129 @@ +/* + * 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.jade.accessor; + +import com.google.common.base.Suppliers; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.util.CommonUtil; + +import java.util.function.Supplier; + +public class EntityAccessorImpl extends AccessorImpl implements EntityAccessor { + + private final Supplier entity; + + public EntityAccessorImpl(Builder builder) { + super(builder.level, builder.player, builder.hit); + entity = builder.entity; + } + + @Override + public Entity getEntity() { + return CommonUtil.wrapPartEntityParent(getRawEntity()); + } + + @Override + public Entity getRawEntity() { + return entity.get(); + } + + @NotNull + @Override + public Object getTarget() { + return getEntity(); + } + + public static class Builder implements EntityAccessor.Builder { + private ServerLevel level; + private Player player; + private Supplier hit; + private Supplier entity; + + @Override + public Builder level(ServerLevel level) { + this.level = level; + return this; + } + + @Override + public Builder player(Player player) { + this.player = player; + return this; + } + + + @Override + public Builder hit(Supplier hit) { + this.hit = hit; + return this; + } + + @Override + public Builder entity(Supplier entity) { + this.entity = entity; + return this; + } + + @Override + public Builder from(EntityAccessor accessor) { + level = accessor.getLevel(); + player = accessor.getPlayer(); + hit = accessor::getHitResult; + entity = accessor::getEntity; + return this; + } + + @Override + public EntityAccessor build() { + return new EntityAccessorImpl(this); + } + } + + public record SyncData(boolean showDetails, int id, int partIndex, Vec3 hitVec) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.BOOL, + SyncData::showDetails, + ByteBufCodecs.VAR_INT, + SyncData::id, + ByteBufCodecs.VAR_INT, + SyncData::partIndex, + ByteBufCodecs.VECTOR3F.map(Vec3::new, Vec3::toVector3f), + SyncData::hitVec, + SyncData::new + ); + + public EntityAccessor unpack(ServerPlayer player) { + Supplier entity = Suppliers.memoize(() -> CommonUtil.getPartEntity(player.level().getEntity(id), partIndex)); + return new EntityAccessorImpl.Builder() + .level(player.level()) + .player(player) + .entity(entity) + .hit(Suppliers.memoize(() -> new EntityHitResult(entity.get(), hitVec))) + .build(); + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java new file mode 100644 index 0000000..bfeabeb --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.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.jade.payload; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; + +public record ClientHandshakePayload(String protocolVersion) implements LeavesCustomPayload { + + @ID + private static final ResourceLocation PACKET_CLIENT_HANDSHAKE = JadeProtocol.id("client_handshake"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.STRING_UTF8, ClientHandshakePayload::protocolVersion, ClientHandshakePayload::new + ); +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java new file mode 100644 index 0000000..50b6666 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java @@ -0,0 +1,37 @@ +/* + * 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.jade.payload; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; + +public record ReceiveDataPayload(CompoundTag tag) implements LeavesCustomPayload { + + @ID + private static final ResourceLocation PACKET_RECEIVE_DATA = JadeProtocol.id("receive_data"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.COMPOUND_TAG, ReceiveDataPayload::tag, ReceiveDataPayload::new + ); +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java new file mode 100644 index 0000000..8a85dd9 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java @@ -0,0 +1,53 @@ +/* + * 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.jade.payload; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessorImpl; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +import java.util.List; +import java.util.Objects; + +import static org.leavesmc.leaves.protocol.jade.JadeProtocol.blockDataProviders; + +public record RequestBlockPayload(BlockAccessorImpl.SyncData data, + List<@Nullable IServerDataProvider> dataProviders) implements LeavesCustomPayload { + + @ID + private static final ResourceLocation PACKET_REQUEST_BLOCK = JadeProtocol.id("request_block"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.composite( + BlockAccessorImpl.SyncData.STREAM_CODEC, + RequestBlockPayload::data, + ByteBufCodecs.>list() + .apply(ByteBufCodecs.idMapper( + $ -> Objects.requireNonNull(blockDataProviders.idMapper()).byId($), + $ -> Objects.requireNonNull(blockDataProviders.idMapper()).getIdOrThrow($))), + RequestBlockPayload::dataProviders, + RequestBlockPayload::new); +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java new file mode 100644 index 0000000..a15a120 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java @@ -0,0 +1,54 @@ +/* + * 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.jade.payload; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessorImpl; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +import java.util.List; +import java.util.Objects; + +import static org.leavesmc.leaves.protocol.jade.JadeProtocol.entityDataProviders; + +public record RequestEntityPayload(EntityAccessorImpl.SyncData data, + List<@Nullable IServerDataProvider> dataProviders) implements LeavesCustomPayload { + + @ID + private static final ResourceLocation PACKET_REQUEST_ENTITY = JadeProtocol.id("request_entity"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.composite( + EntityAccessorImpl.SyncData.STREAM_CODEC, + RequestEntityPayload::data, + ByteBufCodecs.>list() + .apply(ByteBufCodecs.idMapper( + $ -> Objects.requireNonNull(entityDataProviders.idMapper()).byId($), + $ -> Objects.requireNonNull(entityDataProviders.idMapper()).getIdOrThrow($) + )), + RequestEntityPayload::dataProviders, + RequestEntityPayload::new); +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java new file mode 100644 index 0000000..742a54a --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java @@ -0,0 +1,59 @@ +/* + * 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.jade.payload; + + +import com.google.common.collect.Maps; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; + +import java.util.List; +import java.util.Map; + +import static org.leavesmc.leaves.protocol.jade.util.JadeCodec.PRIMITIVE_STREAM_CODEC; + +public record ServerHandshakePayload( + Map serverConfig, + List shearableBlocks, + List blockProviderIds, + List entityProviderIds +) implements LeavesCustomPayload { + + @ID + private static final ResourceLocation PACKET_SERVER_HANDSHAKE = JadeProtocol.id("server_handshake"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.composite( + ByteBufCodecs.map(Maps::newHashMapWithExpectedSize, ResourceLocation.STREAM_CODEC, PRIMITIVE_STREAM_CODEC), + ServerHandshakePayload::serverConfig, + ByteBufCodecs.registry(Registries.BLOCK).apply(ByteBufCodecs.list()), + ServerHandshakePayload::shearableBlocks, + ByteBufCodecs.list().apply(ResourceLocation.STREAM_CODEC), + ServerHandshakePayload::blockProviderIds, + ByteBufCodecs.list().apply(ResourceLocation.STREAM_CODEC), + ServerHandshakePayload::entityProviderIds, + ServerHandshakePayload::new + ); +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java new file mode 100644 index 0000000..391fd21 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java @@ -0,0 +1,29 @@ +/* + * 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.jade.provider; + +import net.minecraft.resources.ResourceLocation; + +public interface IJadeProvider { + + ResourceLocation getUid(); + + default int getDefaultPriority() { + return 0; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java new file mode 100644 index 0000000..11b37b0 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java @@ -0,0 +1,25 @@ +/* + * 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.jade.provider; + +import net.minecraft.nbt.CompoundTag; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; + +public interface IServerDataProvider> extends IJadeProvider { + void appendServerData(CompoundTag data, T accessor); +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java new file mode 100644 index 0000000..0cffe35 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java @@ -0,0 +1,27 @@ +/* + * 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.jade.provider; + +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; + +public interface IServerExtensionProvider extends IJadeProvider { + List> getGroups(Accessor request); +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java new file mode 100644 index 0000000..098422e --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java @@ -0,0 +1,165 @@ +/* + * 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.jade.provider; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.mojang.logging.LogUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Container; +import net.minecraft.world.LockCode; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.WorldlyContainerHolder; +import net.minecraft.world.entity.animal.horse.AbstractHorse; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.vehicle.ContainerEntity; +import net.minecraft.world.inventory.PlayerEnderChestContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.entity.EnderChestBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.util.ItemCollector; +import org.leavesmc.leaves.protocol.jade.util.ItemIterator; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; +import org.slf4j.Logger; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public enum ItemStorageExtensionProvider implements IServerExtensionProvider { + INSTANCE; + + public static final Cache> targetCache = CacheBuilder.newBuilder().weakKeys().expireAfterAccess(60, TimeUnit.SECONDS).build(); + + private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage.default"); + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static ItemCollector createItemCollector(Accessor request) { + if (request.getTarget() instanceof AbstractHorse) { + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(o -> { + if (o instanceof AbstractHorse horse) { + return horse.inventory; + } + return null; + }, 2)); + } + + // TODO BlockEntity like fabric's ItemStorage + + final Container container = findContainer(request); + if (container != null) { + if (container instanceof ChestBlockEntity) { + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(o -> { + if (o instanceof ChestBlockEntity blockEntity) { + if (blockEntity.getBlockState().getBlock() instanceof ChestBlock chestBlock) { + Container compound = null; + if (blockEntity.getLevel() != null) { + compound = ChestBlock.getContainer( + chestBlock, blockEntity.getBlockState(), + blockEntity.getLevel(), blockEntity.getBlockPos(), + true // Bypass lock check + ); + } + if (compound != null) { + return compound; + } + } + return blockEntity; + } + return null; + }, 0)); + } + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(0)); + } + + return ItemCollector.EMPTY; + } + + public static @Nullable Container findContainer(@NotNull Accessor accessor) { + Object target = accessor.getTarget(); + if (target == null && accessor instanceof BlockAccessor blockAccessor && + blockAccessor.getBlock() instanceof WorldlyContainerHolder holder) { + return holder.getContainer(blockAccessor.getBlockState(), accessor.getLevel(), blockAccessor.getPosition()); + } else if (target instanceof Container container) { + return container; + } + return null; + } + + @Override + public List> getGroups(Accessor request) { + Object target = request.getTarget(); + + switch (target) { + case null -> { + return createItemCollector(request).update(request); + } + case RandomizableContainer te when te.getLootTable() != null -> { + return List.of(); + } + case ContainerEntity containerEntity when containerEntity.getContainerLootTable() != null -> { + return List.of(); + } + case EnderChestBlockEntity enderChest when request.getPlayer().getEnderChestInventory().isEmpty() -> { + return List.of(); + } + default -> { + } + } + + Player player = request.getPlayer(); + if (!player.isCreative() && !player.isSpectator() && target instanceof BaseContainerBlockEntity te) { + if (te.lockKey != LockCode.NO_LOCK) { + return List.of(); + } + } + + if (target instanceof EnderChestBlockEntity) { + PlayerEnderChestContainer inventory = player.getEnderChestInventory(); + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(x -> inventory, 0)).update(request); + } + + ItemCollector itemCollector; + try { + itemCollector = targetCache.get(target, () -> createItemCollector(request)); + } catch (ExecutionException e) { + LOGGER.warn("Failed to get item collector for " + target); + return null; + } + + return itemCollector.update(request); + } + + @Override + public ResourceLocation getUid() { + return UNIVERSAL_ITEM_STORAGE; + } + + @Override + public int getDefaultPriority() { + return 9999; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java new file mode 100644 index 0000000..912073c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.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.jade.provider; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.LockCode; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.util.CommonUtil; +import org.leavesmc.leaves.protocol.jade.util.ItemCollector; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; +import java.util.Map; + +public abstract class ItemStorageProvider> implements IServerDataProvider { + + private static final StreamCodec>>> STREAM_CODEC = ViewGroup.listCodec(ItemStack.OPTIONAL_STREAM_CODEC); + + private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage"); + + public static ForBlock getBlock() { + return ForBlock.INSTANCE; + } + + public static ForEntity getEntity() { + return ForEntity.INSTANCE; + } + + public static void putData(CompoundTag tag, @NotNull Accessor accessor) { + Object target = accessor.getTarget(); + Player player = accessor.getPlayer(); + Map.Entry>> entry = CommonUtil.getServerExtensionData(accessor, JadeProtocol.itemStorageProviders); + if (entry != null) { + List> groups = entry.getValue(); + for (ViewGroup group : groups) { + if (group.views.size() > ItemCollector.MAX_SIZE) { + group.views = group.views.subList(0, ItemCollector.MAX_SIZE); + } + } + tag.put(UNIVERSAL_ITEM_STORAGE.toString(), accessor.encodeAsNbt(STREAM_CODEC, entry)); + return; + } + if (target instanceof RandomizableContainer containerEntity && containerEntity.getLootTable() != null) { + tag.putBoolean("Loot", true); + } else if (!player.isCreative() && !player.isSpectator() && target instanceof BaseContainerBlockEntity te) { + if (te.lockKey != LockCode.NO_LOCK) { + tag.putBoolean("Locked", true); + } + } + } + + @Override + public ResourceLocation getUid() { + return UNIVERSAL_ITEM_STORAGE; + } + + @Override + public void appendServerData(CompoundTag tag, @NotNull T accessor) { + if (accessor.getTarget() instanceof AbstractFurnaceBlockEntity) { + return; + } + putData(tag, accessor); + } + + @Override + public int getDefaultPriority() { + return 1000; + } + + public static class ForBlock extends ItemStorageProvider { + private static final ForBlock INSTANCE = new ForBlock(); + } + + public static class ForEntity extends ItemStorageProvider { + private static final ForEntity INSTANCE = new ForEntity(); + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java new file mode 100644 index 0000000..d007151 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.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.jade.provider; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; + +public interface StreamServerDataProvider, D> extends IServerDataProvider { + + @Override + default void appendServerData(CompoundTag data, T accessor) { + D value = streamData(accessor); + if (value != null) { + data.put(getUid().toString(), accessor.encodeAsNbt(streamCodec(), value)); + } + } + + @Nullable + D streamData(T accessor); + + StreamCodec streamCodec(); +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java new file mode 100644 index 0000000..c48c7dc --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java @@ -0,0 +1,51 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BeehiveBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum BeehiveProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_BEEHIVE = JadeProtocol.mc_id("beehive"); + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.BYTE.cast(); + } + + @Override + public Byte streamData(@NotNull BlockAccessor accessor) { + BeehiveBlockEntity beehive = (BeehiveBlockEntity) accessor.getBlockEntity(); + int bees = beehive.getOccupantCount(); + return (byte) (beehive.isFull() ? bees : -bees); + } + + @Override + public ResourceLocation getUid() { + return MC_BEEHIVE; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockNameProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockNameProvider.java new file mode 100644 index 0000000..7421d9d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockNameProvider.java @@ -0,0 +1,77 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentSerialization; +import net.minecraft.network.chat.contents.TranslatableContents; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.Nameable; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.state.properties.ChestType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum BlockNameProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation CORE_OBJECT_NAME = JadeProtocol.id("object_name"); + + @Override + @Nullable + public Component streamData(@NotNull BlockAccessor accessor) { + if (!(accessor.getBlockEntity() instanceof Nameable nameable)) { + return null; + } + if (nameable instanceof ChestBlockEntity && accessor.getBlock() instanceof ChestBlock && accessor.getBlockState().getValue(ChestBlock.TYPE) != ChestType.SINGLE) { + MenuProvider menuProvider = accessor.getBlockState().getMenuProvider(accessor.getLevel(), accessor.getPosition()); + if (menuProvider != null) { + Component name = menuProvider.getDisplayName(); + if (!(name.getContents() instanceof TranslatableContents contents) || !"container.chestDouble".equals(contents.getKey())) { + return name; + } + } + } else if (nameable.hasCustomName()) { + return nameable.getDisplayName(); + } + return accessor.getBlockEntity().components().get(DataComponents.ITEM_NAME); + } + + @Override + public StreamCodec streamCodec() { + return ComponentSerialization.STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return CORE_OBJECT_NAME; + } + + @Override + public int getDefaultPriority() { + return -10100; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java new file mode 100644 index 0000000..00e4add --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java @@ -0,0 +1,60 @@ +/* + * 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.jade.provider.block; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum BrewingStandProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_BREWING_STAND = JadeProtocol.mc_id("brewing_stand"); + + @Override + public @NotNull Data streamData(@NotNull BlockAccessor accessor) { + BrewingStandBlockEntity brewingStand = (BrewingStandBlockEntity) accessor.getBlockEntity(); + return new Data(brewingStand.fuel, brewingStand.brewTime); + } + + @Override + public @NotNull StreamCodec streamCodec() { + return Data.STREAM_CODEC.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_BREWING_STAND; + } + + public record Data(int fuel, int time) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.VAR_INT, + Data::fuel, + ByteBufCodecs.VAR_INT, + Data::time, + Data::new); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java new file mode 100644 index 0000000..72a5be0 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java @@ -0,0 +1,72 @@ +/* + * 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.jade.provider.block; + +import com.google.common.collect.Lists; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.block.entity.CampfireBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; + +public enum CampfireProvider implements IServerExtensionProvider { + INSTANCE; + + private static final MapCodec COOKING_TIME_CODEC = Codec.INT.fieldOf("jade:cooking"); + private static final ResourceLocation MC_CAMPFIRE = JadeProtocol.mc_id("campfire"); + + @Override + public @Nullable @Unmodifiable List> getGroups(@NotNull Accessor request) { + if (request.getTarget() instanceof CampfireBlockEntity campfire) { + List list = Lists.newArrayList(); + for (int i = 0; i < campfire.cookingTime.length; i++) { + ItemStack stack = campfire.getItems().get(i); + if (stack.isEmpty()) { + continue; + } + stack = stack.copy(); + + CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .update(NbtOps.INSTANCE, COOKING_TIME_CODEC, campfire.cookingTime[i] - campfire.cookingProgress[i]) + .getOrThrow(); + stack.set(DataComponents.CUSTOM_DATA, customData); + + list.add(stack); + } + return List.of(new ViewGroup<>(list)); + } + return null; + } + + @Override + public ResourceLocation getUid() { + return MC_CAMPFIRE; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java new file mode 100644 index 0000000..4d0a9b9 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java @@ -0,0 +1,61 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.ChiseledBookShelfBlock; +import net.minecraft.world.level.block.entity.ChiseledBookShelfBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.ItemStorageProvider; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum ChiseledBookshelfProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_CHISELED_BOOKSHELF = JadeProtocol.mc_id("chiseled_bookshelf"); + + @Override + public @Nullable ItemStack streamData(@NotNull BlockAccessor accessor) { + int slot = ((ChiseledBookShelfBlock) accessor.getBlock()).getHitSlot(accessor.getHitResult(), accessor.getBlockState()).orElse(-1); + if (slot == -1) { + return null; + } + return ((ChiseledBookShelfBlockEntity) accessor.getBlockEntity()).getItem(slot); + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_CHISELED_BOOKSHELF; + } + + @Override + public int getDefaultPriority() { + return ItemStorageProvider.getBlock().getDefaultPriority() + 1; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java new file mode 100644 index 0000000..60b0cf5 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java @@ -0,0 +1,57 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.CommandBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum CommandBlockProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_COMMAND_BLOCK = JadeProtocol.mc_id("command_block"); + + @Nullable + public String streamData(@NotNull BlockAccessor accessor) { + if (!accessor.getPlayer().canUseGameMasterBlocks()) { + return null; + } + String command = ((CommandBlockEntity) accessor.getBlockEntity()).getCommandBlock().getCommand(); + if (command.length() > 40) { + command = command.substring(0, 37) + "..."; + } + return command; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.STRING_UTF8.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_COMMAND_BLOCK; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java new file mode 100644 index 0000000..0e69f47 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java @@ -0,0 +1,68 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +import java.util.List; + +public enum FurnaceProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_FURNACE = JadeProtocol.mc_id("furnace"); + + @Override + public @NotNull Data streamData(@NotNull BlockAccessor accessor) { + AbstractFurnaceBlockEntity furnace = (AbstractFurnaceBlockEntity) accessor.getBlockEntity(); + return new Data( + furnace.cookingTimer, + furnace.cookingTotalTime, + List.of(furnace.getItem(0), furnace.getItem(1), furnace.getItem(2)) + ); + } + + @Override + public StreamCodec streamCodec() { + return Data.STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_FURNACE; + } + + public record Data(int progress, int total, List inventory) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.VAR_INT, + Data::progress, + ByteBufCodecs.VAR_INT, + Data::total, + ItemStack.OPTIONAL_LIST_STREAM_CODEC, + Data::inventory, + Data::new); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java new file mode 100644 index 0000000..4501e79 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java @@ -0,0 +1,54 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum HopperLockProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_HOPPER_LOCK = JadeProtocol.mc_id("hopper_lock"); + + @Override + public Boolean streamData(@NotNull BlockAccessor accessor) { + return !accessor.getBlockState().getValue(BlockStateProperties.ENABLED); + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.BOOL.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_HOPPER_LOCK; + } + + @Override + public int getDefaultPriority() { + return BlockNameProvider.INSTANCE.getDefaultPriority() + 10; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java new file mode 100644 index 0000000..74a05ef --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java @@ -0,0 +1,49 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.JukeboxBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum JukeboxProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_JUKEBOX = JadeProtocol.mc_id("jukebox"); + + @Override + public @NotNull ItemStack streamData(BlockAccessor accessor) { + return ((JukeboxBlockEntity) accessor.getBlockEntity()).getTheItem(); + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_JUKEBOX; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java new file mode 100644 index 0000000..89c609d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.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.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.LecternBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum LecternProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_LECTERN = JadeProtocol.mc_id("lectern"); + + @Override + public @NotNull ItemStack streamData(@NotNull BlockAccessor accessor) { + return ((LecternBlockEntity) accessor.getBlockEntity()).getBook(); + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + + @Override + public ResourceLocation getUid() { + return MC_LECTERN; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java new file mode 100644 index 0000000..073eda0 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java @@ -0,0 +1,59 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity; +import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerStateData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum MobSpawnerCooldownProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_MOB_SPAWNER_COOLDOWN = JadeProtocol.mc_id("mob_spawner.cooldown"); + + @Override + public @Nullable Integer streamData(@NotNull BlockAccessor accessor) { + TrialSpawnerBlockEntity spawner = (TrialSpawnerBlockEntity) accessor.getBlockEntity(); + TrialSpawnerStateData spawnerData = spawner.getTrialSpawner().getStateData(); + ServerLevel level = accessor.getLevel(); + if (spawner.getTrialSpawner().canSpawnInLevel(level) && level.getGameTime() < spawnerData.cooldownEndsAt) { + return (int) (spawnerData.cooldownEndsAt - level.getGameTime()); + } + return null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + + @Override + public ResourceLocation getUid() { + return MC_MOB_SPAWNER_COOLDOWN; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java new file mode 100644 index 0000000..4bb35bf --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java @@ -0,0 +1,53 @@ +/* + * 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.jade.provider.block; + +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.CalibratedSculkSensorBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.CalibratedSculkSensorBlockEntity; +import net.minecraft.world.level.block.entity.ComparatorBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +public enum RedstoneProvider implements IServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_REDSTONE = JadeProtocol.mc_id("redstone"); + + @Override + public void appendServerData(CompoundTag data, @NotNull BlockAccessor accessor) { + BlockEntity blockEntity = accessor.getBlockEntity(); + if (blockEntity instanceof ComparatorBlockEntity comparator) { + data.putInt("Signal", comparator.getOutputSignal()); + } else if (blockEntity instanceof CalibratedSculkSensorBlockEntity) { + Direction direction = accessor.getBlockState().getValue(CalibratedSculkSensorBlock.FACING).getOpposite(); + int signal = accessor.getLevel().getSignal(accessor.getPosition().relative(direction), direction); + data.putInt("Signal", signal); + } + } + + @Override + public ResourceLocation getUid() { + return MC_REDSTONE; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java new file mode 100644 index 0000000..eee70fa --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java @@ -0,0 +1,65 @@ +/* + * 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.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityReference; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.OwnableEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; +import org.leavesmc.leaves.protocol.jade.util.CommonUtil; + +import java.util.UUID; + +public enum AnimalOwnerProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_ANIMAL_OWNER = JadeProtocol.mc_id("animal_owner"); + + public static UUID getOwnerUUID(Entity entity) { + if (entity instanceof OwnableEntity ownableEntity) { + EntityReference reference = ownableEntity.getOwnerReference(); + if (reference != null) { + return reference.getUUID(); + } + } + return null; + } + + @Override + public String streamData(@NotNull EntityAccessor accessor) { + return CommonUtil.getLastKnownUsername(getOwnerUUID(accessor.getEntity())); + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.STRING_UTF8.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_ANIMAL_OWNER; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java new file mode 100644 index 0000000..be53df3 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java @@ -0,0 +1,61 @@ +/* + * 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.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.allay.Allay; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum MobBreedingProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_MOB_BREEDING = JadeProtocol.mc_id("mob_breeding"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = 0; + Entity entity = accessor.getEntity(); + if (entity instanceof Allay allay) { + if (allay.duplicationCooldown > 0 && allay.duplicationCooldown < Integer.MAX_VALUE) { + time = (int) allay.duplicationCooldown; + } + } else { + time = ((Animal) entity).getAge(); + } + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_MOB_BREEDING; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java new file mode 100644 index 0000000..bae9777 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java @@ -0,0 +1,60 @@ +/* + * 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.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.frog.Tadpole; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum MobGrowthProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_MOB_GROWTH = JadeProtocol.mc_id("mob_growth"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = -1; + Entity entity = accessor.getEntity(); + if (entity instanceof AgeableMob ageable) { + time = -ageable.getAge(); + } else if (entity instanceof Tadpole tadpole) { + time = tadpole.getTicksLeftUntilAdult(); + } + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + + @Override + public ResourceLocation getUid() { + return MC_MOB_GROWTH; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java new file mode 100644 index 0000000..dfb08a3 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java @@ -0,0 +1,59 @@ +/* + * 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.jade.provider.entity; + +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.ai.memory.MemoryModuleType; +import net.minecraft.world.entity.animal.Chicken; +import net.minecraft.world.entity.animal.armadillo.Armadillo; +import net.minecraft.world.entity.animal.sniffer.Sniffer; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +public enum NextEntityDropProvider implements IServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_NEXT_ENTITY_DROP = JadeProtocol.mc_id("next_entity_drop"); + + @Override + public void appendServerData(CompoundTag tag, @NotNull EntityAccessor accessor) { + int max = 24000 * 2; + if (accessor.getEntity() instanceof Chicken chicken) { + if (!chicken.isBaby() && chicken.eggTime < max) { + tag.putInt("NextEggIn", chicken.eggTime); + } + } else if (accessor.getEntity() instanceof Armadillo armadillo) { + if (!armadillo.isBaby() && armadillo.scuteTime < max) { + tag.putInt("NextScuteIn", armadillo.scuteTime); + } + } else if (accessor.getEntity() instanceof Sniffer sniffer) { + long time = sniffer.getBrain().getTimeUntilExpiry(MemoryModuleType.SNIFF_COOLDOWN); + if (time > 0 && time < max) { + tag.putInt("NextSniffIn", (int) time); + } + } + } + + @Override + public ResourceLocation getUid() { + return MC_NEXT_ENTITY_DROP; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java new file mode 100644 index 0000000..d4b5578 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java @@ -0,0 +1,52 @@ +/* + * 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.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum PetArmorProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_PET_ARMOR = JadeProtocol.mc_id("pet_armor"); + + @Nullable + @Override + public ItemStack streamData(@NotNull EntityAccessor accessor) { + ItemStack armor = ((Mob) accessor.getEntity()).getBodyArmorItem(); + return armor.isEmpty() ? null : armor; + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_PET_ARMOR; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java new file mode 100644 index 0000000..eeb0094 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.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.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.LivingEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +import java.util.List; + +public enum StatusEffectsProvider implements StreamServerDataProvider> { + INSTANCE; + + + private static final StreamCodec> STREAM_CODEC = ByteBufCodecs.list() + .apply(MobEffectInstance.STREAM_CODEC); + private static final ResourceLocation MC_POTION_EFFECTS = JadeProtocol.mc_id("potion_effects"); + + @Override + @Nullable + public List streamData(@NotNull EntityAccessor accessor) { + List effects = ((LivingEntity) accessor.getEntity()).getActiveEffects() + .stream() + .filter(MobEffectInstance::isVisible) + .toList(); + return effects.isEmpty() ? null : effects; + } + + @Override + public StreamCodec> streamCodec() { + return STREAM_CODEC; + } + + + @Override + public ResourceLocation getUid() { + return MC_POTION_EFFECTS; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java new file mode 100644 index 0000000..e3bfff1 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java @@ -0,0 +1,51 @@ +/* + * 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.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.monster.ZombieVillager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum ZombieVillagerProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_ZOMBIE_VILLAGER = JadeProtocol.mc_id("zombie_villager"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = ((ZombieVillager) accessor.getEntity()).villagerConversionTime; + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_ZOMBIE_VILLAGER; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java new file mode 100644 index 0000000..487d50d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java @@ -0,0 +1,54 @@ +/* + * 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.jade.tool; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.component.Tool; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.List; + +public class ShearsToolHandler { + + private static final ShearsToolHandler INSTANCE = new ShearsToolHandler(); + + private final List tools; + + public ShearsToolHandler() { + this.tools = List.of(Items.SHEARS.getDefaultInstance()); + } + + public static ShearsToolHandler getInstance() { + return INSTANCE; + } + + public ItemStack test(BlockState state) { + for (ItemStack toolItem : tools) { + if (toolItem.isCorrectToolForDrops(state)) { + return toolItem; + } + Tool tool = toolItem.get(DataComponents.TOOL); + if (tool != null && tool.getMiningSpeed(state) > tool.defaultMiningSpeed()) { + return toolItem; + } + } + return ItemStack.EMPTY; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java new file mode 100644 index 0000000..c294b69 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.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.jade.util; + +import com.mojang.authlib.GameProfile; +import com.mojang.logging.LogUtils; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.boss.EnderDragonPart; +import net.minecraft.world.entity.boss.enderdragon.EnderDragon; +import net.minecraft.world.level.block.entity.SkullBlockEntity; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; +import org.slf4j.Logger; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class CommonUtil { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static Entity wrapPartEntityParent(Entity target) { + if (target instanceof EnderDragonPart part) { + return part.parentMob; + } + return target; + } + + public static Entity getPartEntity(Entity parent, int index) { + if (parent == null) { + return null; + } + if (index < 0) { + return parent; + } + if (parent instanceof EnderDragon dragon) { + EnderDragonPart[] parts = dragon.getSubEntities(); + if (index < parts.length) { + return parts[index]; + } + } + return parent; + } + + + @Nullable + public static String getLastKnownUsername(@Nullable UUID uuid) { + if (uuid == null) { + return null; + } + Optional optional = SkullBlockEntity.fetchGameProfile(String.valueOf(uuid)).getNow(Optional.empty()); + return optional.map(GameProfile::getName).orElse(null); + } + + + public static Map.Entry>> getServerExtensionData( + Accessor accessor, + WrappedHierarchyLookup> lookup) { + for (var provider : lookup.wrappedGet(accessor)) { + List> groups; + try { + groups = provider.getGroups(accessor); + } catch (Exception e) { + LOGGER.warn(e.toString()); + continue; + } + if (groups != null) { + return Map.entry(provider.getUid(), groups); + } + } + return null; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java new file mode 100644 index 0000000..592b4ed --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java @@ -0,0 +1,147 @@ +/* + * 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.jade.util; + +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.*; +import com.mojang.logging.LogUtils; +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; +import org.slf4j.Logger; + +import java.util.*; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +public class HierarchyLookup implements IHierarchyLookup { + private static final Logger LOGGER = LogUtils.getLogger(); + private final Class baseClass; + private final Cache, List> resultCache = CacheBuilder.newBuilder().build(); + private final boolean singleton; + protected boolean idMapped; + @Nullable + protected IdMapper idMapper; + private ListMultimap, T> objects = ArrayListMultimap.create(); + + public HierarchyLookup(Class baseClass) { + this(baseClass, false); + } + + public HierarchyLookup(Class baseClass, boolean singleton) { + this.baseClass = baseClass; + this.singleton = singleton; + } + + @Override + public void idMapped() { + this.idMapped = true; + } + + @Override + @Nullable + public IdMapper idMapper() { + return idMapper; + } + + @Override + public void register(Class clazz, T provider) { + Preconditions.checkArgument(isClassAcceptable(clazz), "Class %s is not acceptable", clazz); + Objects.requireNonNull(provider.getUid()); + JadeProtocol.priorities.put(provider); + objects.put(clazz, provider); + } + + @Override + public boolean isClassAcceptable(Class clazz) { + return baseClass.isAssignableFrom(clazz); + } + + @Override + public List get(Class clazz) { + try { + return resultCache.get(clazz, () -> { + List list = Lists.newArrayList(); + getInternal(clazz, list); + list = ImmutableList.sortedCopyOf(COMPARATOR, list); + if (singleton && !list.isEmpty()) { + return ImmutableList.of(list.getFirst()); + } + return list; + }); + } catch (ExecutionException e) { + LOGGER.warn("HierarchyLookup error", e); + } + return List.of(); + } + + private void getInternal(Class clazz, List list) { + if (clazz != baseClass && clazz != Object.class) { + getInternal(clazz.getSuperclass(), list); + } + list.addAll(objects.get(clazz)); + } + + @Override + public boolean isEmpty() { + return objects.isEmpty(); + } + + @Override + public Stream, Collection>> entries() { + return objects.asMap().entrySet().stream(); + } + + @Override + public void invalidate() { + resultCache.invalidateAll(); + } + + @Override + public void loadComplete(PriorityStore priorityStore) { + objects.asMap().forEach((clazz, list) -> { + if (list.size() < 2) { + return; + } + Set set = Sets.newHashSetWithExpectedSize(list.size()); + for (T provider : list) { + if (set.contains(provider.getUid())) { + throw new IllegalStateException("Duplicate UID: %s for %s".formatted(provider.getUid(), list.stream() + .filter(p -> p.getUid().equals(provider.getUid())) + .map(p -> p.getClass().getName()) + .toList() + )); + } + set.add(provider.getUid()); + } + }); + + objects = ImmutableListMultimap., T>builder() + .orderValuesBy(Comparator.comparingInt(priorityStore::byValue)) + .putAll(objects) + .build(); + + if (idMapped) { + idMapper = createIdMapper(); + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java new file mode 100644 index 0000000..7279f46 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java @@ -0,0 +1,84 @@ +/* + * 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.jade.util; + +import com.google.common.collect.Streams; +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +import java.util.*; +import java.util.stream.Stream; + +public interface IHierarchyLookup { + + Comparator COMPARATOR = Comparator.comparingInt(provider -> JadeProtocol.priorities.byValue(provider)); + + default IHierarchyLookup cast() { + return this; + } + + void idMapped(); + + @Nullable + IdMapper idMapper(); + + default List mappedIds() { + return Streams.stream(Objects.requireNonNull(idMapper())) + .map(IJadeProvider::getUid) + .toList(); + } + + void register(Class clazz, T provider); + + boolean isClassAcceptable(Class clazz); + + default List get(Object obj) { + if (obj == null) { + return List.of(); + } + return get(obj.getClass()); + } + + List get(Class clazz); + + boolean isEmpty(); + + Stream, Collection>> entries(); + + void invalidate(); + + void loadComplete(PriorityStore priorityStore); + + default IdMapper createIdMapper() { + List list = entries().flatMap(entry -> entry.getValue().stream()).toList(); + IdMapper idMapper = idMapper(); + if (idMapper == null) { + idMapper = new IdMapper<>(list.size()); + } + for (T provider : list) { + if (idMapper.getId(provider) == IdMapper.DEFAULT) { + idMapper.add(provider); + } + } + return idMapper; + } +} + diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java new file mode 100644 index 0000000..210c860 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java @@ -0,0 +1,138 @@ +/* + * 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.jade.util; + +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.item.component.TooltipDisplay; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; + +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; + +public class ItemCollector { + public static final int MAX_SIZE = 54; + public static final ItemCollector EMPTY = new ItemCollector<>(null); + private static final Predicate SHOWN = stack -> { + if (stack.isEmpty()) { + return false; + } + if (stack.getOrDefault(DataComponents.TOOLTIP_DISPLAY, TooltipDisplay.DEFAULT).hideTooltip()) { + return false; + } + if (stack.hasNonDefault(DataComponents.CUSTOM_MODEL_DATA) || stack.hasNonDefault(DataComponents.ITEM_MODEL)) { + CompoundTag tag = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY).copyTag(); + for (String key : tag.keySet()) { + if (key.toLowerCase(Locale.ENGLISH).endsWith("clear") && tag.getBooleanOr(key, true)) { + return false; + } + } + } + return true; + }; + private final Object2IntLinkedOpenHashMap items = new Object2IntLinkedOpenHashMap<>(); + private final ItemIterator iterator; + public long version; + public long lastTimeFinished; + public boolean lastTimeIsEmpty; + public List> mergedResult; + + public ItemCollector(ItemIterator iterator) { + this.iterator = iterator; + } + + public List> update(Accessor request) { + if (iterator == null) { + return null; + } + T container = iterator.find(request.getTarget()); + if (container == null) { + return null; + } + long currentVersion = iterator.getVersion(container); + long gameTime = request.getLevel().getServer().getTickCount(); + if (mergedResult != null && iterator.isFinished()) { + if (version == currentVersion) { + return mergedResult; // content not changed + } + if (lastTimeFinished + 5 > gameTime) { + return mergedResult; // avoid update too frequently + } + iterator.reset(); + } + AtomicInteger count = new AtomicInteger(); + iterator.populate(container).forEach(stack -> { + count.incrementAndGet(); + if (SHOWN.test(stack)) { + ItemDefinition def = new ItemDefinition(stack); + items.addTo(def, stack.getCount()); + } + }); + iterator.afterPopulate(container, count.get()); + if (mergedResult != null && !iterator.isFinished()) { + updateCollectingProgress(mergedResult.getFirst()); + return mergedResult; + } + List partialResult = items.object2IntEntrySet().stream().limit(MAX_SIZE).map(entry -> { + ItemDefinition def = entry.getKey(); + return def.toStack(entry.getIntValue()); + }).toList(); + List> groups = List.of(updateCollectingProgress(new ViewGroup<>(partialResult))); + if (iterator.isFinished()) { + mergedResult = groups; + lastTimeIsEmpty = mergedResult.getFirst().views.isEmpty(); + version = currentVersion; + lastTimeFinished = gameTime; + items.clear(); + } + return groups; + } + + protected ViewGroup updateCollectingProgress(ViewGroup group) { + if (lastTimeIsEmpty && group.views.isEmpty()) { + return group; + } + float progress = iterator.getCollectingProgress(); + CompoundTag data = group.getExtraData(); + if (Float.isNaN(progress) || progress >= 1) { + data.remove("Collecting"); + } else { + data.putFloat("Collecting", progress); + } + return group; + } + + public record ItemDefinition(Item item, DataComponentPatch components) { + ItemDefinition(ItemStack stack) { + this(stack.getItem(), stack.getComponentsPatch()); + } + + public ItemStack toStack(int count) { + ItemStack itemStack = new ItemStack(item, count); + itemStack.applyComponents(components); + return itemStack; + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java new file mode 100644 index 0000000..5245a7e --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java @@ -0,0 +1,119 @@ +/* + * 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.jade.util; + +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public abstract class ItemIterator { + public static final AtomicLong version = new AtomicLong(); + protected final Function containerFinder; + protected final int fromIndex; + protected boolean finished; + protected int currentIndex; + protected float progress; + + protected ItemIterator(Function containerFinder, int fromIndex) { + this.containerFinder = containerFinder; + this.currentIndex = this.fromIndex = fromIndex; + } + + public @Nullable T find(Object target) { + return containerFinder.apply(target); + } + + public final boolean isFinished() { + return finished; + } + + public long getVersion(T container) { + return version.getAndIncrement(); + } + + public abstract Stream populate(T container); + + protected abstract int getSlotCount(T container); + + public void reset() { + currentIndex = fromIndex; + finished = false; + } + + public void afterPopulate(T container, int count) { + currentIndex += count; + if (count == 0 || currentIndex >= 10000) { + finished = true; + } + progress = (float) (currentIndex - fromIndex) / (getSlotCount(container) - fromIndex); + } + + public float getCollectingProgress() { + return Float.NaN; + } + + public static abstract class SlottedItemIterator extends ItemIterator { + + public SlottedItemIterator(Function containerFinder, int fromIndex) { + super(containerFinder, fromIndex); + } + + protected abstract ItemStack getItemInSlot(T container, int slot); + + @Override + public Stream populate(T container) { + int slotCount = getSlotCount(container); + int toIndex = currentIndex + ItemCollector.MAX_SIZE * 2; + if (toIndex >= slotCount) { + toIndex = slotCount; + finished = true; + } + return IntStream.range(currentIndex, toIndex).mapToObj(slot -> getItemInSlot(container, slot)); + } + + @Override + public float getCollectingProgress() { + return progress; + } + } + + public static class ContainerItemIterator extends SlottedItemIterator { + public ContainerItemIterator(int fromIndex) { + this(Container.class::cast, fromIndex); + } + + public ContainerItemIterator(Function containerFinder, int fromIndex) { + super(containerFinder, fromIndex); + } + + @Override + protected int getSlotCount(Container container) { + return container.getContainerSize(); + } + + @Override + protected ItemStack getItemInSlot(Container container, int slot) { + return container.getItem(slot); + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java new file mode 100644 index 0000000..31f6658 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java @@ -0,0 +1,77 @@ +/* + * 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.jade.util; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.NotNull; + +public class JadeCodec { + public static final StreamCodec PRIMITIVE_STREAM_CODEC = new StreamCodec<>() { + @Override + public @NotNull Object decode(@NotNull ByteBuf buf) { + byte b = buf.readByte(); + if (b == 0) { + return false; + } else if (b == 1) { + return true; + } else if (b == 2) { + return ByteBufCodecs.VAR_INT.decode(buf); + } else if (b == 3) { + return ByteBufCodecs.FLOAT.decode(buf); + } else if (b == 4) { + return ByteBufCodecs.STRING_UTF8.decode(buf); + } else if (b > 20) { + return b - 20; + } + throw new IllegalArgumentException("Unknown primitive type: " + b); + } + + @Override + public void encode(@NotNull ByteBuf buf, @NotNull Object o) { + switch (o) { + case Boolean b -> buf.writeByte(b ? 1 : 0); + case Number n -> { + float f = n.floatValue(); + if (f != (int) f) { + buf.writeByte(3); + ByteBufCodecs.FLOAT.encode(buf, f); + } + int i = n.intValue(); + if (i <= Byte.MAX_VALUE - 20 && i >= 0) { + buf.writeByte(i + 20); + } else { + ByteBufCodecs.VAR_INT.encode(buf, i); + } + } + case String s -> { + buf.writeByte(4); + ByteBufCodecs.STRING_UTF8.encode(buf, s); + } + case Enum anEnum -> { + buf.writeByte(4); + ByteBufCodecs.STRING_UTF8.encode(buf, anEnum.name()); + } + case null -> throw new NullPointerException(); + default -> + throw new IllegalArgumentException("Unknown primitive type: %s (%s)".formatted(o, o.getClass())); + } + } + }; +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java new file mode 100644 index 0000000..d359f4c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java @@ -0,0 +1,126 @@ +/* + * 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.jade.util; + +import com.google.common.collect.Lists; +import net.minecraft.advancements.critereon.ItemPredicate; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.storage.loot.LootPool; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraft.world.level.storage.loot.entries.AlternativesEntry; +import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; +import net.minecraft.world.level.storage.loot.entries.NestedLootTable; +import net.minecraft.world.level.storage.loot.predicates.AnyOfCondition; +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; +import net.minecraft.world.level.storage.loot.predicates.MatchTool; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.tool.ShearsToolHandler; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +public class LootTableMineableCollector { + + private final HolderGetter lootRegistry; + private final ItemStack toolItem; + + public LootTableMineableCollector(HolderGetter lootRegistry, ItemStack toolItem) { + this.lootRegistry = lootRegistry; + this.toolItem = toolItem; + } + + public static @NotNull List execute(HolderGetter lootRegistry, ItemStack toolItem) { + LootTableMineableCollector collector = new LootTableMineableCollector(lootRegistry, toolItem); + List list = Lists.newArrayList(); + for (Block block : BuiltInRegistries.BLOCK) { + if (!ShearsToolHandler.getInstance().test(block.defaultBlockState()).isEmpty()) { + continue; + } + + if (block.getLootTable().isPresent()) { + LootTable lootTable = lootRegistry.get(block.getLootTable().get()).map(Holder::value).orElse(null); + if (collector.doLootTable(lootTable)) { + list.add(block); + } + } + } + return list; + } + + public static boolean isCorrectConditions(@NotNull List conditions, ItemStack toolItem) { + if (conditions.size() != 1) { + return false; + } + + LootItemCondition condition = conditions.getFirst(); + if (condition instanceof MatchTool(Optional predicate)) { + ItemPredicate itemPredicate = predicate.orElse(null); + return itemPredicate != null && itemPredicate.test(toolItem); + } else if (condition instanceof AnyOfCondition anyOfCondition) { + for (LootItemCondition child : anyOfCondition.terms) { + if (isCorrectConditions(List.of(child), toolItem)) { + return true; + } + } + } + return false; + } + + private boolean doLootTable(LootTable lootTable) { + if (lootTable == null || lootTable == LootTable.EMPTY) { + return false; + } + + for (LootPool pool : lootTable.pools) { + if (doLootPool(pool)) { + return true; + } + } + return false; + } + + private boolean doLootPool(@NotNull LootPool lootPool) { + for (LootPoolEntryContainer entry : lootPool.entries) { + if (doLootPoolEntry(entry)) { + return true; + } + } + return false; + } + + private boolean doLootPoolEntry(LootPoolEntryContainer entry) { + if (entry instanceof AlternativesEntry alternativesEntry) { + for (LootPoolEntryContainer child : alternativesEntry.children) { + if (doLootPoolEntry(child)) { + return true; + } + } + } else if (entry instanceof NestedLootTable nestedLootTable) { + LootTable lootTable = nestedLootTable.contents.map($ -> lootRegistry.get($).map(Holder::value).orElse(null), Function.identity()); + return doLootTable(lootTable); + } else { + return isCorrectConditions(entry.conditions, toolItem); + } + return false; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java new file mode 100644 index 0000000..2d8ca38 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.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.jade.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import com.mojang.logging.LogUtils; +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; +import org.slf4j.Logger; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +public class PairHierarchyLookup implements IHierarchyLookup { + private static final Logger LOGGER = LogUtils.getLogger(); + public final IHierarchyLookup first; + public final IHierarchyLookup second; + private final Cache, Class>, List> mergedCache = CacheBuilder.newBuilder().build(); + protected boolean idMapped; + @Nullable + protected IdMapper idMapper; + + public PairHierarchyLookup(IHierarchyLookup first, IHierarchyLookup second) { + this.first = first; + this.second = second; + } + + @SuppressWarnings("unchecked") + public List getMerged(Object first, Object second) { + Objects.requireNonNull(first); + Objects.requireNonNull(second); + try { + return (List) mergedCache.get(Pair.of(first.getClass(), second.getClass()), () -> { + List firstList = this.first.get(first); + List secondList = this.second.get(second); + if (firstList.isEmpty()) { + return secondList; + } else if (secondList.isEmpty()) { + return firstList; + } + return ImmutableList.sortedCopyOf(COMPARATOR, Iterables.concat(firstList, secondList)); + }); + } catch (ExecutionException e) { + LOGGER.warn(e.toString()); + } + return List.of(); + } + + @Override + public void idMapped() { + idMapped = true; + } + + @Override + public @Nullable IdMapper idMapper() { + return idMapper; + } + + @Override + public void register(Class clazz, T provider) { + if (first.isClassAcceptable(clazz)) { + first.register(clazz, provider); + } else if (second.isClassAcceptable(clazz)) { + second.register(clazz, provider); + } else { + throw new IllegalArgumentException("Class " + clazz + " is not acceptable"); + } + } + + @Override + public boolean isClassAcceptable(Class clazz) { + return first.isClassAcceptable(clazz) || second.isClassAcceptable(clazz); + } + + @Override + public List get(Class clazz) { + List result = first.get(clazz); + if (result.isEmpty()) { + result = second.get(clazz); + } + return result; + } + + @Override + public boolean isEmpty() { + return first.isEmpty() && second.isEmpty(); + } + + @Override + public Stream, Collection>> entries() { + return Stream.concat(first.entries(), second.entries()); + } + + @Override + public void invalidate() { + first.invalidate(); + second.invalidate(); + mergedCache.invalidateAll(); + } + + @Override + public void loadComplete(PriorityStore priorityStore) { + first.loadComplete(priorityStore); + second.loadComplete(priorityStore); + if (idMapped) { + idMapper = createIdMapper(); + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java new file mode 100644 index 0000000..595bede --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java @@ -0,0 +1,57 @@ +/* + * 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.jade.util; + +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; + +import java.util.Objects; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +public class PriorityStore { + + private final Object2IntMap priorities = new Object2IntLinkedOpenHashMap<>(); + private final Function keyGetter; + private final ToIntFunction defaultPriorityGetter; + + public PriorityStore(ToIntFunction defaultPriorityGetter, Function keyGetter) { + this.defaultPriorityGetter = defaultPriorityGetter; + this.keyGetter = keyGetter; + } + + public void put(V provider) { + Objects.requireNonNull(provider); + put(provider, defaultPriorityGetter.applyAsInt(provider)); + } + + public void put(V provider, int priority) { + Objects.requireNonNull(provider); + K uid = keyGetter.apply(provider); + Objects.requireNonNull(uid); + priorities.put(uid, priority); + } + + public int byValue(V value) { + return byKey(keyGetter.apply(value)); + } + + public int byKey(K id) { + return priorities.getInt(id); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java new file mode 100644 index 0000000..9133b9c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java @@ -0,0 +1,75 @@ +/* + * 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.jade.util; + +import io.netty.buffer.ByteBuf; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class ViewGroup { + public List views; + @Nullable + public String id; + @Nullable + protected CompoundTag extraData; + + public ViewGroup(List views) { + this(views, Optional.empty(), Optional.empty()); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public ViewGroup(List views, Optional id, Optional extraData) { + this.views = views; + this.id = id.orElse(null); + this.extraData = extraData.orElse(null); + } + + public static StreamCodec> codec(StreamCodec viewCodec) { + return StreamCodec.composite( + ByteBufCodecs.list().apply(viewCodec), + $ -> $.views, + ByteBufCodecs.optional(ByteBufCodecs.STRING_UTF8), + $ -> Optional.ofNullable($.id), + ByteBufCodecs.optional(ByteBufCodecs.COMPOUND_TAG), + $ -> Optional.ofNullable($.extraData), + ViewGroup::new); + } + + public static StreamCodec>>> listCodec(StreamCodec viewCodec) { + return StreamCodec.composite( + ResourceLocation.STREAM_CODEC, + Map.Entry::getKey, + ByteBufCodecs.>list().apply(codec(viewCodec)), + Map.Entry::getValue, + Map::entry); + } + + public CompoundTag getExtraData() { + if (extraData == null) { + extraData = new CompoundTag(); + } + return extraData; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java new file mode 100644 index 0000000..66d8427 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java @@ -0,0 +1,124 @@ +/* + * 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.jade.util; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.Block; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; + +public class WrappedHierarchyLookup extends HierarchyLookup { + public final List, Function, @Nullable Object>>> overrides = Lists.newArrayList(); + private boolean empty = true; + + public WrappedHierarchyLookup() { + super(Object.class); + } + + @NotNull + public static WrappedHierarchyLookup forAccessor() { + WrappedHierarchyLookup lookup = new WrappedHierarchyLookup<>(); + lookup.overrides.add(Pair.of( + new HierarchyLookup<>(Block.class), accessor -> { + if (accessor instanceof BlockAccessor blockAccessor) { + return blockAccessor.getBlock(); + } + return null; + })); + return lookup; + } + + public List wrappedGet(Accessor accessor) { + Set set = Sets.newLinkedHashSet(); + for (var override : overrides) { + Object o = override.getRight().apply(accessor); + if (o != null) { + set.addAll(override.getLeft().get(o)); + } + } + set.addAll(get(accessor.getTarget())); + return ImmutableList.sortedCopyOf(COMPARATOR, set); + } + + @Override + public void register(Class clazz, T provider) { + for (var override : overrides) { + if (override.getLeft().isClassAcceptable(clazz)) { + override.getLeft().register(clazz, provider); + empty = false; + return; + } + } + super.register(clazz, provider); + empty = false; + } + + @Override + public boolean isClassAcceptable(Class clazz) { + for (var override : overrides) { + if (override.getLeft().isClassAcceptable(clazz)) { + return true; + } + } + return super.isClassAcceptable(clazz); + } + + @Override + public void invalidate() { + for (var override : overrides) { + override.getLeft().invalidate(); + } + super.invalidate(); + } + + @Override + public void loadComplete(PriorityStore priorityStore) { + for (var override : overrides) { + override.getLeft().loadComplete(priorityStore); + } + super.loadComplete(priorityStore); + } + + @Override + public boolean isEmpty() { + return empty; + } + + @Override + public Stream, Collection>> entries() { + Stream, Collection>> stream = super.entries(); + for (var override : overrides) { + stream = Stream.concat(stream, override.getLeft().entries()); + } + return stream; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java new file mode 100644 index 0000000..14fafe9 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/PacketTransformer.java @@ -0,0 +1,163 @@ +/* + * 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.rei; + +import com.mojang.logging.LogUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.custom.DiscardedPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.slf4j.Logger; + +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.function.BiConsumer; + +public class PacketTransformer { + + private static final Logger LOGGER = LogUtils.getLogger(); + + private static final byte START = 0x0; + private static final byte PART = 0x1; + private static final byte END = 0x2; + private static final byte ONLY = 0x3; + + private final Map cache = Collections.synchronizedMap(new HashMap<>()); + + public static DiscardedPayload wrapRei(ResourceLocation location, FriendlyByteBuf buf) { + FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); + newBuf.writeByteArray(ByteBufUtil.getBytes(buf)); + return new DiscardedPayload(location, ByteBufUtil.getBytes(newBuf)); + } + + public void inbound(ResourceLocation id, RegistryFriendlyByteBuf buf, ServerPlayer player, BiConsumer consumer) { + UUID key = player.getUUID(); + PartData data; + buf.readVarInt(); + switch (buf.readByte()) { + case START -> { + int partsNum = buf.readInt(); + data = new PartData(id, partsNum); + if (cache.put(key, data) != null) { + LOGGER.warn("Received invalid START packet for SplitPacketTransformer with packet id " + id); + } + buf.retain(); + data.parts.add(buf); + } + case PART -> { + if ((data = cache.get(key)) == null) { + LOGGER.warn("Received invalid PART packet for SplitPacketTransformer with packet id " + id); + buf.release(); + } else if (!data.id.equals(id)) { + LOGGER.warn("Received invalid PART packet for SplitPacketTransformer with packet id " + id + ", id in cache is {}" + data.id); + buf.release(); + for (RegistryFriendlyByteBuf part : data.parts) { + if (part != buf) { + part.release(); + } + } + cache.remove(key); + } else { + buf.retain(); + data.parts.add(buf); + } + } + case END -> { + if ((data = cache.get(key)) == null) { + LOGGER.warn("Received invalid END packet for SplitPacketTransformer with packet id {}" + id); + buf.release(); + } else if (!data.id.equals(id)) { + LOGGER.warn("Received invalid END packet for SplitPacketTransformer with packet id " + id + ", id in cache is {}" + data.id); + buf.release(); + for (RegistryFriendlyByteBuf part : data.parts) { + if (part != buf) { + part.release(); + } + } + cache.remove(key); + } else { + buf.retain(); + data.parts.add(buf); + } + if (data == null) { + return; + } + if (data.parts.size() != data.partsNum) { + LOGGER.warn("Received invalid END packet for SplitPacketTransformer with packet id " + id + " with size " + data.parts + ", parts expected is {}" + data.partsNum); + for (RegistryFriendlyByteBuf part : data.parts) { + if (part != buf) { + part.release(); + } + } + } else { + RegistryFriendlyByteBuf byteBuf = new RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(data.parts.toArray(new ByteBuf[0])), buf.registryAccess()); + consumer.accept(data.id, byteBuf); + byteBuf.release(); + } + cache.remove(key); + } + case ONLY -> consumer.accept(id, buf); + default -> throw new IllegalStateException("Illegal split packet header!"); + } + } + + public void outbound(ResourceLocation id, RegistryFriendlyByteBuf buf, BiConsumer consumer) { + int maxSize = 1048576 - 1 - 20 - id.toString().getBytes(StandardCharsets.UTF_8).length; + if (buf.readableBytes() <= maxSize) { + ByteBuf stateBuf = Unpooled.buffer(1); + stateBuf.writeByte(ONLY); + RegistryFriendlyByteBuf packetBuffer = new RegistryFriendlyByteBuf(Unpooled.wrappedBuffer(stateBuf, buf), buf.registryAccess()); + consumer.accept(id, packetBuffer); + } else { + int partSize = maxSize - 4; + int parts = (int) Math.ceil(buf.readableBytes() / (float) partSize); + for (int i = 0; i < parts; i++) { + RegistryFriendlyByteBuf packetBuffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), buf.registryAccess()); + if (i == 0) { + packetBuffer.writeByte(START); + packetBuffer.writeInt(parts); + } else if (i == parts - 1) { + packetBuffer.writeByte(END); + } else { + packetBuffer.writeByte(PART); + } + int next = Math.min(buf.readableBytes(), partSize); + packetBuffer.writeBytes(buf.retainedSlice(buf.readerIndex(), next)); + buf.skipBytes(next); + consumer.accept(id, packetBuffer); + } + buf.release(); + } + } + + private static class PartData { + private final ResourceLocation id; + private final int partsNum; + private final List parts; + + public PartData(ResourceLocation id, int partsNum) { + this.id = id; + this.partsNum = partsNum; + this.parts = new ArrayList<>(); + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java new file mode 100644 index 0000000..d9d8526 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/REIServerProtocol.java @@ -0,0 +1,432 @@ +/* + * 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.rei; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.mojang.logging.LogUtils; +import fun.bm.lophine.config.modules.function.protocol.REIServerProtocolConfig; +import io.netty.buffer.Unpooled; +import net.minecraft.ChatFormatting; +import net.minecraft.Util; +import net.minecraft.core.RegistryAccess; +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.RegistryFriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.*; +import org.bukkit.Bukkit; +import org.bukkit.permissions.Permission; +import org.bukkit.permissions.PermissionDefault; +import org.bukkit.plugin.PluginManager; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.plugin.MinecraftInternalPlugin; +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.rei.display.*; +import org.leavesmc.leaves.protocol.rei.payload.DisplaySyncPayload; +import org.leavesmc.leaves.protocol.rei.transfer.InputSlotCrafter; +import org.leavesmc.leaves.protocol.rei.transfer.NewInputSlotCrafter; +import org.leavesmc.leaves.protocol.rei.transfer.slot.PlayerInventorySlotAccessor; +import org.leavesmc.leaves.protocol.rei.transfer.slot.SlotAccessor; +import org.leavesmc.leaves.protocol.rei.transfer.slot.VanillaSlotAccessor; +import org.slf4j.Logger; + +import java.util.*; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.function.BiConsumer; + +@LeavesProtocol.Register(namespace = REIServerProtocol.PROTOCOL_ID) +public class REIServerProtocol implements LeavesProtocol { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final String PROTOCOL_ID = "roughlyenoughitems"; + public static final String CHEAT_PERMISSION = "leaves.protocol.rei.cheat"; + public static final ResourceLocation DELETE_ITEMS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "delete_item"); + public static final ResourceLocation CREATE_ITEMS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "create_item"); + public static final ResourceLocation CREATE_ITEMS_HOTBAR_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "create_item_hotbar"); + public static final ResourceLocation CREATE_ITEMS_GRAB_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "create_item_grab"); + public static final ResourceLocation CREATE_ITEMS_MESSAGE_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "ci_msg"); + public static final ResourceLocation MOVE_ITEMS_NEW_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "move_items_new"); + public static final ResourceLocation NOT_ENOUGH_ITEMS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "og_not_enough"); // this pack is under to-do at rei-client, so we don't handle it + public static final ResourceLocation SYNC_DISPLAYS_PACKET = ResourceLocation.fromNamespaceAndPath("roughlyenoughitems", "sync_displays"); + + public static final Map TRANSFORMERS = Util.make(() -> { + ImmutableMap.Builder builder = ImmutableMap.builder(); + builder.put(SYNC_DISPLAYS_PACKET, new PacketTransformer()); + builder.put(DELETE_ITEMS_PACKET, new PacketTransformer()); + builder.put(CREATE_ITEMS_PACKET, new PacketTransformer()); + builder.put(CREATE_ITEMS_GRAB_PACKET, new PacketTransformer()); + builder.put(CREATE_ITEMS_HOTBAR_PACKET, new PacketTransformer()); + builder.put(MOVE_ITEMS_NEW_PACKET, new PacketTransformer()); + return builder.build(); + }); + private static final Set enabledPlayers = new HashSet<>(); + private static final Executor executor = new ThreadPoolExecutor( + 1, 1, 0L, TimeUnit.MILLISECONDS, + new ArrayBlockingQueue<>(1), + new ThreadPoolExecutor.DiscardOldestPolicy() + ); + private static int minecraftRecipeVer = 0; + private static int nextReiRecipeVer = -1; + private static ImmutableList cachedPayloads; + + @ProtocolHandler.ReloadDataPack + public static void onRecipeReload() { + minecraftRecipeVer = MinecraftServer.getServer().getTickCount(); + } + + @Contract("_ -> new") + public static ResourceLocation id(String path) { + return ResourceLocation.tryBuild(PROTOCOL_ID, path); + } + + public static void onConfigModify(boolean enabled) { + PluginManager pluginManager = Bukkit.getServer().getPluginManager(); + if (enabled) { + if (pluginManager.getPermission(CHEAT_PERMISSION) == null) { + pluginManager.addPermission(new Permission(CHEAT_PERMISSION, PermissionDefault.OP)); + } + } else { + pluginManager.removePermission(CHEAT_PERMISSION); + enabledPlayers.clear(); + } + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { + enabledPlayers.remove(player); + } + + @ProtocolHandler.Ticker + public static void tick() { + if (minecraftRecipeVer != nextReiRecipeVer) { + nextReiRecipeVer = minecraftRecipeVer; + executor.execute(() -> reloadRecipe(nextReiRecipeVer)); + } + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + private static void reloadRecipe(int reiRecipeVer) { + ImmutableList.Builder builder = ImmutableList.builder(); + MinecraftServer server = MinecraftServer.getServer(); + RecipeMap recipeMap = server.getRecipeManager().recipes; + recipeMap.byType(RecipeType.CRAFTING).forEach(holder -> { + switch (holder.value()) { + case ShapedRecipe ignored -> builder.add(new ShapedDisplay((RecipeHolder) holder)); + case ShapelessRecipe ignored -> builder.add(new ShapelessDisplay((RecipeHolder) holder)); + case TransmuteRecipe ignored -> builder.addAll(Display.ofTransmuteRecipe((RecipeHolder) holder)); + case TippedArrowRecipe ignored -> builder.addAll(Display.ofTippedArrowRecipe((RecipeHolder) holder)); + case FireworkRocketRecipe ignored -> + builder.addAll(Display.ofFireworkRocketRecipe((RecipeHolder) holder)); + case MapCloningRecipe ignored -> builder.addAll(Display.ofMapCloningRecipe((RecipeHolder) holder)); + // ignore ArmorDyeRecipe, BannerDuplicateRecipe, BookCloningRecipe, ShieldDecorationRecipe + default -> { + } + } + }); + recipeMap.byType(RecipeType.STONECUTTING).forEach(holder -> builder.add(new StoneCuttingDisplay(holder))); + recipeMap.byType(RecipeType.SMELTING).forEach(holder -> builder.add(new SmeltingDisplay(holder))); + recipeMap.byType(RecipeType.BLASTING).forEach(holder -> builder.add(new BlastingDisplay(holder))); + recipeMap.byType(RecipeType.SMOKING).forEach(holder -> builder.add(new SmokingDisplay(holder))); + recipeMap.byType(RecipeType.CAMPFIRE_COOKING).forEach(holder -> builder.add(new CampfireDisplay(holder))); + recipeMap.byType(RecipeType.SMITHING).forEach(holder -> { + switch (holder.value()) { + case SmithingTrimRecipe ignored -> builder.addAll(Display.ofSmithingTrimRecipe((RecipeHolder) holder)); + case SmithingTransformRecipe ignored -> builder.add(Display.ofTransforming((RecipeHolder) holder)); + default -> { + } + } + }); + + DisplaySyncPayload displaySyncPayload = new DisplaySyncPayload( + DisplaySyncPayload.SyncType.SET, + builder.build(), + reiRecipeVer + ); + + RegistryFriendlyByteBuf s2cBuf = ProtocolUtils.decorate(Unpooled.buffer()); + DisplaySyncPayload.STREAM_CODEC.encode(s2cBuf, displaySyncPayload); + ImmutableList.Builder listBuilder = ImmutableList.builder(); + outboundTransform(s2cBuf, (id, splitBuf) -> + listBuilder.add(PacketTransformer.wrapRei(id, splitBuf)) + ); + + cachedPayloads = listBuilder.build(); + Bukkit.getGlobalRegionScheduler().run(MinecraftInternalPlugin.INSTANCE, (task) -> { + for (ServerPlayer player : enabledPlayers) { + for (CustomPacketPayload payload : cachedPayloads) { + ProtocolUtils.sendPayloadPacket(player, payload); + } + } + }); + } + + @ProtocolHandler.MinecraftRegister(onlyNamespace = true, stage = ProtocolHandler.Stage.GAME) + public static void onPlayerSubscribed(@NotNull ServerPlayer player, ResourceLocation location) { + enabledPlayers.add(player); + String channel = location.getPath(); + if (channel.equals("sync_displays")) { + if (cachedPayloads != null) { + cachedPayloads.forEach(payload -> ProtocolUtils.sendPayloadPacket(player, payload)); + } + } else if (channel.equals("ci_msg")) { + // cheat rei-client into using "delete_item" packet + if (MinecraftServer.getServer().getProfilePermissions(player.getGameProfile()) < 1) { + player.getBukkitEntity().sendOpLevel((byte) 1); + } + } + } + + @ProtocolHandler.BytebufReceiver(key = "delete_item") + public static void handleDeleteItem(ServerPlayer player, RegistryFriendlyByteBuf buf) { + if (!hasCheatPermission(player)) { + return; + } + + inboundTransform(player, DELETE_ITEMS_PACKET, buf, (id, wholeBuf) -> { + AbstractContainerMenu menu = player.containerMenu; + if (!menu.getCarried().isEmpty()) { + menu.setCarried(ItemStack.EMPTY); + menu.broadcastChanges(); + } + }); + } + + @ProtocolHandler.BytebufReceiver(key = "create_item") + public static void handleCreateItem(ServerPlayer player, RegistryFriendlyByteBuf buf) { + if (!hasCheatPermission(player)) { + return; + } + BiConsumer consumer = (ignored, c2sWholeBuf) -> { + FriendlyByteBuf tmpBuf = new FriendlyByteBuf(Unpooled.buffer()).writeBytes(c2sWholeBuf.readByteArray()); + ItemStack itemStack = tmpBuf.readLenientJsonWithCodec(ItemStack.OPTIONAL_CODEC); + if (player.getInventory().add(itemStack.copy())) { + RegistryFriendlyByteBuf s2cWholeBuf = ProtocolUtils.decorate(Unpooled.buffer()); + s2cWholeBuf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, itemStack.copy()); + s2cWholeBuf.writeUtf(player.getScoreboardName(), 32767); + // Due to the bug in REI, no packets are actually sent here. + /* + outboundTransform(CREATE_ITEMS_MESSAGE_PACKET, s2cWholeBuf, (id, s2cSplitBuf) -> { + ProtocolUtils.sendPayloadPacket(player, new BufCustomPacketPayload(new CustomPacketPayload.Type<>(id), ByteBufUtil.getBytes(s2cSplitBuf))); + }); + */ + } else { + player.displayClientMessage(Component.translatable("text.rei.failed_cheat_items"), false); + } + }; + inboundTransform(player, CREATE_ITEMS_PACKET, buf, consumer); + } + + @ProtocolHandler.BytebufReceiver(key = "create_item_grab") + public static void handleCreateItemGrab(ServerPlayer player, RegistryFriendlyByteBuf buf) { + if (!hasCheatPermission(player)) { + return; + } + BiConsumer consumer = (ignored, c2sWholeBuf) -> { + FriendlyByteBuf tmpBuf = new FriendlyByteBuf(Unpooled.buffer()).writeBytes(c2sWholeBuf.readByteArray()); + ItemStack itemStack = tmpBuf.readLenientJsonWithCodec(ItemStack.OPTIONAL_CODEC); + ItemStack stack = itemStack.copy(); + AbstractContainerMenu menu = player.containerMenu; + if (!menu.getCarried().isEmpty() && ItemStack.isSameItemSameComponents(menu.getCarried(), stack)) { + stack.setCount(Mth.clamp(stack.getCount() + menu.getCarried().getCount(), 1, stack.getMaxStackSize())); + } else if (!menu.getCarried().isEmpty()) { + return; + } + menu.setCarried(stack.copy()); + menu.broadcastChanges(); + RegistryFriendlyByteBuf s2cWholeBuf = ProtocolUtils.decorate(Unpooled.buffer()); + s2cWholeBuf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, stack.copy()); + s2cWholeBuf.writeUtf(player.getScoreboardName(), 32767); + // Due to the bug in REI, no packets are actually sent here. + /* + outboundTransform(CREATE_ITEMS_MESSAGE_PACKET, s2cWholeBuf, (id, s2cSplitBuf) -> { + ProtocolUtils.sendPayloadPacket(player, new BufCustomPacketPayload(new CustomPacketPayload.Type<>(id), ByteBufUtil.getBytes(s2cSplitBuf))); + }); + */ + }; + inboundTransform(player, CREATE_ITEMS_GRAB_PACKET, buf, consumer); + } + + @ProtocolHandler.BytebufReceiver(key = "create_item_hotbar") + public static void handleCreateItemHotbar(ServerPlayer player, RegistryFriendlyByteBuf buf) { + if (!hasCheatPermission(player)) { + return; + } + BiConsumer consumer = (ignored, c2sWholeBuf) -> { + FriendlyByteBuf tmpBuf = new FriendlyByteBuf(Unpooled.buffer()).writeBytes(c2sWholeBuf.readByteArray()); + ItemStack stack = tmpBuf.readLenientJsonWithCodec(ItemStack.OPTIONAL_CODEC); + int hotbarSlotId = tmpBuf.readVarInt(); + if (hotbarSlotId >= 0 && hotbarSlotId < 9) { + AbstractContainerMenu menu = player.containerMenu; + player.getInventory().getNonEquipmentItems().set(hotbarSlotId, stack.copy()); + menu.broadcastChanges(); + RegistryFriendlyByteBuf s2cWholeBuf = ProtocolUtils.decorate(Unpooled.buffer()); + s2cWholeBuf.writeJsonWithCodec(ItemStack.OPTIONAL_CODEC, stack.copy()); + s2cWholeBuf.writeUtf(player.getScoreboardName(), 32767); + // Due to the bug in REI, no packets are actually sent here. + /* + outboundTransform(CREATE_ITEMS_MESSAGE_PACKET, s2cWholeBuf, (id, s2cSplitBuf) -> { + ProtocolUtils.sendPayloadPacket(player, new BufCustomPacketPayload(new CustomPacketPayload.Type<>(id), ByteBufUtil.getBytes(s2cSplitBuf))); + }); + */ + } else { + player.displayClientMessage(Component.translatable("text.rei.failed_cheat_items"), false); + } + }; + inboundTransform(player, CREATE_ITEMS_HOTBAR_PACKET, buf, consumer); + } + + @ProtocolHandler.BytebufReceiver(key = "move_items_new") + public static void handleMoveItem(ServerPlayer player, RegistryFriendlyByteBuf buf) { + BiConsumer consumer = (ignored, c2sWholeBuf) -> { + FriendlyByteBuf tmpBuf = new FriendlyByteBuf(Unpooled.buffer()).writeBytes(c2sWholeBuf.readByteArray()); + AbstractContainerMenu container = player.containerMenu; + tmpBuf.readResourceLocation(); + try { + boolean shift = tmpBuf.readBoolean(); + try { + CompoundTag nbt = tmpBuf.readNbt(); + if (nbt == null) { + throw new IllegalStateException("NBT data is null"); + } + int version = nbt.getInt("Version").orElse(-1); + if (version != 1) { + throw new IllegalStateException("Server and client REI protocol version mismatch! Server: 1, Client: " + version); + } + + List> recipes = readInputs(player.registryAccess(), nbt.getListOrEmpty("Inputs")); + List input = readSlots(container, player, nbt.getListOrEmpty("InputSlots")); + List inventory = readSlots(container, player, nbt.getListOrEmpty("InventorySlots")); + NewInputSlotCrafter crafter = new NewInputSlotCrafter<>(container, input, inventory, recipes); + Bukkit.getGlobalRegionScheduler().run(MinecraftInternalPlugin.INSTANCE, (task) -> { + try { + crafter.fillInputSlots(player, shift); + } catch (InputSlotCrafter.NotEnoughMaterialsException ignored1) { + } catch (IllegalStateException e) { + player.sendSystemMessage(Component.translatable(e.getMessage()).withStyle(ChatFormatting.RED)); + } catch (Exception e) { + player.sendSystemMessage(Component.translatable("error.rei.internal.error", e.getMessage()).withStyle(ChatFormatting.RED)); + LOGGER.warn("Failed to move items for player " + player.getScoreboardName(), e); + } + }); + } catch (IllegalStateException e) { + player.sendSystemMessage(Component.translatable(e.getMessage()).withStyle(ChatFormatting.RED)); + } catch (Exception e) { + player.sendSystemMessage(Component.translatable("error.rei.internal.error", e.getMessage()).withStyle(ChatFormatting.RED)); + LOGGER.warn("Failed to move items for player " + player.getScoreboardName(), e); + } + } catch (Exception e) { + LOGGER.warn("Failed to move items for player " + player.getScoreboardName(), e); + } + }; + inboundTransform(player, MOVE_ITEMS_NEW_PACKET, buf, consumer); + } + + private static void inboundTransform(ServerPlayer player, ResourceLocation id, RegistryFriendlyByteBuf buf, BiConsumer consumer) { + PacketTransformer transformer = TRANSFORMERS.get(id); + if (transformer != null) { + transformer.inbound(id, buf, player, consumer); + } else { + consumer.accept(id, buf); + } + } + + private static void outboundTransform(RegistryFriendlyByteBuf buf, BiConsumer consumer) { + PacketTransformer transformer = TRANSFORMERS.get(SYNC_DISPLAYS_PACKET); + if (transformer != null) { + transformer.outbound(SYNC_DISPLAYS_PACKET, buf, consumer); + } else { + consumer.accept(SYNC_DISPLAYS_PACKET, buf); + } + } + + private static boolean hasCheatPermission(ServerPlayer player) { + if (player.getBukkitEntity().hasPermission(CHEAT_PERMISSION)) { + return true; + } + player.displayClientMessage(Component.translatable("text.rei.no_permission_cheat").withStyle(ChatFormatting.RED), false); + return false; + } + + @Override + public boolean isActive() { + return REIServerProtocolConfig.enabled; + } + + @Override + public int tickerInterval(String tickerID) { + return 200; + } + + private static List> readInputs(RegistryAccess registryAccess, ListTag tag) { + List> items = new ArrayList<>(); + for (Tag t : tag) { + CompoundTag compoundTag = (CompoundTag) t; + compoundTag.getInt("Index").orElseThrow(); + ListTag ingredientList = compoundTag.getListOrEmpty("Ingredient"); + List slotItems = new ArrayList<>(); + for (Tag ingredient : ingredientList) { + CompoundTag ingredientTag = (CompoundTag) ingredient; + ItemStack stack = ItemStack.OPTIONAL_CODEC.parse( + registryAccess.createSerializationContext(NbtOps.INSTANCE), + ingredientTag.get("value") + ).getOrThrow(); + slotItems.add(stack); + } + items.add(slotItems); + } + return items; + } + + private static List readSlots(AbstractContainerMenu menu, ServerPlayer player, ListTag tag) { + List slots = new ArrayList<>(); + for (Tag t : tag) { + CompoundTag compoundTag = (CompoundTag) t; + String id = compoundTag.getString("id").orElseThrow(); + if (!id.startsWith(PROTOCOL_ID + ":")) { + throw new IllegalStateException("Invalid slot id: " + id + ", expected to start with '" + PROTOCOL_ID + ":'"); + } + id = id.substring((PROTOCOL_ID + ":").length()); + int slot = compoundTag.getInt("Slot").orElseThrow(); + SlotAccessor accessor = switch (id) { + case "vanilla" -> new VanillaSlotAccessor(menu.slots.get(slot)); + case "player" -> new PlayerInventorySlotAccessor(player, slot); + default -> throw new IllegalStateException("Unknown container id: " + id); + }; + slots.add(accessor); + } + return slots; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/BlastingDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/BlastingDisplay.java new file mode 100644 index 0000000..23cb77c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/BlastingDisplay.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.rei.display; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; + +public class BlastingDisplay extends CookingDisplay { + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/blasting"); + + public BlastingDisplay(RecipeHolder recipe) { + super(recipe); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CampfireDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CampfireDisplay.java new file mode 100644 index 0000000..ca68d7b --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CampfireDisplay.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.rei.display; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.CampfireCookingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; + +public class CampfireDisplay extends CookingDisplay { + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/campfire"); + + public CampfireDisplay(RecipeHolder recipe) { + super(recipe); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CookingDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CookingDisplay.java new file mode 100644 index 0000000..534f992 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CookingDisplay.java @@ -0,0 +1,84 @@ +/* + * 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.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.SingleRecipeInput; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; + +public abstract class CookingDisplay extends Display { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CookingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CookingDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + CookingDisplay::getOptionalLocation, + ByteBufCodecs.FLOAT, + CookingDisplay::getXp, + ByteBufCodecs.DOUBLE, + CookingDisplay::getCookTime, + CookingDisplay::of + ); + protected float xp; + protected double cookTime; + + private CookingDisplay(@NotNull List inputs, @NotNull List outputs, @NotNull ResourceLocation id, float xp, double cookTime) { + super(inputs, outputs, id); + this.xp = xp; + this.cookTime = cookTime; + } + + public CookingDisplay(RecipeHolder recipe) { + this( + List.of(EntryIngredient.ofIngredient(recipe.value().input())), + List.of(EntryIngredient.of(recipe.value().assemble(new SingleRecipeInput(ItemStack.EMPTY), CraftRegistry.getMinecraftRegistry()))), + recipe.id().location(), + recipe.value().experience(), + recipe.value().cookingTime() + ); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static CookingDisplay of(@NotNull List inputs, @NotNull List outputs, @NotNull Optional id, float xp, double cookTime) { + throw new UnsupportedOperationException(); + } + + public float getXp() { + return xp; + } + + public double getCookTime() { + return cookTime; + } + + public StreamCodec streamCodec() { + return CODEC; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CraftingDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CraftingDisplay.java new file mode 100644 index 0000000..44a7d8a --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CraftingDisplay.java @@ -0,0 +1,38 @@ +/* + * 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.rei.display; + +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; + +public abstract class CraftingDisplay extends Display { + + public CraftingDisplay(@NotNull List inputs, + @NotNull List outputs, + @NotNull ResourceLocation location) { + super(inputs, outputs, location); + } + + public abstract int getWidth(); + + public abstract int getHeight(); + +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CustomDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CustomDisplay.java new file mode 100644 index 0000000..e52ca20 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/CustomDisplay.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.protocol.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.BitSet; +import java.util.List; +import java.util.Optional; + +public class CustomDisplay extends CraftingDisplay { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CustomDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CustomDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + CustomDisplay::getOptionalLocation, + CustomDisplay::of + ); + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/crafting/custom"); + private final int width; + private final int height; + + /** + * see me.shedaniel.rei.plugin.common.displays.crafting.DefaultCustomDisplay#DefaultCustomDisplay + */ + public CustomDisplay(@NotNull List inputs, @NotNull List outputs, @NotNull ResourceLocation location) { + super(inputs, outputs, location); + BitSet row = new BitSet(3); + BitSet column = new BitSet(3); + for (int i = 0; i < 9; i++) { + if (i < inputs.size()) { + EntryIngredient stacks = inputs.get(i); + if (stacks.stream().anyMatch(stack -> !stack.isEmpty())) { + row.set((i - (i % 3)) / 3); + column.set(i % 3); + } + } + } + this.width = column.cardinality(); + this.height = row.cardinality(); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static CustomDisplay of(@NotNull List inputs, @NotNull List outputs, @NotNull Optional id) { + throw new UnsupportedOperationException(); + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + public StreamCodec streamCodec() { + return CODEC; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/Display.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/Display.java new file mode 100644 index 0000000..a52bd5f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/Display.java @@ -0,0 +1,297 @@ +/* + * 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.rei.display; + +import com.google.common.collect.ImmutableList; +import net.minecraft.core.*; +import net.minecraft.core.component.DataComponents; +import net.minecraft.core.registries.Registries; +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.MinecraftServer; +import net.minecraft.tags.TagKey; +import net.minecraft.util.context.ContextMap; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.item.alchemy.PotionContents; +import net.minecraft.world.item.component.Fireworks; +import net.minecraft.world.item.component.ProvidesTrimMaterial; +import net.minecraft.world.item.crafting.*; +import net.minecraft.world.item.crafting.display.*; +import net.minecraft.world.item.equipment.trim.TrimMaterial; +import net.minecraft.world.level.ItemLike; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.*; +import java.util.stream.Stream; + +/** + * A display to be used alongside Roughly Enough Items. + *

+ * see me.shedaniel.rei.api.common.display.Display + */ +public abstract class Display { + + protected ResourceLocation id; + + protected List inputs; + + protected List outputs; + + public Display(@NotNull List inputs, + @NotNull List outputs, + @NotNull ResourceLocation id) { + this.inputs = inputs; + this.outputs = outputs; + this.id = id; + } + + @SuppressWarnings("unchecked") + public static StreamCodec dispatchCodec() { + return new StreamCodec<>() { + @NotNull + @Override + public Display decode(@NotNull RegistryFriendlyByteBuf buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public void encode(@NotNull RegistryFriendlyByteBuf buffer, @NotNull Display display) { + new FriendlyByteBuf(buffer).writeResourceLocation(display.getSerializerId()); + ((StreamCodec) display.streamCodec()).encode(buffer, display); + } + }; + } + + public static Collection ofTransmuteRecipe(@NotNull RecipeHolder recipeHolder) { + TransmuteRecipe recipe = recipeHolder.value(); + List displays = recipe.display(); + List displayList = new ArrayList<>(); + if (!displays.isEmpty()) { + RecipeDisplay recipeDisplay = displays.getFirst(); + if (recipeDisplay instanceof ShapelessCraftingRecipeDisplay shapelessRecipeDisplay) { + displayList.add(new ShapelessDisplay(shapelessRecipeDisplay, recipeHolder.id().location())); + } else if (recipeDisplay instanceof ShapedCraftingRecipeDisplay shapelessRecipe) { + displayList.add(new ShapedDisplay(shapelessRecipe, recipeHolder.id().location())); + } + } + return displayList; + } + + /** + * see me.shedaniel.rei.plugin.client.categories.crafting.filler.TippedArrowRecipeFiller#apply + */ + @NotNull + public static Collection ofTippedArrowRecipe(@NotNull RecipeHolder recipeHolder) { + EntryIngredient arrowIngredient = EntryIngredient.of(Items.ARROW); + Set registeredPotions = new HashSet<>(); + List displays = new ArrayList<>(); + MinecraftServer.getServer().registryAccess().lookup(Registries.POTION).stream() + .flatMap(Registry::listElements) + .map(reference -> PotionContents.createItemStack(Items.LINGERING_POTION, reference)) + .forEach(itemStack -> { + PotionContents potion = itemStack.get(DataComponents.POTION_CONTENTS); + if (potion == null || potion.potion().isEmpty()) { + return; + } + if (potion.potion().get().unwrapKey().isPresent() && registeredPotions.add(potion.potion().get().unwrapKey().get().location())) { + List input = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + input.add(arrowIngredient); + } + input.add(EntryIngredient.of(itemStack)); + for (int i = 0; i < 4; i++) { + input.add(arrowIngredient); + } + ItemStack outputStack = new ItemStack(Items.TIPPED_ARROW, 8); + outputStack.set(DataComponents.POTION_CONTENTS, potion); + displays.add(new CustomDisplay(input, List.of(EntryIngredient.of(outputStack)), recipeHolder.id().location())); + } + }); + return displays; + } + + /** + * see me.shedaniel.rei.plugin.client.categories.crafting.filler.TippedArrowRecipeFiller#apply + */ + @NotNull + public static Collection ofFireworkRocketRecipe(@NotNull RecipeHolder recipeHolder) { + EntryIngredient[] inputs = new EntryIngredient[4]; + inputs[0] = EntryIngredient.of(Items.GUNPOWDER); + inputs[1] = EntryIngredient.of(Items.PAPER); + inputs[2] = EntryIngredient.of(new ItemStack(Items.AIR), new ItemStack(Items.GUNPOWDER), new ItemStack(Items.GUNPOWDER)); + inputs[3] = EntryIngredient.of(new ItemStack(Items.AIR), new ItemStack(Items.AIR), new ItemStack(Items.GUNPOWDER)); + ItemStack[] outputs = new ItemStack[3]; + for (int i = 0; i < 3; i++) { + outputs[i] = new ItemStack(Items.FIREWORK_ROCKET, 3); + outputs[i].set(DataComponents.FIREWORKS, new Fireworks(i + 1, List.of())); + } + return Collections.singleton(new ShapelessDisplay(List.of(inputs), List.of(EntryIngredient.of(outputs)), recipeHolder.id().location())); + } + + /** + * see me.shedaniel.rei.plugin.client.categories.crafting.filler.MapCloningRecipeFiller#apply + */ + @NotNull + public static Collection ofMapCloningRecipe(@NotNull RecipeHolder recipeHolder) { + return Collections.singleton( + new ShapelessDisplay( + List.of(EntryIngredient.of(Items.FILLED_MAP), EntryIngredient.of(Items.MAP)), + List.of(EntryIngredient.of(new ItemStack(Items.FILLED_MAP, 2))), + recipeHolder.id().location()) + ); + } + + /** + * see me.shedaniel.rei.plugin.common.displays.DefaultSmithingDisplay#ofTransforming + */ + @NotNull + public static SmithingDisplay ofTransforming(RecipeHolder recipeHolder) { + return new SmithingDisplay( + List.of( + recipeHolder.value().templateIngredient().map(EntryIngredient::ofIngredient).orElse(EntryIngredient.empty()), + EntryIngredient.ofIngredient(recipeHolder.value().baseIngredient()), + recipeHolder.value().additionIngredient().map(EntryIngredient::ofIngredient).orElse(EntryIngredient.empty()) + ), + List.of(ofSlotDisplay(recipeHolder.value().getResult())), + SmithingDisplay.SmithingRecipeType.TRANSFORM, + recipeHolder.id().location() + ); + } + + /** + * see me.shedaniel.rei.plugin.common.displays.DefaultSmithingDisplay#fromTrimming + */ + @SuppressWarnings("deprecation") + @NotNull + public static Collection ofSmithingTrimRecipe(@NotNull RecipeHolder recipeHolder) { + RegistryAccess registryAccess = MinecraftServer.getServer().registryAccess(); + SmithingTrimRecipe recipe = recipeHolder.value(); + List displays = new ArrayList<>(); + for (Holder additionStack : (Iterable>) recipe.additionIngredient().map(Ingredient::items).orElse(Stream.of())::iterator) { + Holder trimMaterial = getMaterialFromIngredient(registryAccess, additionStack).orElse(null); + if (trimMaterial == null) { + continue; + } + + EntryIngredient baseIngredient = EntryIngredient.ofIngredient(recipe.baseIngredient()); + displays.add(new SmithingDisplay.Trimming(List.of( + recipe.templateIngredient().map(EntryIngredient::ofIngredient).orElse(EntryIngredient.empty()), + baseIngredient, + EntryIngredient.ofItemHolder(additionStack) + ), List.of(baseIngredient), SmithingDisplay.SmithingRecipeType.TRIM, recipeHolder.id().location(), recipe.pattern())); + + } + return displays; + } + + private static Optional> getMaterialFromIngredient(HolderLookup.Provider provider, Holder item) { + ProvidesTrimMaterial providesTrimMaterial = new ItemStack(item).get(DataComponents.PROVIDES_TRIM_MATERIAL); + return providesTrimMaterial != null ? providesTrimMaterial.unwrap(provider) : Optional.empty(); + } + + public static EntryIngredient ofSlotDisplay(SlotDisplay slot) { + return switch (slot) { + case SlotDisplay.Empty ignored -> EntryIngredient.empty(); + case SlotDisplay.ItemSlotDisplay s -> EntryIngredient.of(s.item().value()); + case SlotDisplay.ItemStackSlotDisplay s -> EntryIngredient.of(s.stack()); + case SlotDisplay.TagSlotDisplay s -> ofItemTag(s.tag()); + case SlotDisplay.Composite s -> { + ArrayList list = new ArrayList<>(); + for (SlotDisplay slotDisplay : s.contents()) { + ofSlotDisplay(slotDisplay).stream().forEach(list::add); + } + yield EntryIngredient.of(list.toArray(new ItemStack[0])); + } + // REI Bad idea + case SlotDisplay.AnyFuel ignored -> EntryIngredient.empty(); + default -> { + RegistryAccess access = MinecraftServer.getServer().registryAccess(); + try { + List stacks = slot.resolveForStacks(new ContextMap.Builder() + .withParameter(SlotDisplayContext.REGISTRIES, access) + .create(SlotDisplayContext.CONTEXT)); + yield EntryIngredient.of(stacks.toArray(new ItemStack[0])); + } catch (Exception e) { + MinecraftServer.LOGGER.warn("Failed to resolve slot display: {}", slot, e); + yield EntryIngredient.empty(); + } + } + }; + } + + public static List ofSlotDisplays(Collection slots) { + if (slots instanceof Collection collection && collection.isEmpty()) { + return Collections.emptyList(); + } + ImmutableList.Builder ingredients = ImmutableList.builder(); + for (SlotDisplay slot : slots) { + ingredients.add(ofSlotDisplay(slot)); + } + return ingredients.build(); + } + + public static EntryIngredient ofItemTag(TagKey tagKey) { + HolderGetter getter = MinecraftServer.getServer().registryAccess().lookupOrThrow(tagKey.registry()); + HolderSet.Named holders = getter.get(tagKey).orElse(null); + if (holders == null) { + return EntryIngredient.empty(); + } + + int size = holders.size(); + if (size == 0) { + return EntryIngredient.empty(); + } + if (size == 1) { + return EntryIngredient.of(new ItemStack(holders.get(0).value())); + } + + List stackList = new ArrayList<>(); + for (Holder t : holders) { + ItemStack stack = new ItemStack(t.value()); + if (!stack.isEmpty()) { + stackList.add(stack); + } + } + return EntryIngredient.of(stackList.toArray(new ItemStack[0])); + } + + public List getInputEntries() { + return inputs; + } + + public List getOutputEntries() { + return outputs; + } + + public ResourceLocation getDisplayLocation() { + return id; + } + + public Optional getOptionalLocation() { + return Optional.ofNullable(id); + } + + public abstract ResourceLocation getSerializerId(); + + public abstract StreamCodec streamCodec(); +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapedDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapedDisplay.java new file mode 100644 index 0000000..d205fbc --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapedDisplay.java @@ -0,0 +1,112 @@ +/* + * 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.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.ShapedRecipe; +import net.minecraft.world.item.crafting.display.ShapedCraftingRecipeDisplay; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; + +/** + * see me.shedaniel.rei.plugin.common.displays.crafting.DefaultShapedDisplay#DefaultShapedDisplay(RecipeHolder) + */ +public class ShapedDisplay extends CraftingDisplay { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CraftingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CraftingDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + CraftingDisplay::getOptionalLocation, + ByteBufCodecs.INT, + CraftingDisplay::getWidth, + ByteBufCodecs.INT, + CraftingDisplay::getHeight, + ShapedDisplay::of + ); + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/crafting/shaped"); + private final int width; + private final int height; + + public ShapedDisplay(@NotNull RecipeHolder recipeHolder) { + super( + ofIngredient(recipeHolder.value()), + List.of(EntryIngredient.of(recipeHolder.value().assemble(CraftingInput.EMPTY, CraftRegistry.getMinecraftRegistry()))), + recipeHolder.id().location() + ); + this.width = recipeHolder.value().getWidth(); + this.height = recipeHolder.value().getHeight(); + } + + public ShapedDisplay(@NotNull ShapedCraftingRecipeDisplay recipeDisplay, ResourceLocation id) { + super( + Display.ofSlotDisplays(recipeDisplay.ingredients()), + List.of(ofSlotDisplay(recipeDisplay.result())), + id + ); + this.width = recipeDisplay.width(); + this.height = recipeDisplay.height(); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static CraftingDisplay of(List inputs, List outputs, Optional location, int width, int height) { + throw new UnsupportedOperationException(); + } + + private static List ofIngredient(ShapedRecipe recipe) { + return recipe.getIngredients().stream().map(ingredient -> { + if (ingredient.isEmpty()) { + return EntryIngredient.empty(); + } + ItemStack[] itemStacks = ingredient.get().items() + .map(itemHolder -> new ItemStack(itemHolder, 1)) + .toArray(ItemStack[]::new); + return EntryIngredient.of(itemStacks); + }).toList(); + } + + @Override + public int getWidth() { + return width; + } + + @Override + public int getHeight() { + return height; + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + public StreamCodec streamCodec() { + return CODEC; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapelessDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapelessDisplay.java new file mode 100644 index 0000000..fdfea54 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/ShapelessDisplay.java @@ -0,0 +1,94 @@ +/* + * 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.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.CraftingInput; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.ShapelessRecipe; +import net.minecraft.world.item.crafting.display.ShapelessCraftingRecipeDisplay; +import org.apache.commons.lang3.NotImplementedException; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; + +public class ShapelessDisplay extends CraftingDisplay { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CraftingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + CraftingDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + CraftingDisplay::getOptionalLocation, + ShapelessDisplay::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/crafting/shapeless"); + + public ShapelessDisplay(@NotNull List inputs, + @NotNull List outputs, + @NotNull ResourceLocation location) { + super(inputs, outputs, location); + } + + public ShapelessDisplay(@NotNull RecipeHolder recipeHolder) { + this( + recipeHolder.value().placementInfo().ingredients().stream().map(EntryIngredient::ofIngredient).toList(), + List.of(EntryIngredient.of(recipeHolder.value().assemble(CraftingInput.EMPTY, CraftRegistry.getMinecraftRegistry()))), + recipeHolder.id().location() + ); + } + + public ShapelessDisplay(@NotNull ShapelessCraftingRecipeDisplay recipeDisplay, ResourceLocation id) { + this( + ofSlotDisplays(recipeDisplay.ingredients()), + List.of(ofSlotDisplay(recipeDisplay.result())), + id + ); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static CraftingDisplay of(List inputs, List outputs, Optional location) { + throw new NotImplementedException(); + } + + @Override + public int getWidth() { + return getInputEntries().size() > 4 ? 3 : 2; + } + + @Override + public int getHeight() { + return getInputEntries().size() > 4 ? 3 : 2; + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + public StreamCodec streamCodec() { + return CODEC; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmeltingDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmeltingDisplay.java new file mode 100644 index 0000000..6152766 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmeltingDisplay.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.rei.display; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.SmeltingRecipe; + +public class SmeltingDisplay extends CookingDisplay { + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/smelting"); + + public SmeltingDisplay(RecipeHolder recipe) { + super(recipe); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmithingDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmithingDisplay.java new file mode 100644 index 0000000..303f290 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmithingDisplay.java @@ -0,0 +1,141 @@ +/* + * 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.rei.display; + +import com.mojang.serialization.Codec; +import io.netty.buffer.ByteBuf; +import net.minecraft.core.Holder; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.ByIdMap; +import net.minecraft.world.item.equipment.trim.TrimPattern; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; +import java.util.function.IntFunction; + +public class SmithingDisplay extends Display { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + SmithingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + SmithingDisplay::getOutputEntries, + ByteBufCodecs.optional(SmithingRecipeType.STREAM_CODEC), + SmithingDisplay::getOptionalType, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + SmithingDisplay::getOptionalLocation, + SmithingDisplay::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/smithing"); + + private final SmithingRecipeType type; + + public SmithingDisplay( + @NotNull List inputs, + @NotNull List outputs, + @NotNull SmithingRecipeType type, + @NotNull ResourceLocation location + ) { + super(inputs, outputs, location); + this.type = type; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static SmithingDisplay of(List inputs, List outputs, Optional type, Optional location) { + throw new UnsupportedOperationException(); + } + + public Optional getOptionalType() { + return Optional.of(type); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + @Override + public StreamCodec streamCodec() { + return CODEC; + } + + public enum SmithingRecipeType { + TRIM, + TRANSFORM, + ; + + public static final Codec CODEC = Codec.STRING.xmap(SmithingRecipeType::valueOf, SmithingRecipeType::name); + public static final IntFunction BY_ID = ByIdMap.continuous(Enum::ordinal, values(), ByIdMap.OutOfBoundsStrategy.ZERO); + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, Enum::ordinal); + } + + public static class Trimming extends SmithingDisplay { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + SmithingDisplay.Trimming::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + SmithingDisplay.Trimming::getOutputEntries, + ByteBufCodecs.optional(SmithingRecipeType.STREAM_CODEC), + SmithingDisplay.Trimming::getOptionalType, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + SmithingDisplay.Trimming::getOptionalLocation, + TrimPattern.STREAM_CODEC, + SmithingDisplay.Trimming::pattern, + SmithingDisplay.Trimming::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/smithing/trimming"); + + private final Holder pattern; + + public Trimming( + @NotNull List inputs, + @NotNull List outputs, + @NotNull SmithingRecipeType type, + @NotNull ResourceLocation location, + @NotNull Holder pattern + ) { + super(inputs, outputs, type, location); + this.pattern = pattern; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static Trimming of(List inputs, List outputs, Optional type, Optional location, Holder pattern) { + throw new UnsupportedOperationException(); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + @Override + public StreamCodec streamCodec() { + return CODEC; + } + + public Holder pattern() { + return pattern; + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmokingDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmokingDisplay.java new file mode 100644 index 0000000..7ece342 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/SmokingDisplay.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.rei.display; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.crafting.AbstractCookingRecipe; +import net.minecraft.world.item.crafting.RecipeHolder; + +public class SmokingDisplay extends CookingDisplay { + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/smoking"); + + public SmokingDisplay(RecipeHolder recipe) { + super(recipe); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/StoneCuttingDisplay.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/StoneCuttingDisplay.java new file mode 100644 index 0000000..4180494 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/display/StoneCuttingDisplay.java @@ -0,0 +1,77 @@ +/* + * 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.rei.display; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.item.crafting.SingleRecipeInput; +import net.minecraft.world.item.crafting.StonecutterRecipe; +import org.bukkit.craftbukkit.CraftRegistry; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.rei.ingredient.EntryIngredient; + +import java.util.List; +import java.util.Optional; + +/** + * see me.shedaniel.rei.plugin.common.displays.DefaultStoneCuttingDisplay + */ +public class StoneCuttingDisplay extends Display { + private static final StreamCodec CODEC = StreamCodec.composite( + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + StoneCuttingDisplay::getInputEntries, + EntryIngredient.CODEC.apply(ByteBufCodecs.list()), + StoneCuttingDisplay::getOutputEntries, + ByteBufCodecs.optional(ResourceLocation.STREAM_CODEC), + StoneCuttingDisplay::getOptionalLocation, + StoneCuttingDisplay::of + ); + + private static final ResourceLocation SERIALIZER_ID = ResourceLocation.tryBuild("minecraft", "default/stone_cutting"); + + public StoneCuttingDisplay(@NotNull List inputs, @NotNull List outputs, @NotNull ResourceLocation id) { + super(inputs, outputs, id); + } + + public StoneCuttingDisplay(RecipeHolder recipeHolder) { + this( + List.of(EntryIngredient.ofIngredient(recipeHolder.value().input())), + List.of(EntryIngredient.of(recipeHolder.value().assemble(new SingleRecipeInput(ItemStack.EMPTY), CraftRegistry.getMinecraftRegistry()))), + recipeHolder.id().location() + ); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + private static StoneCuttingDisplay of(@NotNull List inputs, @NotNull List outputs, @NotNull Optional id) { + throw new UnsupportedOperationException(); + } + + @Override + public ResourceLocation getSerializerId() { + return SERIALIZER_ID; + } + + @Override + public StreamCodec streamCodec() { + return CODEC; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/ingredient/EntryIngredient.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/ingredient/EntryIngredient.java new file mode 100644 index 0000000..e2f6654 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/ingredient/EntryIngredient.java @@ -0,0 +1,116 @@ +/* + * 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.rei.ingredient; + +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.core.registries.Registries; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.crafting.Ingredient; +import net.minecraft.world.level.ItemLike; +import org.jetbrains.annotations.NotNull; + +import java.util.Arrays; +import java.util.Objects; +import java.util.stream.Stream; + +public class EntryIngredient { + private static final StreamCodec> ITEM_STREAM_CODEC = ByteBufCodecs.holderRegistry(Registries.ITEM); + private static final ResourceLocation ITEM_ID = ResourceLocation.withDefaultNamespace("item"); + public static final StreamCodec CODEC = new StreamCodec<>() { + @NotNull + @Override + public EntryIngredient decode(@NotNull RegistryFriendlyByteBuf buffer) { + throw new UnsupportedOperationException(); + } + + @Override + public void encode(@NotNull RegistryFriendlyByteBuf buffer, @NotNull EntryIngredient value) { + ByteBufCodecs.writeCount(buffer, value.size(), Integer.MAX_VALUE); + value.stream().forEach(itemStack -> { + buffer.writeResourceLocation(ITEM_ID); + if (itemStack.isEmpty()) { + buffer.writeVarInt(0); + } else { + buffer.writeVarInt(itemStack.getCount()); + ITEM_STREAM_CODEC.encode(buffer, itemStack.getItemHolder()); + DataComponentPatch.STREAM_CODEC.encode(buffer, itemStack.components.asPatch()); + } + }); + } + }; + private static final EntryIngredient EMPTY = new EntryIngredient(new ItemStack[0]); + @NotNull + private final ItemStack[] array; + + private EntryIngredient(@NotNull ItemStack[] array) { + this.array = Objects.requireNonNull(array); + } + + public static EntryIngredient empty() { + return EMPTY; + } + + public static EntryIngredient ofItemHolder(@NotNull Holder item) { + return EntryIngredient.of(item.value()); + } + + public static EntryIngredient of(@NotNull ItemLike item) { + return EntryIngredient.of(new ItemStack(item)); + } + + public static EntryIngredient of(@NotNull ItemStack itemStack) { + return new EntryIngredient(new ItemStack[]{itemStack}); + } + + public static EntryIngredient of(@NotNull ItemStack... itemStacks) { + return new EntryIngredient(Arrays.copyOf(itemStacks, itemStacks.length)); + } + + @SuppressWarnings("deprecation") + public static EntryIngredient ofIngredient(Ingredient ingredient) { + if (ingredient.isEmpty()) { + return EntryIngredient.empty(); + } + ItemStack[] itemStacks = ingredient.items() + .map(itemHolder -> itemHolder.value().getDefaultInstance()) + .toArray(ItemStack[]::new); + return EntryIngredient.of(itemStacks); + } + + public Stream stream() { + return Arrays.stream(array); + } + + public boolean isEmpty() { + return array.length == 0; + } + + public ItemStack get(int index) { + return array[index].copy(); + } + + public int size() { + return array.length; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/DisplaySyncPayload.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/DisplaySyncPayload.java new file mode 100644 index 0000000..a288964 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/payload/DisplaySyncPayload.java @@ -0,0 +1,93 @@ +/* + * 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.rei.payload; + +import com.mojang.logging.LogUtils; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.util.ByIdMap; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.rei.display.Display; +import org.slf4j.Logger; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Objects; +import java.util.function.IntFunction; +import java.util.function.UnaryOperator; + +// This payload will never be sent to the client. We use PacketTransformer to send split payload. +public record DisplaySyncPayload( + SyncType syncType, + Collection displays, + long version +) implements LeavesCustomPayload { + + private static final Logger LOGGER = LogUtils.getLogger(); + + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + SyncType.STREAM_CODEC, + DisplaySyncPayload::syncType, + Display.dispatchCodec().apply(codec -> new StreamCodec() { + @Override + public void encode(@NotNull RegistryFriendlyByteBuf buf, @NotNull Display display) { + RegistryFriendlyByteBuf tmpBuf = new RegistryFriendlyByteBuf(Unpooled.buffer(), buf.registryAccess()); + try { + codec.encode(tmpBuf, display); + } catch (Exception e) { + tmpBuf.release(); + buf.writeBoolean(false); + LOGGER.warn("Failed to encode display: " + display, e); + return; + } + buf.writeBoolean(true); + RegistryFriendlyByteBuf.writeByteArray(buf, ByteBufUtil.getBytes(tmpBuf)); + tmpBuf.release(); + } + + @NotNull + @Override + public Display decode(@NotNull RegistryFriendlyByteBuf buf) { + // The DisplayDecoder will not be called on the server side + throw new UnsupportedOperationException(); + } + } + ).apply(ByteBufCodecs.>collection(ArrayList::new)).map( + collection -> collection.stream().filter(Objects::nonNull).toList(), + UnaryOperator.identity() + ), + DisplaySyncPayload::displays, + ByteBufCodecs.LONG, + DisplaySyncPayload::version, + DisplaySyncPayload::new + ); + + + public enum SyncType { + APPEND, + SET; + + public static final IntFunction BY_ID = ByIdMap.continuous(Enum::ordinal, values(), ByIdMap.OutOfBoundsStrategy.ZERO); + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.idMapper(BY_ID, Enum::ordinal); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/InputSlotCrafter.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/InputSlotCrafter.java new file mode 100644 index 0000000..b2fab13 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/InputSlotCrafter.java @@ -0,0 +1,176 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.leavesmc.leaves.protocol.rei.transfer; + +import net.minecraft.core.component.DataComponents; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.rei.transfer.slot.SlotAccessor; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +public abstract class InputSlotCrafter { + protected T container; + private Iterable inputStacks; + private Iterable inventoryStacks; + protected ServerPlayer player; + + protected InputSlotCrafter(T container) { + this.container = container; + } + + public void fillInputSlots(ServerPlayer player, boolean hasShift) { + this.player = player; + this.inventoryStacks = this.getInventorySlots(); + this.inputStacks = this.getInputSlots(); + + // Return the already placed items on the grid + this.cleanInputs(); + + ItemRecipeFinder recipeFinder = new ItemRecipeFinder(); + this.populateRecipeFinder(recipeFinder); + List> ingredients = new ArrayList<>(this.getInputs()); + + if (recipeFinder.findRecipe(ingredients, 1, null)) { + this.fillInputSlots(recipeFinder, ingredients, hasShift); + } else { + this.cleanInputs(); + this.markDirty(); + throw new NotEnoughMaterialsException(); + } + + this.markDirty(); + } + + protected abstract Iterable getInputSlots(); + + protected abstract Iterable getInventorySlots(); + + protected abstract List> getInputs(); + + protected abstract void populateRecipeFinder(ItemRecipeFinder recipeFinder); + + protected abstract void markDirty(); + + public void alignRecipeToGrid(Iterable inputStacks, Iterator recipeItems, int craftsAmount) { + for (SlotAccessor inputStack : inputStacks) { + if (!recipeItems.hasNext()) { + return; + } + + this.acceptAlignedInput(recipeItems.next(), inputStack, craftsAmount); + } + } + + public void acceptAlignedInput(ItemStack toBeTakenStack, SlotAccessor inputStack, int craftsAmount) { + if (!toBeTakenStack.isEmpty()) { + for (int i = 0; i < craftsAmount; ++i) { + this.fillInputSlot(inputStack, toBeTakenStack); + } + } + } + + protected void fillInputSlot(SlotAccessor slot, ItemStack toBeTakenStack) { + SlotAccessor takenSlot = this.takeInventoryStack(toBeTakenStack); + if (takenSlot != null) { + ItemStack takenStack = takenSlot.getItemStack().copy(); + if (!takenStack.isEmpty()) { + if (takenStack.getCount() > 1) { + takenSlot.takeStack(1); + } else { + takenSlot.setItemStack(ItemStack.EMPTY); + } + + takenStack.setCount(1); + if (!slot.canPlace(takenStack)) { + return; + } + + if (slot.getItemStack().isEmpty()) { + slot.setItemStack(takenStack); + } else { + slot.getItemStack().grow(1); + } + } + } + } + + protected void fillInputSlots(ItemRecipeFinder recipeFinder, List> ingredients, boolean hasShift) { + int recipeCrafts = recipeFinder.countRecipeCrafts(ingredients, Integer.MAX_VALUE, null); + int amountToFill = hasShift ? recipeCrafts : 1; + List recipeItems = new ArrayList<>(); + if (recipeFinder.findRecipe(ingredients, amountToFill, recipeItems::add)) { + int finalCraftsAmount = amountToFill; + + for (ItemStack itemId : recipeItems) { + // Fix issue with empty item id (grid slot) [shift-click issue] + if (itemId.isEmpty()) { + continue; + } + finalCraftsAmount = Math.min(finalCraftsAmount, itemId.getMaxStackSize()); + } + + recipeItems.clear(); + + if (recipeFinder.findRecipe(ingredients, finalCraftsAmount, recipeItems::add)) { + this.cleanInputs(); + this.alignRecipeToGrid(inputStacks, recipeItems.iterator(), finalCraftsAmount); + } + } + } + + protected abstract void cleanInputs(); + + @Nullable + public SlotAccessor takeInventoryStack(ItemStack itemStack) { + boolean rejectedModification = false; + for (SlotAccessor inventoryStack : inventoryStacks) { + ItemStack itemStack1 = inventoryStack.getItemStack(); + if (!itemStack1.isEmpty() && areItemsEqual(itemStack, itemStack1) && !itemStack1.isDamaged() && !itemStack1.isEnchanted() && !itemStack1.has(DataComponents.CUSTOM_NAME)) { + if (!inventoryStack.allowModification(player)) { + rejectedModification = true; + } else { + return inventoryStack; + } + } + } + + if (rejectedModification) { + throw new IllegalStateException("Unable to take item from inventory due to slot not allowing modification! Item requested: " + itemStack); + } + + return null; + } + + private static boolean areItemsEqual(ItemStack stack1, ItemStack stack2) { + return ItemStack.isSameItemSameComponents(stack1, stack2); + } + + public static class NotEnoughMaterialsException extends RuntimeException { + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/ItemRecipeFinder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/ItemRecipeFinder.java new file mode 100644 index 0000000..d6f5a4c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/ItemRecipeFinder.java @@ -0,0 +1,126 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.leavesmc.leaves.protocol.rei.transfer; + +import com.google.common.collect.Interner; +import com.google.common.collect.Interners; +import net.minecraft.core.Holder; +import net.minecraft.core.component.DataComponentPatch; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +public class ItemRecipeFinder { + private final Interner keys = Interners.newWeakInterner(); + private final RecipeFinder finder = new RecipeFinder<>(); + + public boolean contains(ItemStack item) { + return finder.contains(ofKey(item)); + } + + public void take(ItemStack item, int amount) { + finder.take(ofKey(item), amount); + } + + public void put(ItemStack item, int amount) { + finder.put(ofKey(item), amount); + } + + public void addNormalItem(ItemStack itemStack) { + if (Inventory.isUsableForCrafting(itemStack)) { + this.addItem(itemStack); + } + } + + public void addItem(ItemStack itemStack) { + this.addItem(itemStack, itemStack.getMaxStackSize()); + } + + public void addItem(ItemStack itemStack, int i) { + if (!itemStack.isEmpty()) { + int j = Math.min(i, itemStack.getCount()); + this.finder.put(ofKey(itemStack), j); + } + } + + public boolean findRecipe(List> list, int maxCrafts, @Nullable Consumer output) { + return finder.findRecipe(toIngredients(list), maxCrafts, flatten(itemStack -> { + if (output != null) { + output.accept(itemStack); + } + })); + } + + public int countRecipeCrafts(List> list, int maxCrafts, @Nullable Consumer output) { + return finder.countRecipeCrafts(toIngredients(list), maxCrafts, flatten(itemStack -> { + if (output != null) { + output.accept(itemStack); + } + })); + } + + private ItemKey ofKey(ItemStack itemStack) { + return keys.intern(new ItemKey(itemStack.getItemHolder(), itemStack.getComponentsPatch())); + } + + private Ingredient ofKeys(int index, List itemStack) { + return new Ingredient(index, itemStack.stream().map(this::ofKey).toList()); + } + + private List toIngredients(List> list) { + List ingredients = new ArrayList<>(); + + for (int i = 0; i < list.size(); i++) { + List stacks = list.get(i); + if (!stacks.isEmpty()) { + ingredients.add(ofKeys(i, stacks)); + } + } + + return ingredients; + } + + private static BiConsumer flatten(Consumer consumer) { + int[] lastIndex = {-1}; + return (itemKey, ingredient) -> { + for (int i = lastIndex[0] + 1; i < ingredient.index(); i++) { + consumer.accept(ItemStack.EMPTY); + } + consumer.accept(new ItemStack(itemKey.item(), 1, itemKey.patch())); + lastIndex[0] = ingredient.index(); + }; + } + + private record Ingredient(int index, List elements) implements RecipeFinder.Ingredient { + } + + private record ItemKey(Holder item, DataComponentPatch patch) { + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/NewInputSlotCrafter.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/NewInputSlotCrafter.java new file mode 100644 index 0000000..62f4ddd --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/NewInputSlotCrafter.java @@ -0,0 +1,84 @@ +/* + * 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.rei.transfer; + +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import org.leavesmc.leaves.protocol.rei.transfer.slot.SlotAccessor; + +import java.util.HashMap; +import java.util.List; + +public class NewInputSlotCrafter extends InputSlotCrafter { + protected final List inputSlots; + protected final List inventorySlots; + protected final List> inputs; + + public NewInputSlotCrafter(T container, List inputSlots, List inventorySlots, List> inputs) { + super(container); + this.inputSlots = inputSlots; + this.inventorySlots = inventorySlots; + this.inputs = inputs; + } + + @Override + protected Iterable getInputSlots() { + return this.inputSlots; + } + + @Override + protected Iterable getInventorySlots() { + return this.inventorySlots; + } + + @Override + protected List> getInputs() { + return this.inputs; + } + + @Override + protected void populateRecipeFinder(ItemRecipeFinder recipeFinder) { + for (SlotAccessor slot : getInventorySlots()) { + recipeFinder.addNormalItem(slot.getItemStack()); + } + } + + @Override + protected void markDirty() { + player.getInventory().setChanged(); + container.sendAllDataToRemote(); + } + + @Override + protected void cleanInputs() { + for (SlotAccessor slot : getInputSlots()) { + org.bukkit.inventory.ItemStack bukkitStack = slot.getItemStack().getBukkitStack(); + if (bukkitStack.getType().isAir()) { + continue; + } + HashMap notAdded = player.getBukkitEntity().getInventory().addItem(bukkitStack); + if (notAdded.isEmpty()) { + slot.setItemStack(ItemStack.EMPTY); + } else { + org.bukkit.inventory.ItemStack remain = notAdded.values().iterator().next(); + slot.setItemStack(ItemStack.fromBukkitCopy(remain)); + throw new IllegalStateException("rei.rei.no.slot.in.inv"); + } + } + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/RecipeFinder.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/RecipeFinder.java new file mode 100644 index 0000000..d0ea2d8 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/RecipeFinder.java @@ -0,0 +1,409 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.leavesmc.leaves.protocol.rei.transfer; + +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import org.jetbrains.annotations.Nullable; + +import java.util.BitSet; +import java.util.List; +import java.util.Set; +import java.util.function.BiConsumer; + +public class RecipeFinder> { + public final Reference2IntOpenHashMap amounts = new Reference2IntOpenHashMap<>(); + + public boolean contains(T item) { + return this.amounts.getInt(item) > 0; + } + + boolean containsAtLeast(T object, int i) { + return this.amounts.getInt(object) >= i; + } + + public void take(T item, int amount) { + int taken = this.amounts.addTo(item, -amount); + if (taken < amount) { + throw new IllegalStateException("Took " + amount + " items, but only had " + taken); + } + } + + public void put(T item, int amount) { + this.amounts.addTo(item, amount); + } + + public boolean findRecipe(List list, int maxCrafts, @Nullable BiConsumer output) { + return new Filter(list).tryPick(maxCrafts, output); + } + + public int countRecipeCrafts(List list, int maxCrafts, @Nullable BiConsumer output) { + return new Filter(list).tryPickAll(maxCrafts, output); + } + + public void clear() { + this.amounts.clear(); + } + + class Filter { + private final List ingredients; + private final int ingredientCount; + private final List items; + private final int itemCount; + private final BitSet data; + private final IntList path = new IntArrayList(); + + public Filter(final List list) { + this.ingredients = list; + this.ingredientCount = this.ingredients.size(); + this.items = this.getUniqueAvailableIngredientItems(); + this.itemCount = this.items.size(); + this.data = new BitSet(this.visitedIngredientCount() + this.visitedItemCount() + this.satisfiedCount() + this.connectionCount() + this.residualCount()); + this.setInitialConnections(); + } + + private void setInitialConnections() { + for (int i = 0; i < this.ingredientCount; i++) { + List list = this.ingredients.get(i).elements(); + + for (int j = 0; j < this.itemCount; j++) { + if (list.contains(this.items.get(j))) { + this.setConnection(j, i); + } + } + } + } + + public boolean tryPick(int maxCrafts, @Nullable BiConsumer output) { + if (maxCrafts <= 0) { + return true; + } else { + int j = 0; + + while (true) { + IntList intList = this.tryAssigningNewItem(maxCrafts); + if (intList == null) { + boolean bl = j == this.ingredientCount; + boolean bl2 = bl && output != null; + this.clearAllVisited(); + this.clearSatisfied(); + + for (int l = 0; l < this.ingredientCount; l++) { + for (int m = 0; m < this.itemCount; m++) { + if (this.isAssigned(m, l)) { + this.unassign(m, l); + put(this.items.get(m), maxCrafts); + if (bl2) { + output.accept(this.items.get(m), this.ingredients.get(l)); + } + break; + } + } + } + + assert this.data.get(this.residualOffset(), this.residualOffset() + this.residualCount()).isEmpty(); + + return bl; + } + + int k = intList.getInt(0); + take(this.items.get(k), maxCrafts); + int l = intList.size() - 1; + this.setSatisfied(intList.getInt(l)); + j++; + + for (int mx = 0; mx < intList.size() - 1; mx++) { + if (isPathIndexItem(mx)) { + int n = intList.getInt(mx); + int o = intList.getInt(mx + 1); + this.assign(n, o); + } else { + int n = intList.getInt(mx + 1); + int o = intList.getInt(mx); + this.unassign(n, o); + } + } + } + } + } + + private static boolean isPathIndexItem(int i) { + return (i & 1) == 0; + } + + private List getUniqueAvailableIngredientItems() { + Set set = new ReferenceOpenHashSet<>(); + + for (Ingredient ingredient : this.ingredients) { + set.addAll(ingredient.elements()); + } + + set.removeIf(object -> !contains(object)); + return List.copyOf(set); + } + + @Nullable + private IntList tryAssigningNewItem(int i) { + this.clearAllVisited(); + + for (int j = 0; j < this.itemCount; j++) { + if (containsAtLeast(this.items.get(j), i)) { + IntList intList = this.findNewItemAssignmentPath(j); + if (intList != null) { + return intList; + } + } + } + + return null; + } + + @Nullable + private IntList findNewItemAssignmentPath(int i) { + this.path.clear(); + this.visitItem(i); + this.path.add(i); + + while (!this.path.isEmpty()) { + int j = this.path.size(); + int k = this.path.getInt(j - 1); + if (isPathIndexItem(j - 1)) { + for (int l = 0; l < this.ingredientCount; l++) { + if (!this.hasVisitedIngredient(l) && this.hasConnection(k, l) && !this.isAssigned(k, l)) { + this.visitIngredient(l); + this.path.add(l); + break; + } + } + } else { + if (!this.isSatisfied(k)) { + return this.path; + } + + for (int lx = 0; lx < this.itemCount; lx++) { + if (!this.hasVisitedItem(lx) && this.isAssigned(lx, k)) { + assert this.hasConnection(lx, k); + + this.visitItem(lx); + this.path.add(lx); + break; + } + } + } + + int l = this.path.size(); + if (l == j) { + this.path.removeInt(l - 1); + } + } + + return null; + } + + private int visitedIngredientOffset() { + return 0; + } + + private int visitedIngredientCount() { + return this.ingredientCount; + } + + private int visitedItemOffset() { + return this.visitedIngredientOffset() + this.visitedIngredientCount(); + } + + private int visitedItemCount() { + return this.itemCount; + } + + private int satisfiedOffset() { + return this.visitedItemOffset() + this.visitedItemCount(); + } + + private int satisfiedCount() { + return this.ingredientCount; + } + + private int connectionOffset() { + return this.satisfiedOffset() + this.satisfiedCount(); + } + + private int connectionCount() { + return this.ingredientCount * this.itemCount; + } + + private int residualOffset() { + return this.connectionOffset() + this.connectionCount(); + } + + private int residualCount() { + return this.ingredientCount * this.itemCount; + } + + private boolean isSatisfied(int i) { + return this.data.get(this.getSatisfiedIndex(i)); + } + + private void setSatisfied(int i) { + this.data.set(this.getSatisfiedIndex(i)); + } + + private int getSatisfiedIndex(int i) { + assert i >= 0 && i < this.ingredientCount; + + return this.satisfiedOffset() + i; + } + + private void clearSatisfied() { + this.clearRange(this.satisfiedOffset(), this.satisfiedCount()); + } + + private void setConnection(int i, int j) { + this.data.set(this.getConnectionIndex(i, j)); + } + + private boolean hasConnection(int i, int j) { + return this.data.get(this.getConnectionIndex(i, j)); + } + + private int getConnectionIndex(int i, int j) { + assert i >= 0 && i < this.itemCount; + + assert j >= 0 && j < this.ingredientCount; + + return this.connectionOffset() + i * this.ingredientCount + j; + } + + private boolean isAssigned(int i, int j) { + return this.data.get(this.getResidualIndex(i, j)); + } + + private void assign(int i, int j) { + int k = this.getResidualIndex(i, j); + + assert !this.data.get(k); + + this.data.set(k); + } + + private void unassign(int i, int j) { + int k = this.getResidualIndex(i, j); + + assert this.data.get(k); + + this.data.clear(k); + } + + private int getResidualIndex(int i, int j) { + assert i >= 0 && i < this.itemCount; + + assert j >= 0 && j < this.ingredientCount; + + return this.residualOffset() + i * this.ingredientCount + j; + } + + private void visitIngredient(int i) { + this.data.set(this.getVisitedIngredientIndex(i)); + } + + private boolean hasVisitedIngredient(int i) { + return this.data.get(this.getVisitedIngredientIndex(i)); + } + + private int getVisitedIngredientIndex(int i) { + assert i >= 0 && i < this.ingredientCount; + + return this.visitedIngredientOffset() + i; + } + + private void visitItem(int i) { + this.data.set(this.getVisitiedItemIndex(i)); + } + + private boolean hasVisitedItem(int i) { + return this.data.get(this.getVisitiedItemIndex(i)); + } + + private int getVisitiedItemIndex(int i) { + assert i >= 0 && i < this.itemCount; + + return this.visitedItemOffset() + i; + } + + private void clearAllVisited() { + this.clearRange(this.visitedIngredientOffset(), this.visitedIngredientCount()); + this.clearRange(this.visitedItemOffset(), this.visitedItemCount()); + } + + private void clearRange(int i, int j) { + this.data.clear(i, i + j); + } + + public int tryPickAll(int i, @Nullable BiConsumer output) { + int j = 0; + int k = Math.min(i, this.getMinIngredientCount()) + 1; + + while (true) { + int l = (j + k) / 2; + if (this.tryPick(l, null)) { + if (k - j <= 1) { + if (l > 0) { + this.tryPick(l, output); + } + + return l; + } + + j = l; + } else { + k = l; + } + } + } + + private int getMinIngredientCount() { + int i = Integer.MAX_VALUE; + + for (Ingredient ingredient : this.ingredients) { + int j = 0; + + for (T object : ingredient.elements()) { + j = Math.max(j, amounts.getInt(object)); + } + + if (i > 0) { + i = Math.min(i, j); + } + } + + return i; + } + } + + public interface Ingredient { + List elements(); + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/PlayerInventorySlotAccessor.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/PlayerInventorySlotAccessor.java new file mode 100644 index 0000000..490ee89 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/PlayerInventorySlotAccessor.java @@ -0,0 +1,56 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.leavesmc.leaves.protocol.rei.transfer.slot; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public class PlayerInventorySlotAccessor implements SlotAccessor { + protected Player player; + protected int index; + + public PlayerInventorySlotAccessor(Player player, int index) { + this.player = player; + this.index = index; + } + + @Override + public ItemStack getItemStack() { + return player.getInventory().getItem(index); + } + + @Override + public void setItemStack(ItemStack stack) { + this.player.getInventory().setItem(index, stack); + } + + @Override + public void takeStack(int amount) { + this.player.getInventory().removeItem(index, amount); + } + + public int getIndex() { + return index; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/SlotAccessor.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/SlotAccessor.java new file mode 100644 index 0000000..02e4e64 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/SlotAccessor.java @@ -0,0 +1,43 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.leavesmc.leaves.protocol.rei.transfer.slot; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; + +public interface SlotAccessor { + ItemStack getItemStack(); + + void setItemStack(ItemStack stack); + + void takeStack(int amount); + + default boolean allowModification(Player player) { + return true; + } + + default boolean canPlace(ItemStack stack) { + return true; + } +} \ No newline at end of file diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/VanillaSlotAccessor.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/VanillaSlotAccessor.java new file mode 100644 index 0000000..fd51bf3 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/rei/transfer/slot/VanillaSlotAccessor.java @@ -0,0 +1,65 @@ +/* + * This file is licensed under the MIT License, part of Roughly Enough Items. + * Copyright (c) 2018, 2019, 2020, 2021, 2022, 2023 shedaniel + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package org.leavesmc.leaves.protocol.rei.transfer.slot; + +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; + +public class VanillaSlotAccessor implements SlotAccessor { + protected Slot slot; + + public VanillaSlotAccessor(Slot slot) { + this.slot = slot; + } + + @Override + public ItemStack getItemStack() { + return slot.getItem(); + } + + @Override + public void setItemStack(ItemStack stack) { + this.slot.set(stack); + } + + @Override + public void takeStack(int amount) { + slot.remove(amount); + } + + public Slot getSlot() { + return slot; + } + + @Override + public boolean allowModification(Player player) { + return slot.allowModification(player); + } + + @Override + public boolean canPlace(ItemStack stack) { + return slot.mayPlace(stack); + } +} \ No newline at end of file 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 index 629a282..def785d 100644 --- 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 @@ -18,7 +18,7 @@ package org.leavesmc.leaves.protocol.servux; import com.mojang.logging.LogUtils; -import fun.bm.lophine.config.modules.function.ServuxProtocolConfig; +import fun.bm.lophine.config.modules.function.protocol.ServuxProtocolConfig; import io.netty.buffer.Unpooled; import net.minecraft.Util; import net.minecraft.core.BlockPos; 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 index e5bbfbd..4d300cc 100644 --- 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 @@ -20,7 +20,7 @@ package org.leavesmc.leaves.protocol.servux; import com.google.common.collect.HashBasedTable; import com.google.common.collect.Table; import com.mojang.serialization.DataResult; -import fun.bm.lophine.config.modules.function.ServuxProtocolConfig; +import fun.bm.lophine.config.modules.function.protocol.ServuxProtocolConfig; import io.netty.buffer.Unpooled; import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; 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 index deaf96a..5a8bedc 100644 --- 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 @@ -18,7 +18,7 @@ package org.leavesmc.leaves.protocol.servux; import com.mojang.logging.LogUtils; -import fun.bm.lophine.config.modules.function.ServuxProtocolConfig; +import fun.bm.lophine.config.modules.function.protocol.ServuxProtocolConfig; import io.netty.buffer.Unpooled; import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; 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 index 0db50aa..d7be4b0 100644 --- 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 @@ -17,7 +17,7 @@ package org.leavesmc.leaves.protocol.servux.litematics; -import fun.bm.lophine.config.modules.function.ServuxProtocolConfig; +import fun.bm.lophine.config.modules.function.protocol.ServuxProtocolConfig; import io.netty.buffer.Unpooled; import net.kyori.adventure.text.Component; import net.kyori.adventure.text.format.NamedTextColor; 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 index 7652bf0..c5f70e3 100644 --- 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 @@ -18,7 +18,7 @@ package org.leavesmc.leaves.protocol.servux.litematics.placement; import com.google.common.collect.ImmutableMap; -import fun.bm.lophine.config.modules.function.ServuxProtocolConfig; +import fun.bm.lophine.config.modules.function.protocol.ServuxProtocolConfig; import io.papermc.paper.threadedregions.RegionizedServer; import me.earthme.luminol.utils.NullPlugin; import net.kyori.adventure.text.Component; diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java new file mode 100644 index 0000000..2650f4f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java @@ -0,0 +1,396 @@ +/* + * 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.syncmatica; + +import com.mojang.authlib.GameProfile; +import fun.bm.lophine.config.modules.function.protocol.SyncmaticaProtocolConfig; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.syncmatica.exchange.*; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +@LeavesProtocol.Register(namespace = "syncmatica") +public class CommunicationManager implements LeavesProtocol { + + protected static final Collection broadcastTargets = new ArrayList<>(); + protected static final Map downloadState = new HashMap<>(); + protected static final Map modifyState = new HashMap<>(); + protected static final Rotation[] rotOrdinals = Rotation.values(); + protected static final Mirror[] mirOrdinals = Mirror.values(); + private static final Map> downloadingFile = new HashMap<>(); + private static final Map playerMap = new HashMap<>(); + + public CommunicationManager() { + } + + public static GameProfile getGameProfile(final ExchangeTarget exchangeTarget) { + return playerMap.get(exchangeTarget).getGameProfile(); + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerJoin(ServerPlayer player) { + final ExchangeTarget newPlayer = player.connection.exchangeTarget; + final VersionHandshakeServer hi = new VersionHandshakeServer(newPlayer); + playerMap.put(newPlayer, player); + final GameProfile profile = player.getGameProfile(); + SyncmaticaProtocol.getPlayerIdentifierProvider().updateName(profile.getId(), profile.getName()); + startExchangeUnchecked(hi); + } + + @ProtocolHandler.PlayerLeave + public static void onPlayerLeave(ServerPlayer player) { + final ExchangeTarget oldPlayer = player.connection.exchangeTarget; + final Collection potentialMessageTarget = oldPlayer.getExchanges(); + if (potentialMessageTarget != null) { + for (final Exchange target : potentialMessageTarget) { + target.close(false); + handleExchange(target); + } + } + broadcastTargets.remove(oldPlayer); + playerMap.remove(oldPlayer); + } + + @ProtocolHandler.PayloadReceiver(payload = SyncmaticaPayload.class) + public static void onPacketGet(ServerPlayer player, SyncmaticaPayload payload) { + onPacket(player.connection.exchangeTarget, payload.packetType(), payload.data()); + } + + public static void onPacket(final @NotNull ExchangeTarget source, final ResourceLocation id, final FriendlyByteBuf packetBuf) { + Exchange handler = null; + final Collection potentialMessageTarget = source.getExchanges(); + if (potentialMessageTarget != null) { + for (final Exchange target : potentialMessageTarget) { + if (target.checkPacket(id, packetBuf)) { + target.handle(id, packetBuf); + handler = target; + break; + } + } + } + if (handler == null) { + handle(source, id, packetBuf); + } else if (handler.isFinished()) { + notifyClose(handler); + } + } + + protected static void handle(ExchangeTarget source, @NotNull ResourceLocation id, FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.REQUEST_LITEMATIC.identifier)) { + final UUID syncmaticaId = packetBuf.readUUID(); + final ServerPlacement placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(syncmaticaId); + if (placement == null) { + return; + } + final File toUpload = SyncmaticaProtocol.getFileStorage().getLocalLitematic(placement); + final UploadExchange upload; + try { + upload = new UploadExchange(placement, toUpload, source); + } catch (final FileNotFoundException e) { + e.printStackTrace(); + return; + } + startExchange(upload); + return; + } + if (id.equals(PacketType.REGISTER_METADATA.identifier)) { + final ServerPlacement placement = receiveMetaData(packetBuf, source); + if (SyncmaticaProtocol.getSyncmaticManager().getPlacement(placement.getId()) != null) { + cancelShare(source, placement); + return; + } + + final GameProfile profile = playerMap.get(source).getGameProfile(); + final PlayerIdentifier playerIdentifier = SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet(profile); + if (!placement.getOwner().equals(playerIdentifier)) { + placement.setOwner(playerIdentifier); + placement.setLastModifiedBy(playerIdentifier); + } + + if (!SyncmaticaProtocol.getFileStorage().getLocalState(placement).isLocalFileReady()) { + if (SyncmaticaProtocol.getFileStorage().getLocalState(placement) == LocalLitematicState.DOWNLOADING_LITEMATIC) { + downloadingFile.computeIfAbsent(placement.getHash(), key -> new ArrayList<>()).add(placement); + return; + } + try { + download(placement, source); + } catch (final Exception e) { + e.printStackTrace(); + } + return; + } + + addPlacement(source, placement); + return; + } + if (id.equals(PacketType.REMOVE_SYNCMATIC.identifier)) { + final UUID placementId = packetBuf.readUUID(); + final ServerPlacement placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(placementId); + if (placement != null) { + if (!getGameProfile(source).getId().equals(placement.getOwner().uuid)) { + return; + } + + final Exchange modifier = getModifier(placement); + if (modifier != null) { + modifier.close(true); + notifyClose(modifier); + } + SyncmaticaProtocol.getSyncmaticManager().removePlacement(placement); + for (final ExchangeTarget client : broadcastTargets) { + final FriendlyByteBuf newPacketBuf = new FriendlyByteBuf(Unpooled.buffer()); + newPacketBuf.writeUUID(placement.getId()); + client.sendPacket(PacketType.REMOVE_SYNCMATIC.identifier, newPacketBuf); + } + } + } + if (id.equals(PacketType.MODIFY_REQUEST.identifier)) { + final UUID placementId = packetBuf.readUUID(); + final ModifyExchangeServer modifier = new ModifyExchangeServer(placementId, source); + startExchange(modifier); + } + } + + protected static void handleExchange(Exchange exchange) { + if (exchange instanceof DownloadExchange) { + final ServerPlacement p = ((DownloadExchange) exchange).getPlacement(); + + if (exchange.isSuccessful()) { + addPlacement(exchange.getPartner(), p); + if (downloadingFile.containsKey(p.getHash())) { + for (final ServerPlacement placement : downloadingFile.get(p.getHash())) { + addPlacement(exchange.getPartner(), placement); + } + } + } else { + cancelShare(exchange.getPartner(), p); + if (downloadingFile.containsKey(p.getHash())) { + for (final ServerPlacement placement : downloadingFile.get(p.getHash())) { + cancelShare(exchange.getPartner(), placement); + } + } + } + + downloadingFile.remove(p.getHash()); + return; + } + if (exchange instanceof VersionHandshakeServer && exchange.isSuccessful()) { + broadcastTargets.add(exchange.getPartner()); + } + if (exchange instanceof ModifyExchangeServer && exchange.isSuccessful()) { + final ServerPlacement placement = ((ModifyExchangeServer) exchange).getPlacement(); + for (final ExchangeTarget client : broadcastTargets) { + if (client.getFeatureSet().hasFeature(Feature.MODIFY)) { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(placement.getId()); + putPositionData(placement, buf, client); + if (client.getFeatureSet().hasFeature(Feature.CORE_EX)) { + buf.writeUUID(placement.getLastModifiedBy().uuid); + buf.writeUtf(placement.getLastModifiedBy().getName()); + } + client.sendPacket(PacketType.MODIFY.identifier, buf); + } else { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(placement.getId()); + client.sendPacket(PacketType.REMOVE_SYNCMATIC.identifier, buf); + sendMetaData(placement, client); + } + } + } + } + + private static void addPlacement(final ExchangeTarget t, final @NotNull ServerPlacement placement) { + if (SyncmaticaProtocol.getSyncmaticManager().getPlacement(placement.getId()) != null) { + cancelShare(t, placement); + return; + } + SyncmaticaProtocol.getSyncmaticManager().addPlacement(placement); + for (final ExchangeTarget target : broadcastTargets) { + sendMetaData(placement, target); + } + } + + private static void cancelShare(final @NotNull ExchangeTarget source, final @NotNull ServerPlacement placement) { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(placement.getId()); + source.sendPacket(PacketType.CANCEL_SHARE.identifier, FriendlyByteBuf); + } + + public static void sendMetaData(final ServerPlacement metaData, final ExchangeTarget target) { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + putMetaData(metaData, buf, target); + target.sendPacket(PacketType.REGISTER_METADATA.identifier, buf); + } + + public static void putMetaData(final @NotNull ServerPlacement metaData, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { + buf.writeUUID(metaData.getId()); + + buf.writeUtf(SyncmaticaProtocol.sanitizeFileName(metaData.getName())); + buf.writeUUID(metaData.getHash()); + + if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { + buf.writeUUID(metaData.getOwner().uuid); + buf.writeUtf(metaData.getOwner().getName()); + buf.writeUUID(metaData.getLastModifiedBy().uuid); + buf.writeUtf(metaData.getLastModifiedBy().getName()); + } + + putPositionData(metaData, buf, exchangeTarget); + } + + public static void putPositionData(final @NotNull ServerPlacement metaData, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { + buf.writeBlockPos(metaData.getPosition()); + buf.writeUtf(metaData.getDimension()); + buf.writeInt(metaData.getRotation().ordinal()); + buf.writeInt(metaData.getMirror().ordinal()); + + if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { + if (metaData.getSubRegionData().getModificationData() == null) { + buf.writeInt(0); + return; + } + + final Collection regionData = metaData.getSubRegionData().getModificationData().values(); + buf.writeInt(regionData.size()); + + for (final SubRegionPlacementModification subPlacement : regionData) { + buf.writeUtf(subPlacement.name); + buf.writeBlockPos(subPlacement.position); + buf.writeInt(subPlacement.rotation.ordinal()); + buf.writeInt(subPlacement.mirror.ordinal()); + } + } + } + + public static ServerPlacement receiveMetaData(final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { + final UUID id = buf.readUUID(); + + final String fileName = SyncmaticaProtocol.sanitizeFileName(buf.readUtf(32767)); + final UUID hash = buf.readUUID(); + + PlayerIdentifier owner = PlayerIdentifier.MISSING_PLAYER; + PlayerIdentifier lastModifiedBy = PlayerIdentifier.MISSING_PLAYER; + + if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { + final PlayerIdentifierProvider provider = SyncmaticaProtocol.getPlayerIdentifierProvider(); + owner = provider.createOrGet(buf.readUUID(), buf.readUtf(32767)); + lastModifiedBy = provider.createOrGet(buf.readUUID(), buf.readUtf(32767)); + } + + final ServerPlacement placement = new ServerPlacement(id, fileName, hash, owner); + placement.setLastModifiedBy(lastModifiedBy); + + receivePositionData(placement, buf, exchangeTarget); + + return placement; + } + + public static void receivePositionData(final @NotNull ServerPlacement placement, final @NotNull FriendlyByteBuf buf, final @NotNull ExchangeTarget exchangeTarget) { + final BlockPos pos = buf.readBlockPos(); + final String dimensionId = buf.readUtf(32767); + final Rotation rot = rotOrdinals[buf.readInt()]; + final Mirror mir = mirOrdinals[buf.readInt()]; + placement.move(dimensionId, pos, rot, mir); + + if (exchangeTarget.getFeatureSet().hasFeature(Feature.CORE_EX)) { + final SubRegionData subRegionData = placement.getSubRegionData(); + subRegionData.reset(); + final int limit = buf.readInt(); + for (int i = 0; i < limit; i++) { + subRegionData.modify(buf.readUtf(32767), buf.readBlockPos(), rotOrdinals[buf.readInt()], mirOrdinals[buf.readInt()]); + } + } + } + + public static void download(final ServerPlacement syncmatic, final ExchangeTarget source) throws NoSuchAlgorithmException, IOException { + if (!SyncmaticaProtocol.getFileStorage().getLocalState(syncmatic).isReadyForDownload()) { + throw new IllegalArgumentException(syncmatic.toString() + " is not ready for download local state is: " + SyncmaticaProtocol.getFileStorage().getLocalState(syncmatic).toString()); + } + final File toDownload = SyncmaticaProtocol.getFileStorage().createLocalLitematic(syncmatic); + final Exchange downloadExchange = new DownloadExchange(syncmatic, toDownload, source); + setDownloadState(syncmatic, true); + startExchange(downloadExchange); + } + + public static void setDownloadState(final @NotNull ServerPlacement syncmatic, final boolean b) { + downloadState.put(syncmatic.getHash(), b); + } + + public static boolean getDownloadState(final @NotNull ServerPlacement syncmatic) { + return downloadState.getOrDefault(syncmatic.getHash(), false); + } + + public static void setModifier(final @NotNull ServerPlacement syncmatic, final Exchange exchange) { + modifyState.put(syncmatic.getHash(), exchange); + } + + public static Exchange getModifier(final @NotNull ServerPlacement syncmatic) { + return modifyState.get(syncmatic.getHash()); + } + + public static void startExchange(final @NotNull Exchange newExchange) { + if (!broadcastTargets.contains(newExchange.getPartner())) { + throw new IllegalArgumentException(newExchange.getPartner().toString() + " is not a valid ExchangeTarget"); + } + startExchangeUnchecked(newExchange); + } + + protected static void startExchangeUnchecked(final @NotNull Exchange newExchange) { + newExchange.getPartner().getExchanges().add(newExchange); + newExchange.init(); + if (newExchange.isFinished()) { + notifyClose(newExchange); + } + } + + public static void notifyClose(final @NotNull Exchange e) { + e.getPartner().getExchanges().remove(e); + handleExchange(e); + } + + public void sendMessage(final @NotNull ExchangeTarget client, final MessageType type, final String identifier) { + if (client.getFeatureSet().hasFeature(Feature.MESSAGE)) { + final FriendlyByteBuf newPacketBuf = new FriendlyByteBuf(Unpooled.buffer()); + newPacketBuf.writeUtf(type.toString()); + newPacketBuf.writeUtf(identifier); + client.sendPacket(PacketType.MESSAGE.identifier, newPacketBuf); + } else if (playerMap.containsKey(client)) { + final ServerPlayer player = playerMap.get(client); + player.sendSystemMessage(Component.literal("Syncmatica " + type.toString() + " " + identifier)); + } + } + + @Override + public boolean isActive() { + return SyncmaticaProtocolConfig.enabled; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java new file mode 100644 index 0000000..f70e8ac --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.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.syncmatica; + +import org.jetbrains.annotations.Nullable; + +public enum Feature { + CORE, + FEATURE, + MODIFY, + MESSAGE, + QUOTA, + DEBUG, + CORE_EX; + + @Nullable + public static Feature fromString(final String s) { + for (final Feature f : Feature.values()) { + if (f.toString().equals(s)) { + return f; + } + } + return null; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java new file mode 100644 index 0000000..07f3d83 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java @@ -0,0 +1,81 @@ +/* + * 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.syncmatica; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class FeatureSet { + + private static final Map versionFeatures; + + static { + versionFeatures = new HashMap<>(); + versionFeatures.put("0.1", new FeatureSet(Collections.singletonList(Feature.CORE))); + } + + private final Collection features; + + public FeatureSet(final Collection features) { + this.features = features; + } + + @Nullable + public static FeatureSet fromVersionString(@NotNull String version) { + if (version.matches("^\\d+(\\.\\d+){2,4}$")) { + final int minSize = version.indexOf("."); + while (version.length() > minSize) { + if (versionFeatures.containsKey(version)) { + return versionFeatures.get(version); + } + final int lastDot = version.lastIndexOf("."); + version = version.substring(0, lastDot); + } + } + return null; + } + + @NotNull + public static FeatureSet fromString(final @NotNull String features) { + final FeatureSet featureSet = new FeatureSet(new ArrayList<>()); + for (final String feature : features.split("\n")) { + final Feature f = Feature.fromString(feature); + if (f != null) { + featureSet.features.add(f); + } + } + return featureSet; + } + + @Override + public String toString() { + final StringBuilder output = new StringBuilder(); + boolean b = false; + for (final Feature feature : features) { + output.append(b ? "\n" + feature.toString() : feature.toString()); + b = true; + } + return output.toString(); + } + + public boolean hasFeature(final Feature f) { + return features.contains(f); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java new file mode 100644 index 0000000..dbe538b --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java @@ -0,0 +1,97 @@ +/* + * 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.syncmatica; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.UUID; + +public class FileStorage { + + private final HashMap buffer = new HashMap<>(); + + public LocalLitematicState getLocalState(final ServerPlacement placement) { + final File localFile = getSchematicPath(placement); + if (localFile.isFile()) { + if (isDownloading(placement)) { + return LocalLitematicState.DOWNLOADING_LITEMATIC; + } + if ((buffer.containsKey(placement) && buffer.get(placement) == localFile.lastModified()) || hashCompare(localFile, placement)) { + return LocalLitematicState.LOCAL_LITEMATIC_PRESENT; + } + return LocalLitematicState.LOCAL_LITEMATIC_DESYNC; + } + return LocalLitematicState.NO_LOCAL_LITEMATIC; + } + + private boolean isDownloading(final ServerPlacement placement) { + return CommunicationManager.getDownloadState(placement); + } + + public File getLocalLitematic(final ServerPlacement placement) { + if (getLocalState(placement).isLocalFileReady()) { + return getSchematicPath(placement); + } else { + return null; + } + } + + public File createLocalLitematic(final ServerPlacement placement) { + if (getLocalState(placement).isLocalFileReady()) { + throw new IllegalArgumentException(""); + } + final File file = getSchematicPath(placement); + if (file.exists()) { + file.delete(); + } + try { + file.createNewFile(); + } catch (final IOException e) { + e.printStackTrace(); + } + return file; + } + + private boolean hashCompare(final File localFile, final ServerPlacement placement) { + UUID hash = null; + try { + hash = SyncmaticaProtocol.createChecksum(new FileInputStream(localFile)); + } catch (final Exception e) { + e.printStackTrace(); + } + + if (hash == null) { + return false; + } + if (hash.equals(placement.getHash())) { + buffer.put(placement, localFile.lastModified()); + return true; + } + return false; + } + + @Contract("_ -> new") + private @NotNull File getSchematicPath(final @NotNull ServerPlacement placement) { + return new File(SyncmaticaProtocol.getLitematicFolder(), placement.getHash().toString() + ".litematic"); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java new file mode 100644 index 0000000..ad425f5 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java @@ -0,0 +1,41 @@ +/* + * 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.syncmatica; + +public enum LocalLitematicState { + NO_LOCAL_LITEMATIC(true, false), + LOCAL_LITEMATIC_DESYNC(true, false), + DOWNLOADING_LITEMATIC(false, false), + LOCAL_LITEMATIC_PRESENT(false, true); + + private final boolean downloadReady; + private final boolean fileReady; + + LocalLitematicState(final boolean downloadReady, final boolean fileReady) { + this.downloadReady = downloadReady; + this.fileReady = fileReady; + } + + public boolean isReadyForDownload() { + return downloadReady; + } + + public boolean isLocalFileReady() { + return fileReady; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java new file mode 100644 index 0000000..2d9c50d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java @@ -0,0 +1,25 @@ +/* + * 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.syncmatica; + +public enum MessageType { + SUCCESS, + INFO, + WARNING, + ERROR +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java new file mode 100644 index 0000000..8f109cf --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java @@ -0,0 +1,47 @@ +/* + * 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.syncmatica; + +import net.minecraft.resources.ResourceLocation; + +public enum PacketType { + REGISTER_METADATA("register_metadata"), + CANCEL_SHARE("cancel_share"), + REQUEST_LITEMATIC("request_download"), + SEND_LITEMATIC("send_litematic"), + RECEIVED_LITEMATIC("received_litematic"), + FINISHED_LITEMATIC("finished_litematic"), + CANCEL_LITEMATIC("cancel_litematic"), + REMOVE_SYNCMATIC("remove_syncmatic"), + REGISTER_VERSION("register_version"), + CONFIRM_USER("confirm_user"), + FEATURE_REQUEST("feature_request"), + FEATURE("feature"), + MODIFY("modify"), + MODIFY_REQUEST("modify_request"), + MODIFY_REQUEST_DENY("modify_request_deny"), + MODIFY_REQUEST_ACCEPT("modify_request_accept"), + MODIFY_FINISH("modify_finish"), + MESSAGE("mesage"); + + public final ResourceLocation identifier; + + PacketType(final String id) { + identifier = ResourceLocation.tryBuild(SyncmaticaProtocol.PROTOCOL_ID, id); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java new file mode 100644 index 0000000..802c965 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java @@ -0,0 +1,54 @@ +/* + * 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.syncmatica; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import java.util.UUID; + +public class PlayerIdentifier { + + public static final UUID MISSING_PLAYER_UUID = UUID.fromString("4c1b738f-56fa-4011-8273-498c972424ea"); + public static final PlayerIdentifier MISSING_PLAYER = new PlayerIdentifier(MISSING_PLAYER_UUID, "No Player"); + + public final UUID uuid; + private String bufferedPlayerName; + + PlayerIdentifier(final UUID uuid, final String bufferedPlayerName) { + this.uuid = uuid; + this.bufferedPlayerName = bufferedPlayerName; + } + + public String getName() { + return bufferedPlayerName; + } + + public void updatePlayerName(final String name) { + bufferedPlayerName = name; + } + + public JsonObject toJson() { + final JsonObject jsonObject = new JsonObject(); + + jsonObject.add("uuid", new JsonPrimitive(uuid.toString())); + jsonObject.add("name", new JsonPrimitive(bufferedPlayerName)); + + return jsonObject; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java new file mode 100644 index 0000000..4db924e --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java @@ -0,0 +1,63 @@ +/* + * 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.syncmatica; + +import com.google.gson.JsonObject; +import com.mojang.authlib.GameProfile; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class PlayerIdentifierProvider { + + private final Map identifiers = new HashMap<>(); + + public PlayerIdentifierProvider() { + identifiers.put(PlayerIdentifier.MISSING_PLAYER_UUID, PlayerIdentifier.MISSING_PLAYER); + } + + public PlayerIdentifier createOrGet(final ExchangeTarget exchangeTarget) { + return createOrGet(CommunicationManager.getGameProfile(exchangeTarget)); + } + + public PlayerIdentifier createOrGet(final @NotNull GameProfile gameProfile) { + return createOrGet(gameProfile.getId(), gameProfile.getName()); + } + + public PlayerIdentifier createOrGet(final UUID uuid, final String playerName) { + return identifiers.computeIfAbsent(uuid, id -> new PlayerIdentifier(uuid, playerName)); + } + + public void updateName(final UUID uuid, final String playerName) { + createOrGet(uuid, playerName).updatePlayerName(playerName); + } + + public PlayerIdentifier fromJson(final @NotNull JsonObject obj) { + if (!obj.has("uuid") || !obj.has("name")) { + return PlayerIdentifier.MISSING_PLAYER; + } + + final UUID jsonUUID = UUID.fromString(obj.get("uuid").getAsString()); + return identifiers.computeIfAbsent(jsonUUID, + key -> new PlayerIdentifier(jsonUUID, obj.get("name").getAsString()) + ); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java new file mode 100644 index 0000000..33c8e7f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java @@ -0,0 +1,191 @@ +/* + * 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.syncmatica; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +public class ServerPlacement { + + private final UUID id; + + private final String fileName; + private final UUID hashValue; + + private PlayerIdentifier owner; + private PlayerIdentifier lastModifiedBy; + + private ServerPosition origin; + private Rotation rotation; + private Mirror mirror; + + private SubRegionData subRegionData = new SubRegionData(); + + public ServerPlacement(final UUID id, final String fileName, final UUID hashValue, final PlayerIdentifier owner) { + this.id = id; + this.fileName = removeExtension(fileName); + this.hashValue = hashValue; + this.owner = owner; + lastModifiedBy = owner; + } + + private static String removeExtension(final String fileName) { + final int pos = fileName.lastIndexOf("."); + if (pos < 0) { + return fileName; + } + return fileName.substring(0, pos); + } + + @Nullable + public static ServerPlacement fromJson(final @NotNull JsonObject obj) { + if (obj.has("id") + && obj.has("file_name") + && obj.has("hash") + && obj.has("origin") + && obj.has("rotation") + && obj.has("mirror")) { + final UUID id = UUID.fromString(obj.get("id").getAsString()); + final String name = obj.get("file_name").getAsString(); + final UUID hashValue = UUID.fromString(obj.get("hash").getAsString()); + + PlayerIdentifier owner = PlayerIdentifier.MISSING_PLAYER; + if (obj.has("owner")) { + owner = SyncmaticaProtocol.getPlayerIdentifierProvider().fromJson(obj.get("owner").getAsJsonObject()); + } + + final ServerPlacement newPlacement = new ServerPlacement(id, name, hashValue, owner); + final ServerPosition pos = ServerPosition.fromJson(obj.get("origin").getAsJsonObject()); + if (pos == null) { + return null; + } + newPlacement.origin = pos; + newPlacement.rotation = Rotation.valueOf(obj.get("rotation").getAsString()); + newPlacement.mirror = Mirror.valueOf(obj.get("mirror").getAsString()); + + if (obj.has("lastModifiedBy")) { + newPlacement.lastModifiedBy = SyncmaticaProtocol.getPlayerIdentifierProvider() + .fromJson(obj.get("lastModifiedBy").getAsJsonObject()); + } else { + newPlacement.lastModifiedBy = owner; + } + + if (obj.has("subregionData")) { + newPlacement.subRegionData = SubRegionData.fromJson(obj.get("subregionData")); + } + + return newPlacement; + } + + return null; + } + + public UUID getId() { + return id; + } + + public String getName() { + return fileName; + } + + public UUID getHash() { + return hashValue; + } + + public String getDimension() { + return origin.getDimensionId(); + } + + public BlockPos getPosition() { + return origin.getBlockPosition(); + } + + public ServerPosition getOrigin() { + return origin; + } + + public Rotation getRotation() { + return rotation; + } + + public Mirror getMirror() { + return mirror; + } + + public ServerPlacement move(final String dimensionId, final BlockPos origin, final Rotation rotation, final Mirror mirror) { + move(new ServerPosition(origin, dimensionId), rotation, mirror); + return this; + } + + public ServerPlacement move(final ServerPosition origin, final Rotation rotation, final Mirror mirror) { + this.origin = origin; + this.rotation = rotation; + this.mirror = mirror; + return this; + } + + public PlayerIdentifier getOwner() { + return owner; + } + + public void setOwner(final PlayerIdentifier playerIdentifier) { + owner = playerIdentifier; + } + + public PlayerIdentifier getLastModifiedBy() { + return lastModifiedBy; + } + + public void setLastModifiedBy(final PlayerIdentifier lastModifiedBy) { + this.lastModifiedBy = lastModifiedBy; + } + + public SubRegionData getSubRegionData() { + return subRegionData; + } + + public JsonObject toJson() { + final JsonObject obj = new JsonObject(); + obj.add("id", new JsonPrimitive(id.toString())); + + obj.add("file_name", new JsonPrimitive(fileName)); + obj.add("hash", new JsonPrimitive(hashValue.toString())); + + obj.add("origin", origin.toJson()); + obj.add("rotation", new JsonPrimitive(rotation.name())); + obj.add("mirror", new JsonPrimitive(mirror.name())); + + obj.add("owner", owner.toJson()); + if (!owner.equals(lastModifiedBy)) { + obj.add("lastModifiedBy", lastModifiedBy.toJson()); + } + + if (subRegionData.isModified()) { + obj.add("subregionData", subRegionData.toJson()); + } + + return obj; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java new file mode 100644 index 0000000..958b26b --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java @@ -0,0 +1,68 @@ +/* + * 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.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; + +public class ServerPosition { + + private final BlockPos position; + private final String dimensionId; + + public ServerPosition(final BlockPos pos, final String dim) { + position = pos; + dimensionId = dim; + } + + public static ServerPosition fromJson(final JsonObject obj) { + if (obj.has("position") && obj.has("dimension")) { + final int x; + final int y; + final int z; + final JsonArray arr = obj.get("position").getAsJsonArray(); + x = arr.get(0).getAsInt(); + y = arr.get(1).getAsInt(); + z = arr.get(2).getAsInt(); + final BlockPos pos = new BlockPos(x, y, z); + return new ServerPosition(pos, obj.get("dimension").getAsString()); + } + return null; + } + + public BlockPos getBlockPosition() { + return position; + } + + public String getDimensionId() { + return dimensionId; + } + + public JsonObject toJson() { + final JsonObject obj = new JsonObject(); + final JsonArray arr = new JsonArray(); + arr.add(new JsonPrimitive(position.getX())); + arr.add(new JsonPrimitive(position.getY())); + arr.add(new JsonPrimitive(position.getZ())); + obj.add("position", arr); + obj.add("dimension", new JsonPrimitive(dimensionId)); + return obj; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java new file mode 100644 index 0000000..a1c4d09 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java @@ -0,0 +1,107 @@ +/* + * 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.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public class SubRegionData { + + private boolean isModified; + private Map modificationData; // is null when isModified is false + + public SubRegionData() { + this(false, null); + } + + public SubRegionData(final boolean isModified, final Map modificationData) { + this.isModified = isModified; + this.modificationData = modificationData; + } + + @NotNull + public static SubRegionData fromJson(final @NotNull JsonElement obj) { + final SubRegionData newSubRegionData = new SubRegionData(); + + newSubRegionData.isModified = true; + + for (final JsonElement modification : obj.getAsJsonArray()) { + newSubRegionData.modify(SubRegionPlacementModification.fromJson(modification.getAsJsonObject())); + } + + return newSubRegionData; + } + + public void reset() { + isModified = false; + modificationData = null; + } + + public void modify(final String name, final BlockPos position, final Rotation rotation, final Mirror mirror) { + modify(new SubRegionPlacementModification(name, position, rotation, mirror)); + } + + public void modify(final SubRegionPlacementModification subRegionPlacementModification) { + if (subRegionPlacementModification == null) { + return; + } + isModified = true; + if (modificationData == null) { + modificationData = new HashMap<>(); + } + modificationData.put(subRegionPlacementModification.name, subRegionPlacementModification); + } + + public boolean isModified() { + return isModified; + } + + public Map getModificationData() { + return modificationData; + } + + public JsonElement toJson() { + return modificationDataToJson(); + } + + @NotNull + private JsonElement modificationDataToJson() { + final JsonArray arr = new JsonArray(); + + for (final Map.Entry entry : modificationData.entrySet()) { + arr.add(entry.getValue().toJson()); + } + + return arr; + } + + @Override + public String toString() { + if (!isModified) { + return "[]"; + } + return modificationData == null ? "[ERROR:null]" : modificationData.toString(); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java new file mode 100644 index 0000000..ea49d49 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java @@ -0,0 +1,82 @@ +/* + * 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.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SubRegionPlacementModification { + + public final String name; + public final BlockPos position; + public final Rotation rotation; + public final Mirror mirror; + + SubRegionPlacementModification(final String name, final BlockPos position, final Rotation rotation, final Mirror mirror) { + this.name = name; + this.position = position; + this.rotation = rotation; + this.mirror = mirror; + } + + @Nullable + public static SubRegionPlacementModification fromJson(final @NotNull JsonObject obj) { + if (!obj.has("name") || !obj.has("position") || !obj.has("rotation") || !obj.has("mirror")) { + return null; + } + + final String name = obj.get("name").getAsString(); + final JsonArray arr = obj.get("position").getAsJsonArray(); + if (arr.size() != 3) { + return null; + } + + final BlockPos position = new BlockPos(arr.get(0).getAsInt(), arr.get(1).getAsInt(), arr.get(2).getAsInt()); + final Rotation rotation = Rotation.valueOf(obj.get("rotation").getAsString()); + final Mirror mirror = Mirror.valueOf(obj.get("mirror").getAsString()); + + return new SubRegionPlacementModification(name, position, rotation, mirror); + } + + public JsonObject toJson() { + final JsonObject obj = new JsonObject(); + + final JsonArray arr = new JsonArray(); + arr.add(position.getX()); + arr.add(position.getY()); + arr.add(position.getZ()); + obj.add("position", arr); + + obj.add("name", new JsonPrimitive(name)); + obj.add("rotation", new JsonPrimitive(rotation.name())); + obj.add("mirror", new JsonPrimitive(mirror.name())); + + return obj; + } + + @Override + public String toString() { + return String.format("[name=%s, position=%s, rotation=%s, mirror=%s]", name, position, rotation, mirror); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java new file mode 100644 index 0000000..8edf39f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java @@ -0,0 +1,121 @@ +/* + * 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.syncmatica; + +import com.google.gson.*; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class SyncmaticManager { + + public static final String PLACEMENTS_JSON_KEY = "placements"; + private final Map schematics = new HashMap<>(); + + public void addPlacement(final ServerPlacement placement) { + schematics.put(placement.getId(), placement); + updateServerPlacement(); + } + + public ServerPlacement getPlacement(final UUID id) { + return schematics.get(id); + } + + public Collection getAll() { + return schematics.values(); + } + + public void removePlacement(final @NotNull ServerPlacement placement) { + schematics.remove(placement.getId()); + updateServerPlacement(); + } + + public void updateServerPlacement() { + saveServer(); + } + + public void startup() { + loadServer(); + } + + private void saveServer() { + final JsonObject obj = new JsonObject(); + final JsonArray arr = new JsonArray(); + + for (final ServerPlacement p : getAll()) { + arr.add(p.toJson()); + } + + obj.add(PLACEMENTS_JSON_KEY, arr); + final File backup = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json.bak"); + final File incoming = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json.new"); + final File current = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json"); + + try (final FileWriter writer = new FileWriter(incoming)) { + writer.write(new GsonBuilder().setPrettyPrinting().create().toJson(obj)); + } catch (final IOException e) { + e.printStackTrace(); + return; + } + + SyncmaticaProtocol.backupAndReplace(backup.toPath(), current.toPath(), incoming.toPath()); + } + + private void loadServer() { + final File f = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json"); + if (f.exists() && f.isFile() && f.canRead()) { + JsonElement element = null; + try { + final JsonParser parser = new JsonParser(); + final FileReader reader = new FileReader(f); + + element = parser.parse(reader); + reader.close(); + + } catch (final Exception e) { + e.printStackTrace(); + } + if (element == null) { + return; + } + try { + final JsonObject obj = element.getAsJsonObject(); + if (obj == null || !obj.has(PLACEMENTS_JSON_KEY)) { + return; + } + final JsonArray arr = obj.getAsJsonArray(PLACEMENTS_JSON_KEY); + for (final JsonElement elem : arr) { + final ServerPlacement placement = ServerPlacement.fromJson(elem.getAsJsonObject()); + if (placement != null) { + schematics.put(placement.getId(), placement); + } + } + + } catch (final IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java new file mode 100644 index 0000000..7251e3a --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.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.syncmatica; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; + +public record SyncmaticaPayload(ResourceLocation packetType, FriendlyByteBuf data) implements LeavesCustomPayload { + + @ID + private static final ResourceLocation NETWORK_ID = ResourceLocation.tryBuild(SyncmaticaProtocol.PROTOCOL_ID, "main"); + + @Codec + private static final StreamCodec CODEC = StreamCodec.of( + (buf, payload) -> buf.writeResourceLocation(payload.packetType()).writeBytes(payload.data()), + buf -> new SyncmaticaPayload(buf.readResourceLocation(), new FriendlyByteBuf(buf.readBytes(buf.readableBytes()))) + ); +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java new file mode 100644 index 0000000..50ea72e --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java @@ -0,0 +1,151 @@ +/* + * 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.syncmatica; + +import fun.bm.lophine.config.modules.function.protocol.SyncmaticaProtocolConfig; +import org.apache.commons.io.FilenameUtils; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.UUID; + +public class SyncmaticaProtocol { + + public static final String PROTOCOL_ID = "syncmatica"; + public static final String PROTOCOL_VERSION = "leaves-syncmatica-1.1.0"; + private static final File litematicFolder = new File("." + File.separator + "syncmatics"); + private static final PlayerIdentifierProvider playerIdentifierProvider = new PlayerIdentifierProvider(); + private static final CommunicationManager communicationManager = new CommunicationManager(); + private static final FeatureSet featureSet = new FeatureSet(Arrays.asList(Feature.values())); + private static final SyncmaticManager syncmaticManager = new SyncmaticManager(); + private static final FileStorage fileStorage = new FileStorage(); + private static final int[] ILLEGAL_CHARS = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 58, 42, 63, 92, 47, 34, 60, 62, 124}; + private static final String ILLEGAL_PATTERNS = "(^(con|prn|aux|nul|com[0-9]|lpt[0-9])(\\..*)?$)|(^\\.\\.*$)"; + private static boolean loaded = false; + + public static File getLitematicFolder() { + return litematicFolder; + } + + public static PlayerIdentifierProvider getPlayerIdentifierProvider() { + return playerIdentifierProvider; + } + + public static CommunicationManager getCommunicationManager() { + return communicationManager; + } + + public static FeatureSet getFeatureSet() { + return featureSet; + } + + public static SyncmaticManager getSyncmaticManager() { + return syncmaticManager; + } + + public static FileStorage getFileStorage() { + return fileStorage; + } + + public static void init(boolean status) { + if (status && !loaded) { + litematicFolder.mkdirs(); + syncmaticManager.startup(); + loaded = true; + } else if (!status && loaded) { + syncmaticManager.updateServerPlacement(); + loaded = false; + } + } + + @NotNull + public static UUID createChecksum(final @NotNull InputStream fis) throws NoSuchAlgorithmException, IOException { + final byte[] buffer = new byte[4096]; + final MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + int numRead; + + do { + numRead = fis.read(buffer); + if (numRead > 0) { + messageDigest.update(buffer, 0, numRead); + } + } while (numRead != -1); + + fis.close(); + return UUID.nameUUIDFromBytes(messageDigest.digest()); + } + + @NotNull + public static String sanitizeFileName(final @NotNull String badFileName) { + String input = badFileName; + try { + input = FilenameUtils.getName(input); + } catch (Exception ignored) { + } + final StringBuilder sanitized = new StringBuilder(); + final int len = input.codePointCount(0, input.length()); + + for (int i = 0; i < len; i++) { + final int c = input.codePointAt(i); + if (Arrays.binarySearch(ILLEGAL_CHARS, c) < 0) { + sanitized.appendCodePoint(c); + if (sanitized.length() == 255) { + break; + } + } + } + + return sanitized.toString().replaceAll(ILLEGAL_PATTERNS, "_"); + } + + public static boolean isOverQuota(int sent) { + return SyncmaticaProtocolConfig.useQuota && sent > SyncmaticaProtocolConfig.quotaLimit; + } + + public static void backupAndReplace(final Path backup, final Path current, final Path incoming) { + if (!Files.exists(incoming)) { + return; + } + if (overwrite(backup, current, 2) && !overwrite(current, incoming, 4)) { + overwrite(current, backup, 8); + } + } + + private static boolean overwrite(final Path backup, final Path current, final int tries) { + if (!Files.exists(current)) { + return true; + } + try { + Files.deleteIfExists(backup); + Files.move(current, backup); + } catch (final IOException exception) { + if (tries <= 0) { + return false; + } + return overwrite(backup, current, tries - 1); + } + return true; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java new file mode 100644 index 0000000..2c5bfcf --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java @@ -0,0 +1,83 @@ +/* + * This file is part of Leaves (https://github.com/LeavesMC/Leaves) + * + * Leaves is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Leaves is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Leaves. If not, see . + */ + +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.util.UUID; + +public abstract class AbstractExchange implements Exchange { + + private final ExchangeTarget partner; + private boolean success = false; + private boolean finished = false; + + protected AbstractExchange(final ExchangeTarget partner) { + this.partner = partner; + } + + protected static boolean checkUUID(final FriendlyByteBuf sourceBuf, final UUID targetId) { + final int r = sourceBuf.readerIndex(); + final UUID sourceId = sourceBuf.readUUID(); + sourceBuf.readerIndex(r); + return sourceId.equals(targetId); + } + + @Override + public ExchangeTarget getPartner() { + return partner; + } + + @Override + public boolean isFinished() { + return finished; + } + + @Override + public boolean isSuccessful() { + return success; + } + + @Override + public void close(final boolean notifyPartner) { + finished = true; + success = false; + onClose(); + if (notifyPartner) { + sendCancelPacket(); + } + } + + public CommunicationManager getManager() { + return SyncmaticaProtocol.getCommunicationManager(); + } + + protected void sendCancelPacket() { + } + + protected void onClose() { + } + + protected void succeed() { + finished = true; + success = true; + onClose(); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java new file mode 100644 index 0000000..30dc6dd --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java @@ -0,0 +1,141 @@ +/* + * 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.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.*; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.UUID; + +public class DownloadExchange extends AbstractExchange { + + private final ServerPlacement toDownload; + private final OutputStream outputStream; + private final MessageDigest md5; + private final File downloadFile; + private int bytesSent; + + public DownloadExchange(final ServerPlacement syncmatic, final File downloadFile, final ExchangeTarget partner) throws IOException, NoSuchAlgorithmException { + super(partner); + this.downloadFile = downloadFile; + final OutputStream os = new FileOutputStream(downloadFile); + toDownload = syncmatic; + md5 = MessageDigest.getInstance("MD5"); + outputStream = new DigestOutputStream(os, md5); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.SEND_LITEMATIC.identifier) + || id.equals(PacketType.FINISHED_LITEMATIC.identifier) + || id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + return checkUUID(packetBuf, toDownload.getId()); + } + return false; + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.SEND_LITEMATIC.identifier)) { + final int size = packetBuf.readInt(); + bytesSent += size; + if (SyncmaticaProtocol.isOverQuota(bytesSent)) { + close(true); + SyncmaticaProtocol.getCommunicationManager().sendMessage( + getPartner(), + MessageType.ERROR, + "syncmatica.error.cancelled_transmit_exceed_quota" + ); + return; + } + try { + packetBuf.readBytes(outputStream, size); + } catch (final IOException e) { + close(true); + e.printStackTrace(); + return; + } + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toDownload.getId()); + getPartner().sendPacket(PacketType.RECEIVED_LITEMATIC.identifier, FriendlyByteBuf); + return; + } + if (id.equals(PacketType.FINISHED_LITEMATIC.identifier)) { + try { + outputStream.flush(); + } catch (final IOException e) { + close(false); + e.printStackTrace(); + return; + } + final UUID downloadHash = UUID.nameUUIDFromBytes(md5.digest()); + if (downloadHash.equals(toDownload.getHash())) { + succeed(); + } else { + close(false); + } + return; + } + if (id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + close(false); + } + } + + @Override + public void init() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toDownload.getId()); + getPartner().sendPacket(PacketType.REQUEST_LITEMATIC.identifier, FriendlyByteBuf); + } + + @Override + protected void onClose() { + getManager(); + CommunicationManager.setDownloadState(toDownload, false); + try { + outputStream.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + if (!isSuccessful() && downloadFile.exists()) { + downloadFile.delete(); + } + } + + @Override + protected void sendCancelPacket() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toDownload.getId()); + getPartner().sendPacket(PacketType.CANCEL_LITEMATIC.identifier, FriendlyByteBuf); + } + + public ServerPlacement getPlacement() { + return toDownload; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java new file mode 100644 index 0000000..7d8b2d9 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java @@ -0,0 +1,37 @@ +/* + * 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.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +public interface Exchange { + ExchangeTarget getPartner(); + + boolean checkPacket(ResourceLocation id, FriendlyByteBuf packetBuf); + + void handle(ResourceLocation id, FriendlyByteBuf packetBuf); + + boolean isFinished(); + + boolean isSuccessful(); + + void close(boolean notifyPartner); + + void init(); +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java new file mode 100644 index 0000000..981414c --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java @@ -0,0 +1,56 @@ +/* + * 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.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaPayload; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ExchangeTarget { + + private final List ongoingExchanges = new ArrayList<>(); + private final ServerGamePacketListenerImpl client; + private FeatureSet features; + + public ExchangeTarget(final ServerGamePacketListenerImpl client) { + this.client = client; + } + + public void sendPacket(final ResourceLocation id, final FriendlyByteBuf packetBuf) { + ProtocolUtils.sendPayloadPacket(client.player, new SyncmaticaPayload(id, packetBuf)); + } + + public FeatureSet getFeatureSet() { + return features; + } + + public void setFeatureSet(final FeatureSet f) { + features = f; + } + + public Collection getExchanges() { + return ongoingExchanges; + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java new file mode 100644 index 0000000..4aa8f2d --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java @@ -0,0 +1,65 @@ +/* + * 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.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +public abstract class FeatureExchange extends AbstractExchange { + + protected FeatureExchange(final ExchangeTarget partner) { + super(partner); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.FEATURE_REQUEST.identifier) + || id.equals(PacketType.FEATURE.identifier); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.FEATURE_REQUEST.identifier)) { + sendFeatures(); + } else if (id.equals(PacketType.FEATURE.identifier)) { + final FeatureSet fs = FeatureSet.fromString(packetBuf.readUtf(32767)); + getPartner().setFeatureSet(fs); + onFeatureSetReceive(); + } + } + + protected void onFeatureSetReceive() { + succeed(); + } + + public void requestFeatureSet() { + getPartner().sendPacket(PacketType.FEATURE_REQUEST.identifier, new FriendlyByteBuf(Unpooled.buffer())); + } + + private void sendFeatures() { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + final FeatureSet fs = SyncmaticaProtocol.getFeatureSet(); + buf.writeUtf(fs.toString(), 32767); + getPartner().sendPacket(PacketType.FEATURE.identifier, buf); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java new file mode 100644 index 0000000..7aaf882 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java @@ -0,0 +1,95 @@ +/* + * 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.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.*; + +import java.util.UUID; + +public class ModifyExchangeServer extends AbstractExchange { + + private final ServerPlacement placement; + UUID placementId; + + public ModifyExchangeServer(final UUID placeId, final ExchangeTarget partner) { + super(partner); + placementId = placeId; + placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(placementId); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.MODIFY_FINISH.identifier) && checkUUID(packetBuf, placement.getId()); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.MODIFY_FINISH.identifier)) { + CommunicationManager.receivePositionData(placement, packetBuf, getPartner()); + final PlayerIdentifier identifier = SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet( + getPartner() + ); + placement.setLastModifiedBy(identifier); + SyncmaticaProtocol.getSyncmaticManager().updateServerPlacement(); + succeed(); + } + } + + @Override + public void init() { + if (getPlacement() == null || CommunicationManager.getModifier(placement) != null) { + close(true); + } else { + if (SyncmaticaProtocol.getPlayerIdentifierProvider().createOrGet(this.getPartner()).uuid.equals(placement.getOwner().uuid)) { + accept(); + } else { + close(true); + } + } + } + + private void accept() { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(placement.getId()); + getPartner().sendPacket(PacketType.MODIFY_REQUEST_ACCEPT.identifier, buf); + CommunicationManager.setModifier(placement, this); + } + + @Override + protected void sendCancelPacket() { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUUID(placementId); + getPartner().sendPacket(PacketType.MODIFY_REQUEST_DENY.identifier, buf); + } + + public ServerPlacement getPlacement() { + return placement; + } + + @Override + protected void onClose() { + if (CommunicationManager.getModifier(placement) == this) { + CommunicationManager.setModifier(placement, null); + } + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java new file mode 100644 index 0000000..ad170d5 --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java @@ -0,0 +1,114 @@ +/* + * 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.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; + +import java.io.*; + +public class UploadExchange extends AbstractExchange { + + private static final int BUFFER_SIZE = 16384; + + private final ServerPlacement toUpload; + private final InputStream inputStream; + private final byte[] buffer = new byte[BUFFER_SIZE]; + + public UploadExchange(final ServerPlacement syncmatic, final File uploadFile, final ExchangeTarget partner) throws FileNotFoundException { + super(partner); + toUpload = syncmatic; + inputStream = new FileInputStream(uploadFile); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.RECEIVED_LITEMATIC.identifier) + || id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + return checkUUID(packetBuf, toUpload.getId()); + } + return false; + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.RECEIVED_LITEMATIC.identifier)) { + send(); + } + if (id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + close(false); + } + } + + private void send() { + final int bytesRead; + try { + bytesRead = inputStream.read(buffer); + } catch (final IOException e) { + close(true); + e.printStackTrace(); + return; + } + if (bytesRead == -1) { + sendFinish(); + } else { + sendData(bytesRead); + } + } + + private void sendData(final int bytesRead) { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + FriendlyByteBuf.writeInt(bytesRead); + FriendlyByteBuf.writeBytes(buffer, 0, bytesRead); + getPartner().sendPacket(PacketType.SEND_LITEMATIC.identifier, FriendlyByteBuf); + } + + private void sendFinish() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + getPartner().sendPacket(PacketType.FINISHED_LITEMATIC.identifier, FriendlyByteBuf); + succeed(); + } + + @Override + public void init() { + send(); + } + + @Override + protected void onClose() { + try { + inputStream.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + } + + @Override + protected void sendCancelPacket() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + getPartner().sendPacket(PacketType.CANCEL_LITEMATIC.identifier, FriendlyByteBuf); + } +} diff --git a/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java new file mode 100644 index 0000000..e6d1a6f --- /dev/null +++ b/lophine-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java @@ -0,0 +1,79 @@ +/* + * 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.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.*; + +import java.util.Collection; + +public class VersionHandshakeServer extends FeatureExchange { + + public VersionHandshakeServer(final ExchangeTarget partner) { + super(partner); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.REGISTER_VERSION.identifier) + || super.checkPacket(id, packetBuf); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.REGISTER_VERSION.identifier)) { + String partnerVersion = packetBuf.readUtf(); + if (partnerVersion.equals("0.0.1")) { + close(false); + return; + } + final FeatureSet fs = FeatureSet.fromVersionString(partnerVersion); + if (fs == null) { + requestFeatureSet(); + } else { + getPartner().setFeatureSet(fs); + onFeatureSetReceive(); + } + } else { + super.handle(id, packetBuf); + } + + } + + @Override + public void onFeatureSetReceive() { + final FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); + final Collection l = SyncmaticaProtocol.getSyncmaticManager().getAll(); + newBuf.writeInt(l.size()); + for (final ServerPlacement p : l) { + CommunicationManager.putMetaData(p, newBuf, getPartner()); + } + getPartner().sendPacket(PacketType.CONFIRM_USER.identifier, newBuf); + succeed(); + } + + @Override + public void init() { + final FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); + newBuf.writeUtf(SyncmaticaProtocol.PROTOCOL_VERSION); + getPartner().sendPacket(PacketType.REGISTER_VERSION.identifier, newBuf); + } +}