diff --git a/.gitignore b/.gitignore index d5f737e..eff8b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,7 @@ runs/ # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) !gradle-wrapper.jar + +#OMO +.omo + diff --git a/src/main/java/com/ctnhlang/IgnoreLang.java b/src/main/java/com/ctnhlang/IgnoreLang.java index de8141e..42ac057 100644 --- a/src/main/java/com/ctnhlang/IgnoreLang.java +++ b/src/main/java/com/ctnhlang/IgnoreLang.java @@ -7,5 +7,4 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -public @interface IgnoreLang { -} +public @interface IgnoreLang {} diff --git a/src/main/java/com/ctnhlang/langprovider/LangKeyBuilder.java b/src/main/java/com/ctnhlang/langprovider/LangKeyBuilder.java index ba7ee1b..91b670b 100644 --- a/src/main/java/com/ctnhlang/langprovider/LangKeyBuilder.java +++ b/src/main/java/com/ctnhlang/langprovider/LangKeyBuilder.java @@ -93,7 +93,8 @@ private static ClassMetadata fromClass(Class ownerClass) { return new ClassMetadata( domain != null ? domain.value() : "", "", - resolveCategory(domain != null ? domain.value() : "", category != null ? category.value() : "", ownerClass.getSimpleName()), + resolveCategory(domain != null ? domain.value() : "", category != null ? category.value() : "", + ownerClass.getSimpleName()), prefix != null ? resolveAffix(prefix.value(), ownerClass.getSimpleName()) : "", suffix != null ? resolveAffix(suffix.value(), ownerClass.getSimpleName()) : ""); } @@ -119,7 +120,8 @@ private static ClassMetadata fromClassNode(ClassNode classNode) { } } - return new ClassMetadata(domain, root, resolveCategory(domain, category, simpleName(classNode.name)), prefix, suffix); + return new ClassMetadata(domain, root, resolveCategory(domain, category, simpleName(classNode.name)), prefix, + suffix); } private static String getExplicitKey(Field field) { @@ -158,9 +160,7 @@ private static String resolveCategory(String domain, String explicitCategory, St } private static String resolveAffix(String explicitValue, String className) { - return explicitValue == null || explicitValue.isEmpty() - ? className.toLowerCase(Locale.ROOT) - : explicitValue; + return explicitValue == null || explicitValue.isEmpty() ? className.toLowerCase(Locale.ROOT) : explicitValue; } private static String simpleName(String internalName) { diff --git a/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommandChatHelper.java b/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommandChatHelper.java new file mode 100644 index 0000000..0d54c4a --- /dev/null +++ b/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommandChatHelper.java @@ -0,0 +1,195 @@ +package tech.vixhentx.mcmod.ctnhlib.command; + +import net.minecraft.ChatFormatting; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.MutableComponent; +import net.minecraft.network.chat.Style; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * 用于构建 /ctnh 检查命令的点击复制聊天行的辅助工具。 + * 每行输出的文本在点击时会复制对应值内容,并显示悬浮提示。 + */ +public final class CTNHCommandChatHelper { + + /** 单个聊天行值的最大长度,超过该长度将拆分为多个块。 */ + public static final int MAX_VALUE_LINE_LENGTH = 256; + + private CTNHCommandChatHelper() {} + + /** + * 为带标签的值构建一个或多个可点击的聊天行。长值将被拆分为 + * 额外的可复制续行,以避免聊天窗口静默丢弃内容。 + * + * @param label 本地化标签 + * @param value 原始值字符串(不能为 null) + * @return 一个或多个可发送的 {@link Component} 行 + */ + public static List labeledLines(Component label, String value) { + return labeledLines(label, value, ChatFormatting.WHITE); + } + + /** + * 为带标签的值构建一个或多个可点击的聊天行,并用指定颜色显示值部分。 + * 长值将被拆分为额外的可复制续行,以避免聊天窗口静默丢弃内容。 + * + * @param label 本地化标签 + * @param value 原始值字符串(不能为 null) + * @param valueColor 值文本颜色 + * @return 一个或多个可发送的 {@link Component} 行 + */ + public static List labeledLines(Component label, String value, ChatFormatting valueColor) { + return labeledLines(label, value, value, valueColor); + } + + /** + * 为带标签的值构建一个或多个可点击的聊天行,并允许显示值和复制值不同。 + */ + public static List labeledLines(Component label, String value, String copyText, + ChatFormatting valueColor) { + List lines = new ArrayList<>(); + String safe = value == null ? "" : value; + String safeCopy = copyText == null ? safe : copyText; + if (safe.length() <= MAX_VALUE_LINE_LENGTH) { + lines.add(buildLine(label, safe, safeCopy, valueColor)); + return lines; + } + int total = safe.length(); + int chunkIndex = 0; + for (int start = 0; start < total; start += MAX_VALUE_LINE_LENGTH) { + int end = Math.min(total, start + MAX_VALUE_LINE_LENGTH); + String chunk = safe.substring(start, end); + if (chunkIndex == 0) { + lines.add(buildLine(label.copy(), chunk, chunk, valueColor)); + } else { + lines.add(buildContinuationLine(label, chunk, chunk, valueColor, + Component.translatable("command.ctnhlib.copy.hover"))); + } + chunkIndex++; + } + // 尾部摘要行(不可复制,仅用于提示信息)。 + lines.add(Component.translatable("command.ctnhlib.value.truncated", total) + .withStyle(ChatFormatting.GRAY)); + return lines; + } + + /** 为多个标签 ID 构建逐行显示的可复制聊天行,每行点击时仅复制标签 ID。 */ + public static List labeledTagLines(Component label, List tagIds, ChatFormatting tagColor) { + return labeledTagLines(label, tagIds, tagColor, + ignored -> Component.translatable("command.ctnhlib.copy.hover")); + } + + /** 为多个标签 ID 构建逐行显示的可复制聊天行,并允许每行使用自定义悬浮提示。 */ + public static List labeledTagLines(Component label, + List tagIds, + ChatFormatting tagColor, + Function hoverFactory) { + List lines = new ArrayList<>(); + if (tagIds == null || tagIds.isEmpty()) { + lines.addAll(labeledLines(label, + Component.translatable("command.ctnhlib.value.empty").getString(), + tagColor)); + return lines; + } + for (int i = 0; i < tagIds.size(); i++) { + String tagId = tagIds.get(i); + String safe = tagId == null ? "" : tagId; + Component hover = hoverFactory == null ? Component.translatable("command.ctnhlib.copy.hover") : + hoverFactory.apply(safe); + if (i == 0) { + lines.add(buildLine(label.copy(), safe, safe, tagColor, hover)); + } else { + lines.add(buildContinuationLine(label, safe, safe, tagColor, hover)); + } + } + return lines; + } + + /** + * 构建单个聊天行,其可见文本为 "label: value",单击事件将逐字复制提供的 {@code copyText}。 + */ + public static Component buildLine(Component label, String value, String copyText) { + return buildLine(label, value, copyText, ChatFormatting.WHITE); + } + + /** + * 构建单个聊天行,其可见文本为 "label: value",单击事件将逐字复制提供的 {@code copyText}。 + */ + public static Component buildLine(Component label, String value, String copyText, ChatFormatting valueColor) { + return buildLine(label, value, copyText, valueColor, Component.translatable("command.ctnhlib.copy.hover")); + } + + /** + * 构建单个聊天行,其可见文本为 "label: value",并使用自定义悬浮提示。 + */ + public static Component buildLine(Component label, + String value, + String copyText, + ChatFormatting valueColor, + Component hoverText) { + MutableComponent labelStyled = label.copy().withStyle(ChatFormatting.AQUA); + MutableComponent valueStyled = Component.literal(value).withStyle(valueColor); + MutableComponent line = Component.empty() + .append(labelStyled) + .append(Component.literal(": ").withStyle(ChatFormatting.GRAY)) + .append(valueStyled); + + Style style = Style.EMPTY + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, copyText)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverText)); + return line.withStyle(style); + } + + /** + * 构建续行(无标签和冒号),仅显示与首行值列对齐的值部分。 + * 使用空格填充来匹配首行 "label: " 的宽度,然后仅追加带颜色的值。 + */ + private static Component buildContinuationLine(Component label, String value, String copyText, + ChatFormatting valueColor, Component hoverText) { + int pad = label.getString().length() + 2; // "label: " + MutableComponent spacer = Component.literal(" ".repeat(Math.max(0, pad))); + MutableComponent valueStyled = Component.literal(value).withStyle(valueColor); + MutableComponent line = Component.empty().append(spacer).append(valueStyled); + + Style style = Style.EMPTY + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, copyText)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverText)); + return line.withStyle(style); + } + + /** 分区标题行(无复制点击事件)。 */ + public static Component heading(Component component) { + return component.copy().withStyle(ChatFormatting.GOLD); + } + + /** 简单的信息提示行,例如"未包含流体",无复制事件。 */ + public static Component info(Component component) { + return component.copy().withStyle(ChatFormatting.GRAY); + } + + /** 命令失败时使用的红色错误行。 */ + public static Component error(Component component) { + return component.copy().withStyle(ChatFormatting.RED); + } + + /** + * 构建单个点击复制聊天行,其可见文本为提供的 + * {@code visible} 组件(无自动标签前缀),点击事件将复制提供的 {@code copyText}。 + */ + public static Component clickableLine(MutableComponent visible, String copyText) { + return clickableLine(visible, copyText, Component.translatable("command.ctnhlib.copy.hover")); + } + + /** 使用自定义悬浮提示构建无标签点击复制聊天行。 */ + public static Component clickableLine(MutableComponent visible, String copyText, Component hoverText) { + Style style = Style.EMPTY + .withClickEvent(new ClickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, copyText)) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, hoverText)); + return visible.withStyle(style); + } +} diff --git a/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommandInspector.java b/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommandInspector.java new file mode 100644 index 0000000..49c0572 --- /dev/null +++ b/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommandInspector.java @@ -0,0 +1,233 @@ +package tech.vixhentx.mcmod.ctnhlib.command; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.core.Holder; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.tags.TagKey; +import net.minecraft.world.item.BlockItem; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.material.Fluid; +import net.minecraftforge.fluids.FluidStack; +import net.minecraftforge.fluids.FluidUtil; +import net.minecraftforge.fluids.capability.IFluidHandler; +import net.minecraftforge.fluids.capability.IFluidHandlerItem; +import net.minecraftforge.registries.ForgeRegistries; + +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * 用于 /ctnh 检查命令的只读工具辅助类。集中处理物品、方块和流体的 + * 资源 ID/名称/NBT/标签提取,以及通过 {@link RegistryAccess} 进行标签成员 + * 枚举,从而在运行时尊重数据包变更。 + */ +public final class CTNHCommandInspector { + + private CTNHCommandInspector() {} + + /** 描述手持容器中单个流体的轻量级负载数据结构。 */ + public record FluidEntry(FluidStack stack) { + + public Fluid fluid() { + return stack.getFluid(); + } + + public Component displayName() { + return stack.getDisplayName(); + } + + public ResourceLocation id() { + ResourceLocation key = ForgeRegistries.FLUIDS.getKey(stack.getFluid()); + return key != null ? key : ResourceLocation.tryBuild("minecraft", "empty"); + } + + public int amount() { + return stack.getAmount(); + } + + @Nullable + public CompoundTag tag() { + return stack.getTag(); + } + + public List tags() { + List out = new ArrayList<>(); + stack.getFluid().builtInRegistryHolder().tags().forEach(t -> out.add(t.location())); + Collections.sort(out, (a, b) -> a.toString().compareTo(b.toString())); + return out; + } + } + + /** 物品注册 ID,缺失时回退为 {@code minecraft:air}。 */ + public static ResourceLocation itemId(ItemStack stack) { + ResourceLocation id = ForgeRegistries.ITEMS.getKey(stack.getItem()); + return id != null ? id : ResourceLocation.tryBuild("minecraft", "air"); + } + + /** {@link BlockItem} 的方块注册 ID,如果该物品栈不是方块物品则返回 null。 */ + @Nullable + public static ResourceLocation blockId(ItemStack stack) { + if (stack.getItem() instanceof BlockItem blockItem) { + Block block = blockItem.getBlock(); + return ForgeRegistries.BLOCKS.getKey(block); + } + return null; + } + + /** 附加到物品栈物品上的已排序物品标签 ID。 */ + public static List itemTags(ItemStack stack) { + List out = new ArrayList<>(); + stack.getItem().builtInRegistryHolder().tags().forEach(t -> out.add(t.location())); + Collections.sort(out, (a, b) -> a.toString().compareTo(b.toString())); + return out; + } + + /** 附加到 {@link BlockItem} 背后方块上的已排序方块标签 ID。 */ + public static List blockTags(ItemStack stack) { + if (!(stack.getItem() instanceof BlockItem blockItem)) { + return Collections.emptyList(); + } + List out = new ArrayList<>(); + blockItem.getBlock().builtInRegistryHolder().tags().forEach(t -> out.add(t.location())); + Collections.sort(out, (a, b) -> a.toString().compareTo(b.toString())); + return out; + } + + /** + * 枚举物品栈中包含的所有非空流体。多槽容器逐槽遍历其 + * {@link IFluidHandlerItem};单流体物品回退到 + * {@link FluidUtil#getFluidContained(ItemStack)} 以保证兼容性。 + */ + public static List fluidsIn(ItemStack stack) { + if (stack.isEmpty()) { + return Collections.emptyList(); + } + Optional handlerOpt = stack + .getCapability(net.minecraftforge.common.capabilities.ForgeCapabilities.FLUID_HANDLER_ITEM) + .resolve(); + List entries = new ArrayList<>(); + if (handlerOpt.isPresent()) { + IFluidHandler handler = handlerOpt.get(); + int tanks = handler.getTanks(); + for (int i = 0; i < tanks; i++) { + FluidStack fluid = handler.getFluidInTank(i); + if (fluid != null && !fluid.isEmpty()) { + entries.add(new FluidEntry(fluid.copy())); + } + } + if (!entries.isEmpty()) { + return entries; + } + } + FluidUtil.getFluidContained(stack) + .filter(f -> !f.isEmpty()) + .ifPresent(f -> entries.add(new FluidEntry(f.copy()))); + return entries; + } + + /** 将给定的检查类型解析为对应的 {@code ResourceKey>}。 */ + public static ResourceKey> registryKeyFor(InspectType type) { + return switch (type) { + case ITEM -> Registries.ITEM; + case BLOCK -> Registries.BLOCK; + case FLUID -> Registries.FLUID; + }; + } + + /** + * 根据命令源暴露的运行时注册表解析标签。如果标签存在则返回 + * holder set,否则返回 {@link Optional#empty()}。 + */ + @SuppressWarnings({ "unchecked", "rawtypes" }) + public static Optional> resolveTag(CommandSourceStack source, + InspectType type, + ResourceLocation tagId) { + ResourceKey registryKey = registryKeyFor(type); + RegistryAccess access = source.registryAccess(); + Optional> registryOpt = access.registry(registryKey); + if (registryOpt.isEmpty()) { + return Optional.empty(); + } + Registry registry = registryOpt.get(); + TagKey tagKey = TagKey.create(registryKey, tagId); + return registry.getTag(tagKey).map(set -> (HolderSet.Named) set); + } + + /** 按迭代顺序返回标签成员,映射为(显示名称,注册 ID)对。 */ + public static List listTagMembers(InspectType type, HolderSet.Named set) { + List result = new ArrayList<>(); + for (Holder holder : set) { + Object value = holder.value(); + ResourceLocation id = holder.unwrapKey().map(ResourceKey::location).orElse(null); + if (id == null) { + continue; + } + Component name = displayNameFor(type, value, id); + result.add(new TagMember(id, name)); + } + return result; + } + + private static Component displayNameFor(InspectType type, Object value, ResourceLocation id) { + return switch (type) { + case ITEM -> { + if (value instanceof net.minecraft.world.item.Item item) { + yield Component.translatable(item.getDescriptionId()); + } + yield Component.literal(id.toString()); + } + case BLOCK -> { + if (value instanceof Block block) { + yield Component.translatable(block.getDescriptionId()); + } + yield Component.literal(id.toString()); + } + case FLUID -> { + if (value instanceof Fluid fluid) { + yield new FluidStack(fluid, 1).getDisplayName(); + } + yield Component.literal(id.toString()); + } + }; + } + + /** {@link #listTagMembers(InspectType, HolderSet.Named)} 的结果条目。 */ + public record TagMember(ResourceLocation id, Component displayName) {} + + /** 将 CompoundTag 格式化为适合聊天显示的字符串(单行 SNBT 形式)。 */ + public static String prettyNbt(@Nullable CompoundTag tag) { + if (tag == null || tag.isEmpty()) { + return ""; + } + return tag.toString(); + } + + /** 检查目标类型。 */ + + public enum InspectType { + + ITEM, + BLOCK, + FLUID; + + public String registryDisplay() { + return switch (this) { + case ITEM -> "minecraft:item"; + case BLOCK -> "minecraft:block"; + case FLUID -> "minecraft:fluid"; + }; + } + } +} diff --git a/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommands.java b/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommands.java new file mode 100644 index 0000000..7a2c629 --- /dev/null +++ b/src/main/java/tech/vixhentx/mcmod/ctnhlib/command/CTNHCommands.java @@ -0,0 +1,309 @@ +package tech.vixhentx.mcmod.ctnhlib.command; + +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandBuildContext; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.SharedSuggestionProvider; +import net.minecraft.commands.arguments.ResourceLocationArgument; +import net.minecraft.core.HolderSet; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.item.ItemStack; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.builder.LiteralArgumentBuilder; +import com.mojang.brigadier.context.CommandContext; +import com.mojang.brigadier.suggestion.SuggestionProvider; +import tech.vixhentx.mcmod.ctnhlib.command.CTNHCommandInspector.FluidEntry; +import tech.vixhentx.mcmod.ctnhlib.command.CTNHCommandInspector.InspectType; +import tech.vixhentx.mcmod.ctnhlib.command.CTNHCommandInspector.TagMember; + +import java.util.List; +import java.util.Optional; + +import static net.minecraft.commands.Commands.argument; +import static net.minecraft.commands.Commands.literal; + +/** + * Brigadier 注册入口,用于 {@code /ctnh} 检查命令。 + * 权限等级为 0,因此所有玩家均可执行。 + */ +public final class CTNHCommands { + + private static final ChatFormatting NAME_COLOR = ChatFormatting.AQUA; + private static final ChatFormatting RESOURCE_ID_COLOR = ChatFormatting.GREEN; + private static final ChatFormatting COUNT_COLOR = ChatFormatting.YELLOW; + private static final ChatFormatting NBT_COLOR = ChatFormatting.LIGHT_PURPLE; + private static final ChatFormatting TAG_COLOR = ChatFormatting.DARK_GREEN; + private static final ChatFormatting MOD_COLOR = ChatFormatting.DARK_AQUA; + + /** 根据请求的检查类型,建议已知的物品/方块/流体标签 ID。 */ + private static final SuggestionProvider SUGGEST_TAGS = (ctx, builder) -> { + InspectType type = parseInspectType(ctx); + if (type == null) { + return builder.buildFuture(); + } + RegistryAccess access = ctx.getSource().registryAccess(); + ResourceKey> registryKey = CTNHCommandInspector.registryKeyFor(type); + Optional> registry = access.registry(registryKey); + registry.ifPresent(r -> SharedSuggestionProvider.suggestResource( + r.getTagNames().map(t -> t.location()), builder)); + return builder.buildFuture(); + }; + + private CTNHCommands() {} + + public static void register(CommandDispatcher dispatcher, + @SuppressWarnings("unused") CommandBuildContext buildContext) { + LiteralArgumentBuilder root = literal("ctnh") + .requires(src -> src.hasPermission(0)) + .then(literal("hand") + .executes(CTNHCommands::executeHand)) + .then(literal("showtag") + .then(literal("item") + .then(argument("tag", ResourceLocationArgument.id()) + .suggests(SUGGEST_TAGS) + .executes(ctx -> executeShowTag(ctx, InspectType.ITEM)))) + .then(literal("block") + .then(argument("tag", ResourceLocationArgument.id()) + .suggests(SUGGEST_TAGS) + .executes(ctx -> executeShowTag(ctx, InspectType.BLOCK)))) + .then(literal("fluid") + .then(argument("tag", ResourceLocationArgument.id()) + .suggests(SUGGEST_TAGS) + .executes(ctx -> executeShowTag(ctx, InspectType.FLUID))))); + dispatcher.register(root); + } + + private static InspectType parseInspectType(CommandContext ctx) { + String input = ctx.getInput(); + if (input.contains(" item ")) return InspectType.ITEM; + if (input.contains(" block ")) return InspectType.BLOCK; + if (input.contains(" fluid ")) return InspectType.FLUID; + return null; + } + + // ---- /ctnh hand 手持物品检查 ---------------------------------------------------- + + private static int executeHand(CommandContext ctx) { + CommandSourceStack source = ctx.getSource(); + ServerPlayer player = source.getPlayer(); + if (player == null) { + source.sendFailure(CTNHCommandChatHelper.error( + Component.translatable("command.ctnhlib.error.player_only"))); + return 0; + } + ItemStack stack = player.getItemInHand(InteractionHand.MAIN_HAND); + if (stack.isEmpty()) { + source.sendFailure(CTNHCommandChatHelper.error( + Component.translatable("command.ctnhlib.error.empty_hand"))); + return 0; + } + sendHandReport(player, stack); + return 1; + } + + private static void sendHandReport(ServerPlayer player, ItemStack stack) { + ResourceLocation itemId = CTNHCommandInspector.itemId(stack); + Component itemName = stack.getHoverName(); + // 物品信息 + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.item_name"), + itemName.getString(), + NAME_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.item_id"), + itemId.toString(), + RESOURCE_ID_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.item_mod"), + modDisplay(itemId), + itemId.getNamespace(), + MOD_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.item_count"), + String.valueOf(stack.getCount()), + COUNT_COLOR); + + CompoundTag tag = stack.getTag(); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.item_nbt"), + CTNHCommandInspector.prettyNbt(tag), + NBT_COLOR); + + CommandSourceStack source = player.createCommandSourceStack(); + sendTagLines(player, + source, + Component.translatable("command.ctnhlib.hand.item_tags"), + CTNHCommandInspector.itemTags(stack), + InspectType.ITEM); + + // 方块信息(如适用) + ResourceLocation blockId = CTNHCommandInspector.blockId(stack); + if (blockId != null) { + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.block_name"), + Component.translatable(stack.getItem().getDescriptionId()).getString(), + NAME_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.block_id"), + blockId.toString(), + RESOURCE_ID_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.block_mod"), + modDisplay(blockId), + blockId.getNamespace(), + MOD_COLOR); + sendTagLines(player, + source, + Component.translatable("command.ctnhlib.hand.block_tags"), + CTNHCommandInspector.blockTags(stack), + InspectType.BLOCK); + } + + // 流体信息(如适用) + List fluids = CTNHCommandInspector.fluidsIn(stack); + if (fluids.isEmpty()) { + player.sendSystemMessage(CTNHCommandChatHelper.info( + Component.translatable("command.ctnhlib.hand.no_fluid"))); + return; + } + for (int i = 0; i < fluids.size(); i++) { + FluidEntry entry = fluids.get(i); + player.sendSystemMessage(CTNHCommandChatHelper.heading( + Component.translatable("command.ctnhlib.hand.fluid_header", i + 1))); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.fluid_name"), + entry.displayName().getString(), + NAME_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.fluid_id"), + entry.id().toString(), + RESOURCE_ID_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.fluid_mod"), + modDisplay(entry.id()), + entry.id().getNamespace(), + MOD_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.fluid_amount"), + String.valueOf(entry.amount()), + COUNT_COLOR); + sendLabeled(player, + Component.translatable("command.ctnhlib.hand.fluid_nbt"), + CTNHCommandInspector.prettyNbt(entry.tag()), + NBT_COLOR); + sendTagLines(player, + source, + Component.translatable("command.ctnhlib.hand.fluid_tags"), + entry.tags(), + InspectType.FLUID); + } + } + + // ---- /ctnh showtag 标签展示 ---------------------------------------------------- + + private static int executeShowTag(CommandContext ctx, InspectType type) { + CommandSourceStack source = ctx.getSource(); + ResourceLocation tagId = ResourceLocationArgument.getId(ctx, "tag"); + Optional> tagSet = CTNHCommandInspector.resolveTag(source, type, tagId); + if (tagSet.isEmpty()) { + source.sendFailure(CTNHCommandChatHelper.error( + Component.translatable("command.ctnhlib.error.unknown_tag", + tagId.toString(), type.registryDisplay()))); + return 0; + } + List members = CTNHCommandInspector.listTagMembers(type, tagSet.get()); + if (members.isEmpty()) { + source.sendFailure(CTNHCommandChatHelper.error( + Component.translatable("command.ctnhlib.error.empty_tag", + tagId.toString(), type.registryDisplay()))); + return 0; + } + Component header = Component.translatable("command.ctnhlib.showtag.header", + tagId.toString(), type.registryDisplay()).withStyle(ChatFormatting.GOLD); + source.sendSuccess(() -> header, false); + Component count = Component.translatable("command.ctnhlib.showtag.count", members.size()) + .withStyle(ChatFormatting.GRAY); + source.sendSuccess(() -> count, false); + for (TagMember member : members) { + String displayName = member.displayName().getString(); + String idString = member.id().toString(); + String copyText = idString; + String langKey = type == InspectType.FLUID ? + "command.ctnhlib.showtag.fluid_member" : + "command.ctnhlib.showtag.member"; + net.minecraft.network.chat.MutableComponent visible = Component + .translatable(langKey, displayName, idString) + .withStyle(ChatFormatting.WHITE); + Component line = CTNHCommandChatHelper.clickableLine(visible, copyText); + source.sendSuccess(() -> line, false); + } + return members.size(); + } + + // ---- 辅助方法 ------------------------------------------------------------------- + + private static void sendLabeled(ServerPlayer player, Component label, String value) { + sendLabeled(player, label, value, ChatFormatting.WHITE); + } + + private static void sendLabeled(ServerPlayer player, Component label, String value, ChatFormatting valueColor) { + sendLabeled(player, label, value, value, valueColor); + } + + private static void sendLabeled(ServerPlayer player, + Component label, + String value, + String copyText, + ChatFormatting valueColor) { + String safeValue = value == null || value.isEmpty() ? + Component.translatable("command.ctnhlib.value.empty").getString() : + value; + for (Component line : CTNHCommandChatHelper.labeledLines(label, safeValue, copyText, valueColor)) { + player.sendSystemMessage(line); + } + } + + private static void sendTagLines(ServerPlayer player, + CommandSourceStack source, + Component label, + List tagIds, + InspectType type) { + List tagIdStrings = tagIds == null ? + List.of() : + tagIds.stream().map(ResourceLocation::toString).toList(); + for (Component line : CTNHCommandChatHelper.labeledTagLines(label, tagIdStrings, TAG_COLOR, + tagId -> tagHover(type, tagMemberCount(source, type, ResourceLocation.tryParse(tagId))))) { + player.sendSystemMessage(line); + } + } + + private static String modDisplay(ResourceLocation id) { + return "@" + id.getNamespace(); + } + + private static int tagMemberCount(CommandSourceStack source, InspectType type, ResourceLocation tagId) { + if (tagId == null) { + return 0; + } + return CTNHCommandInspector.resolveTag(source, type, tagId) + .map(set -> CTNHCommandInspector.listTagMembers(type, set).size()) + .orElse(0); + } + + private static Component tagHover(InspectType type, int count) { + String key = switch (type) { + case ITEM -> "command.ctnhlib.copy.hover.item_tag"; + case BLOCK -> "command.ctnhlib.copy.hover.block_tag"; + case FLUID -> "command.ctnhlib.copy.hover.fluid_tag"; + }; + return Component.translatable(key, count); + } +} diff --git a/src/main/java/tech/vixhentx/mcmod/ctnhlib/common/CommonProxy.java b/src/main/java/tech/vixhentx/mcmod/ctnhlib/common/CommonProxy.java index b470561..dc6e6e4 100644 --- a/src/main/java/tech/vixhentx/mcmod/ctnhlib/common/CommonProxy.java +++ b/src/main/java/tech/vixhentx/mcmod/ctnhlib/common/CommonProxy.java @@ -4,12 +4,15 @@ import net.minecraft.server.packs.PackType; import net.minecraft.server.packs.repository.Pack; +import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.AddPackFindersEvent; +import net.minecraftforge.event.RegisterCommandsEvent; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import com.tterrag.registrate.util.entry.ItemEntry; +import tech.vixhentx.mcmod.ctnhlib.command.CTNHCommands; import tech.vixhentx.mcmod.ctnhlib.data.DataFilterPack; import tech.vixhentx.mcmod.ctnhlib.jade.GTProvidersRegistrar; import tech.vixhentx.mcmod.ctnhlib.registrate.CTNHLibNetworking; @@ -21,6 +24,8 @@ public class CommonProxy { public CommonProxy(FMLJavaModLoadingContext context) { IEventBus eventBus = context.getModEventBus(); eventBus.register(this); + // Forge-bus events such as RegisterCommandsEvent must be subscribed to the global Forge bus. + MinecraftForge.EVENT_BUS.register(CommonProxy.class); init(); ItemEntry multiblockHelper = REGISTRATE @@ -43,4 +48,9 @@ public void registerPackFinders(AddPackFindersEvent event) { DataFilterPack::new)); } } + + @SubscribeEvent + public static void registerCommands(RegisterCommandsEvent event) { + CTNHCommands.register(event.getDispatcher(), event.getBuildContext()); + } } diff --git a/src/main/java/tech/vixhentx/mcmod/ctnhlib/langprovider/Lang.java b/src/main/java/tech/vixhentx/mcmod/ctnhlib/langprovider/Lang.java index fd478d6..583bf4f 100644 --- a/src/main/java/tech/vixhentx/mcmod/ctnhlib/langprovider/Lang.java +++ b/src/main/java/tech/vixhentx/mcmod/ctnhlib/langprovider/Lang.java @@ -1,9 +1,9 @@ package tech.vixhentx.mcmod.ctnhlib.langprovider; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.MutableComponent; import com.ctnhlang.LangFactory; -import net.minecraft.network.chat.MutableComponent; @LangFactory public final class Lang implements com.ctnhlang.Lang { diff --git a/src/main/java/tech/vixhentx/mcmod/ctnhlib/langprovider/LangProcessor.java b/src/main/java/tech/vixhentx/mcmod/ctnhlib/langprovider/LangProcessor.java index 7f72072..ff40328 100644 --- a/src/main/java/tech/vixhentx/mcmod/ctnhlib/langprovider/LangProcessor.java +++ b/src/main/java/tech/vixhentx/mcmod/ctnhlib/langprovider/LangProcessor.java @@ -1,8 +1,5 @@ package tech.vixhentx.mcmod.ctnhlib.langprovider; -import com.ctnhlang.CN; -import com.ctnhlang.EN; -import com.ctnhlang.IgnoreLang; import net.minecraftforge.fml.ModContainer; import net.minecraftforge.fml.ModList; import net.minecraftforge.forgespi.language.IModFileInfo; @@ -10,6 +7,9 @@ import net.minecraftforge.forgespi.language.ModFileScanData; import net.minecraftforge.forgespi.locating.IModFile; +import com.ctnhlang.CN; +import com.ctnhlang.EN; +import com.ctnhlang.IgnoreLang; import com.ctnhlang.langprovider.LangKeyBuilder; import org.objectweb.asm.ClassReader; import org.objectweb.asm.Type; @@ -18,9 +18,9 @@ import tech.vixhentx.mcmod.ctnhlib.CTNHLib; import tech.vixhentx.mcmod.ctnhlib.registrate.CNRegistrate; -import java.lang.annotation.ElementType; import java.io.IOException; import java.io.InputStream; +import java.lang.annotation.ElementType; import java.util.HashMap; import java.util.Map; @@ -119,7 +119,8 @@ private void processField(ModFileScanData.AnnotationData enData, } } if (enValues.length != cnValues.length && enValues.length != 0 && cnValues.length != 0) { - CTNHLib.LOGGER.warn("Mismatched @EN/@CN array lengths on {}#{}", primary.clazz().getClassName(), field.name); + CTNHLib.LOGGER.warn("Mismatched @EN/@CN array lengths on {}#{}", primary.clazz().getClassName(), + field.name); } return; } @@ -139,7 +140,8 @@ private ClassNode readClassNode(String className) throws IOException { return null; } ClassNode classNode = new ClassNode(); - new ClassReader(stream).accept(classNode, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + new ClassReader(stream).accept(classNode, + ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); return classNode; } } diff --git a/src/main/resources/assets/ctnhlib/lang/en_us.json b/src/main/resources/assets/ctnhlib/lang/en_us.json new file mode 100644 index 0000000..ca2d3f8 --- /dev/null +++ b/src/main/resources/assets/ctnhlib/lang/en_us.json @@ -0,0 +1,35 @@ +{ + "command.ctnhlib.error.player_only": "This command can only be used by a player.", + "command.ctnhlib.error.empty_hand": "Your main hand is empty.", + "command.ctnhlib.error.unknown_tag": "Tag %1$s does not exist in registry %2$s.", + "command.ctnhlib.error.empty_tag": "Tag %1$s in registry %2$s has no members.", + "command.ctnhlib.copy.hover": "Click to copy", + "command.ctnhlib.copy.hover.item_tag": "[%1$s items] Click to copy", + "command.ctnhlib.copy.hover.block_tag": "[%1$s blocks] Click to copy", + "command.ctnhlib.copy.hover.fluid_tag": "[%1$s fluids] Click to copy", + "command.ctnhlib.value.empty": "", + "command.ctnhlib.value.none": "", + "command.ctnhlib.value.truncated": " (truncated, %1$s chars)", + "command.ctnhlib.hand.item_name": "Item Name", + "command.ctnhlib.hand.item_id": "Item ID", + "command.ctnhlib.hand.item_mod": "Item Mod", + "command.ctnhlib.hand.item_count": "Item Count", + "command.ctnhlib.hand.item_nbt": "Item NBT", + "command.ctnhlib.hand.item_tags": "Item Tags", + "command.ctnhlib.hand.block_name": "Block Name", + "command.ctnhlib.hand.block_id": "Block ID", + "command.ctnhlib.hand.block_mod": "Block Mod", + "command.ctnhlib.hand.block_tags": "Block Tags", + "command.ctnhlib.hand.fluid_header": "Fluid #%1$s", + "command.ctnhlib.hand.fluid_name": "Fluid Name", + "command.ctnhlib.hand.fluid_id": "Fluid ID", + "command.ctnhlib.hand.fluid_mod": "Fluid Mod", + "command.ctnhlib.hand.fluid_amount": "Fluid Amount (mB)", + "command.ctnhlib.hand.fluid_nbt": "Fluid NBT", + "command.ctnhlib.hand.fluid_tags": "Fluid Tags", + "command.ctnhlib.hand.no_fluid": "No fluid contained.", + "command.ctnhlib.showtag.header": "Tag %1$s in registry %2$s:", + "command.ctnhlib.showtag.count": "Members: %1$s", + "command.ctnhlib.showtag.member": "- %1$s (%2$s)", + "command.ctnhlib.showtag.fluid_member": "- %1$s (%2$s)" +} diff --git a/src/main/resources/assets/ctnhlib/lang/zh_cn.json b/src/main/resources/assets/ctnhlib/lang/zh_cn.json new file mode 100644 index 0000000..3007741 --- /dev/null +++ b/src/main/resources/assets/ctnhlib/lang/zh_cn.json @@ -0,0 +1,35 @@ +{ + "command.ctnhlib.error.player_only": "该指令只能由玩家执行。", + "command.ctnhlib.error.empty_hand": "你的主手为空。", + "command.ctnhlib.error.unknown_tag": "标签 %1$s 在注册表 %2$s 中不存在。", + "command.ctnhlib.error.empty_tag": "注册表 %2$s 中的标签 %1$s 没有任何成员。", + "command.ctnhlib.copy.hover": "点击复制", + "command.ctnhlib.copy.hover.item_tag": "[%1$s个物品] 点击复制", + "command.ctnhlib.copy.hover.block_tag": "[%1$s个方块] 点击复制", + "command.ctnhlib.copy.hover.fluid_tag": "[%1$s个流体] 点击复制", + "command.ctnhlib.value.empty": "<空>", + "command.ctnhlib.value.none": "<无>", + "command.ctnhlib.value.truncated": "(已截断,共 %1$s 字符)", + "command.ctnhlib.hand.item_name": "物品名称", + "command.ctnhlib.hand.item_id": "物品ID", + "command.ctnhlib.hand.item_mod": "物品Mod", + "command.ctnhlib.hand.item_count": "物品数量", + "command.ctnhlib.hand.item_nbt": "物品NBT", + "command.ctnhlib.hand.item_tags": "物品标签", + "command.ctnhlib.hand.block_name": "方块名称", + "command.ctnhlib.hand.block_id": "方块ID", + "command.ctnhlib.hand.block_mod": "方块Mod", + "command.ctnhlib.hand.block_tags": "方块标签", + "command.ctnhlib.hand.fluid_header": "流体 #%1$s", + "command.ctnhlib.hand.fluid_name": "流体名称", + "command.ctnhlib.hand.fluid_id": "流体ID", + "command.ctnhlib.hand.fluid_mod": "流体Mod", + "command.ctnhlib.hand.fluid_amount": "流体数量(mB)", + "command.ctnhlib.hand.fluid_nbt": "流体NBT", + "command.ctnhlib.hand.fluid_tags": "流体标签", + "command.ctnhlib.hand.no_fluid": "无内含流体。", + "command.ctnhlib.showtag.header": "注册表 %2$s 中的标签 %1$s:", + "command.ctnhlib.showtag.count": "成员数:%1$s", + "command.ctnhlib.showtag.member": "- %1$s (%2$s)", + "command.ctnhlib.showtag.fluid_member": "- %1$s (%2$s)" +}