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/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/top/leavesmc/leaves/protocol/JadeProtocol.java b/src/main/java/top/leavesmc/leaves/protocol/JadeProtocol.java new file mode 100644 index 0000000000000000000000000000000000000000..713a3c2b29de57ad47ffe1f0c591ba9164b8c5ff --- /dev/null +++ b/src/main/java/top/leavesmc/leaves/protocol/JadeProtocol.java @@ -0,0 +1,617 @@ +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 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.CommonComponents; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +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.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.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import top.leavesmc.leaves.LeavesConfig; +import top.leavesmc.leaves.protocol.core.LeavesProtocol; +import top.leavesmc.leaves.protocol.core.ProtocolHandler; +import top.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.Collection; +import java.util.List; +import java.util.Locale; +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; + +@LeavesProtocol(namespace = "jade") +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"); + + 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); + } + + @ProtocolHandler.Init + public static void init() { + entityDataProviders.register(Entity.class, ((data, player, world, entity, showDetails) -> { + UUID ownerUUID = null; + if (entity instanceof OwnableEntity) { + ownerUUID = ((OwnableEntity) 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", Component.Serializer.toJson(getEffectName(effect))); + if (effect.isInfiniteDuration()) { + compound.putBoolean("Infinite", true); + } else { + compound.putInt("Duration", effect.getDuration()); + } + compound.putBoolean("Bad", effect.getEffect().getCategory() == MobEffectCategory.HARMFUL); + list.add(compound); + } + data.put("StatusEffects", 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); + } + } + } + } + })); + } + + @ProtocolHandler.PlayerJoin + public static void onPlayerJoin(ServerPlayer player) { + if (LeavesConfig.jadeProtocol) { + ProtocolUtils.sendPayloadPacket(player, PACKET_SERVER_PING, buf -> buf.writeUtf("{}")); + } + } + + @ProtocolHandler.PayloadReceiver(payload = RequestEntityPayload.class, payloadId = "request_entity") + public static void requestEntityData(ServerPlayer player, RequestEntityPayload payload) { + if (!LeavesConfig.jadeProtocol) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + Level world = player.level(); + boolean showDetails = payload.showDetails; + Entity entity = world.getEntity(payload.entityId); + 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()); + + ProtocolUtils.sendPayloadPacket(player, PACKET_RECEIVE_DATA, buf -> buf.writeNbt(tag)); + }); + } + + @ProtocolHandler.PayloadReceiver(payload = RequestTilePayload.class, payloadId = "request_tile") + public static void requestTileData(ServerPlayer player, RequestTilePayload payload) { + if (!LeavesConfig.jadeProtocol) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + boolean showDetails = payload.showDetails; + BlockHitResult result = payload.hitResult; + 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()); + + ProtocolUtils.sendPayloadPacket(player, PACKET_RECEIVE_DATA, buf -> buf.writeNbt(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 interface IJadeProvider { + void saveData(CompoundTag data, ServerPlayer player, Level world, T object, boolean showDetails); + } + + public record RequestEntityPayload(boolean showDetails, int entityId) implements CustomPacketPayload { + + private static final ResourceLocation PACKET_REQUEST_ENTITY = JadeProtocol.id("request_entity"); + + public RequestEntityPayload(ResourceLocation id, FriendlyByteBuf buf) { + this(buf.readBoolean(), buf.readInt()); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeBoolean(showDetails); + buf.writeInt(entityId); + } + + @Override + @NotNull + public ResourceLocation id() { + return PACKET_REQUEST_ENTITY; + } + } + + public record RequestTilePayload(boolean showDetails, BlockHitResult hitResult) implements CustomPacketPayload { + + private static final ResourceLocation PACKET_REQUEST_TILE = JadeProtocol.id("request_tile"); + + public RequestTilePayload(ResourceLocation id, FriendlyByteBuf buf) { + this(buf.readBoolean(), buf.readBlockHitResult()); + } + + @Override + public void write(FriendlyByteBuf buf) { + buf.writeBoolean(showDetails); + buf.writeBlockHitResult(hitResult); + } + + @Override + @NotNull + public ResourceLocation id() { + return PACKET_REQUEST_TILE; + } + } + + // Power by Jade + + public static Component getEffectName(MobEffectInstance mobEffectInstance) { + MutableComponent mutableComponent = mobEffectInstance.getEffect().getDisplayName().copy(); + if (mobEffectInstance.getAmplifier() >= 1 && mobEffectInstance.getAmplifier() <= 9) { + mutableComponent.append(CommonComponents.SPACE).append(Component.translatable("enchantment.level." + (mobEffectInstance.getAmplifier() + 1))); + } + return mutableComponent; + } + + 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)); + } + } +}