From 1d451b444178875ec124bbecd30fc1cb4a9553e6 Mon Sep 17 00:00:00 2001 From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> Date: Fri, 11 Jul 2025 01:25:21 +0300 Subject: [PATCH] add leaves protocols, load config a little bit earlier --- build-data/divinemc.at | 8 + .../features/0002-Configuration.patch | 18 +- ...03-Completely-remove-Mojang-profiler.patch | 8 +- .../0010-Pufferfish-SIMD-support.patch | 6 +- .../0064-Implement-NoChatReports.patch | 4 +- ...-SparklyPaper-Parallel-world-ticking.patch | 4 +- ...-Disable-offline-warn-if-using-proxy.patch | 4 +- .../features/0092-Leaves-Protocol-Core.patch | 146 ++++++ .../0093-Leaves-Xaero-s-Map-Protocol.patch | 20 + .../0094-Leaves-Syncmatica-Protocol.patch | 28 ++ .../features/0023-Leaves-Protocol-Core.patch | 28 ++ .../bxteam/divinemc/config/DivineConfig.java | 38 ++ .../org/leavesmc/leaves/LeavesLogger.java | 24 + .../leaves/protocol/AppleSkinProtocol.java | 122 +++++ .../leaves/protocol/XaeroMapProtocol.java | 46 ++ .../protocol/core/LeavesCustomPayload.java | 29 ++ .../leaves/protocol/core/LeavesProtocol.java | 20 + .../protocol/core/LeavesProtocolManager.java | 455 ++++++++++++++++++ .../leaves/protocol/core/ProtocolHandler.java | 61 +++ .../leaves/protocol/core/ProtocolUtils.java | 43 ++ .../core/invoker/AbstractInvokerHolder.java | 69 +++ .../invoker/BytebufReceiverInvokerHolder.java | 18 + .../core/invoker/EmptyInvokerHolder.java | 15 + .../core/invoker/InitInvokerHolder.java | 16 + .../MinecraftRegisterInvokerHolder.java | 18 + .../invoker/PayloadReceiverInvokerHolder.java | 18 + .../core/invoker/PlayerInvokerHolder.java | 16 + .../leaves/protocol/jade/JadeProtocol.java | 290 +++++++++++ .../protocol/jade/accessor/Accessor.java | 22 + .../protocol/jade/accessor/AccessorImpl.java | 60 +++ .../protocol/jade/accessor/BlockAccessor.java | 44 ++ .../jade/accessor/BlockAccessorImpl.java | 143 ++++++ .../jade/accessor/EntityAccessor.java | 42 ++ .../jade/accessor/EntityAccessorImpl.java | 112 +++++ .../jade/payload/ClientHandshakePayload.java | 19 + .../jade/payload/ReceiveDataPayload.java | 20 + .../jade/payload/RequestBlockPayload.java | 35 ++ .../jade/payload/RequestEntityPayload.java | 36 ++ .../jade/payload/ServerHandshakePayload.java | 37 ++ .../protocol/jade/provider/IJadeProvider.java | 12 + .../jade/provider/IServerDataProvider.java | 8 + .../provider/IServerExtensionProvider.java | 10 + .../ItemStorageExtensionProvider.java | 142 ++++++ .../jade/provider/ItemStorageProvider.java | 87 ++++ .../provider/StreamServerDataProvider.java | 23 + .../jade/provider/block/BeehiveProvider.java | 34 ++ .../provider/block/BlockNameProvider.java | 60 +++ .../provider/block/BrewingStandProvider.java | 43 ++ .../jade/provider/block/CampfireProvider.java | 55 +++ .../block/ChiseledBookshelfProvider.java | 44 ++ .../provider/block/CommandBlockProvider.java | 40 ++ .../jade/provider/block/FurnaceProvider.java | 51 ++ .../provider/block/HopperLockProvider.java | 37 ++ .../jade/provider/block/JukeboxProvider.java | 32 ++ .../jade/provider/block/LecternProvider.java | 33 ++ .../block/MobSpawnerCooldownProvider.java | 42 ++ .../jade/provider/block/RedstoneProvider.java | 36 ++ .../provider/entity/AnimalOwnerProvider.java | 48 ++ .../provider/entity/MobBreedingProvider.java | 44 ++ .../provider/entity/MobGrowthProvider.java | 43 ++ .../entity/NextEntityDropProvider.java | 42 ++ .../provider/entity/PetArmorProvider.java | 35 ++ .../entity/StatusEffectsProvider.java | 45 ++ .../entity/ZombieVillagerProvider.java | 34 ++ .../protocol/jade/tool/ShearsToolHandler.java | 37 ++ .../leaves/protocol/jade/util/CommonUtil.java | 72 +++ .../protocol/jade/util/HierarchyLookup.java | 138 ++++++ .../protocol/jade/util/IHierarchyLookup.java | 71 +++ .../protocol/jade/util/ItemCollector.java | 121 +++++ .../protocol/jade/util/ItemIterator.java | 102 ++++ .../leaves/protocol/jade/util/JadeCodec.java | 59 +++ .../jade/util/LootTableMineableCollector.java | 109 +++++ .../jade/util/PairHierarchyLookup.java | 115 +++++ .../protocol/jade/util/PriorityStore.java | 40 ++ .../leaves/protocol/jade/util/ViewGroup.java | 58 +++ .../jade/util/WrappedHierarchyLookup.java | 107 ++++ .../syncmatica/CommunicationManager.java | 389 +++++++++++++++ .../leaves/protocol/syncmatica/Feature.java | 23 + .../protocol/syncmatica/FeatureSet.java | 68 +++ .../protocol/syncmatica/FileStorage.java | 80 +++ .../syncmatica/LocalLitematicState.java | 24 + .../protocol/syncmatica/MessageType.java | 8 + .../protocol/syncmatica/PacketType.java | 30 ++ .../protocol/syncmatica/PlayerIdentifier.java | 37 ++ .../syncmatica/PlayerIdentifierProvider.java | 46 ++ .../protocol/syncmatica/ServerPlacement.java | 166 +++++++ .../protocol/syncmatica/ServerPosition.java | 51 ++ .../protocol/syncmatica/SubRegionData.java | 90 ++++ .../SubRegionPlacementModification.java | 65 +++ .../protocol/syncmatica/SyncmaticManager.java | 108 +++++ .../syncmatica/SyncmaticaPayload.java | 18 + .../syncmatica/SyncmaticaProtocol.java | 127 +++++ .../syncmatica/exchange/AbstractExchange.java | 66 +++ .../syncmatica/exchange/DownloadExchange.java | 128 +++++ .../syncmatica/exchange/Exchange.java | 20 + .../syncmatica/exchange/ExchangeTarget.java | 39 ++ .../syncmatica/exchange/FeatureExchange.java | 48 ++ .../exchange/ModifyExchangeServer.java | 82 ++++ .../syncmatica/exchange/UploadExchange.java | 101 ++++ .../exchange/VersionHandshakeServer.java | 66 +++ .../org/leavesmc/leaves/util/NbtUtils.java | 75 +++ 101 files changed, 6187 insertions(+), 19 deletions(-) create mode 100644 divinemc-server/minecraft-patches/features/0092-Leaves-Protocol-Core.patch create mode 100644 divinemc-server/minecraft-patches/features/0093-Leaves-Xaero-s-Map-Protocol.patch create mode 100644 divinemc-server/minecraft-patches/features/0094-Leaves-Syncmatica-Protocol.patch create mode 100644 divinemc-server/paper-patches/features/0023-Leaves-Protocol-Core.patch create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/LeavesLogger.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/AbstractInvokerHolder.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/BytebufReceiverInvokerHolder.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/EmptyInvokerHolder.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/InitInvokerHolder.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/MinecraftRegisterInvokerHolder.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PayloadReceiverInvokerHolder.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PlayerInvokerHolder.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockNameProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java create mode 100644 divinemc-server/src/main/java/org/leavesmc/leaves/util/NbtUtils.java diff --git a/build-data/divinemc.at b/build-data/divinemc.at index 457eb6b..016f220 100644 --- a/build-data/divinemc.at +++ b/build-data/divinemc.at @@ -12,6 +12,8 @@ public net.minecraft.world.entity.ai.memory.NearestVisibleLivingEntities lineOfS public net.minecraft.world.entity.ai.memory.NearestVisibleLivingEntities nearbyEntities public net.minecraft.world.entity.ai.sensing.Sensor scanRate public net.minecraft.world.entity.ai.sensing.Sensor timeToTick +public net.minecraft.world.entity.animal.armadillo.Armadillo scuteTime +public net.minecraft.world.entity.animal.frog.Tadpole getTicksLeftUntilAdult()I public net.minecraft.world.level.chunk.PaletteResize public net.minecraft.world.level.entity.EntityTickList entities public net.minecraft.world.level.levelgen.DensityFunctions$BlendAlpha @@ -41,4 +43,10 @@ public net.minecraft.world.level.levelgen.Xoroshiro128PlusPlus seedLo public net.minecraft.world.level.levelgen.XoroshiroRandomSource randomNumberGenerator public net.minecraft.world.level.levelgen.structure.pools.StructureTemplatePool rawTemplates public net.minecraft.world.level.pathfinder.SwimNodeEvaluator allowBreaching +public net.minecraft.world.level.storage.loot.LootPool entries +public net.minecraft.world.level.storage.loot.LootTable pools +public net.minecraft.world.level.storage.loot.entries.CompositeEntryBase children +public net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer conditions +public net.minecraft.world.level.storage.loot.entries.NestedLootTable contents +public net.minecraft.world.level.storage.loot.predicates.CompositeLootItemCondition terms public-f ca.spottedleaf.moonrise.paper.PaperHooks diff --git a/divinemc-server/minecraft-patches/features/0002-Configuration.patch b/divinemc-server/minecraft-patches/features/0002-Configuration.patch index 298d176..192f9bb 100644 --- a/divinemc-server/minecraft-patches/features/0002-Configuration.patch +++ b/divinemc-server/minecraft-patches/features/0002-Configuration.patch @@ -5,17 +5,23 @@ Subject: [PATCH] Configuration diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index 670553243d26e2faab8a21f099a846d4d1df7927..329aeeafd51aee4da289b70ff68cdfe5401cc91a 100644 +index 670553243d26e2faab8a21f099a846d4d1df7927..83bd88eb9944481642f912c2d4862dfecdec2246 100644 --- a/net/minecraft/server/dedicated/DedicatedServer.java +++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -193,6 +193,10 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface +@@ -162,6 +162,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + this.setLocalIp(properties.serverIp); + } + ++ org.bxteam.divinemc.config.DivineConfig.init((java.io.File) options.valueOf("divinemc-settings")); // DivineMC - Configuration ++ + // Spigot start + this.setPlayerList(new DedicatedPlayerList(this, this.registries(), this.playerDataStorage)); + org.spigotmc.SpigotConfig.init((java.io.File) this.options.valueOf("spigot-settings")); +@@ -193,6 +195,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface } org.purpurmc.purpur.PurpurConfig.registerCommands(); // Purpur end - Purpur config files -+ // DivineMC start - Configuration -+ org.bxteam.divinemc.config.DivineConfig.init((java.io.File) options.valueOf("divinemc-settings")); -+ org.bxteam.divinemc.command.DivineCommands.registerCommands(this); -+ // DivineMC end - Configuration ++ org.bxteam.divinemc.command.DivineCommands.registerCommands(this); // DivineMC - Configuration com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now this.setPvpAllowed(properties.pvp); diff --git a/divinemc-server/minecraft-patches/features/0003-Completely-remove-Mojang-profiler.patch b/divinemc-server/minecraft-patches/features/0003-Completely-remove-Mojang-profiler.patch index 160c8d5..c6c93b2 100644 --- a/divinemc-server/minecraft-patches/features/0003-Completely-remove-Mojang-profiler.patch +++ b/divinemc-server/minecraft-patches/features/0003-Completely-remove-Mojang-profiler.patch @@ -1067,10 +1067,10 @@ index b10cb4a73df58a5fe64e88868733ba41616f59e4..9f9cbe6056f8a4eeca64c40872d7403b + // DivineMC end - Completely remove Mojang profiler } diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index 329aeeafd51aee4da289b70ff68cdfe5401cc91a..653988aed936761385f245c520cc9521351664bf 100644 +index 83bd88eb9944481642f912c2d4862dfecdec2246..06657a0fbf65dd095519d706dd5a8e1d8b6b381a 100644 --- a/net/minecraft/server/dedicated/DedicatedServer.java +++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -790,12 +790,6 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface +@@ -789,12 +789,6 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface return this.settings.getProperties().serverResourcePackInfo; } @@ -4493,7 +4493,7 @@ index cf6ff7b7b4a007d7ff4b3c5a25d4f5a36422c683..c5275d6069a491c3c2b2de175b76fb87 } diff --git a/net/minecraft/world/entity/animal/armadillo/Armadillo.java b/net/minecraft/world/entity/animal/armadillo/Armadillo.java -index 0da5c51c4830cf1826261f4d8877303b34c6cb87..6fbeaff7178a21338920d6738767033260b7a726 100644 +index e4578193f58417c7ef2776bb3d831ba55c553aec..2a7a078e05e16e73e43a24e108d207bce2e876bb 100644 --- a/net/minecraft/world/entity/animal/armadillo/Armadillo.java +++ b/net/minecraft/world/entity/animal/armadillo/Armadillo.java @@ -23,8 +23,6 @@ import net.minecraft.util.ByIdMap; @@ -4604,7 +4604,7 @@ index 1d5079602e7ae1042e2bb92209dded4007f703da..c6e4966d3e4fdb7c91577fc1693fb669 } diff --git a/net/minecraft/world/entity/animal/frog/Tadpole.java b/net/minecraft/world/entity/animal/frog/Tadpole.java -index 6932e85b3db0205f9a69d9ef965a934f100e6bcf..c0e12a6e5dd2b7e12e4cc40f6795228de6b470cc 100644 +index 3bb197054f5197c0b8c4e2d4714d695255d5ecfa..f85626b690b02908fac3979d277b293ec48aa451 100644 --- a/net/minecraft/world/entity/animal/frog/Tadpole.java +++ b/net/minecraft/world/entity/animal/frog/Tadpole.java @@ -12,8 +12,6 @@ import net.minecraft.server.level.ServerLevel; diff --git a/divinemc-server/minecraft-patches/features/0010-Pufferfish-SIMD-support.patch b/divinemc-server/minecraft-patches/features/0010-Pufferfish-SIMD-support.patch index ef6ab18..b7094ad 100644 --- a/divinemc-server/minecraft-patches/features/0010-Pufferfish-SIMD-support.patch +++ b/divinemc-server/minecraft-patches/features/0010-Pufferfish-SIMD-support.patch @@ -7,11 +7,11 @@ Original license: GPL v3 Original project: https://github.com/pufferfish-gg/Pufferfish diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index 653988aed936761385f245c520cc9521351664bf..f963be0d96a54a9077d2d66e9abe73ba8484d653 100644 +index 06657a0fbf65dd095519d706dd5a8e1d8b6b381a..a947fc31f6261531b6f28e5af577e74ca84f2132 100644 --- a/net/minecraft/server/dedicated/DedicatedServer.java +++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -199,6 +199,26 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface - // DivineMC end - Configuration +@@ -198,6 +198,26 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface + org.bxteam.divinemc.command.DivineCommands.registerCommands(this); // DivineMC - Configuration com.destroystokyo.paper.VersionHistoryManager.INSTANCE.getClass(); // Paper - load version history now + // DivineMC start - Pufferfish SIMD diff --git a/divinemc-server/minecraft-patches/features/0064-Implement-NoChatReports.patch b/divinemc-server/minecraft-patches/features/0064-Implement-NoChatReports.patch index f5a2a51..7c2fc7a 100644 --- a/divinemc-server/minecraft-patches/features/0064-Implement-NoChatReports.patch +++ b/divinemc-server/minecraft-patches/features/0064-Implement-NoChatReports.patch @@ -206,10 +206,10 @@ index a491be4250de3199c3e1aa9e5482b568692bd2f5..c88826db76c28c536e6c36c5592d69c1 private static final String PREFIX = "data:image/png;base64,"; public static final Codec CODEC = Codec.STRING.comapFlatMap(string -> { diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index f963be0d96a54a9077d2d66e9abe73ba8484d653..efe1e50af314c4b8f6b30c7eb0a8dd14aec327a9 100644 +index a947fc31f6261531b6f28e5af577e74ca84f2132..021e1cb762d23ebe885a3f190ba2431e1db99bb8 100644 --- a/net/minecraft/server/dedicated/DedicatedServer.java +++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -624,6 +624,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface +@@ -623,6 +623,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface @Override public boolean enforceSecureProfile() { diff --git a/divinemc-server/minecraft-patches/features/0078-SparklyPaper-Parallel-world-ticking.patch b/divinemc-server/minecraft-patches/features/0078-SparklyPaper-Parallel-world-ticking.patch index 5b270ce..ab00baa 100644 --- a/divinemc-server/minecraft-patches/features/0078-SparklyPaper-Parallel-world-ticking.patch +++ b/divinemc-server/minecraft-patches/features/0078-SparklyPaper-Parallel-world-ticking.patch @@ -333,10 +333,10 @@ index 10e5469df1800bcdfb3f8cb4045ee25a4bafc58c..8efed0ffdc906b6c1ba054831e481f53 } } else if (this.visible.remove(advancementHolder)) { diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index efe1e50af314c4b8f6b30c7eb0a8dd14aec327a9..41900b602d763fdd1f393e29ef6c54ae2694d7bd 100644 +index 021e1cb762d23ebe885a3f190ba2431e1db99bb8..8820d1789192247c52b4d821abf2dd23c0bf1b62 100644 --- a/net/minecraft/server/dedicated/DedicatedServer.java +++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -219,6 +219,13 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface +@@ -218,6 +218,13 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface } // DivineMC end - Pufferfish SIMD diff --git a/divinemc-server/minecraft-patches/features/0086-Disable-offline-warn-if-using-proxy.patch b/divinemc-server/minecraft-patches/features/0086-Disable-offline-warn-if-using-proxy.patch index f603014..d0b1ce8 100644 --- a/divinemc-server/minecraft-patches/features/0086-Disable-offline-warn-if-using-proxy.patch +++ b/divinemc-server/minecraft-patches/features/0086-Disable-offline-warn-if-using-proxy.patch @@ -5,10 +5,10 @@ Subject: [PATCH] Disable offline warn if using proxy diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java -index 41900b602d763fdd1f393e29ef6c54ae2694d7bd..0e66aa733dd9b0c23ef01bb38243a0cab13d9ecf 100644 +index 8820d1789192247c52b4d821abf2dd23c0bf1b62..5a2b9632a1e46b512a1379923765c1b8a28250b9 100644 --- a/net/minecraft/server/dedicated/DedicatedServer.java +++ b/net/minecraft/server/dedicated/DedicatedServer.java -@@ -306,7 +306,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface +@@ -305,7 +305,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface String proxyFlavor = (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) ? "Velocity" : "BungeeCord"; String proxyLink = (io.papermc.paper.configuration.GlobalConfiguration.get().proxies.velocity.enabled) ? "https://docs.papermc.io/velocity/security" : "http://www.spigotmc.org/wiki/firewall-guide/"; // Paper end - Add Velocity IP Forwarding Support diff --git a/divinemc-server/minecraft-patches/features/0092-Leaves-Protocol-Core.patch b/divinemc-server/minecraft-patches/features/0092-Leaves-Protocol-Core.patch new file mode 100644 index 0000000..642ff50 --- /dev/null +++ b/divinemc-server/minecraft-patches/features/0092-Leaves-Protocol-Core.patch @@ -0,0 +1,146 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> +Date: Thu, 10 Jul 2025 22:11:47 +0300 +Subject: [PATCH] Leaves: Protocol Core + +Original project: https://github.com/LeavesMC/Leaves +Original license: GPLv3 + +diff --git a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java +index fb263fa1f30a7dfcb7ec2656abfb38e5fe88eac9..c3be4c2fd4a544967322a45d3b8c0fe78a0684a5 100644 +--- a/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java ++++ b/net/minecraft/network/protocol/common/custom/CustomPacketPayload.java +@@ -40,13 +40,22 @@ public interface CustomPacketPayload { + + @Override + public void encode(B buffer, CustomPacketPayload value) { ++ // DivineMC start - Leaves Protocol Core ++ if (value instanceof org.leavesmc.leaves.protocol.core.LeavesCustomPayload payload) { ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.encode(buffer, payload); ++ return; ++ } ++ // DivineMC end - Leaves Protocol Core + this.writeCap(buffer, value.type(), value); + } + + @Override + public CustomPacketPayload decode(B buffer) { + ResourceLocation resourceLocation = buffer.readResourceLocation(); +- return (CustomPacketPayload)this.findCodec(resourceLocation).decode(buffer); ++ // DivineMC start - Leaves Protocol Core ++ var payload = org.leavesmc.leaves.protocol.core.LeavesProtocolManager.decode(resourceLocation, buffer); ++ return java.util.Objects.requireNonNullElseGet(payload, () -> this.findCodec(resourceLocation).decode(buffer)); ++ // DivineMC end - Leaves Protocol Core + } + }; + } +diff --git a/net/minecraft/network/protocol/common/custom/DiscardedPayload.java b/net/minecraft/network/protocol/common/custom/DiscardedPayload.java +index 62b9d9486c15a1ec6527f786df4e9fc483390bcb..36d8b93182cc44e3bea245800ea9e2719333ac65 100644 +--- a/net/minecraft/network/protocol/common/custom/DiscardedPayload.java ++++ b/net/minecraft/network/protocol/common/custom/DiscardedPayload.java +@@ -4,12 +4,12 @@ import net.minecraft.network.FriendlyByteBuf; + import net.minecraft.network.codec.StreamCodec; + import net.minecraft.resources.ResourceLocation; + +-public record DiscardedPayload(ResourceLocation id, byte[] data) implements CustomPacketPayload { // Paper - store data ++public record DiscardedPayload(ResourceLocation id, byte @org.jetbrains.annotations.Nullable [] data) implements CustomPacketPayload { // Paper - store data // DivineMC - Leaves Protocol Core + public static StreamCodec codec(ResourceLocation id, int maxSize) { + return CustomPacketPayload.codec((value, output) -> { + // Paper start + // Always write data +- output.writeBytes(value.data); ++ if (value.data != null) output.writeBytes(value.data); // DivineMC - Leaves Protocol Core + }, buffer -> { + int i = buffer.readableBytes(); + if (i >= 0 && i <= maxSize) { +diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java +index b31a4edee0616a63026f7a4335205f2d99d2f641..0072f3f07b1962adc1766930bb9a2f709cb76e6e 100644 +--- a/net/minecraft/server/MinecraftServer.java ++++ b/net/minecraft/server/MinecraftServer.java +@@ -1788,6 +1788,8 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop +Date: Thu, 10 Jul 2025 22:15:39 +0300 +Subject: [PATCH] Leaves: Xaero's Map Protocol + +Original project: https://github.com/LeavesMC/Leaves +Original license: GPLv3 + +diff --git a/net/minecraft/server/players/PlayerList.java b/net/minecraft/server/players/PlayerList.java +index f65599e41ca77756cc9bfb87c4a86606eed127cf..753f921042c8fcfc0eb6c2dca7f80751df4909e3 100644 +--- a/net/minecraft/server/players/PlayerList.java ++++ b/net/minecraft/server/players/PlayerList.java +@@ -1172,6 +1172,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); // DivineMC - Leaves: Xaero's Map Protocol + if (level.isRaining()) { + // CraftBukkit start - handle player weather + // player.connection.send(new ClientboundGameEventPacket(ClientboundGameEventPacket.START_RAINING, 0.0F)); diff --git a/divinemc-server/minecraft-patches/features/0094-Leaves-Syncmatica-Protocol.patch b/divinemc-server/minecraft-patches/features/0094-Leaves-Syncmatica-Protocol.patch new file mode 100644 index 0000000..e211dd3 --- /dev/null +++ b/divinemc-server/minecraft-patches/features/0094-Leaves-Syncmatica-Protocol.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> +Date: Thu, 10 Jul 2025 22:17:00 +0300 +Subject: [PATCH] Leaves: Syncmatica Protocol + +Original project: https://github.com/LeavesMC/Leaves +Original license: GPLv3 + +diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +index 9d771e7fba94c09df602a249f58a9caf1d339bcf..db85f8f905425316f893d0adccdeef53517faba8 100644 +--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java ++++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java +@@ -313,6 +313,7 @@ public class ServerGamePacketListenerImpl + private static final int MAX_SIGN_LINE_LENGTH = Integer.getInteger("Paper.maxSignLength", 80); // Paper - Limit client sign length + private final io.papermc.paper.event.packet.ClientTickEndEvent tickEndEvent; // Paper - add client tick end event + public final io.papermc.paper.connection.PaperPlayerGameConnection playerGameConnection; // Paper ++ public final org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget exchangeTarget; // DivineMC - Leaves: Syncmatica Protocol + + public ServerGamePacketListenerImpl(MinecraftServer server, Connection connection, ServerPlayer player, CommonListenerCookie cookie) { + super(server, connection, cookie); +@@ -324,6 +325,7 @@ public class ServerGamePacketListenerImpl + 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.playerGameConnection = new io.papermc.paper.connection.PaperPlayerGameConnection(this); // Paper ++ this.exchangeTarget = new org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget(this); // DivineMC - Leaves: Syncmatica Protocol + } + + // Paper start - configuration phase API diff --git a/divinemc-server/paper-patches/features/0023-Leaves-Protocol-Core.patch b/divinemc-server/paper-patches/features/0023-Leaves-Protocol-Core.patch new file mode 100644 index 0000000..f005be4 --- /dev/null +++ b/divinemc-server/paper-patches/features/0023-Leaves-Protocol-Core.patch @@ -0,0 +1,28 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: NONPLAYT <76615486+NONPLAYT@users.noreply.github.com> +Date: Thu, 10 Jul 2025 22:12:19 +0300 +Subject: [PATCH] Leaves: Protocol Core + +Original project: https://github.com/LeavesMC/Leaves +Original license: GPLv3 + +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index bf549edfc876f1d831b550864b3f54278bcdd20c..bf53f7343f5c8dd6f3bd995cfcebe7b88472659b 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -510,6 +510,7 @@ public final class CraftServer implements Server { + this.potionBrewer = new io.papermc.paper.potion.PaperPotionBrewer(console); // Paper - custom potion mixes + datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper + this.spark = new io.papermc.paper.SparksFly(this); // Paper - spark ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.init(); // DivineMC - Leaves Protocol Core + } + + public boolean getCommandBlockOverride(String command) { +@@ -1099,6 +1100,7 @@ public final class CraftServer implements Server { + org.purpurmc.purpur.PurpurConfig.registerCommands(); // Purpur - Purpur config files + this.overrideAllCommandBlockCommands = this.commandsConfiguration.getStringList("command-block-overrides").contains("*"); + this.ignoreVanillaPermissions = this.commandsConfiguration.getBoolean("ignore-vanilla-permissions"); ++ org.leavesmc.leaves.protocol.core.LeavesProtocolManager.handleServerReload(); // DivineMC - Leaves Protocol Core + + int pollCount = 0; + diff --git a/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java b/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java index 881346b..72b96ee 100644 --- a/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java +++ b/divinemc-server/src/main/java/org/bxteam/divinemc/config/DivineConfig.java @@ -26,6 +26,7 @@ import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Random; @SuppressWarnings({"unused", "SameParameterValue"}) public class DivineConfig { @@ -688,9 +689,20 @@ public class DivineConfig { public static boolean noChatReportsDemandOnClient = false; public static String noChatReportsDisconnectDemandOnClientMessage = "You do not have No Chat Reports, and this server is configured to require it on client!"; + // Protocols + public static boolean protocolsAppleSkinEnabled = false; + public static int protocolsAppleSkinSyncTickInterval = 20; + public static boolean protocolsJadeEnabled = false; + public static boolean protocolsMapsXaeroMapEnabled = false; + public static int protocolsMapsXaeroMapServerId = new Random().nextInt(); + public static boolean protocolsSyncMaticaEnabled = false; + public static boolean protocolsSyncMaticaQuota = false; + public static int protocolsSyncMaticaQuotaLimit = 40000000; + public static void load() { networkSettings(); noChatReports(); + protocols(); } private static void networkSettings() { @@ -718,6 +730,32 @@ public class DivineConfig { noChatReportsDisconnectDemandOnClientMessage = getString(ConfigCategory.NETWORK.key("no-chat-reports.disconnect-demand-on-client-message"), noChatReportsDisconnectDemandOnClientMessage, "Message to send to the client when they are disconnected for not having No Chat Reports"); } + + private static void protocols() { + // AppleSkin + protocolsAppleSkinEnabled = getBoolean(ConfigCategory.NETWORK.key("protocols.appleskin.appleskin-enable"), protocolsAppleSkinEnabled, + "Enables AppleSkin protocol support"); + protocolsAppleSkinSyncTickInterval = getInt(ConfigCategory.NETWORK.key("protocols.appleskin.sync-tick-interval"), protocolsAppleSkinSyncTickInterval, + "Sync tick interval for AppleSkin protocol"); + + // Jade + protocolsJadeEnabled = getBoolean(ConfigCategory.NETWORK.key("protocols.jade.jade-enable"), protocolsJadeEnabled, + "Enables Jade protocol support"); + + // Xaero's Map + protocolsMapsXaeroMapEnabled = getBoolean(ConfigCategory.NETWORK.key("protocols.xaeromap.xaeromap-enable"), protocolsMapsXaeroMapEnabled, + "Enables Xaero's Map protocol support"); + protocolsMapsXaeroMapServerId = getInt(ConfigCategory.NETWORK.key("protocols.xaeromap.xaero-map-server-id"), protocolsMapsXaeroMapServerId, + "Server ID for Xaero's Map protocol"); + + // Syncmatica + protocolsSyncMaticaEnabled = getBoolean(ConfigCategory.NETWORK.key("protocols.syncmatica.syncmatica-enable"), protocolsSyncMaticaEnabled, + "Enables SyncMatica protocol support"); + protocolsSyncMaticaQuota = getBoolean(ConfigCategory.NETWORK.key("protocols.syncmatica.quota"), protocolsSyncMaticaQuota, + "Enables quota system for SyncMatica"); + protocolsSyncMaticaQuotaLimit = getInt(ConfigCategory.NETWORK.key("protocols.syncmatica.quota-limit"), protocolsSyncMaticaQuotaLimit, + "Quota limit for SyncMatica protocol"); + } } private static void checkExperimentalFeatures() { diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/LeavesLogger.java b/divinemc-server/src/main/java/org/leavesmc/leaves/LeavesLogger.java new file mode 100644 index 0000000..bc45935 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/LeavesLogger.java @@ -0,0 +1,24 @@ +package org.leavesmc.leaves; + +import org.bukkit.Bukkit; + +import java.util.logging.Level; +import java.util.logging.Logger; + +public class LeavesLogger extends Logger { + public static final LeavesLogger LOGGER = new LeavesLogger(); + + private LeavesLogger() { + super("Leaves", null); + setParent(Bukkit.getLogger()); + setLevel(Level.ALL); + } + + public void severe(String msg, Exception exception) { + this.log(Level.SEVERE, msg, exception); + } + + public void warning(String msg, Exception exception) { + this.log(Level.WARNING, msg, exception); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java new file mode 100644 index 0000000..2e551b1 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/AppleSkinProtocol.java @@ -0,0 +1,122 @@ +package org.leavesmc.leaves.protocol; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.food.FoodData; +import net.minecraft.world.level.GameRules; +import org.bxteam.divinemc.config.DivineConfig; +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; + +@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); + resetPlayerData(player); + } + + @ProtocolHandler.MinecraftRegister(onlyNamespace = true) + public static void onPlayerSubscribed(@NotNull ServerPlayer player, ResourceLocation id) { + subscribedChannels.computeIfAbsent(player, k -> new HashSet<>()).add(id.getPath()); + } + + @ProtocolHandler.Ticker + public static void tick() { + for (Map.Entry> entry : subscribedChannels.entrySet()) { + ServerPlayer player = entry.getKey(); + FoodData data = player.getFoodData(); + + for (String channel : entry.getValue()) { + switch (channel) { + case "saturation" -> { + float saturation = data.getSaturationLevel(); + Float previousSaturation = previousSaturationLevels.get(player); + if (previousSaturation == null || saturation != previousSaturation) { + ProtocolUtils.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 DivineConfig.NetworkCategory.protocolsAppleSkinSyncTickInterval; + } + + @Override + public boolean isActive() { + return DivineConfig.NetworkCategory.protocolsAppleSkinEnabled; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java new file mode 100644 index 0000000..9731e97 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/XaeroMapProtocol.java @@ -0,0 +1,46 @@ +package org.leavesmc.leaves.protocol; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.bxteam.divinemc.config.DivineConfig; +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 (DivineConfig.NetworkCategory.protocolsMapsXaeroMapEnabled) { + ProtocolUtils.sendBytebufPacket(player, MINIMAP_KEY, buf -> { + buf.writeByte(0); + buf.writeInt(DivineConfig.NetworkCategory.protocolsMapsXaeroMapServerId); + }); + ProtocolUtils.sendBytebufPacket(player, WORLDMAP_KEY, buf -> { + buf.writeByte(0); + buf.writeInt(DivineConfig.NetworkCategory.protocolsMapsXaeroMapServerId); + }); + } + } + + @Override + public boolean isActive() { + return DivineConfig.NetworkCategory.protocolsMapsXaeroMapEnabled; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java new file mode 100644 index 0000000..76cf606 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesCustomPayload.java @@ -0,0 +1,29 @@ +package org.leavesmc.leaves.protocol.core; + +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public interface LeavesCustomPayload extends CustomPacketPayload { + Type LEAVES_TYPE = new Type<>(ResourceLocation.fromNamespaceAndPath("leaves", "custom_payload")); + + @Override + default @NotNull Type type() { + return LEAVES_TYPE; + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface ID { + } + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @interface Codec { + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java new file mode 100644 index 0000000..a284840 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocol.java @@ -0,0 +1,20 @@ +package org.leavesmc.leaves.protocol.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public interface LeavesProtocol { + boolean isActive(); + + default int tickerInterval(String tickerID) { + return 1; + } + + @Target(ElementType.TYPE) + @Retention(RetentionPolicy.RUNTIME) + @interface Register { + String namespace(); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java new file mode 100644 index 0000000..ab74a47 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/LeavesProtocolManager.java @@ -0,0 +1,455 @@ +package org.leavesmc.leaves.protocol.core; + +import io.netty.buffer.ByteBuf; +import io.papermc.paper.connection.PluginMessageBridgeImpl; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.invoker.BytebufReceiverInvokerHolder; +import org.leavesmc.leaves.protocol.core.invoker.EmptyInvokerHolder; +import org.leavesmc.leaves.protocol.core.invoker.InitInvokerHolder; +import org.leavesmc.leaves.protocol.core.invoker.MinecraftRegisterInvokerHolder; +import org.leavesmc.leaves.protocol.core.invoker.PayloadReceiverInvokerHolder; +import org.leavesmc.leaves.protocol.core.invoker.PlayerInvokerHolder; + +import org.bukkit.Bukkit; +import org.bukkit.craftbukkit.entity.CraftPlayer; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.JarURLConnection; +import java.net.URL; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class LeavesProtocolManager { + private static final LeavesLogger LOGGER = LeavesLogger.LOGGER; + + private static final Map, PayloadReceiverInvokerHolder> PAYLOAD_RECEIVERS = new HashMap<>(); + private static final Map, ResourceLocation> IDS = new HashMap<>(); + private static final Map, StreamCodec> CODECS = new HashMap<>(); + private static final Map> ID2CODEC = new HashMap<>(); + + private static final Map STRICT_BYTEBUF_RECEIVERS = new HashMap<>(); + private static final Map NAMESPACED_BYTEBUF_RECEIVERS = new HashMap<>(); + private static final List GENERIC_BYTEBUF_RECEIVERS = new ArrayList<>(); + + private static final Map STRICT_MINECRAFT_REGISTER = new HashMap<>(); + private static final Map NAMESPACED_MINECRAFT_REGISTER = new HashMap<>(); + private static final List WILD_MINECRAFT_REGISTER = new ArrayList<>(); + + private static final List> TICKERS = new ArrayList<>(); + + private static final List> PLAYER_JOIN = new ArrayList<>(); + private static final List> PLAYER_LEAVE = new ArrayList<>(); + private static final List> RELOAD_SERVER = new ArrayList<>(); + private static final List> RELOAD_DATAPACK = new ArrayList<>(); + + @SuppressWarnings("unchecked") + public static void init() { + for (Class clazz : getClasses("org.leavesmc.leaves.protocol")) { + if (LeavesCustomPayload.class.isAssignableFrom(clazz) && !clazz.equals(LeavesCustomPayload.class)) { + for (Field field : clazz.getDeclaredFields()) { + field.setAccessible(true); + if (!Modifier.isStatic(field.getModifiers())) { + continue; + } + try { + final LeavesCustomPayload.ID id = field.getAnnotation(LeavesCustomPayload.ID.class); + if (id != null && field.getType().equals(ResourceLocation.class)) { + IDS.put((Class) clazz, (ResourceLocation) field.get(null)); + } + final LeavesCustomPayload.Codec codec = field.getAnnotation(LeavesCustomPayload.Codec.class); + if (codec != null && field.getType().equals(StreamCodec.class)) { + CODECS.put((Class) clazz, (StreamCodec) field.get(null)); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } + continue; + } + + final LeavesProtocol.Register register = clazz.getAnnotation(LeavesProtocol.Register.class); + if (register == null) { + continue; + } + LeavesProtocol protocol; + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + protocol = (LeavesProtocol) constructor.newInstance(); + } catch (Throwable throwable) { + LOGGER.severe("Failed to load class " + clazz.getName() + ". " + throwable); + return; + } + + boolean active = true; + try { + Method isActiveMethod = clazz.getDeclaredMethod("isActive"); + isActiveMethod.setAccessible(true); + active = (Boolean) isActiveMethod.invoke(protocol); + } catch (Throwable e) { + LOGGER.warning("Failed to check isActive for " + clazz.getName() + ": " + e); + continue; + } + + for (final Method method : clazz.getDeclaredMethods()) { + if (method.isBridge() || method.isSynthetic()) { + continue; + } + method.setAccessible(true); + + final ProtocolHandler.Init init = method.getAnnotation(ProtocolHandler.Init.class); + if (init != null) { + InitInvokerHolder holder = new InitInvokerHolder(protocol, method, init); + try { + holder.invoke(); + } catch (RuntimeException exception) { + LOGGER.severe("Failed to invoke init method " + method.getName() + " in " + clazz.getName() + ", " + exception.getCause() + ": " + exception.getMessage()); + } + continue; + } + + if (!active) continue; + + final ProtocolHandler.ReloadServer reloadServer = method.getAnnotation(ProtocolHandler.ReloadServer.class); + if (reloadServer != null) { + RELOAD_SERVER.add(new EmptyInvokerHolder<>(protocol, method, reloadServer)); + continue; + } + + final ProtocolHandler.ReloadDataPack reloadDataPack = method.getAnnotation(ProtocolHandler.ReloadDataPack.class); + if (reloadDataPack != null) { + RELOAD_DATAPACK.add(new EmptyInvokerHolder<>(protocol, method, reloadDataPack)); + continue; + } + + final ProtocolHandler.PayloadReceiver payloadReceiver = method.getAnnotation(ProtocolHandler.PayloadReceiver.class); + if (payloadReceiver != null) { + PAYLOAD_RECEIVERS.put(payloadReceiver.payload(), new PayloadReceiverInvokerHolder(protocol, method, payloadReceiver)); + continue; + } + + final ProtocolHandler.BytebufReceiver bytebufReceiver = method.getAnnotation(ProtocolHandler.BytebufReceiver.class); + if (bytebufReceiver != null) { + String key = bytebufReceiver.key(); + BytebufReceiverInvokerHolder holder = new BytebufReceiverInvokerHolder(protocol, method, bytebufReceiver); + if (bytebufReceiver.onlyNamespace()) { + NAMESPACED_BYTEBUF_RECEIVERS.put(key.isEmpty() ? register.namespace() : key, holder); + } else { + if (key.isEmpty()) { + GENERIC_BYTEBUF_RECEIVERS.add(holder); + } else { + if (key.contains(":")) { + STRICT_BYTEBUF_RECEIVERS.put(key, holder); + } else { + STRICT_BYTEBUF_RECEIVERS.put(register.namespace() + ":" + key, holder); + } + } + } + continue; + } + + final ProtocolHandler.Ticker ticker = method.getAnnotation(ProtocolHandler.Ticker.class); + if (ticker != null) { + TICKERS.add(new EmptyInvokerHolder<>(protocol, method, ticker)); + continue; + } + + final ProtocolHandler.PlayerJoin playerJoin = method.getAnnotation(ProtocolHandler.PlayerJoin.class); + if (playerJoin != null) { + PLAYER_JOIN.add(new PlayerInvokerHolder<>(protocol, method, playerJoin)); + continue; + } + + final ProtocolHandler.PlayerLeave playerLeave = method.getAnnotation(ProtocolHandler.PlayerLeave.class); + if (playerLeave != null) { + PLAYER_LEAVE.add(new PlayerInvokerHolder<>(protocol, method, playerLeave)); + continue; + } + + final ProtocolHandler.MinecraftRegister minecraftRegister = method.getAnnotation(ProtocolHandler.MinecraftRegister.class); + if (minecraftRegister != null) { + String key = minecraftRegister.key(); + MinecraftRegisterInvokerHolder holder = new MinecraftRegisterInvokerHolder(protocol, method, minecraftRegister); + if (minecraftRegister.onlyNamespace()) { + NAMESPACED_MINECRAFT_REGISTER.put(key.isEmpty() ? register.namespace() : key, holder); + } else { + if (key.isEmpty()) { + WILD_MINECRAFT_REGISTER.add(holder); + } else { + if (key.contains(":")) { + STRICT_MINECRAFT_REGISTER.put(key, holder); + } else { + STRICT_MINECRAFT_REGISTER.put(register.namespace() + ":" + key, holder); + } + } + } + } + } + } + for (var idInfo : IDS.entrySet()) { + var codec = CODECS.get(idInfo.getKey()); + if (codec == null) { + throw new IllegalArgumentException("Payload " + idInfo.getKey() + " is not configured correctly"); + } + ID2CODEC.put(idInfo.getValue(), codec); + } + // Log all found instances + LOGGER.info("Protocol initialization complete. Found instances:"); + LOGGER.info(" Payload receivers: " + PAYLOAD_RECEIVERS.size()); + PAYLOAD_RECEIVERS.forEach((clazz, holder) -> + LOGGER.info(" " + clazz.getSimpleName() + " -> " + holder.getClass().getSimpleName())); + + LOGGER.info(" IDs: " + IDS.size()); + IDS.forEach((clazz, id) -> + LOGGER.info(" " + clazz.getSimpleName() + " -> " + id)); + + LOGGER.info(" Codecs: " + CODECS.size()); + CODECS.forEach((clazz, codec) -> + LOGGER.info(" " + clazz.getSimpleName() + " -> " + codec.getClass().getSimpleName())); + + LOGGER.info(" Strict bytebuf receivers: " + STRICT_BYTEBUF_RECEIVERS.size()); + STRICT_BYTEBUF_RECEIVERS.forEach((key, holder) -> + LOGGER.info(" " + key + " -> " + holder.getClass().getSimpleName())); + + LOGGER.info(" Namespaced bytebuf receivers: " + NAMESPACED_BYTEBUF_RECEIVERS.size()); + NAMESPACED_BYTEBUF_RECEIVERS.forEach((key, holder) -> + LOGGER.info(" " + key + " -> " + holder.getClass().getSimpleName())); + + LOGGER.info(" Generic bytebuf receivers: " + GENERIC_BYTEBUF_RECEIVERS.size()); + GENERIC_BYTEBUF_RECEIVERS.forEach(holder -> + LOGGER.info(" " + holder.getClass().getSimpleName())); + + LOGGER.info(" Tickers: " + TICKERS.size()); + LOGGER.info(" Player join handlers: " + PLAYER_JOIN.size()); + LOGGER.info(" Player leave handlers: " + PLAYER_LEAVE.size()); + LOGGER.info(" Server reload handlers: " + RELOAD_SERVER.size()); + LOGGER.info(" DataPack reload handlers: " + RELOAD_DATAPACK.size()); + + LOGGER.info(" Strict minecraft register: " + STRICT_MINECRAFT_REGISTER.size()); + STRICT_MINECRAFT_REGISTER.forEach((key, holder) -> + LOGGER.info(" " + key + " -> " + holder.getClass().getSimpleName())); + + LOGGER.info(" Namespaced minecraft register: " + NAMESPACED_MINECRAFT_REGISTER.size()); + NAMESPACED_MINECRAFT_REGISTER.forEach((key, holder) -> + LOGGER.info(" " + key + " -> " + holder.getClass().getSimpleName())); + + LOGGER.info(" Wild minecraft register: " + WILD_MINECRAFT_REGISTER.size()); + WILD_MINECRAFT_REGISTER.forEach(holder -> + LOGGER.info(" " + holder.getClass().getSimpleName())); + } + + public static LeavesCustomPayload decode(ResourceLocation location, FriendlyByteBuf buf) { + var codec = ID2CODEC.get(location); + if (codec == null) { + return null; + } + return codec.decode(ProtocolUtils.decorate(buf)); + } + + public static void encode(FriendlyByteBuf buf, LeavesCustomPayload payload) { + var location = IDS.get(payload.getClass()); + var codec = CODECS.get(payload.getClass()); + if (location == null || codec == null) { + throw new IllegalArgumentException("Payload " + payload.getClass() + " is not configured correctly " + location + " " + codec); + } + buf.writeResourceLocation(location); + codec.encode(ProtocolUtils.decorate(buf), payload); + } + + public static void handlePayload(ServerPlayer player, LeavesCustomPayload payload) { + PayloadReceiverInvokerHolder holder; + if ((holder = PAYLOAD_RECEIVERS.get(payload.getClass())) != null) { + holder.invoke(player, payload); + } + } + + public static boolean handleBytebuf(ServerPlayer player, ResourceLocation location, ByteBuf buf) { + RegistryFriendlyByteBuf buf1 = ProtocolUtils.decorate(buf); + BytebufReceiverInvokerHolder holder; + if ((holder = STRICT_BYTEBUF_RECEIVERS.get(location.toString())) != null) { + holder.invoke(player, buf1); + return true; + } + if ((holder = NAMESPACED_BYTEBUF_RECEIVERS.get(location.getNamespace())) != null) { + if (holder.invoke(player, buf1)) { + return true; + } + } + for (var holder1 : GENERIC_BYTEBUF_RECEIVERS) { + if (holder1.invoke(player, buf1)) { + return true; + } + } + return false; + } + + public static void handleTick(long tickCount) { + for (var tickerInfo : TICKERS) { + if (tickCount % tickerInfo.owner().tickerInterval(tickerInfo.handler().tickerId()) == 0) { + tickerInfo.invoke(); + } + } + } + + public static void handlePlayerJoin(ServerPlayer player) { + sendKnownId(player); + for (var join : PLAYER_JOIN) { + join.invoke(player); + } + } + + public static void handlePlayerLeave(ServerPlayer player) { + for (var leave : PLAYER_LEAVE) { + leave.invoke(player); + } + } + + public static void handleServerReload() { + for (var reload : RELOAD_SERVER) { + reload.invoke(); + } + } + + public static void handleDataPackReload() { + for (var reload : RELOAD_DATAPACK) { + reload.invoke(); + } + } + + public static void handleMinecraftRegister(String channelId, PluginMessageBridgeImpl bridge) { + ServerPlayer player = null; + if (bridge instanceof CraftPlayer craftPlayer) { + player = craftPlayer.getHandle(); + } + + if (player == null) { + return; + } + + ResourceLocation location = ResourceLocation.tryParse(channelId); + if (location == null) { + return; + } + + for (var wildHolder : WILD_MINECRAFT_REGISTER) { + wildHolder.invoke(player, location); + } + + MinecraftRegisterInvokerHolder holder; + if ((holder = STRICT_MINECRAFT_REGISTER.get(location.toString())) != null) { + holder.invoke(player, location); + } + if ((holder = NAMESPACED_MINECRAFT_REGISTER.get(location.getNamespace())) != null) { + holder.invoke(player, location); + } + } + + private static void sendKnownId(ServerPlayer player) { + Set set = new HashSet<>(); + PAYLOAD_RECEIVERS.forEach((clazz, holder) -> set.add(IDS.get(clazz).toString())); + STRICT_BYTEBUF_RECEIVERS.forEach((key, holder) -> set.add(key)); + if (set.isEmpty()) return; + ProtocolUtils.sendBytebufPacket(player, ResourceLocation.fromNamespaceAndPath("minecraft", "register"), buf -> { + for (String channel : set) { + buf.writeBytes(channel.getBytes(StandardCharsets.US_ASCII)); + buf.writeByte(0); + } + buf.writerIndex(Math.max(buf.writerIndex() - 1, 0)); + }); + } + + public static Set> getClasses(String pack) { + Set> classes = new LinkedHashSet<>(); + String packageDirName = pack.replace('.', '/'); + Enumeration dirs; + try { + dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName); + while (dirs.hasMoreElements()) { + URL url = dirs.nextElement(); + String protocol = url.getProtocol(); + if ("file".equals(protocol)) { + String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8); + findClassesInPackageByFile(pack, filePath, classes); + } else if ("jar".equals(protocol)) { + JarFile jar; + try { + jar = ((JarURLConnection) url.openConnection()).getJarFile(); + Enumeration entries = jar.entries(); + findClassesInPackageByJar(pack, entries, packageDirName, classes); + } catch (IOException exception) { + LOGGER.warning("Failed to load jar file, " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + } catch (IOException exception) { + LOGGER.warning("Failed to load classes, " + exception.getCause() + ": " + exception.getMessage()); + } + return classes; + } + + private static void findClassesInPackageByFile(String packageName, String packagePath, Set> classes) { + File dir = new File(packagePath); + if (!dir.exists() || !dir.isDirectory()) { + return; + } + File[] dirfiles = dir.listFiles((file) -> file.isDirectory() || file.getName().endsWith(".class")); + if (dirfiles != null) { + for (File file : dirfiles) { + if (file.isDirectory()) { + findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), classes); + } else { + String className = file.getName().substring(0, file.getName().length() - 6); + try { + classes.add(Class.forName(packageName + '.' + className)); + } catch (ClassNotFoundException exception) { + LOGGER.warning("Failed to load class " + className + ", " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + } + } + + private static void findClassesInPackageByJar(String packageName, Enumeration entries, String packageDirName, Set> classes) { + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String name = entry.getName(); + if (name.charAt(0) == '/') { + name = name.substring(1); + } + if (name.startsWith(packageDirName)) { + int idx = name.lastIndexOf('/'); + if (idx != -1) { + packageName = name.substring(0, idx).replace('/', '.'); + } + if (name.endsWith(".class") && !entry.isDirectory()) { + String className = name.substring(packageName.length() + 1, name.length() - 6); + try { + classes.add(Class.forName(packageName + '.' + className)); + } catch (ClassNotFoundException exception) { + LOGGER.warning("Failed to load class " + className + ", " + exception.getCause() + ": " + exception.getMessage()); + } + } + } + } + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java new file mode 100644 index 0000000..8bf4a4c --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolHandler.java @@ -0,0 +1,61 @@ +package org.leavesmc.leaves.protocol.core; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +public class ProtocolHandler { + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface Init { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface PayloadReceiver { + Class payload(); + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface BytebufReceiver { + String key() default ""; + + boolean onlyNamespace() default false; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface Ticker { + String tickerId() default ""; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface PlayerJoin { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface PlayerLeave { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ReloadServer { + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface MinecraftRegister { + String key() default ""; + + boolean onlyNamespace() default false; + } + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + public @interface ReloadDataPack { + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java new file mode 100644 index 0000000..82cb8ab --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/ProtocolUtils.java @@ -0,0 +1,43 @@ +package org.leavesmc.leaves.protocol.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.papermc.paper.ServerBuildInfo; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.network.protocol.common.custom.DiscardedPayload; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import org.jetbrains.annotations.NotNull; + +import java.util.function.Consumer; +import java.util.function.Function; + +public class ProtocolUtils { + private static final Function bufDecorator = buf -> buf instanceof RegistryFriendlyByteBuf registry ? registry : new RegistryFriendlyByteBuf(buf, MinecraftServer.getServer().registryAccess()); + + public static String buildProtocolVersion(String protocol) { + return protocol + "-leaves-" + ServerBuildInfo.buildInfo().asString(ServerBuildInfo.StringRepresentation.VERSION_SIMPLE); + } + + public static void sendEmptyPacket(ServerPlayer player, ResourceLocation id) { + player.internalConnection.send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, null))); + } + + public static void sendBytebufPacket(@NotNull ServerPlayer player, ResourceLocation id, Consumer consumer) { + RegistryFriendlyByteBuf buf = decorate(Unpooled.buffer()); + consumer.accept(buf); + player.internalConnection.send(new ClientboundCustomPayloadPacket(new DiscardedPayload(id, ByteBufUtil.getBytes(buf)))); + } + + public static void sendPayloadPacket(ServerPlayer player, CustomPacketPayload payload) { + player.internalConnection.send(new ClientboundCustomPayloadPacket(payload)); + } + + public static RegistryFriendlyByteBuf decorate(ByteBuf buf) { + return bufDecorator.apply(buf); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/AbstractInvokerHolder.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/AbstractInvokerHolder.java new file mode 100644 index 0000000..ebb2bee --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/AbstractInvokerHolder.java @@ -0,0 +1,69 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; + +public abstract class AbstractInvokerHolder { + protected final LeavesProtocol owner; + protected final Method invoker; + protected final T handler; + protected final Class returnType; + protected final Class[] parameterTypes; + + protected AbstractInvokerHolder(LeavesProtocol owner, Method invoker, T handler, @Nullable Class returnType, @NotNull Class... parameterTypes) { + this.owner = owner; + this.invoker = invoker; + this.handler = handler; + this.returnType = returnType; + this.parameterTypes = parameterTypes; + + validateMethodSignature(); + } + + protected void validateMethodSignature() { + if (returnType != null && !returnType.isAssignableFrom(invoker.getReturnType())) { + throw new IllegalArgumentException("Return type mismatch in " + owner.getClass().getName() + "#" + invoker.getName() + + ": expected " + returnType.getName() + " but found " + invoker.getReturnType().getName()); + } + + Class[] methodParamTypes = invoker.getParameterTypes(); + if (methodParamTypes.length != parameterTypes.length) { + throw new IllegalArgumentException("Parameter count mismatch in " + owner.getClass().getName() + "#" + invoker.getName() + + ": expected " + parameterTypes.length + " but found " + methodParamTypes.length); + } + + for (int i = 0; i < parameterTypes.length; i++) { + if (!parameterTypes[i].isAssignableFrom(methodParamTypes[i])) { + throw new IllegalArgumentException("Parameter type mismatch in " + owner.getClass().getName() + "#" + invoker.getName() + + " at index " + i + ": expected " + parameterTypes[i].getName() + " but found " + methodParamTypes[i].getName()); + } + } + } + + public LeavesProtocol owner() { + return owner; + } + + public T handler() { + return handler; + } + + protected Object invoke0(boolean force, Object... args) { + if (!force && !owner.isActive()) { + return null; + } + try { + if (Modifier.isStatic(invoker.getModifiers())) { + return invoker.invoke(null, args); + } else { + return invoker.invoke(owner, args); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/BytebufReceiverInvokerHolder.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/BytebufReceiverInvokerHolder.java new file mode 100644 index 0000000..e2c01b8 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/BytebufReceiverInvokerHolder.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; + +import java.lang.reflect.Method; + +public class BytebufReceiverInvokerHolder extends AbstractInvokerHolder { + public BytebufReceiverInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.BytebufReceiver handler) { + super(owner, invoker, handler, null, ServerPlayer.class, FriendlyByteBuf.class); + } + + public boolean invoke(ServerPlayer player, FriendlyByteBuf buf) { + return invoke0(false, player, buf) instanceof Boolean b && b; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/EmptyInvokerHolder.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/EmptyInvokerHolder.java new file mode 100644 index 0000000..d6e18d3 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/EmptyInvokerHolder.java @@ -0,0 +1,15 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import org.leavesmc.leaves.protocol.core.LeavesProtocol; + +import java.lang.reflect.Method; + +public class EmptyInvokerHolder extends AbstractInvokerHolder { + public EmptyInvokerHolder(LeavesProtocol owner, Method invoker, T handler) { + super(owner, invoker, handler, null); + } + + public void invoke() { + invoke0(false); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/InitInvokerHolder.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/InitInvokerHolder.java new file mode 100644 index 0000000..128aaf8 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/InitInvokerHolder.java @@ -0,0 +1,16 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; + +import java.lang.reflect.Method; + +public class InitInvokerHolder extends AbstractInvokerHolder { + public InitInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.Init handler) { + super(owner, invoker, handler, null); + } + + public void invoke() { + invoke0(true); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/MinecraftRegisterInvokerHolder.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/MinecraftRegisterInvokerHolder.java new file mode 100644 index 0000000..c6766bc --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/MinecraftRegisterInvokerHolder.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; + +import java.lang.reflect.Method; + +public class MinecraftRegisterInvokerHolder extends AbstractInvokerHolder { + public MinecraftRegisterInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.MinecraftRegister handler) { + super(owner, invoker, handler, null, ServerPlayer.class, ResourceLocation.class); + } + + public void invoke(ServerPlayer player, ResourceLocation id) { + invoke0(false, player, id); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PayloadReceiverInvokerHolder.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PayloadReceiverInvokerHolder.java new file mode 100644 index 0000000..bed868b --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PayloadReceiverInvokerHolder.java @@ -0,0 +1,18 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import net.minecraft.server.level.ServerPlayer; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; + +import java.lang.reflect.Method; + +public class PayloadReceiverInvokerHolder extends AbstractInvokerHolder { + public PayloadReceiverInvokerHolder(LeavesProtocol owner, Method invoker, ProtocolHandler.PayloadReceiver handler) { + super(owner, invoker, handler, null, ServerPlayer.class, handler.payload()); + } + + public void invoke(ServerPlayer player, LeavesCustomPayload payload) { + invoke0(false, player, payload); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PlayerInvokerHolder.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PlayerInvokerHolder.java new file mode 100644 index 0000000..5ed2c2d --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/core/invoker/PlayerInvokerHolder.java @@ -0,0 +1,16 @@ +package org.leavesmc.leaves.protocol.core.invoker; + +import net.minecraft.server.level.ServerPlayer; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; + +import java.lang.reflect.Method; + +public class PlayerInvokerHolder extends AbstractInvokerHolder { + public PlayerInvokerHolder(LeavesProtocol owner, Method invoker, T handler) { + super(owner, invoker, handler, null, ServerPlayer.class); + } + + public void invoke(ServerPlayer player) { + invoke0(false, player); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java new file mode 100644 index 0000000..d5727e8 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java @@ -0,0 +1,290 @@ +package org.leavesmc.leaves.protocol.jade; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.Chicken; +import net.minecraft.world.entity.animal.allay.Allay; +import net.minecraft.world.entity.animal.armadillo.Armadillo; +import net.minecraft.world.entity.animal.frog.Tadpole; +import net.minecraft.world.entity.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.AbstractFurnaceBlockEntity; +import net.minecraft.world.level.block.entity.BeehiveBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; +import net.minecraft.world.level.block.entity.CalibratedSculkSensorBlockEntity; +import net.minecraft.world.level.block.entity.ChiseledBookShelfBlockEntity; +import net.minecraft.world.level.block.entity.CommandBlockEntity; +import net.minecraft.world.level.block.entity.ComparatorBlockEntity; +import net.minecraft.world.level.block.entity.HopperBlockEntity; +import net.minecraft.world.level.block.entity.JukeboxBlockEntity; +import net.minecraft.world.level.block.entity.LecternBlockEntity; +import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity; +import org.bukkit.Bukkit; +import org.bxteam.divinemc.config.DivineConfig; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.payload.ClientHandshakePayload; +import org.leavesmc.leaves.protocol.jade.payload.ReceiveDataPayload; +import org.leavesmc.leaves.protocol.jade.payload.RequestBlockPayload; +import org.leavesmc.leaves.protocol.jade.payload.RequestEntityPayload; +import org.leavesmc.leaves.protocol.jade.payload.ServerHandshakePayload; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; +import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; +import org.leavesmc.leaves.protocol.jade.provider.ItemStorageExtensionProvider; +import org.leavesmc.leaves.protocol.jade.provider.ItemStorageProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.BeehiveProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.BlockNameProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.BrewingStandProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.CampfireProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.ChiseledBookshelfProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.CommandBlockProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.FurnaceProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.HopperLockProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.JukeboxProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.LecternProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.MobSpawnerCooldownProvider; +import org.leavesmc.leaves.protocol.jade.provider.block.RedstoneProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.AnimalOwnerProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.MobBreedingProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.MobGrowthProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.NextEntityDropProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.PetArmorProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.StatusEffectsProvider; +import org.leavesmc.leaves.protocol.jade.provider.entity.ZombieVillagerProvider; +import org.leavesmc.leaves.protocol.jade.util.HierarchyLookup; +import org.leavesmc.leaves.protocol.jade.util.LootTableMineableCollector; +import org.leavesmc.leaves.protocol.jade.util.PairHierarchyLookup; +import org.leavesmc.leaves.protocol.jade.util.PriorityStore; +import org.leavesmc.leaves.protocol.jade.util.WrappedHierarchyLookup; +import org.leavesmc.leaves.util.NbtUtils; +import org.purpurmc.purpur.util.MinecraftInternalPlugin; + +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 { + + public static final String PROTOCOL_ID = "jade"; + public static final String PROTOCOL_VERSION = "7"; + public static final HierarchyLookup> entityDataProviders = new HierarchyLookup<>(Entity.class); + public static final PairHierarchyLookup> blockDataProviders = new PairHierarchyLookup<>(new HierarchyLookup<>(Block.class), new HierarchyLookup<>(BlockEntity.class)); + public static final WrappedHierarchyLookup> itemStorageProviders = WrappedHierarchyLookup.forAccessor(); + private static final Set enabledPlayers = new HashSet<>(); + private static final org.purpurmc.purpur.util.MinecraftInternalPlugin minecraftInternalPlugin = new org.purpurmc.purpur.util.MinecraftInternalPlugin(); + + 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) { + Bukkit.getGlobalRegionScheduler().run(minecraftInternalPlugin, (task) -> { + EntityAccessor accessor = payload.data().unpack(player); + if (accessor == null) { + return; + } + + Entity entity = accessor.getEntity(); + double maxDistance = Mth.square(player.entityInteractionRange() + 21); + if (entity == null || player.distanceToSqr(entity) > maxDistance) { + return; + } + + List> providers = entityDataProviders.get(entity); + if (providers.isEmpty()) { + return; + } + + CompoundTag tag = new CompoundTag(); + for (IServerDataProvider provider : providers) { + if (!payload.dataProviders().contains(provider)) { + continue; + } + try { + provider.appendServerData(tag, accessor); + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Error while saving data for entity " + entity); + } + } + tag.putInt("EntityId", entity.getId()); + + ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag)); + }); + } + + @ProtocolHandler.PayloadReceiver(payload = RequestBlockPayload.class) + public static void requestBlockData(ServerPlayer player, RequestBlockPayload payload) { + Bukkit.getGlobalRegionScheduler().run(minecraftInternalPlugin, (task) -> { + BlockAccessor accessor = payload.data().unpack(player); + if (accessor == null) { + return; + } + + BlockPos pos = accessor.getPosition(); + Block block = accessor.getBlock(); + BlockEntity blockEntity = accessor.getBlockEntity(); + double maxDistance = Mth.square(player.blockInteractionRange() + 21); + if (pos.distSqr(player.blockPosition()) > maxDistance || !accessor.getLevel().isLoaded(pos)) { + return; + } + + List> providers; + if (blockEntity != null) { + providers = blockDataProviders.getMerged(block, blockEntity); + } else { + providers = blockDataProviders.first.get(block); + } + + if (providers.isEmpty()) { + return; + } + + CompoundTag tag = new CompoundTag(); + for (IServerDataProvider provider : providers) { + if (!payload.dataProviders().contains(provider)) { + continue; + } + try { + provider.appendServerData(tag, accessor); + } catch (Exception e) { + LeavesLogger.LOGGER.warning("Error while saving data for block " + accessor.getBlockState()); + } + } + NbtUtils.writeBlockPosToTag(pos, tag); + tag.putString("BlockId", BuiltInRegistries.BLOCK.getKey(block).toString()); + + ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag)); + }); + } + + @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(); + LeavesLogger.LOGGER.severe("Failed to collect shearable blocks"); + } + } + + @Override + public boolean isActive() { + return DivineConfig.NetworkCategory.protocolsJadeEnabled; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java new file mode 100644 index 0000000..6bc2253 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/Accessor.java @@ -0,0 +1,22 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamEncoder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.HitResult; +import org.jetbrains.annotations.Nullable; + +public interface Accessor { + Level getLevel(); + + Player getPlayer(); + + Tag encodeAsNbt(StreamEncoder codec, D value); + + T getHitResult(); + + @Nullable + Object getTarget(); +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java new file mode 100644 index 0000000..a6d91d9 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/AccessorImpl.java @@ -0,0 +1,60 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import io.netty.buffer.Unpooled; +import net.minecraft.nbt.ByteArrayTag; +import net.minecraft.nbt.Tag; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamEncoder; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.HitResult; +import org.apache.commons.lang3.ArrayUtils; + +import java.util.function.Supplier; + +public abstract class AccessorImpl implements Accessor { + + private final Level level; + private final Player player; + private final Supplier hit; + protected boolean verify; + private RegistryFriendlyByteBuf buffer; + + public AccessorImpl(Level level, Player player, Supplier hit) { + this.level = level; + this.player = player; + this.hit = hit; + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public Player getPlayer() { + return player; + } + + private RegistryFriendlyByteBuf buffer() { + if (buffer == null) { + buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), level.registryAccess()); + } + buffer.clear(); + return buffer; + } + + @Override + public Tag encodeAsNbt(StreamEncoder streamCodec, D value) { + RegistryFriendlyByteBuf buffer = buffer(); + streamCodec.encode(buffer, value); + ByteArrayTag tag = new ByteArrayTag(ArrayUtils.subarray(buffer.array(), 0, buffer.readableBytes())); + buffer.clear(); + return tag; + } + + @Override + public T getHitResult() { + return hit.get(); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java new file mode 100644 index 0000000..133415a --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java @@ -0,0 +1,44 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Supplier; + +public interface BlockAccessor extends Accessor { + + Block getBlock(); + + BlockState getBlockState(); + + BlockEntity getBlockEntity(); + + BlockPos getPosition(); + + @ApiStatus.NonExtendable + interface Builder { + Builder level(Level 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java new file mode 100644 index 0000000..a94bb9e --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessorImpl.java @@ -0,0 +1,143 @@ +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.ServerPlayer; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.phys.BlockHitResult; +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 Level level; + private Player player; + private BlockHitResult hit; + private BlockState blockState = Blocks.AIR.defaultBlockState(); + private Supplier blockEntity; + + @Override + public Builder level(Level level) { + this.level = level; + return this; + } + + @Override + public Builder player(Player player) { + this.player = player; + return this; + } + + @Override + public Builder 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java new file mode 100644 index 0000000..00fd87b --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java @@ -0,0 +1,42 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.EntityHitResult; +import org.jetbrains.annotations.ApiStatus; + +import java.util.function.Supplier; + +public interface EntityAccessor extends Accessor { + + Entity getEntity(); + + /** + * For part entity like ender dragon's, getEntity() will return the parent entity. + */ + Entity getRawEntity(); + + @ApiStatus.NonExtendable + interface Builder { + Builder level(Level level); + + Builder player(Player player); + + 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java new file mode 100644 index 0000000..56166c6 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessorImpl.java @@ -0,0 +1,112 @@ +package org.leavesmc.leaves.protocol.jade.accessor; + +import com.google.common.base.Suppliers; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.EntityHitResult; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.util.CommonUtil; + +import java.util.function.Supplier; + +public class EntityAccessorImpl extends AccessorImpl implements EntityAccessor { + + private final Supplier entity; + + public EntityAccessorImpl(Builder builder) { + super(builder.level, builder.player, builder.hit); + 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 Level level; + private Player player; + private Supplier hit; + private Supplier entity; + + @Override + public Builder level(Level level) { + this.level = level; + return this; + } + + @Override + public Builder player(Player player) { + this.player = player; + return this; + } + + + @Override + public Builder 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java new file mode 100644 index 0000000..1620d68 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ClientHandshakePayload.java @@ -0,0 +1,19 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java new file mode 100644 index 0000000..b2630fc --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ReceiveDataPayload.java @@ -0,0 +1,20 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java new file mode 100644 index 0000000..9d32bf2 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestBlockPayload.java @@ -0,0 +1,35 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java new file mode 100644 index 0000000..70dd289 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/RequestEntityPayload.java @@ -0,0 +1,36 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java new file mode 100644 index 0000000..4955e0e --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/payload/ServerHandshakePayload.java @@ -0,0 +1,37 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java new file mode 100644 index 0000000..d62fc8f --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java @@ -0,0 +1,12 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import net.minecraft.resources.ResourceLocation; + +public interface IJadeProvider { + + ResourceLocation getUid(); + + default int getDefaultPriority() { + return 0; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java new file mode 100644 index 0000000..7d839f1 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerDataProvider.java @@ -0,0 +1,8 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import net.minecraft.nbt.CompoundTag; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; + +public interface IServerDataProvider> extends IJadeProvider { + void appendServerData(CompoundTag data, T accessor); +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java new file mode 100644 index 0000000..6e32eed --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java @@ -0,0 +1,10 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; + +public interface IServerExtensionProvider extends IJadeProvider { + List> getGroups(Accessor request); +} \ No newline at end of file diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java new file mode 100644 index 0000000..2999e79 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java @@ -0,0 +1,142 @@ +package org.leavesmc.leaves.protocol.jade.provider; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.Container; +import net.minecraft.world.LockCode; +import net.minecraft.world.RandomizableContainer; +import net.minecraft.world.WorldlyContainerHolder; +import net.minecraft.world.entity.animal.horse.AbstractHorse; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.vehicle.ContainerEntity; +import net.minecraft.world.inventory.PlayerEnderChestContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.EnderChestBlock; +import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +import net.minecraft.world.level.block.entity.EnderChestBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.util.ItemCollector; +import org.leavesmc.leaves.protocol.jade.util.ItemIterator; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public enum ItemStorageExtensionProvider implements IServerExtensionProvider { + INSTANCE; + + public static final Cache> targetCache = CacheBuilder.newBuilder().weakKeys().expireAfterAccess(60, TimeUnit.SECONDS).build(); + + private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage.default"); + + public static ItemCollector createItemCollector(Accessor request) { + if (request.getTarget() instanceof AbstractHorse) { + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(o -> { + if (o instanceof AbstractHorse horse) { + return horse.inventory; + } + return null; + }, 2)); + } + + // TODO BlockEntity like fabric's ItemStorage + + final Container container = findContainer(request); + if (container != null) { + if (container instanceof ChestBlockEntity) { + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(o -> { + if (o instanceof ChestBlockEntity blockEntity) { + if (blockEntity.getBlockState().getBlock() instanceof ChestBlock chestBlock) { + Container compound = null; + if (blockEntity.getLevel() != null) { + compound = ChestBlock.getContainer(chestBlock, blockEntity.getBlockState(), blockEntity.getLevel(), blockEntity.getBlockPos(), false); + } + if (compound != null) { + return compound; + } + } + return blockEntity; + } + return null; + }, 0)); + } + return new ItemCollector<>(new ItemIterator.ContainerItemIterator(0)); + } + + return ItemCollector.EMPTY; + } + + public static @Nullable Container findContainer(@NotNull Accessor accessor) { + Object target = accessor.getTarget(); + if (target == null && accessor instanceof BlockAccessor blockAccessor && + blockAccessor.getBlock() instanceof WorldlyContainerHolder holder) { + return holder.getContainer(blockAccessor.getBlockState(), accessor.getLevel(), blockAccessor.getPosition()); + } else if (target instanceof Container container) { + return container; + } + return null; + } + + @Override + public 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) { + LeavesLogger.LOGGER.severe("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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java new file mode 100644 index 0000000..7d6cd6d --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageProvider.java @@ -0,0 +1,87 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java new file mode 100644 index 0000000..fde7c8e --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/StreamServerDataProvider.java @@ -0,0 +1,23 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java new file mode 100644 index 0000000..ee92d79 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java @@ -0,0 +1,34 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BeehiveBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum BeehiveProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_BEEHIVE = JadeProtocol.mc_id("beehive"); + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.BYTE.cast(); + } + + @Override + public Byte streamData(@NotNull BlockAccessor accessor) { + BeehiveBlockEntity beehive = (BeehiveBlockEntity) accessor.getBlockEntity(); + int bees = beehive.getOccupantCount(); + return (byte) (beehive.isFull() ? bees : -bees); + } + + @Override + public ResourceLocation getUid() { + return MC_BEEHIVE; + } +} \ No newline at end of file diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockNameProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockNameProvider.java new file mode 100644 index 0000000..4904ef0 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockNameProvider.java @@ -0,0 +1,60 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java new file mode 100644 index 0000000..4266550 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java @@ -0,0 +1,43 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum BrewingStandProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_BREWING_STAND = JadeProtocol.mc_id("brewing_stand"); + + @Override + public @NotNull Data streamData(@NotNull BlockAccessor accessor) { + BrewingStandBlockEntity brewingStand = (BrewingStandBlockEntity) accessor.getBlockEntity(); + return new Data(brewingStand.fuel, brewingStand.brewTime); + } + + @Override + public @NotNull StreamCodec streamCodec() { + return Data.STREAM_CODEC.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_BREWING_STAND; + } + + public record Data(int fuel, int time) { + public static final StreamCodec STREAM_CODEC = StreamCodec.composite( + ByteBufCodecs.VAR_INT, + Data::fuel, + ByteBufCodecs.VAR_INT, + Data::time, + Data::new); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java new file mode 100644 index 0000000..2deb377 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java @@ -0,0 +1,55 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import com.google.common.collect.Lists; +import com.mojang.serialization.Codec; +import com.mojang.serialization.MapCodec; +import net.minecraft.core.component.DataComponents; +import net.minecraft.nbt.NbtOps; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.component.CustomData; +import net.minecraft.world.level.block.entity.CampfireBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Unmodifiable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; +import org.leavesmc.leaves.protocol.jade.util.ViewGroup; + +import java.util.List; + +public enum CampfireProvider implements IServerExtensionProvider { + INSTANCE; + + private static final MapCodec COOKING_TIME_CODEC = Codec.INT.fieldOf("jade:cooking"); + private static final ResourceLocation MC_CAMPFIRE = JadeProtocol.mc_id("campfire"); + + @Override + public @Nullable @Unmodifiable List> getGroups(@NotNull Accessor request) { + if (request.getTarget() instanceof CampfireBlockEntity campfire) { + List list = Lists.newArrayList(); + for (int i = 0; i < campfire.cookingTime.length; i++) { + ItemStack stack = campfire.getItems().get(i); + if (stack.isEmpty()) { + continue; + } + stack = stack.copy(); + + CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY) + .update(NbtOps.INSTANCE, COOKING_TIME_CODEC, campfire.cookingTime[i] - campfire.cookingProgress[i]) + .getOrThrow(); + stack.set(DataComponents.CUSTOM_DATA, customData); + + list.add(stack); + } + return List.of(new ViewGroup<>(list)); + } + return null; + } + + @Override + public ResourceLocation getUid() { + return MC_CAMPFIRE; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java new file mode 100644 index 0000000..12fdbc3 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java @@ -0,0 +1,44 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java new file mode 100644 index 0000000..5f71fad --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java @@ -0,0 +1,40 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.entity.CommandBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum CommandBlockProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_COMMAND_BLOCK = JadeProtocol.mc_id("command_block"); + + @Nullable + public String streamData(@NotNull BlockAccessor accessor) { + if (!accessor.getPlayer().canUseGameMasterBlocks()) { + return null; + } + String command = ((CommandBlockEntity) accessor.getBlockEntity()).getCommandBlock().getCommand(); + if (command.length() > 40) { + command = command.substring(0, 37) + "..."; + } + return command; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.STRING_UTF8.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_COMMAND_BLOCK; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java new file mode 100644 index 0000000..5e5f2cc --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java @@ -0,0 +1,51 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java new file mode 100644 index 0000000..2459acd --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/HopperLockProvider.java @@ -0,0 +1,37 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java new file mode 100644 index 0000000..0b6e224 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java @@ -0,0 +1,32 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.JukeboxBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum JukeboxProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_JUKEBOX = JadeProtocol.mc_id("jukebox"); + + @Override + public @NotNull ItemStack streamData(BlockAccessor accessor) { + return ((JukeboxBlockEntity) accessor.getBlockEntity()).getTheItem(); + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_JUKEBOX; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java new file mode 100644 index 0000000..c363bd6 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java @@ -0,0 +1,33 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.entity.LecternBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum LecternProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_LECTERN = JadeProtocol.mc_id("lectern"); + + @Override + public @NotNull ItemStack streamData(@NotNull BlockAccessor accessor) { + return ((LecternBlockEntity) accessor.getBlockEntity()).getBook(); + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + + @Override + public ResourceLocation getUid() { + return MC_LECTERN; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java new file mode 100644 index 0000000..feb636d --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java @@ -0,0 +1,42 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity; +import net.minecraft.world.level.block.entity.trialspawner.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 = ((ServerLevel) accessor.getLevel()); + if (spawner.getTrialSpawner().canSpawnInLevel(level) && level.getGameTime() < spawnerData.cooldownEndsAt) { + return (int) (spawnerData.cooldownEndsAt - level.getGameTime()); + } + return null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + + @Override + public ResourceLocation getUid() { + return MC_MOB_SPAWNER_COOLDOWN; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java new file mode 100644 index 0000000..1cdcf21 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java @@ -0,0 +1,36 @@ +package org.leavesmc.leaves.protocol.jade.provider.block; + +import net.minecraft.core.Direction; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.level.block.CalibratedSculkSensorBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.CalibratedSculkSensorBlockEntity; +import net.minecraft.world.level.block.entity.ComparatorBlockEntity; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider; + +public enum RedstoneProvider implements IServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_REDSTONE = JadeProtocol.mc_id("redstone"); + + @Override + public void appendServerData(CompoundTag data, @NotNull BlockAccessor accessor) { + BlockEntity blockEntity = accessor.getBlockEntity(); + if (blockEntity instanceof ComparatorBlockEntity comparator) { + data.putInt("Signal", comparator.getOutputSignal()); + } else if (blockEntity instanceof CalibratedSculkSensorBlockEntity) { + Direction direction = accessor.getBlockState().getValue(CalibratedSculkSensorBlock.FACING).getOpposite(); + int signal = accessor.getLevel().getSignal(accessor.getPosition().relative(direction), direction); + data.putInt("Signal", signal); + } + } + + @Override + public ResourceLocation getUid() { + return MC_REDSTONE; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java new file mode 100644 index 0000000..495815a --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java @@ -0,0 +1,48 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java new file mode 100644 index 0000000..0acba2f --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java @@ -0,0 +1,44 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.Animal; +import net.minecraft.world.entity.animal.allay.Allay; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum MobBreedingProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_MOB_BREEDING = JadeProtocol.mc_id("mob_breeding"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = 0; + Entity entity = accessor.getEntity(); + if (entity instanceof Allay allay) { + if (allay.duplicationCooldown > 0 && allay.duplicationCooldown < Integer.MAX_VALUE) { + time = (int) allay.duplicationCooldown; + } + } else { + time = ((Animal) entity).getAge(); + } + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_MOB_BREEDING; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java new file mode 100644 index 0000000..44f5f4b --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java @@ -0,0 +1,43 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.animal.frog.Tadpole; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum MobGrowthProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_MOB_GROWTH = JadeProtocol.mc_id("mob_growth"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = -1; + Entity entity = accessor.getEntity(); + if (entity instanceof AgeableMob ageable) { + time = -ageable.getAge(); + } else if (entity instanceof Tadpole tadpole) { + time = tadpole.getTicksLeftUntilAdult(); + } + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + + @Override + public ResourceLocation getUid() { + return MC_MOB_GROWTH; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java new file mode 100644 index 0000000..f7d24fd --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java @@ -0,0 +1,42 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java new file mode 100644 index 0000000..cb8d173 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/PetArmorProvider.java @@ -0,0 +1,35 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum PetArmorProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_PET_ARMOR = JadeProtocol.mc_id("pet_armor"); + + @Nullable + @Override + public ItemStack streamData(@NotNull EntityAccessor accessor) { + ItemStack armor = ((Mob) accessor.getEntity()).getBodyArmorItem(); + return armor.isEmpty() ? null : armor; + } + + @Override + public StreamCodec streamCodec() { + return ItemStack.OPTIONAL_STREAM_CODEC; + } + + @Override + public ResourceLocation getUid() { + return MC_PET_ARMOR; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java new file mode 100644 index 0000000..7470627 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java @@ -0,0 +1,45 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.LivingEntity; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +import java.util.List; + +public enum StatusEffectsProvider implements StreamServerDataProvider> { + INSTANCE; + + + private static final StreamCodec> STREAM_CODEC = ByteBufCodecs.list() + .apply(MobEffectInstance.STREAM_CODEC); + private static final ResourceLocation MC_POTION_EFFECTS = JadeProtocol.mc_id("potion_effects"); + + @Override + @Nullable + public List streamData(@NotNull EntityAccessor accessor) { + List effects = ((LivingEntity) accessor.getEntity()).getActiveEffects() + .stream() + .filter(MobEffectInstance::isVisible) + .toList(); + return effects.isEmpty() ? null : effects; + } + + @Override + public StreamCodec> streamCodec() { + return STREAM_CODEC; + } + + + @Override + public ResourceLocation getUid() { + return MC_POTION_EFFECTS; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java new file mode 100644 index 0000000..b7c9afd --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java @@ -0,0 +1,34 @@ +package org.leavesmc.leaves.protocol.jade.provider.entity; + +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.monster.ZombieVillager; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor; +import org.leavesmc.leaves.protocol.jade.provider.StreamServerDataProvider; + +public enum ZombieVillagerProvider implements StreamServerDataProvider { + INSTANCE; + + private static final ResourceLocation MC_ZOMBIE_VILLAGER = JadeProtocol.mc_id("zombie_villager"); + + @Override + public @Nullable Integer streamData(@NotNull EntityAccessor accessor) { + int time = ((ZombieVillager) accessor.getEntity()).villagerConversionTime; + return time > 0 ? time : null; + } + + @Override + public @NotNull StreamCodec streamCodec() { + return ByteBufCodecs.VAR_INT.cast(); + } + + @Override + public ResourceLocation getUid() { + return MC_ZOMBIE_VILLAGER; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java new file mode 100644 index 0000000..9bbe516 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java @@ -0,0 +1,37 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java new file mode 100644 index 0000000..4e48a26 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/CommonUtil.java @@ -0,0 +1,72 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.mojang.authlib.GameProfile; +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.LeavesLogger; +import org.leavesmc.leaves.protocol.jade.accessor.Accessor; +import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +public class CommonUtil { + + public static Entity wrapPartEntityParent(Entity target) { + if (target instanceof EnderDragonPart part) { + return part.parentMob; + } + return target; + } + + public static Entity getPartEntity(Entity parent, int index) { + if (parent == null) { + return null; + } + if (index < 0) { + return parent; + } + if (parent instanceof EnderDragon dragon) { + EnderDragonPart[] parts = dragon.getSubEntities(); + if (index < parts.length) { + return parts[index]; + } + } + return parent; + } + + + @Nullable + public static String getLastKnownUsername(@Nullable UUID uuid) { + if (uuid == null) { + return null; + } + Optional optional = SkullBlockEntity.fetchGameProfile(String.valueOf(uuid)).getNow(Optional.empty()); + return optional.map(GameProfile::getName).orElse(null); + } + + + public static Map.Entry>> getServerExtensionData( + Accessor accessor, + WrappedHierarchyLookup> lookup) { + for (var provider : lookup.wrappedGet(accessor)) { + List> groups; + try { + groups = provider.getGroups(accessor); + } catch (Exception e) { + LeavesLogger.LOGGER.severe(e.toString()); + continue; + } + if (groups != null) { + return Map.entry(provider.getUid(), groups); + } + } + return null; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java new file mode 100644 index 0000000..b2736d7 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java @@ -0,0 +1,138 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.google.common.base.Preconditions; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableListMultimap; +import com.google.common.collect.ListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.jade.JadeProtocol; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.stream.Stream; + +public class HierarchyLookup implements IHierarchyLookup { + private final Class baseClass; + private final Cache, List> resultCache = CacheBuilder.newBuilder().build(); + private final boolean singleton; + protected boolean idMapped; + @Nullable + protected IdMapper idMapper; + private ListMultimap, T> objects = ArrayListMultimap.create(); + + public HierarchyLookup(Class baseClass) { + this(baseClass, false); + } + + public HierarchyLookup(Class baseClass, boolean singleton) { + this.baseClass = baseClass; + this.singleton = singleton; + } + + @Override + public void idMapped() { + this.idMapped = true; + } + + @Override + @Nullable + public IdMapper idMapper() { + return idMapper; + } + + @Override + public void register(Class clazz, T provider) { + Preconditions.checkArgument(isClassAcceptable(clazz), "Class %s is not acceptable", clazz); + Objects.requireNonNull(provider.getUid()); + JadeProtocol.priorities.put(provider); + objects.put(clazz, provider); + } + + @Override + public boolean isClassAcceptable(Class clazz) { + return baseClass.isAssignableFrom(clazz); + } + + @Override + public List get(Class clazz) { + try { + return resultCache.get(clazz, () -> { + List list = Lists.newArrayList(); + getInternal(clazz, list); + list = ImmutableList.sortedCopyOf(COMPARATOR, list); + if (singleton && !list.isEmpty()) { + return ImmutableList.of(list.getFirst()); + } + return list; + }); + } catch (ExecutionException e) { + LeavesLogger.LOGGER.warning("HierarchyLookup error", e); + } + return List.of(); + } + + private void getInternal(Class clazz, List list) { + if (clazz != baseClass && clazz != Object.class) { + getInternal(clazz.getSuperclass(), list); + } + list.addAll(objects.get(clazz)); + } + + @Override + public boolean isEmpty() { + return objects.isEmpty(); + } + + @Override + public Stream, Collection>> entries() { + return objects.asMap().entrySet().stream(); + } + + @Override + public void invalidate() { + resultCache.invalidateAll(); + } + + @Override + public void loadComplete(PriorityStore priorityStore) { + objects.asMap().forEach((clazz, list) -> { + if (list.size() < 2) { + return; + } + Set set = Sets.newHashSetWithExpectedSize(list.size()); + for (T provider : list) { + if (set.contains(provider.getUid())) { + throw new IllegalStateException("Duplicate UID: %s for %s".formatted(provider.getUid(), list.stream() + .filter(p -> p.getUid().equals(provider.getUid())) + .map(p -> p.getClass().getName()) + .toList() + )); + } + set.add(provider.getUid()); + } + }); + + objects = ImmutableListMultimap., T>builder() + .orderValuesBy(Comparator.comparingInt(priorityStore::byValue)) + .putAll(objects) + .build(); + + if (idMapped) { + idMapper = createIdMapper(); + } + } +} \ No newline at end of file diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java new file mode 100644 index 0000000..c160eaa --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java @@ -0,0 +1,71 @@ +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.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java new file mode 100644 index 0000000..c23cb19 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java @@ -0,0 +1,121 @@ +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().getGameTime(); + if (mergedResult != null && iterator.isFinished()) { + if (version == currentVersion) { + return mergedResult; // content not changed + } + if (lastTimeFinished + 5 > gameTime) { + return mergedResult; // avoid update too frequently + } + iterator.reset(); + } + AtomicInteger count = new AtomicInteger(); + iterator.populate(container).forEach(stack -> { + count.incrementAndGet(); + if (SHOWN.test(stack)) { + ItemDefinition def = new ItemDefinition(stack); + items.addTo(def, stack.getCount()); + } + }); + iterator.afterPopulate(count.get()); + if (mergedResult != null && !iterator.isFinished()) { + updateCollectingProgress(mergedResult.getFirst()); + return mergedResult; + } + List partialResult = items.object2IntEntrySet().stream().limit(MAX_SIZE).map(entry -> { + ItemDefinition def = entry.getKey(); + return def.toStack(entry.getIntValue()); + }).toList(); + List> groups = List.of(updateCollectingProgress(new ViewGroup<>(partialResult))); + if (iterator.isFinished()) { + mergedResult = groups; + lastTimeIsEmpty = mergedResult.getFirst().views.isEmpty(); + version = currentVersion; + lastTimeFinished = gameTime; + items.clear(); + } + return groups; + } + + protected ViewGroup updateCollectingProgress(ViewGroup group) { + if (lastTimeIsEmpty && group.views.isEmpty()) { + return group; + } + float progress = iterator.getCollectingProgress(); + CompoundTag data = group.getExtraData(); + if (Float.isNaN(progress) || progress >= 1) { + data.remove("Collecting"); + } else { + data.putFloat("Collecting", progress); + } + return group; + } + + public record ItemDefinition(Item item, DataComponentPatch components) { + ItemDefinition(ItemStack stack) { + this(stack.getItem(), stack.getComponentsPatch()); + } + + public ItemStack toStack(int count) { + ItemStack itemStack = new ItemStack(item, count); + itemStack.applyComponents(components); + return itemStack; + } + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java new file mode 100644 index 0000000..4d65e9a --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java @@ -0,0 +1,102 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import net.minecraft.world.Container; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public abstract class ItemIterator { + public static final AtomicLong version = new AtomicLong(); + protected final Function containerFinder; + protected final int fromIndex; + protected boolean finished; + protected int currentIndex; + + protected ItemIterator(Function containerFinder, int fromIndex) { + this.containerFinder = containerFinder; + this.currentIndex = this.fromIndex = fromIndex; + } + + public @Nullable T find(Object target) { + return containerFinder.apply(target); + } + + public final boolean isFinished() { + return finished; + } + + public long getVersion(T container) { + return version.getAndIncrement(); + } + + public abstract Stream populate(T container); + + public void reset() { + currentIndex = fromIndex; + finished = false; + } + + public void afterPopulate(int count) { + currentIndex += count; + if (count == 0 || currentIndex >= 10000) { + finished = true; + } + } + + public float getCollectingProgress() { + return Float.NaN; + } + + public static abstract class SlottedItemIterator extends ItemIterator { + protected float progress; + + public SlottedItemIterator(Function containerFinder, int fromIndex) { + super(containerFinder, fromIndex); + } + + protected abstract int getSlotCount(T container); + + protected abstract ItemStack getItemInSlot(T container, int slot); + + @Override + public Stream populate(T container) { + int slotCount = getSlotCount(container); + int toIndex = currentIndex + ItemCollector.MAX_SIZE * 2; + if (toIndex >= slotCount) { + toIndex = slotCount; + finished = true; + } + progress = (float) (currentIndex - fromIndex) / (slotCount - fromIndex); + return IntStream.range(currentIndex, toIndex).mapToObj(slot -> getItemInSlot(container, slot)); + } + + @Override + public float getCollectingProgress() { + return progress; + } + } + + public static class ContainerItemIterator extends SlottedItemIterator { + public ContainerItemIterator(int fromIndex) { + this(Container.class::cast, fromIndex); + } + + public ContainerItemIterator(Function containerFinder, int fromIndex) { + super(containerFinder, fromIndex); + } + + @Override + protected int getSlotCount(Container container) { + return container.getContainerSize(); + } + + @Override + protected ItemStack getItemInSlot(Container container, int slot) { + return container.getItem(slot); + } + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java new file mode 100644 index 0000000..a046ae4 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/JadeCodec.java @@ -0,0 +1,59 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import io.netty.buffer.ByteBuf; +import net.minecraft.network.codec.ByteBufCodecs; +import net.minecraft.network.codec.StreamCodec; +import org.jetbrains.annotations.NotNull; + +public class JadeCodec { + public static final StreamCodec PRIMITIVE_STREAM_CODEC = new StreamCodec<>() { + @Override + public @NotNull Object decode(@NotNull ByteBuf buf) { + byte b = buf.readByte(); + if (b == 0) { + return false; + } else if (b == 1) { + return true; + } else if (b == 2) { + return ByteBufCodecs.VAR_INT.decode(buf); + } else if (b == 3) { + return ByteBufCodecs.FLOAT.decode(buf); + } else if (b == 4) { + return ByteBufCodecs.STRING_UTF8.decode(buf); + } else if (b > 20) { + return b - 20; + } + throw new IllegalArgumentException("Unknown primitive type: " + b); + } + + @Override + public void encode(@NotNull ByteBuf buf, @NotNull Object o) { + switch (o) { + case Boolean b -> buf.writeByte(b ? 1 : 0); + case Number n -> { + float f = n.floatValue(); + if (f != (int) f) { + buf.writeByte(3); + ByteBufCodecs.FLOAT.encode(buf, f); + } + int i = n.intValue(); + if (i <= Byte.MAX_VALUE - 20 && i >= 0) { + buf.writeByte(i + 20); + } else { + ByteBufCodecs.VAR_INT.encode(buf, i); + } + } + case String s -> { + buf.writeByte(4); + ByteBufCodecs.STRING_UTF8.encode(buf, s); + } + case Enum anEnum -> { + buf.writeByte(4); + ByteBufCodecs.STRING_UTF8.encode(buf, anEnum.name()); + } + case null -> throw new NullPointerException(); + default -> throw new IllegalArgumentException("Unknown primitive type: %s (%s)".formatted(o, o.getClass())); + } + } + }; +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java new file mode 100644 index 0000000..9c580ab --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java @@ -0,0 +1,109 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.google.common.collect.Lists; +import net.minecraft.advancements.critereon.ItemPredicate; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderGetter; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.storage.loot.LootPool; +import net.minecraft.world.level.storage.loot.LootTable; +import net.minecraft.world.level.storage.loot.entries.AlternativesEntry; +import net.minecraft.world.level.storage.loot.entries.LootPoolEntryContainer; +import net.minecraft.world.level.storage.loot.entries.NestedLootTable; +import net.minecraft.world.level.storage.loot.predicates.AnyOfCondition; +import net.minecraft.world.level.storage.loot.predicates.LootItemCondition; +import net.minecraft.world.level.storage.loot.predicates.MatchTool; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.jade.tool.ShearsToolHandler; + +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +public class LootTableMineableCollector { + + private final HolderGetter lootRegistry; + private final ItemStack toolItem; + + public LootTableMineableCollector(HolderGetter lootRegistry, ItemStack toolItem) { + this.lootRegistry = lootRegistry; + this.toolItem = toolItem; + } + + public static @NotNull List execute(HolderGetter lootRegistry, ItemStack toolItem) { + LootTableMineableCollector collector = new LootTableMineableCollector(lootRegistry, toolItem); + List list = Lists.newArrayList(); + for (Block block : BuiltInRegistries.BLOCK) { + if (!ShearsToolHandler.getInstance().test(block.defaultBlockState()).isEmpty()) { + continue; + } + + if (block.getLootTable().isPresent()) { + LootTable lootTable = lootRegistry.get(block.getLootTable().get()).map(Holder::value).orElse(null); + if (collector.doLootTable(lootTable)) { + list.add(block); + } + } + } + return list; + } + + 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java new file mode 100644 index 0000000..9e0dd9c --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java @@ -0,0 +1,115 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import net.minecraft.core.IdMapper; +import net.minecraft.resources.ResourceLocation; +import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.Nullable; +import org.leavesmc.leaves.LeavesLogger; +import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider; + +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 { + 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) { + LeavesLogger.LOGGER.severe(e.toString()); + } + return List.of(); + } + + @Override + public void idMapped() { + idMapped = true; + } + + @Override + public @Nullable IdMapper idMapper() { + return idMapper; + } + + @Override + public void register(Class clazz, T provider) { + if (first.isClassAcceptable(clazz)) { + first.register(clazz, provider); + } else if (second.isClassAcceptable(clazz)) { + second.register(clazz, provider); + } else { + throw new IllegalArgumentException("Class " + clazz + " is not acceptable"); + } + } + + @Override + public boolean isClassAcceptable(Class clazz) { + return first.isClassAcceptable(clazz) || second.isClassAcceptable(clazz); + } + + @Override + public List get(Class clazz) { + List result = first.get(clazz); + if (result.isEmpty()) { + result = second.get(clazz); + } + return result; + } + + @Override + public boolean isEmpty() { + return first.isEmpty() && second.isEmpty(); + } + + @Override + public Stream, Collection>> entries() { + return Stream.concat(first.entries(), second.entries()); + } + + @Override + public void invalidate() { + first.invalidate(); + second.invalidate(); + mergedCache.invalidateAll(); + } + + @Override + public void loadComplete(PriorityStore priorityStore) { + first.loadComplete(priorityStore); + second.loadComplete(priorityStore); + if (idMapped) { + idMapper = createIdMapper(); + } + } +} \ No newline at end of file diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java new file mode 100644 index 0000000..da4d5a7 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java @@ -0,0 +1,40 @@ +package org.leavesmc.leaves.protocol.jade.util; + +import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2IntMap; + +import java.util.Objects; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +public class PriorityStore { + + private final Object2IntMap priorities = new Object2IntLinkedOpenHashMap<>(); + private final Function keyGetter; + private final ToIntFunction defaultPriorityGetter; + + public PriorityStore(ToIntFunction defaultPriorityGetter, Function keyGetter) { + this.defaultPriorityGetter = defaultPriorityGetter; + this.keyGetter = keyGetter; + } + + public void put(V provider) { + Objects.requireNonNull(provider); + put(provider, defaultPriorityGetter.applyAsInt(provider)); + } + + public void put(V provider, int priority) { + Objects.requireNonNull(provider); + K uid = keyGetter.apply(provider); + Objects.requireNonNull(uid); + priorities.put(uid, priority); + } + + public int byValue(V value) { + return byKey(keyGetter.apply(value)); + } + + public int byKey(K id) { + return priorities.getInt(id); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java new file mode 100644 index 0000000..56f3e4e --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java @@ -0,0 +1,58 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java new file mode 100644 index 0000000..be8abe2 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java @@ -0,0 +1,107 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java new file mode 100644 index 0000000..c9fcf90 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/CommunicationManager.java @@ -0,0 +1,389 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.mojang.authlib.GameProfile; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.bxteam.divinemc.config.DivineConfig; +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.DownloadExchange; +import org.leavesmc.leaves.protocol.syncmatica.exchange.Exchange; +import org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget; +import org.leavesmc.leaves.protocol.syncmatica.exchange.ModifyExchangeServer; +import org.leavesmc.leaves.protocol.syncmatica.exchange.UploadExchange; +import org.leavesmc.leaves.protocol.syncmatica.exchange.VersionHandshakeServer; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@LeavesProtocol.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 DivineConfig.NetworkCategory.protocolsSyncMaticaEnabled; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java new file mode 100644 index 0000000..7cb3465 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/Feature.java @@ -0,0 +1,23 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import org.jetbrains.annotations.Nullable; + +public enum Feature { + CORE, + FEATURE, + MODIFY, + MESSAGE, + QUOTA, + DEBUG, + CORE_EX; + + @Nullable + public static Feature fromString(final String s) { + for (final Feature f : Feature.values()) { + if (f.toString().equals(s)) { + return f; + } + } + return null; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java new file mode 100644 index 0000000..fdd2e32 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FeatureSet.java @@ -0,0 +1,68 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +public class FeatureSet { + + private static final Map versionFeatures; + + 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java new file mode 100644 index 0000000..a630b12 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/FileStorage.java @@ -0,0 +1,80 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.HashMap; +import java.util.UUID; + +public class FileStorage { + + private final HashMap buffer = new HashMap<>(); + + public LocalLitematicState getLocalState(final ServerPlacement placement) { + final File localFile = getSchematicPath(placement); + if (localFile.isFile()) { + if (isDownloading(placement)) { + return LocalLitematicState.DOWNLOADING_LITEMATIC; + } + if ((buffer.containsKey(placement) && buffer.get(placement) == localFile.lastModified()) || hashCompare(localFile, placement)) { + return LocalLitematicState.LOCAL_LITEMATIC_PRESENT; + } + return LocalLitematicState.LOCAL_LITEMATIC_DESYNC; + } + return LocalLitematicState.NO_LOCAL_LITEMATIC; + } + + private boolean isDownloading(final ServerPlacement placement) { + return 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java new file mode 100644 index 0000000..299c573 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/LocalLitematicState.java @@ -0,0 +1,24 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +public enum LocalLitematicState { + NO_LOCAL_LITEMATIC(true, false), + LOCAL_LITEMATIC_DESYNC(true, false), + DOWNLOADING_LITEMATIC(false, false), + LOCAL_LITEMATIC_PRESENT(false, true); + + private final boolean downloadReady; + private final boolean fileReady; + + LocalLitematicState(final boolean downloadReady, final boolean fileReady) { + this.downloadReady = downloadReady; + this.fileReady = fileReady; + } + + public boolean isReadyForDownload() { + return downloadReady; + } + + public boolean isLocalFileReady() { + return fileReady; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java new file mode 100644 index 0000000..b56ca12 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/MessageType.java @@ -0,0 +1,8 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +public enum MessageType { + SUCCESS, + INFO, + WARNING, + ERROR +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java new file mode 100644 index 0000000..d364382 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PacketType.java @@ -0,0 +1,30 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import net.minecraft.resources.ResourceLocation; + +public enum PacketType { + REGISTER_METADATA("register_metadata"), + CANCEL_SHARE("cancel_share"), + REQUEST_LITEMATIC("request_download"), + SEND_LITEMATIC("send_litematic"), + RECEIVED_LITEMATIC("received_litematic"), + FINISHED_LITEMATIC("finished_litematic"), + CANCEL_LITEMATIC("cancel_litematic"), + REMOVE_SYNCMATIC("remove_syncmatic"), + REGISTER_VERSION("register_version"), + CONFIRM_USER("confirm_user"), + FEATURE_REQUEST("feature_request"), + FEATURE("feature"), + MODIFY("modify"), + MODIFY_REQUEST("modify_request"), + MODIFY_REQUEST_DENY("modify_request_deny"), + MODIFY_REQUEST_ACCEPT("modify_request_accept"), + MODIFY_FINISH("modify_finish"), + MESSAGE("mesage"); + + public final ResourceLocation identifier; + + PacketType(final String id) { + identifier = ResourceLocation.tryBuild(SyncmaticaProtocol.PROTOCOL_ID, id); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java new file mode 100644 index 0000000..b5891b0 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifier.java @@ -0,0 +1,37 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; + +import java.util.UUID; + +public class PlayerIdentifier { + + public static final UUID MISSING_PLAYER_UUID = UUID.fromString("4c1b738f-56fa-4011-8273-498c972424ea"); + public static final PlayerIdentifier MISSING_PLAYER = new PlayerIdentifier(MISSING_PLAYER_UUID, "No Player"); + + public final UUID uuid; + private String bufferedPlayerName; + + PlayerIdentifier(final UUID uuid, final String bufferedPlayerName) { + this.uuid = uuid; + this.bufferedPlayerName = bufferedPlayerName; + } + + public String getName() { + return bufferedPlayerName; + } + + public void updatePlayerName(final String name) { + bufferedPlayerName = name; + } + + public JsonObject toJson() { + final JsonObject jsonObject = new JsonObject(); + + jsonObject.add("uuid", new JsonPrimitive(uuid.toString())); + jsonObject.add("name", new JsonPrimitive(bufferedPlayerName)); + + return jsonObject; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java new file mode 100644 index 0000000..4a6248f --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/PlayerIdentifierProvider.java @@ -0,0 +1,46 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonObject; +import com.mojang.authlib.GameProfile; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.exchange.ExchangeTarget; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class PlayerIdentifierProvider { + + private final Map identifiers = new HashMap<>(); + + public PlayerIdentifierProvider() { + identifiers.put(PlayerIdentifier.MISSING_PLAYER_UUID, PlayerIdentifier.MISSING_PLAYER); + } + + public PlayerIdentifier createOrGet(final ExchangeTarget exchangeTarget) { + return createOrGet(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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java new file mode 100644 index 0000000..2c7ca6c --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPlacement.java @@ -0,0 +1,166 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.UUID; + +public class ServerPlacement { + + private final UUID id; + + private final String fileName; + private final UUID hashValue; + + private PlayerIdentifier owner; + private PlayerIdentifier lastModifiedBy; + + private ServerPosition origin; + private Rotation rotation; + private Mirror mirror; + + private SubRegionData subRegionData = new SubRegionData(); + + public ServerPlacement(final UUID id, final String fileName, final UUID hashValue, final PlayerIdentifier owner) { + this.id = id; + this.fileName = fileName; + this.hashValue = hashValue; + this.owner = owner; + lastModifiedBy = owner; + } + + @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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java new file mode 100644 index 0000000..63573bc --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/ServerPosition.java @@ -0,0 +1,51 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; + +public class ServerPosition { + + private final BlockPos position; + private final String dimensionId; + + public ServerPosition(final BlockPos pos, final String dim) { + position = pos; + dimensionId = dim; + } + + public 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java new file mode 100644 index 0000000..09bf795 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionData.java @@ -0,0 +1,90 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; + +import java.util.HashMap; +import java.util.Map; + +public class SubRegionData { + + private boolean isModified; + private Map modificationData; // is null when isModified is false + + public SubRegionData() { + this(false, null); + } + + public SubRegionData(final boolean isModified, final Map modificationData) { + this.isModified = isModified; + this.modificationData = modificationData; + } + + @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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java new file mode 100644 index 0000000..2410075 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SubRegionPlacementModification.java @@ -0,0 +1,65 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonPrimitive; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.Mirror; +import net.minecraft.world.level.block.Rotation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class SubRegionPlacementModification { + + public final String name; + public final BlockPos position; + public final Rotation rotation; + public final Mirror mirror; + + SubRegionPlacementModification(final String name, final BlockPos position, final Rotation rotation, final Mirror mirror) { + this.name = name; + this.position = position; + this.rotation = rotation; + this.mirror = mirror; + } + + @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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java new file mode 100644 index 0000000..27a056b --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticManager.java @@ -0,0 +1,108 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class SyncmaticManager { + + public static final String PLACEMENTS_JSON_KEY = "placements"; + private final Map schematics = new HashMap<>(); + + public void addPlacement(final ServerPlacement placement) { + schematics.put(placement.getId(), placement); + updateServerPlacement(); + } + + public ServerPlacement getPlacement(final UUID id) { + return schematics.get(id); + } + + public Collection getAll() { + return schematics.values(); + } + + public void removePlacement(final @NotNull ServerPlacement placement) { + schematics.remove(placement.getId()); + updateServerPlacement(); + } + + public void updateServerPlacement() { + saveServer(); + } + + public void startup() { + loadServer(); + } + + private void saveServer() { + final JsonObject obj = new JsonObject(); + final JsonArray arr = new JsonArray(); + + for (final ServerPlacement p : getAll()) { + arr.add(p.toJson()); + } + + obj.add(PLACEMENTS_JSON_KEY, arr); + final File backup = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json.bak"); + final File incoming = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json.new"); + final File current = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json"); + + try (final FileWriter writer = new FileWriter(incoming)) { + writer.write(new GsonBuilder().setPrettyPrinting().create().toJson(obj)); + } catch (final IOException e) { + e.printStackTrace(); + return; + } + + SyncmaticaProtocol.backupAndReplace(backup.toPath(), current.toPath(), incoming.toPath()); + } + + private void loadServer() { + final File f = new File(SyncmaticaProtocol.getLitematicFolder(), "placements.json"); + if (f.exists() && f.isFile() && f.canRead()) { + JsonElement element = null; + try { + final JsonParser parser = new JsonParser(); + final FileReader reader = new FileReader(f); + + element = parser.parse(reader); + reader.close(); + + } catch (final Exception e) { + e.printStackTrace(); + } + if (element == null) { + return; + } + try { + final JsonObject obj = element.getAsJsonObject(); + if (obj == null || !obj.has(PLACEMENTS_JSON_KEY)) { + return; + } + final JsonArray arr = obj.getAsJsonArray(PLACEMENTS_JSON_KEY); + for (final JsonElement elem : arr) { + final ServerPlacement placement = ServerPlacement.fromJson(elem.getAsJsonObject()); + if (placement != null) { + schematics.put(placement.getId(), placement); + } + } + + } catch (final IllegalStateException | NullPointerException e) { + e.printStackTrace(); + } + } + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java new file mode 100644 index 0000000..d161e73 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaPayload.java @@ -0,0 +1,18 @@ +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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java new file mode 100644 index 0000000..fdd9550 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/SyncmaticaProtocol.java @@ -0,0 +1,127 @@ +package org.leavesmc.leaves.protocol.syncmatica; + +import org.bxteam.divinemc.config.DivineConfig; +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, 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) { + final StringBuilder sanitized = new StringBuilder(); + final int len = badFileName.codePointCount(0, badFileName.length()); + + for (int i = 0; i < len; i++) { + final int c = badFileName.codePointAt(i); + if (Arrays.binarySearch(ILLEGAL_CHARS, c) < 0) { + sanitized.appendCodePoint(c); + if (sanitized.length() == 255) { + break; + } + } + } + + return sanitized.toString().replaceAll(ILLEGAL_PATTERNS, "_"); + } + + public static boolean isOverQuota(int sent) { + return DivineConfig.NetworkCategory.protocolsSyncMaticaQuota && sent > DivineConfig.NetworkCategory.protocolsSyncMaticaQuotaLimit; + } + + 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java new file mode 100644 index 0000000..aa40f0a --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/AbstractExchange.java @@ -0,0 +1,66 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.util.UUID; + +public abstract class AbstractExchange implements Exchange { + + private 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java new file mode 100644 index 0000000..3bf1c27 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/DownloadExchange.java @@ -0,0 +1,128 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; +import org.leavesmc.leaves.protocol.syncmatica.MessageType; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.UUID; + +public class DownloadExchange extends AbstractExchange { + + private final ServerPlacement toDownload; + private final OutputStream outputStream; + private final MessageDigest md5; + private final File downloadFile; + private int bytesSent; + + public DownloadExchange(final ServerPlacement syncmatic, final File downloadFile, final ExchangeTarget partner) throws IOException, NoSuchAlgorithmException { + super(partner); + this.downloadFile = downloadFile; + final OutputStream os = new FileOutputStream(downloadFile); + toDownload = syncmatic; + md5 = MessageDigest.getInstance("MD5"); + outputStream = new DigestOutputStream(os, md5); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.SEND_LITEMATIC.identifier) + || id.equals(PacketType.FINISHED_LITEMATIC.identifier) + || id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + return checkUUID(packetBuf, toDownload.getId()); + } + return false; + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.SEND_LITEMATIC.identifier)) { + final int size = packetBuf.readInt(); + bytesSent += size; + if (SyncmaticaProtocol.isOverQuota(bytesSent)) { + close(true); + SyncmaticaProtocol.getCommunicationManager().sendMessage( + getPartner(), + MessageType.ERROR, + "syncmatica.error.cancelled_transmit_exceed_quota" + ); + 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java new file mode 100644 index 0000000..0f45ef7 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/Exchange.java @@ -0,0 +1,20 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +public interface Exchange { + ExchangeTarget getPartner(); + + boolean checkPacket(ResourceLocation id, FriendlyByteBuf packetBuf); + + void handle(ResourceLocation id, FriendlyByteBuf packetBuf); + + boolean isFinished(); + + boolean isSuccessful(); + + void close(boolean notifyPartner); + + void init(); +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java new file mode 100644 index 0000000..b0da7ab --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ExchangeTarget.java @@ -0,0 +1,39 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.network.ServerGamePacketListenerImpl; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; +import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaPayload; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +public class ExchangeTarget { + + private final List ongoingExchanges = new ArrayList<>(); + private final ServerGamePacketListenerImpl client; + private FeatureSet features; + + public ExchangeTarget(final ServerGamePacketListenerImpl client) { + this.client = client; + } + + public void sendPacket(final ResourceLocation id, final FriendlyByteBuf packetBuf) { + ProtocolUtils.sendPayloadPacket(client.player, new SyncmaticaPayload(id, packetBuf)); + } + + public FeatureSet getFeatureSet() { + return features; + } + + public void setFeatureSet(final FeatureSet f) { + features = f; + } + + public Collection getExchanges() { + return ongoingExchanges; + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java new file mode 100644 index 0000000..45fc019 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/FeatureExchange.java @@ -0,0 +1,48 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +public abstract class FeatureExchange extends AbstractExchange { + + protected FeatureExchange(final ExchangeTarget partner) { + super(partner); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.FEATURE_REQUEST.identifier) + || id.equals(PacketType.FEATURE.identifier); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.FEATURE_REQUEST.identifier)) { + sendFeatures(); + } else if (id.equals(PacketType.FEATURE.identifier)) { + final FeatureSet fs = FeatureSet.fromString(packetBuf.readUtf(32767)); + getPartner().setFeatureSet(fs); + onFeatureSetReceive(); + } + } + + protected void onFeatureSetReceive() { + succeed(); + } + + public void requestFeatureSet() { + getPartner().sendPacket(PacketType.FEATURE_REQUEST.identifier, new FriendlyByteBuf(Unpooled.buffer())); + } + + private void sendFeatures() { + final FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + final FeatureSet fs = SyncmaticaProtocol.getFeatureSet(); + buf.writeUtf(fs.toString(), 32767); + getPartner().sendPacket(PacketType.FEATURE.identifier, buf); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java new file mode 100644 index 0000000..f7f423c --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/ModifyExchangeServer.java @@ -0,0 +1,82 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.PlayerIdentifier; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.util.UUID; + +public class ModifyExchangeServer extends AbstractExchange { + + private final ServerPlacement placement; + UUID placementId; + + public ModifyExchangeServer(final UUID placeId, final ExchangeTarget partner) { + super(partner); + placementId = placeId; + placement = SyncmaticaProtocol.getSyncmaticManager().getPlacement(placementId); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.MODIFY_FINISH.identifier) && checkUUID(packetBuf, placement.getId()); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.MODIFY_FINISH.identifier)) { + 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/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java new file mode 100644 index 0000000..065a399 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/UploadExchange.java @@ -0,0 +1,101 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; + +public class UploadExchange extends AbstractExchange { + + private static final int BUFFER_SIZE = 16384; + + private final ServerPlacement toUpload; + private final InputStream inputStream; + private final byte[] buffer = new byte[BUFFER_SIZE]; + + public UploadExchange(final ServerPlacement syncmatic, final File uploadFile, final ExchangeTarget partner) throws FileNotFoundException { + super(partner); + toUpload = syncmatic; + inputStream = new FileInputStream(uploadFile); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.RECEIVED_LITEMATIC.identifier) + || id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + return checkUUID(packetBuf, toUpload.getId()); + } + return false; + } + + @Override + public void handle(final @NotNull ResourceLocation id, final @NotNull FriendlyByteBuf packetBuf) { + packetBuf.readUUID(); + if (id.equals(PacketType.RECEIVED_LITEMATIC.identifier)) { + send(); + } + if (id.equals(PacketType.CANCEL_LITEMATIC.identifier)) { + close(false); + } + } + + private void send() { + final int bytesRead; + try { + bytesRead = inputStream.read(buffer); + } catch (final IOException e) { + close(true); + e.printStackTrace(); + return; + } + if (bytesRead == -1) { + sendFinish(); + } else { + sendData(bytesRead); + } + } + + private void sendData(final int bytesRead) { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + FriendlyByteBuf.writeInt(bytesRead); + FriendlyByteBuf.writeBytes(buffer, 0, bytesRead); + getPartner().sendPacket(PacketType.SEND_LITEMATIC.identifier, FriendlyByteBuf); + } + + private void sendFinish() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + getPartner().sendPacket(PacketType.FINISHED_LITEMATIC.identifier, FriendlyByteBuf); + succeed(); + } + + @Override + public void init() { + send(); + } + + @Override + protected void onClose() { + try { + inputStream.close(); + } catch (final IOException e) { + e.printStackTrace(); + } + } + + @Override + protected void sendCancelPacket() { + final FriendlyByteBuf FriendlyByteBuf = new FriendlyByteBuf(Unpooled.buffer()); + FriendlyByteBuf.writeUUID(toUpload.getId()); + getPartner().sendPacket(PacketType.CANCEL_LITEMATIC.identifier, FriendlyByteBuf); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java new file mode 100644 index 0000000..cb7aed6 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/protocol/syncmatica/exchange/VersionHandshakeServer.java @@ -0,0 +1,66 @@ +package org.leavesmc.leaves.protocol.syncmatica.exchange; + +import io.netty.buffer.Unpooled; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.protocol.syncmatica.CommunicationManager; +import org.leavesmc.leaves.protocol.syncmatica.FeatureSet; +import org.leavesmc.leaves.protocol.syncmatica.PacketType; +import org.leavesmc.leaves.protocol.syncmatica.ServerPlacement; +import org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol; + +import java.util.Collection; + +public class VersionHandshakeServer extends FeatureExchange { + + public VersionHandshakeServer(final ExchangeTarget partner) { + super(partner); + } + + @Override + public boolean checkPacket(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + return id.equals(PacketType.REGISTER_VERSION.identifier) + || super.checkPacket(id, packetBuf); + } + + @Override + public void handle(final @NotNull ResourceLocation id, final FriendlyByteBuf packetBuf) { + if (id.equals(PacketType.REGISTER_VERSION.identifier)) { + String partnerVersion = packetBuf.readUtf(); + if (partnerVersion.equals("0.0.1")) { + close(false); + return; + } + final FeatureSet fs = FeatureSet.fromVersionString(partnerVersion); + if (fs == null) { + requestFeatureSet(); + } else { + getPartner().setFeatureSet(fs); + onFeatureSetReceive(); + } + } else { + super.handle(id, packetBuf); + } + + } + + @Override + public void onFeatureSetReceive() { + final FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); + final Collection l = SyncmaticaProtocol.getSyncmaticManager().getAll(); + newBuf.writeInt(l.size()); + for (final ServerPlacement p : l) { + CommunicationManager.putMetaData(p, newBuf, getPartner()); + } + getPartner().sendPacket(PacketType.CONFIRM_USER.identifier, newBuf); + succeed(); + } + + @Override + public void init() { + final FriendlyByteBuf newBuf = new FriendlyByteBuf(Unpooled.buffer()); + newBuf.writeUtf(SyncmaticaProtocol.PROTOCOL_VERSION); + getPartner().sendPacket(PacketType.REGISTER_VERSION.identifier, newBuf); + } +} diff --git a/divinemc-server/src/main/java/org/leavesmc/leaves/util/NbtUtils.java b/divinemc-server/src/main/java/org/leavesmc/leaves/util/NbtUtils.java new file mode 100644 index 0000000..3c75d50 --- /dev/null +++ b/divinemc-server/src/main/java/org/leavesmc/leaves/util/NbtUtils.java @@ -0,0 +1,75 @@ +package org.leavesmc.leaves.util; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.DoubleTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class NbtUtils { + + public static void writeBlockPosToTag(Vec3i pos, CompoundTag tag) { + tag.putInt("x", pos.getX()); + tag.putInt("y", pos.getY()); + tag.putInt("z", pos.getZ()); + } + + @Nullable + public static BlockPos readBlockPos(@Nullable CompoundTag tag) { + if (tag != null && tag.contains("x") && tag.contains("y") && tag.contains("z")) { + return new BlockPos(tag.getIntOr("x", 0), tag.getIntOr("y", 0), tag.getIntOr("z", 0)); + } + + return null; + } + + public static void writeEntityPositionToTag(Vec3 pos, CompoundTag tag) { + ListTag posList = new ListTag(); + + posList.add(DoubleTag.valueOf(pos.x)); + posList.add(DoubleTag.valueOf(pos.y)); + posList.add(DoubleTag.valueOf(pos.z)); + tag.put("Pos", posList); + } + + @Nullable + public static Vec3 readVec3(@Nullable CompoundTag tag) { + if (tag != null && tag.contains("dx") && tag.contains("dy") && tag.contains("dz")) { + return new Vec3(tag.getDoubleOr("dx", 0.0), tag.getDoubleOr("dy", 0.0), tag.getDoubleOr("dz", 0.0)); + } + return null; + } + + @Nullable + public static Vec3 readEntityPositionFromTag(@Nullable CompoundTag tag) { + if (tag == null || !tag.contains("Pos")) { + return null; + } + ListTag tagList = tag.getListOrEmpty("Pos"); + if (tagList.size() != 3) { + return null; + } + return new Vec3(tagList.getDoubleOr(0, 0.0), tagList.getDoubleOr(1, 0.0), tagList.getDoubleOr(2, 0.0)); + } + + @Nullable + public static Vec3i readVec3iFromTag(@Nullable CompoundTag tag) { + if (tag != null && tag.contains("x") && tag.contains("y") && tag.contains("z")) { + return new Vec3i(tag.getIntOr("x", 0), tag.getIntOr("y", 0), tag.getIntOr("z", 0)); + } + return null; + } + + public static BlockPos readBlockPosFromArrayTag(@NotNull CompoundTag tag, String tagName) { + if (tag.contains(tagName)) { + int[] pos = tag.getIntArray(tagName).orElse(new int[0]); + if (pos.length == 3) { + return new BlockPos(pos[0], pos[1], pos[2]); + } + } + return null; + } +}