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/server/network/ServerGamePacketListenerImpl.java b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java index 74f74d5f898875a50ee66c279854a0d263f6eccf..066a044e721b82d908d22528141cdc504343e6b5 100644 --- a/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java +++ b/src/main/java/net/minecraft/server/network/ServerGamePacketListenerImpl.java @@ -3564,6 +3564,9 @@ public class ServerGamePacketListenerImpl implements ServerPlayerConnection, Tic if (top.leavesmc.leaves.LeavesConfig.bborProtocol && packet.identifier.equals(top.leavesmc.leaves.protocol.BBORProtocol.SUBSCRIBE)) { top.leavesmc.leaves.protocol.BBORProtocol.onPlayerSubscribed(player); } + if (top.leavesmc.leaves.LeavesConfig.jadeProtocol && ProtocolUtils.isNamespacePacket(packet, top.leavesmc.leaves.protocol.JadeProtocol.PROTOCOL_ID)) { + top.leavesmc.leaves.protocol.JadeProtocol.handlePacket(server, player, packet); + } } catch (Exception ex) { ServerGamePacketListenerImpl.LOGGER.error("Couldn\'t dispatch custom payload", ex); this.disconnect("Invalid custom payload!", org.bukkit.event.player.PlayerKickEvent.Cause.INVALID_PAYLOAD); diff --git a/src/main/java/net/minecraft/server/players/PlayerList.java b/src/main/java/net/minecraft/server/players/PlayerList.java index 666d7e33a26bc973fa87561503f032ec7f61bd0b..52b7d704f1a13e215da586a45da9c554f40f3d86 100644 --- a/src/main/java/net/minecraft/server/players/PlayerList.java +++ b/src/main/java/net/minecraft/server/players/PlayerList.java @@ -358,6 +358,7 @@ public abstract class PlayerList { // Leaves end - bot support top.leavesmc.leaves.protocol.PcaSyncProtocol.onJoin(player); // Leaves - pca top.leavesmc.leaves.protocol.BBORProtocol.onPlayerLoggedIn(player); // Leaves - bbor + top.leavesmc.leaves.protocol.JadeProtocol.onPlayerJoin(player); // Leaves - Jade final net.kyori.adventure.text.Component jm = playerJoinEvent.joinMessage(); 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 4aeab90e778629c355189dfe79c39c4b21f5f5ac..fe8c9b7e7956837829b4fe3eb449b2c093f7cea3 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 @@ -253,7 +253,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/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java index 19b68c4a1807342cbe08420380877680a2519054..9fba7105b7e70ad05c243ee6cec27a8ebe875d83 100644 --- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java +++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java @@ -468,6 +468,7 @@ public final class CraftServer implements Server { } datapackManager = new io.papermc.paper.datapack.PaperDatapackManager(console.getPackRepository()); // Paper top.leavesmc.leaves.protocol.PcaSyncProtocol.init(); // Leaves - pca + top.leavesmc.leaves.protocol.JadeProtocol.init(); // Leaves - Jade } public boolean getCommandBlockOverride(String command) { @@ -1074,6 +1075,11 @@ public final class CraftServer implements Server { top.leavesmc.leaves.protocol.BBORProtocol.loggedOutAllPlayer(); } // Leaves end - bbor + // Leaves start - Jade + if (top.leavesmc.leaves.LeavesConfig.jadeProtocol) { + top.leavesmc.leaves.protocol.JadeProtocol.enableAllPlayer(); + } + // Leaves end - Jade for (ServerLevel world : this.console.getAllLevels()) { // world.serverLevelData.setDifficulty(config.difficulty); // Paper - per level difficulty world.setSpawnSettings(world.serverLevelData.getDifficulty() != Difficulty.PEACEFUL && config.spawnMonsters, config.spawnAnimals); // Paper - per level difficulty (from MinecraftServer#setDifficulty(ServerLevel, Difficulty, boolean)) diff --git a/src/main/java/top/leavesmc/leaves/protocol/JadeProtocol.java b/src/main/java/top/leavesmc/leaves/protocol/JadeProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..2d366c44e1809d5d046017e08542453acc920520 --- /dev/null +++ b/src/main/java/top/leavesmc/leaves/protocol/JadeProtocol.java @@ -0,0 +1,579 @@ +package top.leavesmc.leaves.protocol; + +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.ListMultimap; +import com.google.common.collect.Lists; +import com.mojang.authlib.GameProfile; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.core.registries.BuiltInRegistries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.chat.Component; +import net.minecraft.network.protocol.game.ServerboundCustomPayloadPacket; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.Container; +import net.minecraft.world.LockCode; +import net.minecraft.world.Nameable; +import net.minecraft.world.effect.MobEffectCategory; +import net.minecraft.world.effect.MobEffectInstance; +import net.minecraft.world.entity.AgeableMob; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.OwnableEntity; +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.frog.Tadpole; +import net.minecraft.world.entity.animal.horse.AbstractHorse; +import net.minecraft.world.entity.vehicle.ContainerEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.item.Items; +import net.minecraft.world.level.BaseCommandBlock; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.ChestBlock; +import net.minecraft.world.level.block.ChiseledBookShelfBlock; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.world.level.block.entity.BaseContainerBlockEntity; +import net.minecraft.world.level.block.entity.BeehiveBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.BrewingStandBlockEntity; +import net.minecraft.world.level.block.entity.ChestBlockEntity; +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.EnderChestBlockEntity; +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.RandomizableContainerBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BlockStateProperties; +import net.minecraft.world.phys.BlockHitResult; +import org.apache.commons.lang3.mutable.MutableInt; +import org.apache.logging.log4j.util.TriConsumer; +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import top.leavesmc.leaves.LeavesConfig; +import top.leavesmc.leaves.LeavesLogger; +import top.leavesmc.leaves.util.ProtocolUtils; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.function.Function; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +public class JadeProtocol { + + public static final String PROTOCOL_ID = "jade"; + + // send + public static final ResourceLocation PACKET_SERVER_PING = id("server_ping"); + public static final ResourceLocation PACKET_RECEIVE_DATA = id("receive_data"); + // call + public static final ResourceLocation PACKET_REQUEST_ENTITY = id("request_entity"); + public static final ResourceLocation PACKET_REQUEST_TILE = id("request_tile"); + + private static final HierarchyLookup> entityDataProviders = new HierarchyLookup<>(Entity.class); + private static final HierarchyLookup> tileDataProviders = new HierarchyLookup<>(BlockEntity.class); + + public static final int MAX_DISTANCE_SQR = 900; + + @Contract("_ -> new") + public static @NotNull ResourceLocation id(String path) { + return new ResourceLocation(PROTOCOL_ID, path); + } + + public static void onPlayerJoin(ServerPlayer player) { + if (LeavesConfig.jadeProtocol) { + FriendlyByteBuf buf = new FriendlyByteBuf(Unpooled.buffer()); + buf.writeUtf("{}"); + ProtocolUtils.sendPayloadPacket(player, PACKET_SERVER_PING, buf); + } + } + + public static void enableAllPlayer() { + for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) { + onPlayerJoin(player); + } + } + + private static final Map> globalReceivers = new HashMap<>(); + + private static void registerGlobalReceiver(ResourceLocation identifier, TriConsumer consumer) { + globalReceivers.put(identifier, consumer); + } + + public static void handlePacket(MinecraftServer server, ServerPlayer player, @NotNull ServerboundCustomPayloadPacket packet) { + globalReceivers.getOrDefault(packet.identifier, ((server1, player1, buf) -> LeavesLogger.LOGGER.severe("NullPotion"))).accept(server, player, packet.data); + } + + public static void init() { + entityDataProviders.register(Entity.class, ((data, player, world, entity, showDetails) -> { + UUID ownerUUID = null; + if (entity instanceof OwnableEntity) { + ownerUUID = ((OwnableEntity) entity).getOwnerUUID(); + } else if (entity instanceof AbstractHorse) { + ownerUUID = ((AbstractHorse) entity).getOwnerUUID(); + } + if (ownerUUID != null) { + MinecraftServer.getServer().getProfileCache().get(ownerUUID).map(GameProfile::getName).ifPresent(name -> data.putString("OwnerName", name)); + } + })); + entityDataProviders.register(LivingEntity.class, ((data, player, world, entity, showDetails) -> { + LivingEntity living = (LivingEntity) entity; + Collection effects = living.getActiveEffects(); + if (effects.isEmpty()) { + return; + } + ListTag list = new ListTag(); + for (MobEffectInstance effect : effects) { + CompoundTag compound = new CompoundTag(); + compound.putString("Name", effect.getDescriptionId()); + compound.putInt("Amplifier", effect.getAmplifier()); + int duration = Math.min(32767, effect.getDuration()); + compound.putInt("Duration", duration); + compound.putBoolean("Bad", effect.getEffect().getCategory() == MobEffectCategory.HARMFUL); + list.add(compound); + } + data.put("Potions", list); + })); + entityDataProviders.register(AgeableMob.class, ((data, player, world, entity, showDetails) -> { + int time = -((AgeableMob) entity).getAge(); + if (time > 0) { + data.putInt("GrowingTime", time); + } + })); + entityDataProviders.register(Tadpole.class, ((data, player, world, entity, showDetails) -> { + int time = ((Tadpole) entity).getTicksLeftUntilAdult(); + if (time > 0) { + data.putInt("GrowingTime", time); + } + })); + entityDataProviders.register(Animal.class, ((data, player, world, entity, showDetails) -> { + int time = ((Animal) entity).getAge(); + if (time > 0) { + data.putInt("BreedingCD", time); + } + })); + entityDataProviders.register(Allay.class, ((data, player, world, entity, showDetails) -> { + int time = 0; + Allay allay = (Allay) entity; + if (allay.duplicationCooldown > 0 && allay.duplicationCooldown < Integer.MAX_VALUE) { + time = (int) allay.duplicationCooldown; + } + if (time > 0) { + data.putInt("BreedingCD", time); + } + })); + entityDataProviders.register(Chicken.class, ((data, player, world, entity, showDetails) -> { + data.putInt("NextEgg", ((Chicken) entity).eggTime / 20); + })); + entityDataProviders.register(Entity.class, ((tag, player, world, object, showDetails) -> { + var groups = getGroups(player, (ServerLevel) world, object, showDetails); + if (groups != null) { + if (ViewGroup.saveList(tag, "JadeItemStorage", groups, item -> { + CompoundTag itemTag = new CompoundTag(); + int count = item.getCount(); + if (count > 64) item.setCount(1); + item.save(itemTag); + if (count > 64) itemTag.putInt("NewCount", count); + return itemTag; + })) { + tag.putString("JadeItemStorageUid", EntityType.getKey(object.getType()).toString()); + } else { + if (object instanceof ContainerEntity containerEntity && containerEntity.getLootTable() != null) { + tag.putBoolean("Loot", true); + } + } + } + })); + + tileDataProviders.register(BrewingStandBlockEntity.class, ((data, player, world, object, showDetails) -> { + if (object instanceof BrewingStandBlockEntity brewingStand) { + CompoundTag compound = new CompoundTag(); + compound.putInt("Time", brewingStand.brewTime); + compound.putInt("Fuel", brewingStand.fuel); + data.put("BrewingStand", compound); + } + })); + tileDataProviders.register(BeehiveBlockEntity.class, ((data, player, world, object, showDetails) -> { + data.getAllKeys().clear(); + BeehiveBlockEntity beehive = (BeehiveBlockEntity) object; + data.putByte("Bees", (byte) beehive.getOccupantCount()); + data.putBoolean("Full", beehive.isFull()); + })); + tileDataProviders.register(CommandBlockEntity.class, ((data, player, world, object, showDetails) -> { + if (object == null || !player.canUseGameMasterBlocks()) { + return; + } + BaseCommandBlock logic = ((CommandBlockEntity) object).getCommandBlock(); + String command = logic.getCommand(); + if (command.isEmpty()) { + return; + } + if (command.length() > 40) { + command = command.substring(0, 37) + "..."; + } + data.putString("Command", command); + })); + tileDataProviders.register(JukeboxBlockEntity.class, ((data, player, world, object, showDetails) -> { + if (object instanceof JukeboxBlockEntity jukebox) { + ItemStack stack = jukebox.getFirstItem(); + if (!stack.isEmpty()) { + data.put("Record", stack.save(new CompoundTag())); + } + } + })); + tileDataProviders.register(LecternBlockEntity.class, ((data, player, world, object, showDetails) -> { + ItemStack stack = ((LecternBlockEntity) object).getBook(); + if (!stack.isEmpty()) { + if (stack.hasCustomHoverName() || stack.getItem() != Items.WRITABLE_BOOK) { + data.put("Book", stack.save(new CompoundTag())); + } + } + })); + tileDataProviders.register(ComparatorBlockEntity.class, ((data, player, world, object, showDetails) -> { + data.putInt("Signal", ((ComparatorBlockEntity) object).getOutputSignal()); + })); + tileDataProviders.register(HopperBlockEntity.class, ((data, player, world, object, showDetails) -> { + BlockState state = object.getBlockState(); + if (state.hasProperty(BlockStateProperties.ENABLED) && !state.getValue(BlockStateProperties.ENABLED)) { + data.putBoolean("HopperLocked", true); + } + })); + tileDataProviders.register(AbstractFurnaceBlockEntity.class, ((data, player, world, object, showDetails) -> { + AbstractFurnaceBlockEntity furnace = (AbstractFurnaceBlockEntity) object; + ListTag items = new ListTag(); + for (int i = 0; i < 3; i++) { + items.add(furnace.getItem(i).save(new CompoundTag())); + } + data.put("furnace", items); + CompoundTag furnaceTag = furnace.saveWithoutMetadata(); + data.putInt("progress", furnaceTag.getInt("CookTime")); + data.putInt("total", furnaceTag.getInt("CookTimeTotal")); + })); + tileDataProviders.register(BlockEntity.class, ((data, player, world, object, showDetails) -> { + if (object instanceof Nameable nameable) { + if (nameable.hasCustomName()) { + data.putString("givenName", Component.Serializer.toJson(nameable.getCustomName())); + } + } + })); + tileDataProviders.register(ChiseledBookShelfBlockEntity.class, ((data, player, world, object, showDetails) -> { + ChiseledBookShelfBlockEntity bookShelf = (ChiseledBookShelfBlockEntity) object; + if (!bookShelf.isEmpty()) { + data.put("Bookshelf", bookShelf.saveWithoutMetadata()); + } + })); + tileDataProviders.register(BlockEntity.class, ((tag, player, world, object, showDetails) -> { + if (object instanceof AbstractFurnaceBlockEntity) { + return; + } + var groups = getGroups(player, (ServerLevel) world, object, showDetails); + if (groups != null) { + if (ViewGroup.saveList(tag, "JadeItemStorage", groups, item -> { + CompoundTag itemTag = new CompoundTag(); + int count = item.getCount(); + if (count > 64) item.setCount(1); + item.save(itemTag); + if (count > 64) itemTag.putInt("NewCount", count); + return itemTag; + })) { + tag.putString("JadeItemStorageUid", BlockEntityType.getKey(object.getType()).toString()); + } else { + if (object instanceof RandomizableContainerBlockEntity te && te.lootTable != null) { + tag.putBoolean("Loot", true); + return; + } + if (!player.isCreative() && !player.isSpectator() && object instanceof BaseContainerBlockEntity te) { + if (te.lockKey != LockCode.NO_LOCK) { + tag.putBoolean("Locked", true); + } + } + } + } + })); + + registerGlobalReceiver(PACKET_REQUEST_TILE, JadeProtocol::requestTileData); + registerGlobalReceiver(PACKET_REQUEST_ENTITY, JadeProtocol::requestEntityData); + } + + public static void requestEntityData(MinecraftServer server, ServerPlayer player, FriendlyByteBuf buf) { + if (!LeavesConfig.jadeProtocol) { + return; + } + + Level world = player.level(); + boolean showDetails = buf.readBoolean(); + Entity entity = world.getEntity(buf.readVarInt()); + if (entity == null || player.distanceToSqr(entity) > MAX_DISTANCE_SQR) { + return; + } + + server.execute(() -> { + var providers = entityDataProviders.get(entity); + if (providers.isEmpty()) { + return; + } + + CompoundTag tag = new CompoundTag(); + for (IJadeProvider provider : providers) { + try { + provider.saveData(tag, player, world, entity, showDetails); + } catch (Exception e) { + e.printStackTrace(); + } + } + tag.putInt("WailaEntityID", entity.getId()); + + FriendlyByteBuf buf1 = new FriendlyByteBuf(Unpooled.buffer()); + buf1.writeNbt(tag); + ProtocolUtils.sendPayloadPacket(player, PACKET_RECEIVE_DATA, buf1); + }); + } + + public static void requestTileData(MinecraftServer server, ServerPlayer player, FriendlyByteBuf buf) { + if (!LeavesConfig.jadeProtocol) { + return; + } + + boolean showDetails = buf.readBoolean(); + BlockHitResult result = buf.readBlockHitResult(); + BlockPos pos = result.getBlockPos(); + Level world = player.level(); + if (pos.distSqr(player.blockPosition()) > MAX_DISTANCE_SQR || !world.isLoaded(pos)) { + return; + } + + server.execute(() -> { + BlockEntity tile = world.getBlockEntity(pos); + if (tile == null) return; + + List> providers = tileDataProviders.get(tile); + if (providers.isEmpty()) { + return; + } + + CompoundTag tag = new CompoundTag(); + for (IJadeProvider provider : providers) { + try { + provider.saveData(tag, player, world, tile, showDetails); + } catch (Exception e) { + e.printStackTrace(); + } + } + + tag.putInt("x", pos.getX()); + tag.putInt("y", pos.getY()); + tag.putInt("z", pos.getZ()); + tag.putString("id", BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(tile.getType()).toString()); + + FriendlyByteBuf buf1 = new FriendlyByteBuf(Unpooled.buffer()); + buf1.writeNbt(tag); + ProtocolUtils.sendPayloadPacket(player, PACKET_RECEIVE_DATA, buf1); + }); + + } + + public interface IJadeProvider { + public void saveData(CompoundTag data, ServerPlayer player, Level world, T object, boolean showDetails); + } + + // Power by Jade + + public static List> getGroups(ServerPlayer player, ServerLevel world, Object target, boolean showDetails) { + if (target instanceof RandomizableContainerBlockEntity te && te.lootTable != null) { + return List.of(); + } + if (target instanceof ContainerEntity containerEntity && containerEntity.getLootTable() != null) { + return List.of(); + } + if (!player.isCreative() && !player.isSpectator() && target instanceof BaseContainerBlockEntity te) { + if (te.lockKey != LockCode.NO_LOCK) { + return List.of(); + } + } + + return wrapItemStorage(target, player); + } + + public static List> wrapItemStorage(Object target, ServerPlayer player) { + int size = 54; + if (target instanceof AbstractHorse horse) { + return List.of(fromContainer(horse.inventory, size, 2)); + } + if (target instanceof Container container) { + if (target instanceof ChestBlockEntity be) { + if (be.getBlockState().getBlock() instanceof ChestBlock chestBlock) { + Container compound = ChestBlock.getContainer(chestBlock, be.getBlockState(), be.getLevel(), be.getBlockPos(), false); + if (compound != null) { + container = compound; + } + } + } + return List.of(fromContainer(container, size, 0)); + } + if (player != null && target instanceof EnderChestBlockEntity) { + return List.of(fromContainer(player.getEnderChestInventory(), size, 0)); + } + return null; + } + + public static ViewGroup fromContainer(Container container, int maxSize, int startIndex) { + return compacted(IntStream.range(startIndex, container.getContainerSize()).limit(maxSize * 3L).mapToObj(container::getItem), maxSize); + } + + public static ViewGroup compacted(Stream stream, int maxSize) { + List stacks = Lists.newArrayList(); + MutableInt start = new MutableInt(); + stream.filter(stack -> !stack.isEmpty()).filter(stack -> { + if (stack.hasTag() && stack.getTag().contains("CustomModelData")) { + for (String key : stack.getTag().getAllKeys()) { + if (key.toLowerCase(Locale.ENGLISH).endsWith("clear") && stack.getTag().getBoolean(key)) { + return false; + } + } + } + return true; + }).forEach(stack -> { + int size = stacks.size(); + if (size > maxSize) return; + for (int i = 0; i < size; i++) { + int j = (i + start.intValue()) % size; + if (ItemStack.isSameItemSameTags(stack, stacks.get(j))) { + stacks.get(j).grow(stack.getCount()); + start.setValue(j); + return; + } + } + stacks.add(stack.copy()); + }); + if (stacks.size() > maxSize) stacks.remove(maxSize); + return new ViewGroup<>(stacks); + } + + public static class ViewGroup { + + public final List views; + @Nullable + public String id; + @Nullable + protected CompoundTag extraData; + + public ViewGroup(List views) { + this.views = views; + } + + public void save(CompoundTag tag, Function 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 boolean saveList(CompoundTag tag, String key, List> groups, Function writer) { + if (groups == null || groups.isEmpty()) return false; + ListTag groupList = new ListTag(); + for (ViewGroup 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; + } + + public void setProgress(float progress) { + getExtraData().putFloat("Progress", progress); + } + } + + public static class HierarchyLookup> { + + private final Class baseClass; + private final ListMultimap, T> objects = ArrayListMultimap.create(); + private final Cache, List> resultCache = CacheBuilder.newBuilder().build(); + private final boolean singleton; + + public HierarchyLookup(Class baseClass) { + this(baseClass, false); + } + + public HierarchyLookup(Class baseClass, boolean singleton) { + this.baseClass = baseClass; + this.singleton = singleton; + } + + public void register(Class clazz, T provider) { + Objects.requireNonNull(clazz); + objects.put(clazz, provider); + } + + public List get(Object obj) { + if (obj == null) { + return List.of(); + } + return get(obj.getClass()); + } + + public List get(Class clazz) { + try { + return resultCache.get(clazz, () -> { + List list = Lists.newArrayList(); + getInternal(clazz, list); + if (singleton && !list.isEmpty()) return ImmutableList.of(list.get(0)); + return list; + }); + } catch (ExecutionException e) { + e.printStackTrace(); + } + return List.of(); + } + + private void getInternal(Class clazz, List list) { + if (clazz != baseClass && clazz != Object.class) { + getInternal(clazz.getSuperclass(), list); + } + list.addAll(objects.get(clazz)); + } + } + +}