9
0
mirror of https://github.com/LeavesMC/Leaves.git synced 2025-12-19 14:59:32 +00:00
Files
LeavesMC/patches/server/0038-Jade-Protocol.patch
2024-07-12 14:34:28 +08:00

2711 lines
113 KiB
Diff

From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: violetc <58360096+s-yh-china@users.noreply.github.com>
Date: Sat, 3 Dec 2022 08:57:15 +0800
Subject: [PATCH] Jade Protocol
This patch is Powered by Jade(https://github.com/Snownee/Jade)
diff --git a/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java b/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java
index 729fd2d52dd48e25ee7a077a3ffafc80ecef7c9f..28d6b1d49045c125214c40895efd484e4ae20c2b 100644
--- a/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java
+++ b/src/main/java/net/minecraft/world/entity/animal/armadillo/Armadillo.java
@@ -63,7 +63,7 @@ public class Armadillo extends Animal {
public final AnimationState rollOutAnimationState = new AnimationState();
public final AnimationState rollUpAnimationState = new AnimationState();
public final AnimationState peekAnimationState = new AnimationState();
- private int scuteTime;
+ public int scuteTime; // Leaves - private -> public
private boolean peekReceivedClient = false;
public Armadillo(EntityType<? extends Animal> type, Level world) {
diff --git a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java
index 43046f4a0cff620834ac4647efdcde227185b2ff..a08cd692e332a6caed33cd3db2373e847621ad6a 100644
--- a/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java
+++ b/src/main/java/net/minecraft/world/entity/animal/frog/Tadpole.java
@@ -256,7 +256,7 @@ public class Tadpole extends AbstractFish {
}
- private int getTicksLeftUntilAdult() {
+ public int getTicksLeftUntilAdult() { // Leaves - private -> public
return Math.max(0, Tadpole.ticksToBeFrog - this.age);
}
diff --git a/src/main/java/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java b/src/main/java/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java
index 055f4b87c01ee7ecf7d2a111b72cc5aa85d9fbe8..5d9030f4787a43c56ae9455180badd5658dea35b 100644
--- a/src/main/java/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java
+++ b/src/main/java/net/minecraft/world/level/block/entity/trialspawner/TrialSpawnerData.java
@@ -72,7 +72,7 @@ public class TrialSpawnerData {
});
public final Set<UUID> detectedPlayers;
public final Set<UUID> currentMobs;
- protected long cooldownEndsAt;
+ public long cooldownEndsAt; // Leaves - protected -> public
protected long nextMobSpawnsAt;
protected int totalMobsSpawned;
public Optional<SpawnData> nextSpawnData;
diff --git a/src/main/java/net/minecraft/world/level/storage/loot/LootPool.java b/src/main/java/net/minecraft/world/level/storage/loot/LootPool.java
index 38078c44b35e917d1d243a5f8599aa858d8611de..13dbadfb50278b79b33d9dce10413c93c9e4ff31 100644
--- a/src/main/java/net/minecraft/world/level/storage/loot/LootPool.java
+++ b/src/main/java/net/minecraft/world/level/storage/loot/LootPool.java
@@ -36,7 +36,7 @@ public class LootPool {
)
.apply(instance, LootPool::new)
);
- private final List<LootPoolEntryContainer> entries;
+ public final List<LootPoolEntryContainer> entries; // Leaves - private -> public
private final List<LootItemCondition> conditions;
private final Predicate<LootContext> compositeCondition;
private final List<LootItemFunction> functions;
diff --git a/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java b/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java
index edaf7f1692ae059581f3abc24bb228874e6d114b..f09cfc472d4dbdc8cb0b6a45ef240b25a865ffba 100644
--- a/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java
+++ b/src/main/java/net/minecraft/world/level/storage/loot/LootTable.java
@@ -58,7 +58,7 @@ public class LootTable {
public static final Codec<Holder<LootTable>> CODEC = RegistryFileCodec.create(Registries.LOOT_TABLE, LootTable.DIRECT_CODEC);
private final LootContextParamSet paramSet;
private final Optional<ResourceLocation> randomSequence;
- private final List<LootPool> pools;
+ public final List<LootPool> pools; // Leaves - private -> public
private final List<LootItemFunction> functions;
private final BiFunction<ItemStack, LootContext, ItemStack> compositeFunction;
public CraftLootTable craftLootTable; // CraftBukkit
diff --git a/src/main/java/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java b/src/main/java/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java
index 128792f76f02d74c1ccf84beb8e7973453424639..775fbddf3e3b133e33f54aaa8e8a07d131095e34 100644
--- a/src/main/java/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java
+++ b/src/main/java/net/minecraft/world/level/storage/loot/entries/CompositeEntryBase.java
@@ -9,7 +9,7 @@ import net.minecraft.world.level.storage.loot.ValidationContext;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
public abstract class CompositeEntryBase extends LootPoolEntryContainer {
- protected final List<LootPoolEntryContainer> children;
+ public final List<LootPoolEntryContainer> children; // Leaves - private -> public
private final ComposableEntryContainer composedChildren;
protected CompositeEntryBase(List<LootPoolEntryContainer> terms, List<LootItemCondition> conditions) {
diff --git a/src/main/java/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java b/src/main/java/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java
index 1d2f2bb352abf6772cd20293575fc79e8e64ce3b..b157dfaf1efffebd3f2ae8cb8fcf0972fe742258 100644
--- a/src/main/java/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java
+++ b/src/main/java/net/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer.java
@@ -13,7 +13,7 @@ import net.minecraft.world.level.storage.loot.predicates.ConditionUserBuilder;
import net.minecraft.world.level.storage.loot.predicates.LootItemCondition;
public abstract class LootPoolEntryContainer implements ComposableEntryContainer {
- protected final List<LootItemCondition> conditions;
+ public final List<LootItemCondition> conditions; // Leaves - private -> public
private final Predicate<LootContext> compositeCondition;
protected LootPoolEntryContainer(List<LootItemCondition> conditions) {
diff --git a/src/main/java/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java b/src/main/java/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java
index 71989359192c8f30a1a8d343a2c6cb5b92330491..ec273bd4d0e61f54532df599f00695e8b9d44800 100644
--- a/src/main/java/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java
+++ b/src/main/java/net/minecraft/world/level/storage/loot/entries/NestedLootTable.java
@@ -25,7 +25,7 @@ public class NestedLootTable extends LootPoolSingletonContainer {
.and(singletonFields(instance))
.apply(instance, NestedLootTable::new)
);
- private final Either<ResourceKey<LootTable>, LootTable> contents;
+ public final Either<ResourceKey<LootTable>, LootTable> contents; // Leaves - private -> public
private NestedLootTable(
Either<ResourceKey<LootTable>, LootTable> value, int weight, int quality, List<LootItemCondition> conditions, List<LootItemFunction> functions
diff --git a/src/main/java/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java b/src/main/java/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java
index 30d0133a42ce990352f5c492fcf9beb105364848..1ab2eab686b3a89d406f127a6036c0e2932db4a6 100644
--- a/src/main/java/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java
+++ b/src/main/java/net/minecraft/world/level/storage/loot/predicates/CompositeLootItemCondition.java
@@ -11,7 +11,7 @@ import net.minecraft.world.level.storage.loot.LootContext;
import net.minecraft.world.level.storage.loot.ValidationContext;
public abstract class CompositeLootItemCondition implements LootItemCondition {
- protected final List<LootItemCondition> terms;
+ public final List<LootItemCondition> terms; // Leaves - private -> public
private final Predicate<LootContext> composedPredicate;
protected CompositeLootItemCondition(List<LootItemCondition> terms, Predicate<LootContext> predicate) {
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java b/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java
new file mode 100644
index 0000000000000000000000000000000000000000..0f40d3b95de7cefbf1318109cb45d13732f25534
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/JadeProtocol.java
@@ -0,0 +1,397 @@
+package org.leavesmc.leaves.protocol.jade;
+
+import io.netty.buffer.ByteBuf;
+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.FriendlyByteBuf;
+import net.minecraft.network.RegistryFriendlyByteBuf;
+import net.minecraft.network.codec.ByteBufCodecs;
+import net.minecraft.network.codec.StreamCodec;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.util.Mth;
+import net.minecraft.world.entity.AgeableMob;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.LivingEntity;
+import net.minecraft.world.entity.animal.Animal;
+import net.minecraft.world.entity.animal.Chicken;
+import net.minecraft.world.entity.animal.allay.Allay;
+import net.minecraft.world.entity.animal.armadillo.Armadillo;
+import net.minecraft.world.entity.animal.frog.Tadpole;
+import net.minecraft.world.entity.boss.EnderDragonPart;
+import net.minecraft.world.entity.boss.enderdragon.EnderDragon;
+import net.minecraft.world.entity.monster.ZombieVillager;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.Level;
+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 net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.phys.BlockHitResult;
+import net.minecraft.world.phys.Vec3;
+import org.jetbrains.annotations.Contract;
+import org.jetbrains.annotations.NotNull;
+import org.leavesmc.leaves.LeavesConfig;
+import org.leavesmc.leaves.LeavesLogger;
+import org.leavesmc.leaves.protocol.core.LeavesCustomPayload;
+import org.leavesmc.leaves.protocol.core.LeavesProtocol;
+import org.leavesmc.leaves.protocol.core.ProtocolHandler;
+import org.leavesmc.leaves.protocol.core.ProtocolUtils;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider;
+import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider;
+import org.leavesmc.leaves.protocol.jade.provider.ItemStorageExtensionProvider;
+import org.leavesmc.leaves.protocol.jade.provider.block.BeehiveProvider;
+import org.leavesmc.leaves.protocol.jade.provider.block.BlockStorageProvider;
+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.JukeboxProvider;
+import org.leavesmc.leaves.protocol.jade.provider.block.LecternProvider;
+import org.leavesmc.leaves.protocol.jade.provider.block.MobSpawnerCooldownProvider;
+import org.leavesmc.leaves.protocol.jade.provider.block.ObjectNameProvider;
+import org.leavesmc.leaves.protocol.jade.provider.block.RedstoneProvider;
+import org.leavesmc.leaves.protocol.jade.provider.entity.AnimalOwnerProvider;
+import org.leavesmc.leaves.protocol.jade.provider.entity.EntityStorageProvider;
+import org.leavesmc.leaves.protocol.jade.provider.entity.MobBreedingProvider;
+import org.leavesmc.leaves.protocol.jade.provider.entity.MobGrowthProvider;
+import org.leavesmc.leaves.protocol.jade.provider.entity.NextEntityDropProvider;
+import org.leavesmc.leaves.protocol.jade.provider.entity.StatusEffectsProvider;
+import org.leavesmc.leaves.protocol.jade.provider.entity.ZombieVillagerProvider;
+import org.leavesmc.leaves.protocol.jade.util.HierarchyLookup;
+import org.leavesmc.leaves.protocol.jade.util.LootTableMineableCollector;
+import org.leavesmc.leaves.protocol.jade.util.PairHierarchyLookup;
+import org.leavesmc.leaves.protocol.jade.util.PriorityStore;
+import org.leavesmc.leaves.protocol.jade.util.WrappedHierarchyLookup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.function.Predicate;
+import java.util.stream.Collectors;
+
+@LeavesProtocol(namespace = "jade")
+public class JadeProtocol {
+
+ public static PriorityStore<ResourceLocation, IJadeProvider> priorities;
+ private static List<Block> shearableBlocks = List.of();
+
+ public static final String PROTOCOL_ID = "jade";
+
+ private static final HierarchyLookup<IJadeDataProvider<EntityAccessor>> entityDataProviders = new HierarchyLookup<>(Entity.class);
+ private static final PairHierarchyLookup<IJadeDataProvider<BlockAccessor>> blockDataProviders = new PairHierarchyLookup<>(new HierarchyLookup<>(Block.class), new HierarchyLookup<>(BlockEntity.class));
+
+ public static final WrappedHierarchyLookup<IServerExtensionProvider<ItemStack>> itemStorageProviders = new WrappedHierarchyLookup<>();
+
+ @Contract("_ -> new")
+ public static @NotNull ResourceLocation id(String path) {
+ return new ResourceLocation(PROTOCOL_ID, path);
+ }
+
+ @Contract("_ -> new")
+ public static @NotNull ResourceLocation mc_id(String path) {
+ return ResourceLocation.withDefaultNamespace(path);
+ }
+
+ private static boolean isPrimaryKey(ResourceLocation key) {
+ return !key.getPath().contains(".");
+ }
+
+ private static ResourceLocation getPrimaryKey(ResourceLocation key) {
+ return new ResourceLocation(key.getNamespace(), key.getPath().substring(0, key.getPath().indexOf('.')));
+ }
+
+ @ProtocolHandler.Init
+ public static void init() {
+ priorities = new PriorityStore<>(IJadeProvider::getDefaultPriority, IJadeProvider::getUid);
+ priorities.setSortingFunction((store, allKeys) -> {
+ List<ResourceLocation> keys = allKeys.stream()
+ .filter(JadeProtocol::isPrimaryKey)
+ .sorted(Comparator.comparingInt(store::byKey))
+ .collect(Collectors.toCollection(ArrayList::new));
+ allKeys.stream().filter(Predicate.not(JadeProtocol::isPrimaryKey)).forEach($ -> {
+ int index = keys.indexOf(JadeProtocol.getPrimaryKey($));
+ keys.add(index + 1, $);
+ });
+ return keys;
+ });
+
+ // core plugin
+ blockDataProviders.register(BlockEntity.class, ObjectNameProvider.INSTANCE);
+
+ // universal plugin
+ entityDataProviders.register(Entity.class, EntityStorageProvider.INSTANCE);
+ blockDataProviders.register(Block.class, BlockStorageProvider.INSTANCE);
+
+ itemStorageProviders.register(Object.class, ItemStorageExtensionProvider.INSTANCE);
+ itemStorageProviders.register(Block.class, ItemStorageExtensionProvider.INSTANCE);
+
+ // vanilla plugin
+ entityDataProviders.register(Entity.class, AnimalOwnerProvider.INSTANCE);
+ entityDataProviders.register(LivingEntity.class, StatusEffectsProvider.INSTANCE);
+ entityDataProviders.register(AgeableMob.class, MobGrowthProvider.INSTANCE);
+ entityDataProviders.register(Tadpole.class, MobGrowthProvider.INSTANCE);
+ entityDataProviders.register(Animal.class, MobBreedingProvider.INSTANCE);
+ entityDataProviders.register(Allay.class, MobBreedingProvider.INSTANCE);
+
+ entityDataProviders.register(Chicken.class, NextEntityDropProvider.INSTANCE);
+ entityDataProviders.register(Armadillo.class, NextEntityDropProvider.INSTANCE);
+
+ entityDataProviders.register(ZombieVillager.class, ZombieVillagerProvider.INSTANCE);
+
+ blockDataProviders.register(BrewingStandBlockEntity.class, BrewingStandProvider.INSTANCE);
+ blockDataProviders.register(BeehiveBlockEntity.class, BeehiveProvider.INSTANCE);
+ blockDataProviders.register(CommandBlockEntity.class, CommandBlockProvider.INSTANCE);
+ blockDataProviders.register(JukeboxBlockEntity.class, JukeboxProvider.INSTANCE);
+ blockDataProviders.register(LecternBlockEntity.class, LecternProvider.INSTANCE);
+
+ blockDataProviders.register(ComparatorBlockEntity.class, RedstoneProvider.INSTANCE);
+ blockDataProviders.register(HopperBlockEntity.class, RedstoneProvider.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);
+
+ try {
+ shearableBlocks = Collections.unmodifiableList(LootTableMineableCollector.execute(
+ MinecraftServer.getServer().registryAccess().registryOrThrow(Registries.LOOT_TABLE),
+ Items.SHEARS.getDefaultInstance()));
+ } catch (Throwable ignore) {
+ LeavesLogger.LOGGER.severe("Failed to collect shearable blocks");
+ }
+ }
+
+ @ProtocolHandler.PlayerJoin
+ public static void onPlayerJoin(ServerPlayer player) {
+ if (LeavesConfig.jadeProtocol) {
+ ProtocolUtils.sendPayloadPacket(player, new ServerPingPayload("", shearableBlocks));
+ }
+ }
+
+ @ProtocolHandler.PayloadReceiver(payload = RequestEntityPayload.class, payloadId = "request_entity")
+ public static void requestEntityData(ServerPlayer player, RequestEntityPayload payload) {
+ if (!LeavesConfig.jadeProtocol) {
+ return;
+ }
+
+ MinecraftServer server = MinecraftServer.getServer();
+ server.execute(() -> {
+ Level world = player.level();
+ boolean showDetails = payload.showDetails;
+ Entity entity = world.getEntity(payload.entityId);
+ double maxDistance = Mth.square(player.entityInteractionRange() + 21);
+
+ if (entity == null || player.distanceToSqr(entity) > maxDistance) {
+ return;
+ }
+
+ if (payload.partIndex >= 0 && entity instanceof EnderDragon dragon) {
+ EnderDragonPart[] parts = dragon.getSubEntities();
+ if (payload.partIndex < parts.length) {
+ entity = parts[payload.partIndex];
+ }
+ }
+
+ var providers = entityDataProviders.get(entity);
+ if (providers.isEmpty()) {
+ return;
+ }
+
+ DataAccessor tag = new DataAccessor(world);
+ EntityAccessor accessor = new EntityAccessor(player, world, entity, payload.hitVec, showDetails);
+ for (IJadeDataProvider<EntityAccessor> provider : providers) {
+ try {
+ provider.saveData(tag, accessor);
+ } catch (Exception e) {
+ LeavesLogger.LOGGER.warning("Error while saving data for entity " + entity);
+ }
+ }
+ tag.putInt("EntityId", entity.getId());
+
+ ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag));
+ });
+ }
+
+ @ProtocolHandler.PayloadReceiver(payload = RequestBlockPayload.class, payloadId = "request_block")
+ public static void requestBlockData(ServerPlayer player, RequestBlockPayload payload) {
+ if (!LeavesConfig.jadeProtocol) {
+ return;
+ }
+
+ MinecraftServer server = MinecraftServer.getServer();
+ server.execute(() -> {
+ Level world = player.level();
+ BlockState blockState = payload.blockState;
+ Block block = blockState.getBlock();
+ BlockHitResult result = payload.hitResult;
+ BlockPos pos = result.getBlockPos();
+ boolean showDetails = payload.showDetails;
+
+ double maxDistance = Mth.square(player.blockInteractionRange() + 21);
+ if (pos.distSqr(player.blockPosition()) > maxDistance || !world.isLoaded(pos)) {
+ return;
+ }
+
+ BlockEntity blockEntity = null;
+ if (blockState.hasBlockEntity()) {
+ blockEntity = world.getBlockEntity(pos);
+ }
+
+ List<IJadeDataProvider<BlockAccessor>> providers;
+ if (blockEntity != null) {
+ providers = blockDataProviders.getMerged(block, blockEntity);
+ } else {
+ providers = blockDataProviders.first.get(block);
+ }
+
+ if (providers.isEmpty()) {
+ return;
+ }
+
+ DataAccessor tag = new DataAccessor(world);
+ BlockAccessor accessor = new BlockAccessor(player, world, blockEntity, result, block, blockState, pos, showDetails);
+ for (IJadeDataProvider<BlockAccessor> provider : providers) {
+ try {
+ provider.saveData(tag, accessor);
+ } catch (Exception e) {
+ LeavesLogger.LOGGER.warning("Error while saving data for block " + blockState);
+ }
+ }
+ tag.putInt("x", pos.getX());
+ tag.putInt("y", pos.getY());
+ tag.putInt("z", pos.getZ());
+ tag.putString("BlockId", BuiltInRegistries.BLOCK.getKey(block).toString());
+
+ ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag));
+ });
+ }
+
+ @ProtocolHandler.ReloadServer
+ public static void onServerReload() {
+ if (LeavesConfig.jadeProtocol) {
+ enableAllPlayer();
+ }
+ }
+
+ public static void enableAllPlayer() {
+ for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) {
+ onPlayerJoin(player);
+ }
+ }
+
+ public record RequestEntityPayload(boolean showDetails, int entityId, int partIndex, Vec3 hitVec) implements LeavesCustomPayload<RequestEntityPayload> {
+
+ private static final ResourceLocation PACKET_REQUEST_ENTITY = JadeProtocol.id("request_entity");
+
+ @New
+ public RequestEntityPayload(ResourceLocation id, @NotNull FriendlyByteBuf buf) {
+ this(buf.readBoolean(), buf.readVarInt(), buf.readVarInt(), new Vec3(buf.readVector3f()));
+ }
+
+ @Override
+ public void write(@NotNull FriendlyByteBuf buf) {
+ buf.writeBoolean(showDetails);
+ buf.writeVarInt(entityId);
+ buf.writeVarInt(partIndex);
+ buf.writeVector3f(hitVec.toVector3f());
+ }
+
+ @Override
+ @NotNull
+ public ResourceLocation id() {
+ return PACKET_REQUEST_ENTITY;
+ }
+ }
+
+ public record RequestBlockPayload(boolean showDetails, BlockHitResult hitResult, BlockState blockState, ItemStack fakeBlock) implements LeavesCustomPayload<RequestBlockPayload> {
+
+ private static final ResourceLocation PACKET_REQUEST_BLOCK = JadeProtocol.id("request_block");
+ private static final StreamCodec<RegistryFriendlyByteBuf, ItemStack> ITEM_STACK_CODEC = ItemStack.OPTIONAL_STREAM_CODEC;
+ private static final StreamCodec<ByteBuf, BlockState> BLOCK_STATE_CODEC = ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY);
+
+ @New
+ public RequestBlockPayload(ResourceLocation id, @NotNull FriendlyByteBuf buf) {
+ this(buf.readBoolean(), buf.readBlockHitResult(), BLOCK_STATE_CODEC.decode(buf), ITEM_STACK_CODEC.decode(ProtocolUtils.decorate(buf)));
+ }
+
+ @Override
+ public void write(@NotNull FriendlyByteBuf buf) {
+ buf.writeBoolean(showDetails);
+ buf.writeBlockHitResult(hitResult);
+ BLOCK_STATE_CODEC.encode(buf, blockState);
+ ITEM_STACK_CODEC.encode(ProtocolUtils.decorate(buf), fakeBlock);
+ }
+
+ @Override
+ @NotNull
+ public ResourceLocation id() {
+ return PACKET_REQUEST_BLOCK;
+ }
+ }
+
+ public record ServerPingPayload(String serverConfig, List<Block> shearableBlocks) implements LeavesCustomPayload<ServerPingPayload> {
+
+ private static final ResourceLocation PACKET_SERVER_PING = JadeProtocol.id("server_ping_v1");
+ private static final StreamCodec<RegistryFriendlyByteBuf, List<Block>> SHEARABLE_BLOCKS_CODEC = ByteBufCodecs.registry(Registries.BLOCK).apply(ByteBufCodecs.list());
+
+ @New
+ public ServerPingPayload(ResourceLocation id, @NotNull FriendlyByteBuf buf) {
+ this(buf.readUtf(), SHEARABLE_BLOCKS_CODEC.decode(ProtocolUtils.decorate(buf)));
+ }
+
+ @Override
+ public void write(FriendlyByteBuf buf) {
+ buf.writeUtf(serverConfig);
+ SHEARABLE_BLOCKS_CODEC.encode(ProtocolUtils.decorate(buf), shearableBlocks);
+ }
+
+ @Override
+ public ResourceLocation id() {
+ return PACKET_SERVER_PING;
+ }
+ }
+
+ public record ReceiveDataPayload(CompoundTag tag) implements LeavesCustomPayload<ReceiveDataPayload> {
+
+ private static final ResourceLocation PACKET_RECEIVE_DATA = JadeProtocol.id("receive_data");
+
+ @New
+ public ReceiveDataPayload(ResourceLocation id, FriendlyByteBuf buf) {
+ this(buf.readNbt());
+ }
+
+ @Override
+ public void write(@NotNull FriendlyByteBuf buf) {
+ buf.writeNbt(tag);
+ }
+
+ @Override
+ public ResourceLocation id() {
+ return PACKET_RECEIVE_DATA;
+ }
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..e4b037b6c09c4d7e7c28f5edac65fbb0b0431bb5
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/BlockAccessor.java
@@ -0,0 +1,13 @@
+package org.leavesmc.leaves.protocol.jade.accessor;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.server.level.ServerPlayer;
+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;
+
+public record BlockAccessor(ServerPlayer player, Level world, BlockEntity target, BlockHitResult hitResult,
+ Block block, BlockState blockState, BlockPos pos, boolean showDetails) implements RequestAccessor<BlockEntity> {
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/DataAccessor.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/DataAccessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..f8215ffdc2cfe39ab1be89c31a68ef0925eaa3a6
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/DataAccessor.java
@@ -0,0 +1,32 @@
+package org.leavesmc.leaves.protocol.jade.accessor;
+
+import com.mojang.serialization.DynamicOps;
+import com.mojang.serialization.MapEncoder;
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.NbtOps;
+import net.minecraft.nbt.Tag;
+import net.minecraft.resources.RegistryOps;
+import net.minecraft.world.level.Level;
+
+public class DataAccessor extends CompoundTag {
+
+ private final Level level;
+ private DynamicOps<Tag> ops;
+
+ public DataAccessor(Level level) {
+ this.level = level;
+ }
+
+ public DynamicOps<Tag> nbtOps() {
+ if (ops == null) {
+ ops = RegistryOps.create(NbtOps.INSTANCE, level.registryAccess());
+ }
+
+ return ops;
+ }
+
+ public <D> void writeMapData(MapEncoder<D> codec, D value) {
+ Tag tag = codec.encode(value, nbtOps(), nbtOps().mapBuilder()).build(new CompoundTag()).getOrThrow();
+ this.merge((CompoundTag) tag);
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..b2dac76f4c06259c0fc767894a30564a8ffdbd2f
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/EntityAccessor.java
@@ -0,0 +1,9 @@
+package org.leavesmc.leaves.protocol.jade.accessor;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.phys.Vec3;
+
+public record EntityAccessor(ServerPlayer player, Level world, Entity target, Vec3 hitVec3, boolean showDetails) implements RequestAccessor<Entity> {
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/RequestAccessor.java b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/RequestAccessor.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e490c17cdf25fdb9a7965785bb1189868471c5b
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/accessor/RequestAccessor.java
@@ -0,0 +1,15 @@
+package org.leavesmc.leaves.protocol.jade.accessor;
+
+import net.minecraft.server.level.ServerPlayer;
+import net.minecraft.world.level.Level;
+
+public interface RequestAccessor<T> {
+
+ ServerPlayer player();
+
+ Level world();
+
+ T target();
+
+ boolean showDetails();
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeDataProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeDataProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..710a225a20ba0cfcb9ad7878b5ef797c94890926
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeDataProvider.java
@@ -0,0 +1,8 @@
+package org.leavesmc.leaves.protocol.jade.provider;
+
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.RequestAccessor;
+
+public interface IJadeDataProvider<T extends RequestAccessor<?>> extends IJadeProvider {
+ void saveData(DataAccessor data, T request);
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..d62fc8f96fcdee7dbb0204d2460ff6fee4074e1a
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IJadeProvider.java
@@ -0,0 +1,12 @@
+package org.leavesmc.leaves.protocol.jade.provider;
+
+import net.minecraft.resources.ResourceLocation;
+
+public interface IJadeProvider {
+
+ ResourceLocation getUid();
+
+ default int getDefaultPriority() {
+ return 0;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..14613e35a6785fc599b1520a667e1311eba12f57
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/IServerExtensionProvider.java
@@ -0,0 +1,10 @@
+package org.leavesmc.leaves.protocol.jade.provider;
+
+import org.leavesmc.leaves.protocol.jade.accessor.RequestAccessor;
+import org.leavesmc.leaves.protocol.jade.util.ViewGroup;
+
+import java.util.List;
+
+public interface IServerExtensionProvider<T> extends IJadeProvider {
+ List<ViewGroup<T>> getGroups(RequestAccessor<?> request);
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/ItemStorageExtensionProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..7ef50f5b7437d8ec918d26b7a564161ed08aabbd
--- /dev/null
+++ b/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.WorldlyContainer;
+import net.minecraft.world.WorldlyContainerHolder;
+import net.minecraft.world.entity.animal.horse.AbstractHorse;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.entity.vehicle.ContainerEntity;
+import net.minecraft.world.inventory.PlayerEnderChestContainer;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.ChestBlock;
+import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
+import net.minecraft.world.level.block.entity.ChestBlockEntity;
+import net.minecraft.world.level.block.entity.EnderChestBlockEntity;
+import org.leavesmc.leaves.LeavesLogger;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.RequestAccessor;
+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<ItemStack> {
+ INSTANCE;
+
+ public static final Cache<Object, ItemCollector<?>> targetCache = CacheBuilder.newBuilder().weakKeys().expireAfterAccess(60, TimeUnit.SECONDS).build();
+ public static final Cache<Object, ItemCollector<?>> containerCache = CacheBuilder.newBuilder().weakKeys().expireAfterAccess(120, TimeUnit.SECONDS).build();
+
+ private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage.default");
+
+ @Override
+ public List<ViewGroup<ItemStack>> getGroups(RequestAccessor<?> request) {
+ Object target = request.target();
+ if (target == null && request instanceof BlockAccessor blockAccessor && blockAccessor.block() instanceof WorldlyContainerHolder holder) {
+ WorldlyContainer container = holder.getContainer(blockAccessor.blockState(), request.world(), blockAccessor.pos());
+ return containerGroup(container, request);
+ }
+
+ switch (target) {
+ case null -> {
+ return List.of();
+ }
+ case RandomizableContainer te when te.getLootTable() != null -> {
+ return List.of();
+ }
+ case ContainerEntity containerEntity when containerEntity.getLootTable() != null -> {
+ return List.of();
+ }
+ default -> {
+ }
+ }
+
+ Player player = request.player();
+ 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(0)).update(inventory, request.world().getGameTime());
+ }
+
+ ItemCollector<?> itemCollector;
+ try {
+ itemCollector = targetCache.get(target, () -> createItemCollector(target));
+ } catch (ExecutionException e) {
+ LeavesLogger.LOGGER.severe("Failed to get item collector for " + target);
+ return null;
+ }
+
+ if (itemCollector == ItemCollector.EMPTY) {
+ return null;
+ }
+
+ return itemCollector.update(target, request.world().getGameTime());
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return UNIVERSAL_ITEM_STORAGE;
+ }
+
+ public static List<ViewGroup<ItemStack>> containerGroup(Container container, RequestAccessor<?> accessor) {
+ try {
+ return containerCache.get(container, () -> new ItemCollector<>(new ItemIterator.ContainerItemIterator(0))).update(container, accessor.world().getGameTime());
+ } catch (ExecutionException e) {
+ return null;
+ }
+ }
+
+ public static ItemCollector<?> createItemCollector(Object target) {
+ if (target 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
+
+ if (target instanceof Container) {
+ if (target 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;
+ }
+
+ @Override
+ public int getDefaultPriority() {
+ return IServerExtensionProvider.super.getDefaultPriority() + 1000;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..9d2b6bf80eaaf67b4a9df6bd46470838986a9aee
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BeehiveProvider.java
@@ -0,0 +1,27 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.level.block.entity.BeehiveBlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum BeehiveProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_BEEHIVE = JadeProtocol.mc_id("beehive");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ if (request.target() instanceof BeehiveBlockEntity beehive) {
+ data.putByte("Bees", (byte) beehive.getOccupantCount());
+ data.putBoolean("Full", beehive.isFull());
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_BEEHIVE;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockStorageProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockStorageProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..89f040d390d017807f2baf7ed8925acd62d083bb
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BlockStorageProvider.java
@@ -0,0 +1,65 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import net.minecraft.nbt.CompoundTag;
+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.level.block.entity.BaseContainerBlockEntity;
+import net.minecraft.world.level.block.entity.BlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+import org.leavesmc.leaves.protocol.jade.util.ViewGroup;
+
+public enum BlockStorageProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage.default");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ BlockEntity target = request.target();
+ Player player = request.player();
+ for (var provider : JadeProtocol.itemStorageProviders.get(request)) {
+ var groups = provider.getGroups(request);
+ if (groups == null) {
+ continue;
+ }
+
+ if (ViewGroup.saveList(data, "JadeItemStorage", groups, item -> {
+ int count = item.getCount();
+ if (count > item.getMaxStackSize()) {
+ item.setCount(1);
+ }
+ CompoundTag itemTag = (CompoundTag) item.save(request.world().registryAccess());
+ if (count > item.getMaxStackSize()) {
+ itemTag.putInt("NewCount", count);
+ item.setCount(count);
+ }
+ return itemTag;
+ })) {
+ data.putString("JadeItemStorageUid", provider.getUid().toString());
+ } else if (target instanceof RandomizableContainer containerEntity && containerEntity.getLootTable() != null) {
+ data.putBoolean("Loot", true);
+ } else if (!player.isCreative() && !player.isSpectator() && target instanceof BaseContainerBlockEntity te) {
+ if (te.lockKey != LockCode.NO_LOCK) {
+ data.putBoolean("Locked", true);
+ }
+ }
+ break;
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return UNIVERSAL_ITEM_STORAGE;
+ }
+
+ @Override
+ public int getDefaultPriority() {
+ return IJadeDataProvider.super.getDefaultPriority() + 1000;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..fd4112ed1911171b3c6b5840b7184b5f076617ee
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/BrewingStandProvider.java
@@ -0,0 +1,30 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.level.block.entity.BrewingStandBlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum BrewingStandProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_BREWING_STAND = JadeProtocol.mc_id("brewing_stand");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ if (request.target() instanceof BrewingStandBlockEntity brewingStand) {
+ CompoundTag compound = new CompoundTag();
+ compound.putInt("Time", brewingStand.brewTime);
+ compound.putInt("Fuel", brewingStand.fuel);
+ data.put("BrewingStand", compound);
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_BREWING_STAND;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..a1a479987f2c0b6ff4cfd511cbcac1ea7b1c247b
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CampfireProvider.java
@@ -0,0 +1,52 @@
+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.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.RequestAccessor;
+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<ItemStack> {
+ INSTANCE;
+
+ private static final MapCodec<Integer> COOKING_TIME_CODEC = Codec.INT.fieldOf("jade:cooking");
+ private static final ResourceLocation MC_CAMPFIRE = JadeProtocol.mc_id("campfire");
+
+ @Override
+ public List<ViewGroup<ItemStack>> getGroups(RequestAccessor<?> request) {
+ if (request.target() instanceof CampfireBlockEntity campfire) {
+ List<ItemStack> list = Lists.newArrayList();
+ for (int i = 0; i < campfire.cookingTime.length; i++) {
+ ItemStack stack = campfire.getItems().get(i);
+ if (stack.isEmpty()) {
+ continue;
+ }
+ stack = stack.copy();
+
+ CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY)
+ .update(NbtOps.INSTANCE, COOKING_TIME_CODEC, campfire.cookingTime[i] - campfire.cookingProgress[i])
+ .getOrThrow();
+ stack.set(DataComponents.CUSTOM_DATA, customData);
+
+ list.add(stack);
+ }
+ return List.of(new ViewGroup<>(list));
+ }
+ return null;
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_CAMPFIRE;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..054e44252259ed54b7365072b0bc6dbfce6af466
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ChiseledBookshelfProvider.java
@@ -0,0 +1,43 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import com.mojang.serialization.MapCodec;
+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.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum ChiseledBookshelfProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ public static final MapCodec<ItemStack> BOOK_CODEC = ItemStack.CODEC.fieldOf("book");
+ private static final ResourceLocation MC_CHISELED_BOOKSHELF = JadeProtocol.mc_id("chiseled_bookshelf");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ if (request.target() instanceof ChiseledBookShelfBlockEntity bookshelf) {
+ int slot = ((ChiseledBookShelfBlock) request.block()).getHitSlot(request.hitResult(), request.blockState()).orElse(-1);
+ if (slot == -1) {
+ return;
+ }
+
+ ItemStack book = bookshelf.getItem(slot);
+ if (!book.isEmpty()) {
+ data.writeMapData(BOOK_CODEC, book);
+ }
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_CHISELED_BOOKSHELF;
+ }
+
+ @Override
+ public int getDefaultPriority() {
+ return BlockStorageProvider.INSTANCE.getDefaultPriority() + 1;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..2af51302185a16b3d9eae1e91fc3153273881ccd
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/CommandBlockProvider.java
@@ -0,0 +1,41 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.level.BaseCommandBlock;
+import net.minecraft.world.level.block.entity.CommandBlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum CommandBlockProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_COMMAND_BLOCK = JadeProtocol.mc_id("command_block");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor accessor) {
+ Player player = accessor.player();
+ if (!player.canUseGameMasterBlocks()) {
+ return;
+ }
+
+ if (accessor.target() instanceof CommandBlockEntity commandBlock) {
+ BaseCommandBlock logic = commandBlock.getCommandBlock();
+ String command = logic.getCommand();
+ if (command.isEmpty()) {
+ return;
+ }
+ if (command.length() > 40) {
+ command = command.substring(0, 37) + "...";
+ }
+ data.putString("Command", command);
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_COMMAND_BLOCK;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..e53ede434dbe3d4289b69869958e42b5b208a911
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/FurnaceProvider.java
@@ -0,0 +1,41 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum FurnaceProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_FURNACE = JadeProtocol.mc_id("furnace");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ if (!(request.target() instanceof AbstractFurnaceBlockEntity furnace)) {
+ return;
+ }
+
+ if (furnace.isEmpty()) {
+ return;
+ }
+
+ ListTag items = new ListTag();
+ for (int i = 0; i < 3; i++) {
+ items.add(furnace.getItem(i).saveOptional(request.world().registryAccess()));
+ }
+ data.put("furnace", items);
+ CompoundTag furnaceTag = furnace.saveWithoutMetadata(request.world().registryAccess());
+ data.putInt("progress", furnaceTag.getInt("CookTime"));
+ data.putInt("total", furnaceTag.getInt("CookTimeTotal"));
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_FURNACE;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..6085390614045b94a68d96dacf778af1d31033b3
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/JukeboxProvider.java
@@ -0,0 +1,32 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import com.mojang.serialization.MapCodec;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.block.entity.JukeboxBlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum JukeboxProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final MapCodec<ItemStack> RECORD_CODEC = ItemStack.CODEC.fieldOf("record");
+ private static final ResourceLocation MC_JUKEBOX = JadeProtocol.mc_id("jukebox");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ if (request.target() instanceof JukeboxBlockEntity jukebox) {
+ ItemStack stack = jukebox.getTheItem();
+ if (!stack.isEmpty()) {
+ data.writeMapData(RECORD_CODEC, stack);
+ }
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_JUKEBOX;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..ea6cf2482807b327d8ff10f0aa117b9b9b45c675
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/LecternProvider.java
@@ -0,0 +1,34 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.block.entity.LecternBlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum LecternProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_LECTERN = JadeProtocol.mc_id("lectern");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ if (request.target() instanceof LecternBlockEntity lectern) {
+ ItemStack stack = lectern.getBook();
+ if (!stack.isEmpty()) {
+ if (stack.has(DataComponents.CUSTOM_NAME) || stack.getItem() != Items.WRITABLE_BOOK) {
+ data.writeMapData(ChiseledBookshelfProvider.BOOK_CODEC, stack);
+ }
+ }
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_LECTERN;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..63af91ae159cdcdb1195d98530f6e779449fb60b
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/MobSpawnerCooldownProvider.java
@@ -0,0 +1,32 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity;
+import net.minecraft.world.level.block.entity.trialspawner.TrialSpawnerData;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum MobSpawnerCooldownProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_MOB_SPAWNER_COOLDOWN = JadeProtocol.mc_id("mob_spawner.cooldown");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ if (request.target() instanceof TrialSpawnerBlockEntity spawner) {
+ TrialSpawnerData spawnerData = spawner.getTrialSpawner().getData();
+ Level level = request.world();
+ if (spawner.getTrialSpawner().canSpawnInLevel(level) && level.getGameTime() < spawnerData.cooldownEndsAt) {
+ data.putInt("Cooldown", (int) (spawnerData.cooldownEndsAt - level.getGameTime()));
+ }
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_MOB_SPAWNER_COOLDOWN;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..f001c01b0739f77879791b4a6163f84596a7349a
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/ObjectNameProvider.java
@@ -0,0 +1,53 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import com.mojang.serialization.MapCodec;
+import net.minecraft.network.chat.Component;
+import net.minecraft.network.chat.ComponentSerialization;
+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.BlockEntity;
+import net.minecraft.world.level.block.entity.ChestBlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum ObjectNameProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final MapCodec<Component> GIVEN_NAME_CODEC = ComponentSerialization.CODEC.fieldOf("given_name");
+ private static final ResourceLocation CORE_OBJECT_NAME = JadeProtocol.id("object_name");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ BlockEntity blockEntity = request.target();
+ if (blockEntity instanceof Nameable nameable) {
+ Component name = null;
+
+ if (blockEntity instanceof ChestBlockEntity && request.block() instanceof ChestBlock) {
+ MenuProvider menuProvider = request.blockState().getMenuProvider(request.world(), request.pos());
+ if (menuProvider != null) {
+ name = menuProvider.getDisplayName();
+ }
+ } else if (nameable.hasCustomName()) {
+ name = nameable.getDisplayName();
+ }
+
+ if (name != null) {
+ data.writeMapData(GIVEN_NAME_CODEC, name);
+ }
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return CORE_OBJECT_NAME;
+ }
+
+ @Override
+ public int getDefaultPriority() {
+ return -10100;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..53b6f7dd85875b9f519831b888b45a17c4ec90d6
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/block/RedstoneProvider.java
@@ -0,0 +1,43 @@
+package org.leavesmc.leaves.protocol.jade.provider.block;
+
+import net.minecraft.core.Direction;
+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 net.minecraft.world.level.block.entity.HopperBlockEntity;
+import net.minecraft.world.level.block.state.BlockState;
+import net.minecraft.world.level.block.state.properties.BlockStateProperties;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum RedstoneProvider implements IJadeDataProvider<BlockAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_REDSTONE = JadeProtocol.mc_id("redstone");
+
+ @Override
+ public void saveData(DataAccessor data, BlockAccessor request) {
+ BlockEntity blockEntity = request.target();
+ if (blockEntity instanceof ComparatorBlockEntity comparator) {
+ data.putInt("Signal", comparator.getOutputSignal());
+ } else if (blockEntity instanceof HopperBlockEntity) {
+ BlockState state = request.blockState();
+ if (state.hasProperty(BlockStateProperties.ENABLED) && !state.getValue(BlockStateProperties.ENABLED)) {
+ data.putBoolean("HopperLocked", true);
+ }
+ } else if (blockEntity instanceof CalibratedSculkSensorBlockEntity) {
+ Direction direction = request.blockState().getValue(CalibratedSculkSensorBlock.FACING).getOpposite();
+ int signal = request.world().getSignal(request.pos().relative(direction), direction);
+ data.putInt("Signal", signal);
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_REDSTONE;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..d75c6889c16d77c251fbc5d921d43cee7e2ad4d1
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/AnimalOwnerProvider.java
@@ -0,0 +1,39 @@
+package org.leavesmc.leaves.protocol.jade.provider.entity;
+
+import com.mojang.authlib.GameProfile;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.server.MinecraftServer;
+import net.minecraft.server.players.GameProfileCache;
+import net.minecraft.world.entity.OwnableEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+import java.util.UUID;
+
+public enum AnimalOwnerProvider implements IJadeDataProvider<EntityAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_ANIMAL_OWNER = JadeProtocol.mc_id("animal_owner");
+
+ @Override
+ public void saveData(DataAccessor data, EntityAccessor request) {
+ UUID ownerUUID = null;
+ if (request.target() instanceof OwnableEntity ownable) {
+ ownerUUID = ownable.getOwnerUUID();
+ }
+
+ if (ownerUUID != null) {
+ GameProfileCache cache = MinecraftServer.getServer().getProfileCache();
+ if (cache != null) {
+ cache.get(ownerUUID).map(GameProfile::getName).ifPresent(name -> data.putString("OwnerName", name));
+ }
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_ANIMAL_OWNER;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/EntityStorageProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/EntityStorageProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..851dd6f0a8e8a13746a40c8b372103fd3d03bfc6
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/EntityStorageProvider.java
@@ -0,0 +1,56 @@
+package org.leavesmc.leaves.protocol.jade.provider.entity;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.LockCode;
+import net.minecraft.world.RandomizableContainer;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.player.Player;
+import net.minecraft.world.level.block.entity.BaseContainerBlockEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+import org.leavesmc.leaves.protocol.jade.util.ViewGroup;
+
+public enum EntityStorageProvider implements IJadeDataProvider<EntityAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation UNIVERSAL_ITEM_STORAGE = JadeProtocol.mc_id("item_storage.default");
+
+ @Override
+ public void saveData(DataAccessor data, EntityAccessor request) {
+ for (var provider : JadeProtocol.itemStorageProviders.get(request)) {
+ var groups = provider.getGroups(request);
+ if (groups == null) {
+ continue;
+ }
+
+ if (ViewGroup.saveList(data, "JadeItemStorage", groups, item -> {
+ int count = item.getCount();
+ if (count > item.getMaxStackSize()) {
+ item.setCount(1);
+ }
+ CompoundTag itemTag = (CompoundTag) item.save(request.world().registryAccess());
+ if (count > item.getMaxStackSize()) {
+ itemTag.putInt("NewCount", count);
+ item.setCount(count);
+ }
+ return itemTag;
+ })) {
+ data.putString("JadeItemStorageUid", provider.getUid().toString());
+ }
+ break;
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return UNIVERSAL_ITEM_STORAGE;
+ }
+
+ @Override
+ public int getDefaultPriority() {
+ return IJadeDataProvider.super.getDefaultPriority() + 1000;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..d7ed10be4700c68fe5c04b483ec7f558d3c4c686
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobBreedingProvider.java
@@ -0,0 +1,39 @@
+package org.leavesmc.leaves.protocol.jade.provider.entity;
+
+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.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum MobBreedingProvider implements IJadeDataProvider<EntityAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_MOB_BREEDING = JadeProtocol.mc_id("mob_breeding");
+
+ @Override
+ public void saveData(DataAccessor data, EntityAccessor request) {
+ int time = 0;
+ Entity entity = request.target();
+
+ if (entity instanceof Allay allay) {
+ if (allay.duplicationCooldown > 0 && allay.duplicationCooldown < Integer.MAX_VALUE) {
+ time = (int) allay.duplicationCooldown;
+ }
+ } else if (entity instanceof Animal animal) {
+ time = animal.getAge();
+ }
+
+ if (time > 0) {
+ data.putInt("BreedingCD", time);
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_MOB_BREEDING;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..f7635011da4b099205a8d5ec4445707dbdd0f35c
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/MobGrowthProvider.java
@@ -0,0 +1,37 @@
+package org.leavesmc.leaves.protocol.jade.provider.entity;
+
+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.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum MobGrowthProvider implements IJadeDataProvider<EntityAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_MOB_GROWTH = JadeProtocol.mc_id("mob_growth");
+
+ @Override
+ public void saveData(DataAccessor data, EntityAccessor request) {
+ int time = -1;
+ Entity entity = request.target();
+
+ if (entity instanceof AgeableMob ageable) {
+ time = -ageable.getAge();
+ } else if (entity instanceof Tadpole tadpole) {
+ time = tadpole.getTicksLeftUntilAdult();
+ }
+
+ if (time > 0) {
+ data.putInt("GrowingTime", time);
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_MOB_GROWTH;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..7380ccfe98ad78b3a153da1efd3712a6a780a918
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/NextEntityDropProvider.java
@@ -0,0 +1,37 @@
+package org.leavesmc.leaves.protocol.jade.provider.entity;
+
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.Entity;
+import net.minecraft.world.entity.animal.Chicken;
+import net.minecraft.world.entity.animal.armadillo.Armadillo;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum NextEntityDropProvider implements IJadeDataProvider<EntityAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_NEXT_ENTITY_DROP = JadeProtocol.mc_id("next_entity_drop");
+
+ @Override
+ public void saveData(DataAccessor data, EntityAccessor request) {
+ int max = 24000 * 2;
+ Entity entity = request.target();
+
+ if (entity instanceof Chicken chicken) {
+ if (!chicken.isBaby() && chicken.eggTime < max) {
+ data.putInt("NextEggIn", chicken.eggTime);
+ }
+ } else if (entity instanceof Armadillo armadillo) {
+ if (!armadillo.isBaby() && armadillo.scuteTime < max) {
+ data.putInt("NextScuteIn", armadillo.scuteTime);
+ }
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_NEXT_ENTITY_DROP;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..e92ce371a3d41ab334e4349bb4023c03c89ac73c
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/StatusEffectsProvider.java
@@ -0,0 +1,41 @@
+package org.leavesmc.leaves.protocol.jade.provider.entity;
+
+import com.mojang.serialization.MapCodec;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.effect.MobEffectInstance;
+import net.minecraft.world.entity.LivingEntity;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+import java.util.Collection;
+import java.util.List;
+
+public enum StatusEffectsProvider implements IJadeDataProvider<EntityAccessor> {
+ INSTANCE;
+
+ private static final MapCodec<List<MobEffectInstance>> EFFECTS_CODEC = MobEffectInstance.CODEC.listOf().fieldOf("mob_effects");
+ private static final ResourceLocation MC_POTION_EFFECTS = JadeProtocol.mc_id("potion_effects");
+
+ @Override
+ public void saveData(DataAccessor data, EntityAccessor request) {
+ LivingEntity living = (LivingEntity) request.target();
+ Collection<MobEffectInstance> effects = living.getActiveEffects();
+ if (effects.isEmpty()) {
+ return;
+ }
+
+ List<MobEffectInstance> effectList = effects.stream().filter(MobEffectInstance::isVisible).toList();
+ if (effectList.isEmpty()) {
+ return;
+ }
+
+ data.writeMapData(EFFECTS_CODEC, effectList);
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_POTION_EFFECTS;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..d48ef5d8c72b57ff5525ab06b59724d0f42ad42c
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/provider/entity/ZombieVillagerProvider.java
@@ -0,0 +1,27 @@
+package org.leavesmc.leaves.protocol.jade.provider.entity;
+
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.entity.monster.ZombieVillager;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.accessor.DataAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeDataProvider;
+
+public enum ZombieVillagerProvider implements IJadeDataProvider<EntityAccessor> {
+ INSTANCE;
+
+ private static final ResourceLocation MC_ZOMBIE_VILLAGER = JadeProtocol.mc_id("zombie_villager");
+
+ @Override
+ public void saveData(DataAccessor data, EntityAccessor request) {
+ ZombieVillager entity = (ZombieVillager) request.target();
+ if (entity.villagerConversionTime > 0) {
+ data.putInt("ConversionTime", entity.villagerConversionTime);
+ }
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return MC_ZOMBIE_VILLAGER;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..0e6f19a4cf55d952d8f20bf703635c8584a5f0bd
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ShearsToolHandler.java
@@ -0,0 +1,43 @@
+package org.leavesmc.leaves.protocol.jade.tool;
+
+import com.google.common.collect.Sets;
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.Items;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.Block;
+import net.minecraft.world.level.block.Blocks;
+import net.minecraft.world.level.block.state.BlockState;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+
+public class ShearsToolHandler extends SimpleToolHandler {
+
+ private static final ShearsToolHandler INSTANCE = new ShearsToolHandler();
+
+ public static ShearsToolHandler getInstance() {
+ return INSTANCE;
+ }
+
+ private final Set<Block> shearableBlocks = Sets.newIdentityHashSet();
+
+ public ShearsToolHandler() {
+ super(JadeProtocol.id("shears"), List.of(Items.SHEARS.getDefaultInstance()), true);
+ }
+
+ @Override
+ public ItemStack test(BlockState state, Level world, BlockPos pos) {
+ if (state.is(Blocks.TRIPWIRE) || shearableBlocks.contains(state.getBlock())) {
+ return tools.getFirst();
+ }
+ return super.test(state, world, pos);
+ }
+
+ public void setShearableBlocks(Collection<Block> blocks) {
+ shearableBlocks.clear();
+ shearableBlocks.addAll(blocks);
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..22fee6ecc49bbda94a7d32ee9dcf2a9ee661904b
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/SimpleToolHandler.java
@@ -0,0 +1,67 @@
+package org.leavesmc.leaves.protocol.jade.tool;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import net.minecraft.core.BlockPos;
+import net.minecraft.core.component.DataComponents;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.item.Item;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.item.component.Tool;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockState;
+
+import java.util.List;
+
+public class SimpleToolHandler implements ToolHandler {
+
+ protected final List<ItemStack> tools = Lists.newArrayList();
+ private final ResourceLocation uid;
+ private final boolean skipInstaBreakingBlock;
+
+ protected SimpleToolHandler(ResourceLocation uid, List<ItemStack> tools, boolean skipInstaBreakingBlock) {
+ this.uid = uid;
+ Preconditions.checkArgument(!tools.isEmpty(), "tools cannot be empty");
+ this.tools.addAll(tools);
+ this.skipInstaBreakingBlock = skipInstaBreakingBlock;
+ }
+
+ public static SimpleToolHandler create(ResourceLocation uid, List<Item> tools) {
+ return create(uid, tools, true);
+ }
+
+ public static SimpleToolHandler create(ResourceLocation uid, List<Item> tools, boolean skipInstaBreakingBlock) {
+ return new SimpleToolHandler(uid, Lists.transform(tools, Item::getDefaultInstance), skipInstaBreakingBlock);
+ }
+
+ @Override
+ public ItemStack test(BlockState state, Level world, BlockPos pos) {
+ if (skipInstaBreakingBlock && !state.requiresCorrectToolForDrops() && state.getDestroySpeed(world, pos) == 0) {
+ return ItemStack.EMPTY;
+ }
+ return test(state);
+ }
+
+ public ItemStack test(BlockState state) {
+ for (ItemStack toolItem : tools) {
+ if (toolItem.isCorrectToolForDrops(state)) {
+ return toolItem;
+ }
+ Tool tool = toolItem.get(DataComponents.TOOL);
+ if (tool != null && tool.getMiningSpeed(state) > tool.defaultMiningSpeed()) {
+ return toolItem;
+ }
+ }
+ return ItemStack.EMPTY;
+ }
+
+ @Override
+ public List<ItemStack> getTools() {
+ return tools;
+ }
+
+ @Override
+ public ResourceLocation getUid() {
+ return uid;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java
new file mode 100644
index 0000000000000000000000000000000000000000..18f11e701189ce3615e08c631e31112d53ea5686
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/tool/ToolHandler.java
@@ -0,0 +1,17 @@
+package org.leavesmc.leaves.protocol.jade.tool;
+
+import net.minecraft.core.BlockPos;
+import net.minecraft.world.item.ItemStack;
+import net.minecraft.world.level.Level;
+import net.minecraft.world.level.block.state.BlockState;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider;
+
+import java.util.List;
+
+public interface ToolHandler extends IJadeProvider {
+
+ ItemStack test(BlockState state, Level world, BlockPos pos);
+
+ List<ItemStack> getTools();
+
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..9e88dc87bd4a86c15b2b0d11ac4b095174b1c3d3
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/HierarchyLookup.java
@@ -0,0 +1,120 @@
+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.resources.ResourceLocation;
+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<T extends IJadeProvider> implements IHierarchyLookup<T> {
+
+
+ private final Class<?> baseClass;
+ private final Cache<Class<?>, List<T>> resultCache = CacheBuilder.newBuilder().build();
+ private final boolean singleton;
+ private ListMultimap<Class<?>, 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 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<T> get(Class<?> clazz) {
+ try {
+ return resultCache.get(clazz, () -> {
+ List<T> list = Lists.newArrayList();
+ getInternal(clazz, list);
+ list = ImmutableList.sortedCopyOf(Comparator.comparingInt(JadeProtocol.priorities::byValue), list);
+ if (singleton && !list.isEmpty()) {
+ return ImmutableList.of(list.getFirst());
+ }
+ return list;
+ });
+ } catch (ExecutionException e) {
+ LeavesLogger.LOGGER.severe(e.getMessage());
+ }
+ return List.of();
+ }
+
+ private void getInternal(Class<?> clazz, List<T> 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<Map.Entry<Class<?>, Collection<T>>> entries() {
+ return objects.asMap().entrySet().stream();
+ }
+
+ @Override
+ public void invalidate() {
+ resultCache.invalidateAll();
+ }
+
+ @Override
+ public void loadComplete(PriorityStore<ResourceLocation, IJadeProvider> priorityStore) {
+ objects.asMap().forEach((clazz, list) -> {
+ if (list.size() < 2) {
+ return;
+ }
+ Set<ResourceLocation> 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.<Class<?>, T>builder()
+ .orderValuesBy(Comparator.comparingInt(priorityStore::byValue))
+ .putAll(objects)
+ .build();
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..1bcd562ef4b88308fcfee1dae3675671b10edb15
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/IHierarchyLookup.java
@@ -0,0 +1,37 @@
+package org.leavesmc.leaves.protocol.jade.util;
+
+import net.minecraft.resources.ResourceLocation;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+public interface IHierarchyLookup<T extends IJadeProvider> {
+ default IHierarchyLookup<? extends T> cast() {
+ return this;
+ }
+
+ void register(Class<?> clazz, T provider);
+
+ boolean isClassAcceptable(Class<?> clazz);
+
+ default List<T> get(Object obj) {
+ if (obj == null) {
+ return List.of();
+ }
+ return get(obj.getClass());
+ }
+
+ List<T> get(Class<?> clazz);
+
+ boolean isEmpty();
+
+ Stream<Map.Entry<Class<?>, Collection<T>>> entries();
+
+ void invalidate();
+
+ void loadComplete(PriorityStore<ResourceLocation, IJadeProvider> priorityStore);
+}
+
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java
new file mode 100644
index 0000000000000000000000000000000000000000..1386291a6c651dfbbcecb2e469b1bd943861e4cc
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemCollector.java
@@ -0,0 +1,114 @@
+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 java.util.List;
+import java.util.Locale;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Predicate;
+
+public class ItemCollector<T> {
+ public static final int MAX_SIZE = 54;
+ public static final ItemCollector<?> EMPTY = new ItemCollector<>(null);
+ private static final Predicate<ItemStack> NON_EMPTY = stack -> {
+ if (stack.isEmpty()) {
+ return false;
+ }
+ CustomData customData = stack.getOrDefault(DataComponents.CUSTOM_DATA, CustomData.EMPTY);
+ if (customData.contains("CustomModelData")) {
+ CompoundTag tag = customData.getUnsafe();
+ for (String key : tag.getAllKeys()) {
+ if (key.toLowerCase(Locale.ENGLISH).endsWith("clear") && tag.getBoolean(key)) {
+ return false;
+ }
+ }
+ }
+ return true;
+ };
+ private final Object2IntLinkedOpenHashMap<ItemDefinition> items = new Object2IntLinkedOpenHashMap<>();
+ private final ItemIterator<T> iterator;
+ public long version;
+ public long lastTimeFinished;
+ public List<ViewGroup<ItemStack>> mergedResult;
+
+ public ItemCollector(ItemIterator<T> iterator) {
+ this.iterator = iterator;
+ }
+
+ public List<ViewGroup<ItemStack>> update(Object target, long gameTime) {
+ if (iterator == null) {
+ return null;
+ }
+ T container = iterator.find(target);
+ if (container == null) {
+ return null;
+ }
+ long currentVersion = iterator.getVersion(container);
+ if (mergedResult != null && iterator.isFinished()) {
+ if (version == currentVersion) {
+ return mergedResult; // content not changed
+ }
+ if (lastTimeFinished + 5 > gameTime) {
+ return mergedResult; // avoid update too frequently
+ }
+ iterator.reset();
+ }
+ AtomicInteger count = new AtomicInteger();
+ iterator.populate(container).forEach(stack -> {
+ count.incrementAndGet();
+ if (NON_EMPTY.test(stack)) {
+ ItemDefinition def = new ItemDefinition(stack);
+ items.addTo(def, stack.getCount());
+ }
+ });
+ iterator.afterPopulate(count.get());
+ if (mergedResult != null && !iterator.isFinished()) {
+ updateCollectingProgress(mergedResult.getFirst());
+ return mergedResult;
+ }
+ List<ItemStack> partialResult = items.object2IntEntrySet().stream().limit(54).map(entry -> {
+ ItemDefinition def = entry.getKey();
+ return def.toStack(entry.getIntValue());
+ }).toList();
+ List<ViewGroup<ItemStack>> groups = List.of(updateCollectingProgress(new ViewGroup<>(partialResult)));
+ if (iterator.isFinished()) {
+ mergedResult = groups;
+ version = currentVersion;
+ lastTimeFinished = gameTime;
+ items.clear();
+ }
+ return groups;
+ }
+
+ protected ViewGroup<ItemStack> updateCollectingProgress(ViewGroup<ItemStack> group) {
+ float progress = iterator.getCollectingProgress();
+ CompoundTag data = group.getExtraData();
+ if (Float.isNaN(progress)) {
+ progress = 0;
+ }
+ if (progress >= 1) {
+ data.remove("Collecting");
+ } else {
+ data.putFloat("Collecting", progress);
+ }
+ return group;
+ }
+
+ public record ItemDefinition(Item item, DataComponentPatch components) {
+ ItemDefinition(ItemStack stack) {
+ this(stack.getItem(), stack.getComponentsPatch());
+ }
+
+ public ItemStack toStack(int count) {
+ ItemStack itemStack = new ItemStack(item, count);
+ itemStack.applyComponents(components);
+ return itemStack;
+ }
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java
new file mode 100644
index 0000000000000000000000000000000000000000..4d65e9a8b5224bd268b1bf18bc39a58dc0113850
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ItemIterator.java
@@ -0,0 +1,102 @@
+package org.leavesmc.leaves.protocol.jade.util;
+
+import net.minecraft.world.Container;
+import net.minecraft.world.item.ItemStack;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+public abstract class ItemIterator<T> {
+ public static final AtomicLong version = new AtomicLong();
+ protected final Function<Object, @Nullable T> containerFinder;
+ protected final int fromIndex;
+ protected boolean finished;
+ protected int currentIndex;
+
+ protected ItemIterator(Function<Object, @Nullable T> 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<ItemStack> 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<T> extends ItemIterator<T> {
+ protected float progress;
+
+ public SlottedItemIterator(Function<Object, @Nullable T> containerFinder, int fromIndex) {
+ super(containerFinder, fromIndex);
+ }
+
+ protected abstract int getSlotCount(T container);
+
+ protected abstract ItemStack getItemInSlot(T container, int slot);
+
+ @Override
+ public Stream<ItemStack> 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<Container> {
+ public ContainerItemIterator(int fromIndex) {
+ this(Container.class::cast, fromIndex);
+ }
+
+ public ContainerItemIterator(Function<Object, @Nullable Container> containerFinder, int fromIndex) {
+ super(containerFinder, fromIndex);
+ }
+
+ @Override
+ protected int getSlotCount(Container container) {
+ return container.getContainerSize();
+ }
+
+ @Override
+ protected ItemStack getItemInSlot(Container container, int slot) {
+ return container.getItem(slot);
+ }
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java
new file mode 100644
index 0000000000000000000000000000000000000000..c811f89295964b1cb86c3eea39cd20f979ebceb9
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/LootTableMineableCollector.java
@@ -0,0 +1,104 @@
+package org.leavesmc.leaves.protocol.jade.util;
+
+import com.google.common.collect.Lists;
+import net.minecraft.advancements.critereon.ItemPredicate;
+import net.minecraft.core.Registry;
+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.leavesmc.leaves.protocol.jade.tool.ShearsToolHandler;
+
+import java.util.List;
+import java.util.function.Function;
+
+public class LootTableMineableCollector {
+
+ private final Registry<LootTable> lootRegistry;
+ private final ItemStack toolItem;
+
+ public LootTableMineableCollector(Registry<LootTable> lootRegistry, ItemStack toolItem) {
+ this.lootRegistry = lootRegistry;
+ this.toolItem = toolItem;
+ }
+
+ public static List<Block> execute(Registry<LootTable> lootRegistry, ItemStack toolItem) {
+ LootTableMineableCollector collector = new LootTableMineableCollector(lootRegistry, toolItem);
+ List<Block> list = Lists.newArrayList();
+ for (Block block : BuiltInRegistries.BLOCK) {
+ if (!ShearsToolHandler.getInstance().test(block.defaultBlockState()).isEmpty()) {
+ continue;
+ }
+
+ LootTable lootTable = lootRegistry.get(block.getLootTable());
+ if (collector.doLootTable(lootTable)) {
+ list.add(block);
+ }
+ }
+ return list;
+ }
+
+ private boolean doLootTable(LootTable lootTable) {
+ if (lootTable == null || lootTable == LootTable.EMPTY) {
+ return false;
+ }
+
+ for (LootPool pool : lootTable.pools) {
+ if (doLootPool(pool)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean doLootPool(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, Function.identity());
+ return doLootTable(lootTable);
+ } else {
+ return isCorrectConditions(entry.conditions, toolItem);
+ }
+ return false;
+ }
+
+ public static boolean isCorrectConditions(List<LootItemCondition> conditions, ItemStack toolItem) {
+ if (conditions.size() != 1) {
+ return false;
+ }
+
+ LootItemCondition condition = conditions.getFirst();
+ if (condition instanceof MatchTool matchTool) {
+ ItemPredicate itemPredicate = matchTool.predicate().orElse(null);
+ return itemPredicate != null && itemPredicate.test(toolItem);
+ } else if (condition instanceof AnyOfCondition anyOfCondition) {
+ for (LootItemCondition child : anyOfCondition.terms) {
+ if (isCorrectConditions(List.of(child), toolItem)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..580b299d9dc2514d9758c04caac39a82982c0ca1
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/PairHierarchyLookup.java
@@ -0,0 +1,101 @@
+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.resources.ResourceLocation;
+import org.apache.commons.lang3.tuple.Pair;
+import org.leavesmc.leaves.LeavesLogger;
+import org.leavesmc.leaves.protocol.jade.JadeProtocol;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Stream;
+
+public class PairHierarchyLookup<T extends IJadeProvider> implements IHierarchyLookup<T> {
+
+ public final IHierarchyLookup<T> first;
+ public final IHierarchyLookup<T> second;
+ private final Cache<Pair<Class<?>, Class<?>>, List<T>> mergedCache = CacheBuilder.newBuilder().build();
+
+ public PairHierarchyLookup(IHierarchyLookup<T> first, IHierarchyLookup<T> second) {
+ this.first = first;
+ this.second = second;
+ }
+
+ @SuppressWarnings("unchecked")
+ public <ANY> List<ANY> getMerged(Object first, Object second) {
+ Objects.requireNonNull(first);
+ Objects.requireNonNull(second);
+ try {
+ return (List<ANY>) mergedCache.get(Pair.of(first.getClass(), second.getClass()), () -> {
+ List<T> firstList = this.first.get(first);
+ List<T> secondList = this.second.get(second);
+ if (firstList.isEmpty()) {
+ return secondList;
+ } else if (secondList.isEmpty()) {
+ return firstList;
+ }
+ return ImmutableList.sortedCopyOf(Comparator.comparingInt(JadeProtocol.priorities::byValue), Iterables.concat(firstList, secondList));
+ });
+ } catch (ExecutionException e) {
+ LeavesLogger.LOGGER.severe(e.getMessage());
+ }
+ return List.of();
+ }
+
+ @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<T> get(Class<?> clazz) {
+ List<T> 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<Map.Entry<Class<?>, Collection<T>>> entries() {
+ return Stream.concat(first.entries(), second.entries());
+ }
+
+ @Override
+ public void invalidate() {
+ first.invalidate();
+ second.invalidate();
+ mergedCache.invalidateAll();
+ }
+
+ @Override
+ public void loadComplete(PriorityStore<ResourceLocation, IJadeProvider> priorityStore) {
+ first.loadComplete(priorityStore);
+ second.loadComplete(priorityStore);
+ }
+}
+
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java
new file mode 100644
index 0000000000000000000000000000000000000000..5e94e10e0feea1bc2f4e0495d4ed05810baa1466
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/PriorityStore.java
@@ -0,0 +1,73 @@
+package org.leavesmc.leaves.protocol.jade.util;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Sets;
+import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
+import it.unimi.dsi.fastutil.objects.Object2IntMap;
+
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.function.ToIntFunction;
+
+public class PriorityStore<K, V> {
+
+ private final Object2IntMap<K> priorities = new Object2IntLinkedOpenHashMap<>();
+ private final Function<V, K> keyGetter;
+ private final ToIntFunction<V> defaultPriorityGetter;
+ private ImmutableList<K> sortedList = ImmutableList.of();
+ private BiFunction<PriorityStore<K, V>, Collection<K>, List<K>> sortingFunction = (store, allKeys) -> allKeys.stream()
+ .sorted(Comparator.comparingInt(store::byKey))
+ .toList();
+
+ public PriorityStore(ToIntFunction<V> defaultPriorityGetter, Function<V, K> keyGetter) {
+ this.defaultPriorityGetter = defaultPriorityGetter;
+ this.keyGetter = keyGetter;
+ }
+
+ public void setSortingFunction(BiFunction<PriorityStore<K, V>, Collection<K>, List<K>> sortingFunction) {
+ this.sortingFunction = sortingFunction;
+ }
+
+ 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 void putUnsafe(K key, int priority) {
+ Objects.requireNonNull(key);
+ priorities.put(key, priority);
+ }
+
+ public void sort(Set<K> extraKeys) {
+ Set<K> allKeys = priorities.keySet();
+ if (!extraKeys.isEmpty()) {
+ allKeys = Sets.union(priorities.keySet(), extraKeys);
+ }
+
+ sortedList = ImmutableList.copyOf(sortingFunction.apply(this, allKeys));
+ }
+
+ public int byValue(V value) {
+ return byKey(keyGetter.apply(value));
+ }
+
+ public int byKey(K id) {
+ return priorities.getInt(id);
+ }
+
+ public ImmutableList<K> getSortedList() {
+ return sortedList;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java
new file mode 100644
index 0000000000000000000000000000000000000000..41dff617e766d013e32a64a1b2b1c434623f65c8
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/ViewGroup.java
@@ -0,0 +1,63 @@
+package org.leavesmc.leaves.protocol.jade.util;
+
+import net.minecraft.nbt.CompoundTag;
+import net.minecraft.nbt.ListTag;
+import org.jetbrains.annotations.Nullable;
+
+import java.util.List;
+import java.util.function.Function;
+
+public class ViewGroup<T> {
+
+ public final List<T> views;
+ @Nullable
+ public String id;
+ @Nullable
+ protected CompoundTag extraData;
+
+ public ViewGroup(List<T> views) {
+ this.views = views;
+ }
+
+ public void save(CompoundTag tag, Function<T, CompoundTag> writer) {
+ ListTag list = new ListTag();
+ for (var view : views) {
+ list.add(writer.apply(view));
+ }
+ tag.put("Views", list);
+ if (id != null) {
+ tag.putString("Id", id);
+ }
+ if (extraData != null) {
+ tag.put("Data", extraData);
+ }
+ }
+
+ public static <T> boolean saveList(CompoundTag tag, String key, List<ViewGroup<T>> groups, Function<T, CompoundTag> writer) {
+ if (groups == null || groups.isEmpty()) {
+ return false;
+ }
+
+ ListTag groupList = new ListTag();
+ for (ViewGroup<T> group : groups) {
+ if (group.views.isEmpty()) {
+ continue;
+ }
+ CompoundTag groupTag = new CompoundTag();
+ group.save(groupTag, writer);
+ groupList.add(groupTag);
+ }
+ if (!groupList.isEmpty()) {
+ tag.put(key, groupList);
+ return true;
+ }
+ return false;
+ }
+
+ public CompoundTag getExtraData() {
+ if (extraData == null) {
+ extraData = new CompoundTag();
+ }
+ return extraData;
+ }
+}
diff --git a/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java b/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java
new file mode 100644
index 0000000000000000000000000000000000000000..eec302b45bedb026e1d3a4595ed99b89b2a64655
--- /dev/null
+++ b/src/main/java/org/leavesmc/leaves/protocol/jade/util/WrappedHierarchyLookup.java
@@ -0,0 +1,98 @@
+package org.leavesmc.leaves.protocol.jade.util;
+
+import com.google.common.collect.Lists;
+import net.minecraft.resources.ResourceLocation;
+import net.minecraft.world.level.block.Block;
+import org.apache.commons.lang3.tuple.Pair;
+import org.jetbrains.annotations.Nullable;
+import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
+import org.leavesmc.leaves.protocol.jade.accessor.RequestAccessor;
+import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+public class WrappedHierarchyLookup<T extends IJadeProvider> extends HierarchyLookup<T> {
+
+ public final List<Pair<IHierarchyLookup<T>, Function<RequestAccessor<?>, @Nullable Object>>> overrides = Lists.newArrayList();
+ private boolean empty = true;
+
+ public WrappedHierarchyLookup() {
+ super(Object.class, true);
+ overrides.add(Pair.of(new HierarchyLookup<>(Block.class, true), accessor -> {
+ if (accessor instanceof BlockAccessor blockAccessor) {
+ return blockAccessor.block();
+ }
+ return null;
+ }));
+ }
+
+ public List<T> get(RequestAccessor<?> accessor) {
+ List<T> list = Lists.newArrayList();
+ for (var override : overrides) {
+ Object o = override.getRight().apply(accessor);
+ if (o != null) {
+ list.addAll(override.getLeft().get(o));
+ }
+ }
+ list.addAll(get(accessor.target()));
+ return list;
+ }
+
+ @Override
+ public void register(Class<?> clazz, T provider) {
+ for (var override : overrides) {
+ if (override.getLeft().isClassAcceptable(clazz)) {
+ override.getLeft().register(clazz, provider);
+ empty = false;
+ return;
+ }
+ }
+ super.register(clazz, provider);
+ empty = false;
+ }
+
+ @Override
+ public boolean isClassAcceptable(Class<?> clazz) {
+ for (var override : overrides) {
+ if (override.getLeft().isClassAcceptable(clazz)) {
+ return true;
+ }
+ }
+ return super.isClassAcceptable(clazz);
+ }
+
+ @Override
+ public void invalidate() {
+ for (var override : overrides) {
+ override.getLeft().invalidate();
+ }
+ super.invalidate();
+ }
+
+ @Override
+ public void loadComplete(PriorityStore<ResourceLocation, IJadeProvider> priorityStore) {
+ for (var override : overrides) {
+ override.getLeft().loadComplete(priorityStore);
+ }
+ super.loadComplete(priorityStore);
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return empty;
+ }
+
+ @Override
+ public Stream<Map.Entry<Class<?>, Collection<T>>> entries() {
+ Stream<Map.Entry<Class<?>, Collection<T>>> stream = super.entries();
+ for (var override : overrides) {
+ stream = Stream.concat(stream, override.getLeft().entries());
+ }
+ return stream;
+ }
+}
+