GuiExPatternTerminalMixin优化、添加翻译键

This commit is contained in:
C-H716 2025-10-31 21:00:43 +08:00
parent e2ff23f107
commit 3de1cc5701
4 changed files with 175 additions and 168 deletions

View File

@ -20,7 +20,6 @@ import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import com.glodblock.github.extendedae.network.EPPNetworkHandler;
import com.glodblock.github.glodium.network.packet.CGenericPacket;
import com.google.common.collect.HashMultimap;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
@ -28,7 +27,6 @@ import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
@ -44,7 +42,10 @@ import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static com.extendedae_plus.util.GlobalSendMessage.sendPlayerMessage;
@Pseudo
@SuppressWarnings({"AddedMixinMembersNamePattern"})
@Mixin(value = GuiExPatternTerminal.class)
public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu> {
@Shadow(remap = false) @Final private static int GUI_PADDING_X;
@ -52,14 +53,12 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
@Shadow(remap = false) @Final private static int GUI_HEADER_HEIGHT;
@Shadow(remap = false) @Final private static int ROW_HEIGHT;
@Shadow(remap = false) @Final private static int TEXT_MAX_WIDTH;
@Unique private final Map<Integer, Button> eap$openUIButtons = new HashMap<>();
@Unique private IconButton eap$toggleSlotsButton;
@Unique private boolean eap$showSlots = false; // 默认由配置初始化
@Unique private long eap$currentlyChoicePatterProvider = -1; // 当前选择的样板供应器ID
@Shadow(remap = false) @Final private AETextField searchOutField;
@Shadow(remap = false) @Final private AETextField searchInField;
@Shadow(remap = false) @Final private Set<ItemStack> matchedStack;
@Shadow(remap = false) @Final private Set<PatternContainerRecord> matchedProvider;
@Shadow(remap = false) @Final private HashMultimap<PatternContainerGroup, PatternContainerRecord> byGroup;
@Shadow(remap = false) @Final private HashMap<Long, GuiExPatternTerminal.PatternProviderInfo> infoMap;
@Shadow(remap = false) @Final private Scrollbar scrollbar;
@ -67,24 +66,28 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
@Shadow(remap = false) private int visibleRows;
@Shadow(remap = false) @Final private HashMap<Integer, HighlightButton> highlightBtns;
/* ----- eap 自有字段 ----- */
@Unique private final Map<Integer, Button> openUIButtons = new HashMap<>();
@Unique private IconButton eap$toggleSlotsButton;
@Unique private boolean eap$showSlots = false; // 默认由配置初始化
@Unique private long currentlyChoicePatterProvider = -1; // 当前选择的样板供应器ID
// 按钮更新/缓存状态避免每帧重建
@Unique private boolean buttonsDirty = true; // 当列表或布局变化时置 true
@Unique private int lastScroll = Integer.MIN_VALUE;
@Unique private int lastRowsSize = Integer.MIN_VALUE;
@Unique private int lastVisibleRows = Integer.MIN_VALUE;
public GuiExPatternTerminalMixin(AEBaseMenu menu, Inventory playerInventory, Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
}
/**
* 获取当前选择的样板供应器ID
*/
@Unique
public long getCurrentlyChoicePatternProvider() {
return eap$currentlyChoicePatterProvider;
}
/**
* 设置当前选择的样板供应器ID
*/
@Unique
public void setCurrentlyChoicePatternProvider(long id) {
this.eap$currentlyChoicePatterProvider = id;
this.currentlyChoicePatterProvider = id;
}
/**
@ -104,20 +107,14 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
// 检查是否是有效的编码样板
if (!clickedItem.isEmpty() && PatternDetailsHelper.isEncodedPattern(clickedItem)) {
// 检查是否选择了样板供应器
if (eap$currentlyChoicePatterProvider != -1) {
if (currentlyChoicePatterProvider != -1) {
// 执行快速上传
this.eap$quickUploadPattern(hoveredSlot.getSlotIndex());
// 取消默认的点击行为
cir.setReturnValue(true);
} else {
// 显示提示消息请先选择一个样板供应器
if (this.minecraft.player != null) {
this.minecraft.player.displayClientMessage(
Component.literal("ExtendedAE Plus: 请先选择一个样板供应器点击GroupHeader旁的按钮"),
true
);
}
sendPlayerMessage(Component.translatable("extendedae_plus.screen.upload.select_provider_first"));
}
}
}
@ -129,26 +126,30 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
*/
@Unique
private void eap$quickUploadPattern(int playerSlotIndex) {
if (this.minecraft.player != null) {
// 获取要上传的物品
ItemStack itemToUpload = this.minecraft.player.getInventory().getItem(playerSlotIndex);
if (!itemToUpload.isEmpty() && PatternDetailsHelper.isEncodedPattern(itemToUpload)) {
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("upload", playerSlotIndex, eap$currentlyChoicePatterProvider));
} else {
this.minecraft.player.displayClientMessage(
Component.literal("❌ ExtendedAE Plus: 无效的样板物品"),
true
);
}
if (this.minecraft.player == null) return;
ItemStack itemToUpload = this.minecraft.player.getInventory().getItem(playerSlotIndex);
if (itemToUpload.isEmpty() || !PatternDetailsHelper.isEncodedPattern(itemToUpload)) {
sendPlayerMessage(Component.translatable("extendedae_plus.screen.upload.invalid_pattern"));
return;
}
// 直接使用软依赖类发送包EPPNetworkHandler + CGenericPacket 已在 classpath
try {
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("upload", playerSlotIndex, currentlyChoicePatterProvider));
} catch (Throwable t) {
// 提示玩家网络支持缺失或版本不兼容
sendPlayerMessage(Component.translatable("extendedae_plus.screen.upload.no_network_support"));
}
}
/**
* 尝试打开指定行对应的样板供应器的 UI
* 该方法基于 GroupHeaderRow 获取分组信息再获取该分组下的第一个 PatternContainerRecord
* 并通过 serverId 获取 PatternProviderInfo 来发送 C2S 包打开目标供应器界面
*
* @param rowIndex 要操作的行索引
* 尝试打开指定行对应的样板供应器的 UI
* <p>
* 说明
* - 通过 rows 获取 GroupHeaderRow使用 Accessor 获取 group
* - 通过 byGroup 获取对应 PatternContainerRecord 集合拿第一个 record serverId
* - 通过 infoMap 获取 PatternProviderInfo发送 C2S 包打开目标容器界面
*/
@Unique
private void eap$tryOpenProviderUI(int rowIndex) {
@ -158,65 +159,44 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
PatternContainerGroup group = ((GuiExPatternTerminalGroupHeaderRowAccessor) headerRow).Group();
// 获取该组下的所有 PatternContainerRecord
Set<PatternContainerRecord> containers = byGroup.get(group);
if (containers == null || containers.isEmpty()) {
return; // 分组为空无供应器
}
if (containers == null || containers.isEmpty()) return;
// 取该组下第一个 PatternContainerRecord
PatternContainerRecord firstRecord = containers.iterator().next();
long serverId = firstRecord.getServerId(); // 获取供应器服务器 ID
long serverId = firstRecord.getServerId();
// 通过 infoMap 获取供应器位置信息
GuiExPatternTerminal.PatternProviderInfo patternProviderInfo = infoMap.get(serverId);
if (patternProviderInfo == null) {
// 如果没有位置信息提示玩家
if (this.minecraft != null && this.minecraft.player != null) {
this.minecraft.player.displayClientMessage(
Component.literal("未找到该供应器的位置信息无法打开UI"),
true
);
}
return;
}
GuiExPatternTerminal.PatternProviderInfo info = infoMap.get(serverId);
if (info == null) return;
// 获取位置信息和朝向
BlockPos pos = patternProviderInfo.pos();
Direction face = patternProviderInfo.face();
ResourceKey<Level> playerWorld = patternProviderInfo.playerWorld();
BlockPos pos = info.pos();
Direction face = info.face();
ResourceKey<Level> worldKey = info.playerWorld();
if (pos == null || worldKey == null) return;
// 转换为 C2S 包所需类型
long posLong = pos.asLong();
ResourceLocation dimStr = playerWorld.location();
int faceOrd = (face != null) ?
face.ordinal() :
-1;
// 发送打开 UI C2S
ModNetwork.CHANNEL.sendToServer(new OpenProviderUiC2SPacket(
posLong,
dimStr,
faceOrd
pos.asLong(),
worldKey.location(),
face != null ? face.ordinal() : -1
));
} catch (Throwable ignored) {
// 静默失败不影响界面操作
}
} catch (Exception ignored) {}
}
@Shadow
private void refreshList() {}
@Shadow
private void resetScrollbar() {}
/* ----- Shadow 方法 ----- */
@Shadow(remap = false) private void refreshList() {}
@Shadow(remap = false) private void resetScrollbar() {}
/* ----- 构造注入:创建切换按钮(只设置状态并触发一次 refresh ----- */
@Inject(method = "<init>", at = @At("TAIL"), remap = false)
private void injectConstructor(CallbackInfo ci) {
// 根据配置初始化默认显示/隐藏状态
// 初始化默认显示状态
this.eap$showSlots = ModConfig.INSTANCE.patternTerminalShowSlotsDefault;
// 创建切换槽位显示的按钮
this.eap$toggleSlotsButton = new IconButton((b) -> {
this.eap$showSlots = !this.eap$showSlots; // 开关状态
refreshList();
// 创建切换槽位显示的按钮只切换状态并触发一次 refresh
this.eap$toggleSlotsButton = new IconButton((b) -> {
this.eap$showSlots = !this.eap$showSlots;
// 标记需要更新按钮与高亮映射
this.buttonsDirty = true;
this.refreshList();
this.resetScrollbar();
}) {
@Override
protected Icon getIcon() {
@ -231,148 +211,166 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
this.addToLeftToolbar(this.eap$toggleSlotsButton);
}
/**
* 处理屏幕缩放resize后按钮位置未更新的问题
* - 清理并移除现有的打开UI按钮
* - 尝试重置滚动条并刷新列表
* 缩放后的下一帧drawFG 会基于新的 leftPos/topPos 重建与定位按钮
*/
@Inject(method = "resize", at = @At("TAIL"), remap = false, require = 0)
private void eap$onResize(Minecraft mc, int width, int height, CallbackInfo ci) {
try {
// 移除并清理按钮避免旧位置残留
this.eap$openUIButtons.values().forEach(this::removeWidget);
this.eap$openUIButtons.clear();
refreshList();
resetScrollbar();
} catch (Throwable ignored) {
}
}
@Inject(method = "init", at = @At("TAIL"), remap = false, require = 0)
private void eap$onInit(CallbackInfo ci) {
// 清理旧的打开UI按钮
this.eap$openUIButtons.values().forEach(this::removeWidget);
this.eap$openUIButtons.clear();
// 清理旧的打开UI按钮并标记需要重建
this.openUIButtons.values().forEach(this::removeWidget);
this.openUIButtons.clear();
this.buttonsDirty = true;
}
@Inject(method = "refreshList", at = @At("HEAD"), remap = false)
private void onRefreshListStart(CallbackInfo ci) {
// 更新按钮图标
// 更新 toggle 按钮 tooltip 文本
if (this.eap$toggleSlotsButton != null) {
this.eap$toggleSlotsButton.setTooltip(Tooltip.create(Component.translatable(
this.eap$showSlots ?
"gui.expatternprovider.hide_slots" :
"gui.expatternprovider.show_slots"
this.eap$showSlots ? "gui.expatternprovider.hide_slots" : "gui.expatternprovider.show_slots"
)));
}
// 清理旧的打开UI按钮
this.eap$openUIButtons.values().forEach(this::removeWidget);
this.eap$openUIButtons.clear();
// 清理并标记需要重建 UI 按钮但不在此处做重建
this.openUIButtons.values().forEach(this::removeWidget);
this.openUIButtons.clear();
this.buttonsDirty = true;
}
/**
* refreshList 完成后如果不显示 slots则在 rows 上做压缩并尽量复用 highlightBtns 映射
* 这个实现会尽量复用已有 highlightBtns 的实例避免无谓的对象重建
*/
@Inject(method = "refreshList", at = @At("TAIL"), remap = false)
private void onRefreshListEnd(CallbackInfo ci) {
// 在refreshList结束后根据showSlots状态过滤SlotsRow
if (!this.eap$showSlots) {
try {
// 创建新的索引映射
HashMap<Integer, HighlightButton> newHighlightBtns = new HashMap<>();
int newIndex = 0;
// 移除所有SlotsRow只保留GroupHeaderRow同时重新映射高亮按钮索引
// 遍历 rows保留 GroupHeaderRow 并尝试复用 highlightBtns index -> 按钮
for (int i = 0; i < rows.size(); i++) {
Object row = rows.get(i);
String className = row.getClass().getSimpleName();
if (className.equals("GroupHeaderRow")) {
// 保留GroupHeaderRow并重新映射对应的高亮按钮
@SuppressWarnings("unchecked")
ArrayList<Object> typedRows = (ArrayList<Object>) rows;
typedRows.set(newIndex, row);
// 查找原来在这个位置的高亮按钮
// 原始代码中高亮按钮的索引是在添加GroupHeaderRow之后添加第一个SlotsRow之前设置的
// 所以按钮的索引指向的是第一个SlotsRow的位置
// 我们需要查找索引为 i+1 的按钮第一个SlotsRow的位置
// highlightBtns 在原实现中是放在 GroupHeaderRow 之后第一个 SlotsRow indexi+1
// 尝试复用原映射中 i+1 的按钮若存在
if (highlightBtns.containsKey(i + 1)) {
HighlightButton button = highlightBtns.get(i + 1);
newHighlightBtns.put(newIndex, button);
}
newIndex++;
} else if (className.equals("SlotsRow")) {
// 不保留SlotsRow也不增加newIndex
}
// SlotsRow跳过不保留
}
// 移除多余
// 移除多余
while (rows.size() > newIndex) {
rows.remove(rows.size() - 1);
}
// 更新highlightBtns
// 更新 highlightBtns清理旧 map 并复用 new map
highlightBtns.clear();
highlightBtns.putAll(newHighlightBtns);
// 强制刷新滚动条
resetScrollbar();
// 强制刷新滚动条一次
this.resetScrollbar();
} catch (Exception ignored) {
}
}
// 标记按钮需要重建因为 rows 结构可能已改变
this.buttonsDirty = true;
}
/**
* drawFG 优化仅在需要时创建/移除按钮每帧只更新可见按钮的位置与可见性
*/
@Inject(method = "drawFG", at = @At("TAIL"), remap = false)
private void eap$afterDrawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY, CallbackInfo ci) {
// 动态放置/创建每个组标题后的打开UI按钮
try {
int currentScroll = scrollbar.getCurrentScroll();
int rowsSize = rows.size();
int visRows = this.visibleRows;
// 先隐藏旧按钮避免残留
for (Button b : this.eap$openUIButtons.values()) {
b.visible = false;
// 当列表或滚动或 visibleRows 发生变化时重建或清理按钮按需
boolean needFullUpdate = this.buttonsDirty
|| currentScroll != lastScroll
|| rowsSize != lastRowsSize
|| visRows != lastVisibleRows;
if (needFullUpdate) {
// 清理已经超出范围或者已不存在的按钮
openUIButtons.entrySet().removeIf(entry -> {
int idx = entry.getKey();
if (idx < 0 || idx >= rowsSize) {
removeWidget(entry.getValue());
return true;
}
return false;
});
// 为当前可见窗口内的 GroupHeaderRow 创建按钮如果不存在
for (int i = 0; i < visRows; i++) {
int rowIndex = currentScroll + i;
if (rowIndex < 0 || rowIndex >= rowsSize) continue;
Object row = rows.get(rowIndex);
if (!row.getClass().getSimpleName().equals("GroupHeaderRow")) continue;
// 计算按钮位置与原实现保持一致
int bx = this.leftPos + GUI_PADDING_X + TEXT_MAX_WIDTH - 11;
int by = this.topPos + GUI_PADDING_Y + GUI_HEADER_HEIGHT + i * ROW_HEIGHT - 2;
Button btn = openUIButtons.get(rowIndex);
if (btn == null) {
btn = Button.builder(
Component.literal("UI"),
(b) -> eap$tryOpenProviderUI(rowIndex)
).size(14, 12).build();
btn.setTooltip(Tooltip.create(Component.translatable("extendedae_plus.screen.open_provider_ui")));
openUIButtons.put(rowIndex, btn);
this.addRenderableWidget(btn);
}
btn.setPosition(bx, by);
btn.visible = true;
}
// 将不在当前可见窗口内的按钮隐藏
for (Map.Entry<Integer, Button> e : openUIButtons.entrySet()) {
int idx = e.getKey();
if (idx < currentScroll || idx >= currentScroll + visRows) {
e.getValue().visible = false;
}
}
// 更新缓存状态
this.lastScroll = currentScroll;
this.lastRowsSize = rowsSize;
this.lastVisibleRows = visRows;
this.buttonsDirty = false;
} else {
// 每帧只更新可见按钮位置可能因为 leftPos/topPos 动态变化比如移动窗口
for (int i = 0; i < visRows; i++) {
int rowIndex = currentScroll + i;
Button btn = openUIButtons.get(rowIndex);
if (btn == null) continue;
int bx = this.leftPos + GUI_PADDING_X + TEXT_MAX_WIDTH - 11;
int by = this.topPos + GUI_PADDING_Y + GUI_HEADER_HEIGHT + i * ROW_HEIGHT - 2;
btn.setPosition(bx, by);
btn.visible = true;
}
}
for (int i = 0; i < visibleRows; i++) {
int rowIndex = currentScroll + i;
if (rowIndex < 0 || rowIndex >= rows.size()) {
continue;
}
Object row = rows.get(rowIndex);
if (!row.getClass().getSimpleName().equals("GroupHeaderRow")) {
continue;
}
// 放置按钮位于名称文本右侧与原类 choiceButton 锚点相邻向右偏移 20px
int bx = this.leftPos + GUI_PADDING_X + TEXT_MAX_WIDTH - 11;
int by = this.topPos + GUI_PADDING_Y + GUI_HEADER_HEIGHT + i * ROW_HEIGHT - 2;
Button btn = eap$openUIButtons.get(rowIndex);
if (btn == null) {
btn = Button.builder(Component.literal("UI"), (b) -> {
eap$tryOpenProviderUI(rowIndex);
}).size(14, 12).build();
btn.setTooltip(Tooltip.create(Component.literal("打开该供应器目标容器的界面")));
eap$openUIButtons.put(rowIndex, btn);
this.addRenderableWidget(btn);
}
btn.setPosition(bx, by);
btn.visible = true;
}
// 生产环境移除调试日志
} catch (Throwable ignored) {
}
// 原有的搜索高亮逻辑
// 仅当任一搜索框非空时绘制叠加层与原版行为保持一致
// 原有的搜索高亮绘制仅在搜索激活时绘制
boolean searchActive = (this.searchOutField != null && !this.searchOutField.getValue().isEmpty())
|| (this.searchInField != null && !this.searchInField.getValue().isEmpty());
if (!searchActive) {
return;
}
// 使用 GuiUtil 的通用绘制方法绘制槽位高亮包含彩虹流转效果
GuiUtil.drawPatternSlotHighlights(guiGraphics, this.menu.slots, this.matchedStack, this.matchedProvider);
}
}

View File

@ -7,8 +7,8 @@ import appeng.api.stacks.AEKey;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.helpers.patternprovider.PatternProviderLogicHost;
import appeng.me.service.CraftingService;
import appeng.menu.me.crafting.CraftingCPUMenu;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor;
import com.mojang.logging.LogUtils;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.FriendlyByteBuf;
@ -52,15 +52,14 @@ public class CraftingMonitorJumpC2SPacket {
ServerPlayer player = context.getSender();
if (player == null) return;
// 必须在 CraftingCPU 界面内
if (!(player.containerMenu instanceof appeng.menu.me.crafting.CraftingCPUMenu menu)) {
if (!(player.containerMenu instanceof CraftingCPUMenu menu)) {
return;
}
// 通过菜单 target可能是 BlockEntity/Part/ItemHost IActionHost 获取 Grid
IGrid grid = null;
Object target = ((appeng.menu.AEBaseMenu) menu).getTarget();
Object target = menu.getTarget();
if (target instanceof IActionHost host && host.getActionableNode() != null) {
grid = host.getActionableNode().getGrid();
}
@ -82,9 +81,7 @@ public class CraftingMonitorJumpC2SPacket {
// 2) 遍历提供该样板的 Provider优先 PatternProviderLogic
for (var pattern : patterns) {
var providers = craftingService.getProviders(pattern);
int providerCount = 0;
for (var provider : providers) {
providerCount++;
if (provider instanceof PatternProviderLogic ppl) {
// 使用 accessor 获取 host受保护字段通过 accessor 访问
PatternProviderLogicHost host = ((PatternProviderLogicAccessor) ppl).eap$host();

View File

@ -50,6 +50,8 @@
"extendedae_plus.upload_to_matrix.fail_not_crafting": "Only crafting patterns are supported; processing patterns will be ignored",
"extendedae_plus.upload_to_matrix.fail_no_matrix": "No formed assembly matrix found in the current network",
"extendedae_plus.upload_to_matrix.fail_full": "The assembly matrix pattern inventory is full or cannot insert",
"extendedae_plus.upload_to_matrix.repetition": "The Assembly Matrix already contains the same pattern. Upload skipped and blank pattern returned.",
"extendedae_plus.screen.reload_mapping": "Reload Mapping",
"extendedae_plus.screen.reload_mapping_success": "Overloading mapping successful",
"extendedae_plus.screen.reload_mapping_fail": "Overloading mapping failed: %s",
@ -63,6 +65,11 @@
"extendedae_plus.screen.upload.mapping_deleted": "Deleted %d mapping(s), name = %s",
"extendedae_plus.screen.upload.mapping_not_found": "No mapping found for name '%s'",
"extendedae_plus.screen.upload.name": "Mapping Name",
"extendedae_plus.screen.upload.select_provider_first":"Please click the button on the left to select a pattern provider first",
"extendedae_plus.screen.upload.invalid_pattern": "Invalid Pattern",
"extendedae_plus.screen.upload.no_network_support": "ExtendedAE network support not found (may be missing or incompatible version)",
"extendedae_plus.screen.open_provider_ui": "Open the target container UI of this provider",
"extendedae_plus.button.choose_provider": "Upload Pattern",
"extendedae_plus.pattern.hovertext.player": "Encoded by %s",

View File

@ -66,6 +66,11 @@
"extendedae_plus.screen.upload.mapping_deleted": "已删除 %d 条映射,名称 = %s",
"extendedae_plus.screen.upload.mapping_not_found": "未找到名称为 '%s' 的映射",
"extendedae_plus.screen.upload.name": "映射名",
"extendedae_plus.screen.upload.select_provider_first": "请先点击左侧按钮选择一个样板供应器",
"extendedae_plus.screen.upload.invalid_pattern": "无效样板",
"extendedae_plus.screen.upload.no_network_support": "未找到 ExtendedAE 网络支持(可能未安装或版本不兼容)",
"extendedae_plus.screen.open_provider_ui": "打开该供应器目标容器的界面",
"extendedae_plus.button.choose_provider":"上传样板",
"extendedae_plus.pattern.hovertext.player": "由 %s 编码",