From 0b4443773d412f0024d99d42f2d57664b5504f29 Mon Sep 17 00:00:00 2001 From: GaLicn <133291877+GaLicn@users.noreply.github.com> Date: Thu, 11 Dec 2025 20:31:37 +0800 Subject: [PATCH] =?UTF-8?q?UI=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/labeled_wireless_transceiver_design.md | 75 ++++ .../ae/wireless/LabelNetworkRegistry.java | 20 ++ .../client/ClientRegistrar.java | 2 + .../LabeledWirelessTransceiverScreen.java | 332 ++++++++++++++++++ .../LabeledWirelessTransceiverBlock.java | 12 +- ...LabeledWirelessTransceiverBlockEntity.java | 19 +- .../extendedae_plus/init/ModCreativeTabs.java | 1 + .../extendedae_plus/init/ModMenuTypes.java | 5 + .../com/extendedae_plus/init/ModNetwork.java | 12 + .../menu/LabeledWirelessTransceiverMenu.java | 41 +++ .../network/LabelNetworkActionC2SPacket.java | 5 +- .../network/LabelNetworkListC2SPacket.java | 48 +++ .../network/LabelNetworkListS2CPacket.java | 69 ++++ .../assets/extendedae_plus/lang/en_us.json | 10 + .../assets/extendedae_plus/lang/zh_cn.json | 9 + .../gui/lable_wireless_transceiver_gui.png | Bin 0 -> 1284 bytes 16 files changed, 654 insertions(+), 6 deletions(-) create mode 100644 doc/labeled_wireless_transceiver_design.md create mode 100644 src/main/java/com/extendedae_plus/client/screen/LabeledWirelessTransceiverScreen.java create mode 100644 src/main/java/com/extendedae_plus/menu/LabeledWirelessTransceiverMenu.java create mode 100644 src/main/java/com/extendedae_plus/network/LabelNetworkListC2SPacket.java create mode 100644 src/main/java/com/extendedae_plus/network/LabelNetworkListS2CPacket.java create mode 100644 src/main/resources/assets/extendedae_plus/textures/gui/lable_wireless_transceiver_gui.png diff --git a/doc/labeled_wireless_transceiver_design.md b/doc/labeled_wireless_transceiver_design.md new file mode 100644 index 0000000..1f87f9e --- /dev/null +++ b/doc/labeled_wireless_transceiver_design.md @@ -0,0 +1,75 @@ +# Labeled Wireless Transceiver 设计方案(标签无线收发器) + +## 目标与命名 +- 新方块/物品/BE:Labeled Wireless Transceiver(标签无线收发器),注册名 `labeled_wireless_transceiver`。 +- 功能与旧收发器一致,但增加 UI,使用字符串标签管理频道;底层仍用 long 频率。 +- 旧收发器保留原行为,频段互不干扰。 + +## 交互与 UI(无主从,统一标签界面,虚拟节点中心) +- 方块交互:**取消全部徒手/道具交互,只保留右键打开 UI**。 +- 单一界面,核心元素: + 1) 全局标签-频道列表(从服务端获取 LabelNetworkRegistry 映射),展示标签名、频道号、在线端点数。 + 2) 当前收发器的标签与频道号(只读频道号,标签可编辑/选择)。 + 3) 连接状态:已连接、未连接、离线/超距(可用状态徽标)。 + 4) 操作按钮: + - “新建标签”:输入合法标签,分配频道并创建/获取对应虚拟节点,当前收发器加入该网络。 + - “删除标签”:删除当前标签映射(当网络端点数为 0 时销毁虚拟节点并回收频道),当前收发器清空标签并断开。 + - “设为当前频道”:将当前收发器切换到列表选中的标签网络(连接到该标签的虚拟节点)。 + - “刷新列表”:从服务端重新获取映射与统计。 + 5) 搜索/过滤框(可选):按标签关键字过滤列表。 +- UI 流程(建议): + - 打开 UI → 获取全局映射列表 + 当前端点的标签/频道 + 连接状态 → 填充列表和回显。 + - 应用/切换标签:发送 C2S 包(BlockPos + label + 操作类型),服务端分配/查询频道号、创建或获取虚拟节点并写回 BE。 + - 删除:发送删除操作;服务端移除映射(若网络无端点则销毁虚拟节点,回收频道),将该 BE 频率清零;列表与回显刷新。 + +## 标签网络注册中心(虚拟节点中心) +- 新建 `LabelNetworkRegistry`(SavedData/服务端单例)管理标签网络: + - Key:`(label标准化, 维度或 null 取决于跨维配置, owner/team UUID)` + - Value:`LabelNetwork`:`Set` 端点集合、`VirtualNodeRef virtualNode`(虚拟 AE2 节点句柄)、`long channel`(专用频道号)、在线统计。 +- 虚拟节点: + - 创建:新建/首用标签时,使用 `ManagedGridNode`,`setInWorldNode(false)`,`create(level, null)` 创建非 in-world 节点;`setIdlePowerUsage(0)`(可配置)并设置 visual representation。 + - 维度选择:若跨维开启,统一用主世界;否则按所在维度创建对应虚拟节点。 + - 持久化:SavedData 记录虚拟节点需要的重建信息(labelKey、channel、owner/team);重启时重建虚拟节点并复用频道。 +- 连接拓扑: + - 所有收发器端点直接连接到该标签的虚拟节点(单中心,避免选举和 n² 连接)。 + - 当标签网络端点数为 0 时,销毁虚拟节点并回收频道号。 +- 频道号分配: + - 预留专用频率区间:从 `1_000_000` 起向上分配,防止与旧版收发器冲突。 + - 频道号存放在 LabelNetwork 中,端点读取后设置自己的 `frequency` 字段即可,无需改底层 AE2 API。 +- API 建议: + - `register(endpoint, label, owner, level)`:加入网络,若无虚拟节点则创建并分配频道。 + - `unregister(endpoint)`:移除端点,若网络端点为 0 则销毁虚拟节点并回收频道。 + - `setLabel(endpoint, newLabel)`:先注销再按新标签注册。 + - `listNetworks(owner, dim/null)`:供 UI 拉取“标签-频道-在线数”。 + - `getNetwork(labelKey)`:UI 获取当前标签网络的在线端/状态。 +- 持久化:SavedData 持久化标签→频道号→端点集合(弱引用/位置)及虚拟节点重建所需信息;主线程访问,定期清理无效引用。 +- 校验:字符集 `[A-Za-z0-9_-]`,长度 ≤ 32(或 64),标准化 trim + lower,空串无效。 + +## BE 与逻辑复用 +- BE 保留 `long frequency` 字段与节点/连接逻辑,不分主从;连接目标是标签对应的虚拟节点。 +- 新增字段:`String labelForDisplay`(UI 回显);`long frequency` 由标签网络分配。 +- NBT:继续存 `frequency`(long)和 `label` 字符串;加载后通过 registry 获取频道号并连向虚拟节点。 +- 连接实现:可复用 `WirelessSlaveLink` 的“连接到指定节点”能力,去掉主端假设;或实现单一 `LabelLink`,始终尝试连接虚拟节点(失败则重试/断开)。 + +## 网络与数据包 +- 新包:`LabelNetworkActionC2SPacket`(BlockPos + label + 操作类型[新建/删除/切换])。 +- 服务端处理: + 1) 校验 label → 调用 `LabelNetworkRegistry` 分配/切换/删除;必要时创建/销毁虚拟节点。 + 2) 写入 BE:更新 `frequency`、`labelForDisplay`;指向虚拟节点并重连。 + 3) 反馈消息包含标签、频道号、在线数/连接状态。 + +## 方块与资源 +- 方块模型/贴图/语言键需新增(`block.extendedae_plus.labeled_wireless_transceiver` 等)。 +- 配方独立(避免与旧版冲突)。 +- BlockState:可复用旧收发器的 STATE 显示逻辑。 + +## 配置项(可选) +- 映射区间起始/上限。 +- 标签字符集/长度限制。 +- 跨维度共享开关(与现有无线配置保持一致)。 + +## 风险与注意点 +- 并发分配:务必在服务端单点分配并二次检查注册中心占用,防止重复。 +- 同名稳定:同 owner/team 的同名需返回同一频道号,否则可能导致已有网络断开。 +- 预留区间耗尽:需提示玩家清理无用标签或回退使用旧版数值收发器。 +- 交互收缩:已移除所有徒手 +/- 频率操作,玩家需通过 UI 设置标签。 diff --git a/src/main/java/com/extendedae_plus/ae/wireless/LabelNetworkRegistry.java b/src/main/java/com/extendedae_plus/ae/wireless/LabelNetworkRegistry.java index 723b6f7..953be4d 100644 --- a/src/main/java/com/extendedae_plus/ae/wireless/LabelNetworkRegistry.java +++ b/src/main/java/com/extendedae_plus/ae/wireless/LabelNetworkRegistry.java @@ -23,6 +23,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import java.util.*; +import java.util.Comparator; /** * 标签无线网络注册中心(SavedData)。 @@ -47,6 +48,8 @@ public class LabelNetworkRegistry extends SavedData { return level.getDataStorage().computeIfAbsent(LabelNetworkRegistry::load, LabelNetworkRegistry::new, SAVE_ID); } + public record LabelNetworkSnapshot(String label, long channel) {} + public static LabelNetworkRegistry get(ServerLevel level) { return get(level.getServer()); } @@ -131,6 +134,23 @@ public class LabelNetworkRegistry extends SavedData { return networks.get(key); } + /** + * 获取当前玩家所属网络列表(按标签排序)。 + */ + public synchronized List listNetworks(ServerLevel level, @Nullable UUID placerId) { + UUID owner = placerId == null ? WirelessMasterRegistry.PUBLIC_NETWORK_UUID : WirelessTeamUtil.getNetworkOwnerUUID(level, placerId); + ResourceKey dimKey = ModConfig.INSTANCE.wirelessCrossDimEnable ? null : level.dimension(); + List list = new ArrayList<>(); + for (Map.Entry entry : networks.entrySet()) { + Key key = entry.getKey(); + if (!Objects.equals(key.owner(), owner)) continue; + if (!Objects.equals(key.dim(), dimKey)) continue; + list.add(new LabelNetworkSnapshot(key.label(), entry.getValue().channel())); + } + list.sort(Comparator.comparing(LabelNetworkSnapshot::label)); + return list; + } + /* 序列化 */ @Override diff --git a/src/main/java/com/extendedae_plus/client/ClientRegistrar.java b/src/main/java/com/extendedae_plus/client/ClientRegistrar.java index b096b5b..0938d98 100644 --- a/src/main/java/com/extendedae_plus/client/ClientRegistrar.java +++ b/src/main/java/com/extendedae_plus/client/ClientRegistrar.java @@ -7,6 +7,7 @@ import com.extendedae_plus.ae.menu.EntitySpeedTickerMenu; import com.extendedae_plus.ae.screen.EntitySpeedTickerScreen; import com.extendedae_plus.client.render.crafting.EPlusCraftingCubeModelProvider; import com.extendedae_plus.client.screen.GlobalProviderModesScreen; +import com.extendedae_plus.client.screen.LabeledWirelessTransceiverScreen; import com.extendedae_plus.content.crafting.EPlusCraftingUnitType; import com.extendedae_plus.hooks.BuiltInModelHooks; import com.extendedae_plus.init.ModItems; @@ -60,6 +61,7 @@ public final class ClientRegistrar { */ public static void registerMenuScreens() { MenuScreens.register(ModMenuTypes.NETWORK_PATTERN_CONTROLLER.get(), GlobalProviderModesScreen::new); + MenuScreens.register(ModMenuTypes.LABELED_WIRELESS_TRANSCEIVER.get(), LabeledWirelessTransceiverScreen::new); } /** diff --git a/src/main/java/com/extendedae_plus/client/screen/LabeledWirelessTransceiverScreen.java b/src/main/java/com/extendedae_plus/client/screen/LabeledWirelessTransceiverScreen.java new file mode 100644 index 0000000..b4f0948 --- /dev/null +++ b/src/main/java/com/extendedae_plus/client/screen/LabeledWirelessTransceiverScreen.java @@ -0,0 +1,332 @@ +package com.extendedae_plus.client.screen; + +import com.extendedae_plus.ExtendedAEPlus; +import com.extendedae_plus.ae.wireless.LabelNetworkRegistry; +import com.extendedae_plus.menu.LabeledWirelessTransceiverMenu; +import com.extendedae_plus.network.LabelNetworkActionC2SPacket; +import com.extendedae_plus.network.LabelNetworkListC2SPacket; +import com.extendedae_plus.init.ModNetwork; +import com.mojang.blaze3d.systems.RenderSystem; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.EditBox; +import net.minecraft.client.gui.components.ImageButton; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.core.BlockPos; + +import java.util.ArrayList; +import java.util.List; + +/** + * 标签无线收发器屏幕(UI 占位,等待按钮布局)。 + * 纹理:textures/gui/lable_wireless_transceiver_gui.png,尺寸 194x156。 + */ +public class LabeledWirelessTransceiverScreen extends AbstractContainerScreen { + private static final ResourceLocation TEX = ExtendedAEPlus.id("textures/gui/lable_wireless_transceiver_gui.png"); + private static final int BTN_U = 197; + private static final int BTN_V = 54; + private static final int BTN_W = 28; + private static final int BTN_H = 16; + private static final int TEX_W = 256; + private static final int TEX_H = 256; + + private static final int LIST_X = 8; + private static final int LIST_Y = 21; + private static final int LIST_W = 100; + private static final int LIST_H = 121; // 141-21+1 + private static final int ROW_H = 12; + private static final int VISIBLE_ROWS = LIST_H / ROW_H; // 10 + private static final int SCROLL_X = 111; + private static final int SCROLL_Y = 21; + private static final int SCROLL_W = 6; + private static final int SCROLL_H = LIST_H; + + private EditBox searchBox; + private ImageButton newBtn; + private ImageButton deleteBtn; + private ImageButton setBtn; + private ImageButton disconnectBtn; + + private final BlockPos bePos; + private final List entries = new ArrayList<>(); + private final List filtered = new ArrayList<>(); + private int scrollOffset = 0; + private int selectedIndex = -1; + private String currentLabel = ""; + private long currentChannel = 0L; + + public LabeledWirelessTransceiverScreen(LabeledWirelessTransceiverMenu menu, Inventory inv, Component title) { + super(menu, inv, title); + this.imageWidth = 194; + this.imageHeight = 156; + this.inventoryLabelY = this.imageHeight; // 不显示玩家物品栏标签 + this.bePos = menu.getBlockEntityPos(); + } + + @Override + protected void init() { + super.init(); + // 搜索框:起点(78,4) 终点(189,15) => 宽112 高12 + int sx = this.leftPos + 78; + int sy = this.topPos + 4; + this.searchBox = new EditBox(this.font, sx, sy, 112, 12, Component.empty()); + this.searchBox.setBordered(false); + this.searchBox.setMaxLength(64); + this.searchBox.setVisible(true); + this.searchBox.setFocused(false); + this.searchBox.setResponder(s -> { + applyFilter(); + }); + this.addRenderableWidget(this.searchBox); + + int startX = this.leftPos + 124; + int startY = this.topPos + 91; + int hGap = 8; + int vGap = 10; + int secondColX = startX + BTN_W + hGap; + int secondRowY = startY + BTN_H + vGap; + + this.newBtn = new ImageButton(startX, startY, BTN_W, BTN_H, BTN_U, BTN_V, 0, TEX, TEX_W, TEX_H, + b -> sendSet(searchBox.getValue()), Component.translatable("gui.extendedae_plus.labeled_wireless.button.new")); + this.deleteBtn = new ImageButton(secondColX, startY, BTN_W, BTN_H, BTN_U, BTN_V, 0, TEX, TEX_W, TEX_H, + b -> sendDelete(), Component.translatable("gui.extendedae_plus.labeled_wireless.button.delete")); + this.setBtn = new ImageButton(startX, secondRowY, BTN_W, BTN_H, BTN_U, BTN_V, 0, TEX, TEX_W, TEX_H, + b -> sendSet(getSelectedLabel()), Component.translatable("gui.extendedae_plus.labeled_wireless.button.set")); + this.disconnectBtn = new ImageButton(secondColX, secondRowY, BTN_W, BTN_H, BTN_U, BTN_V, 0, TEX, TEX_W, TEX_H, + b -> sendDisconnect(), Component.translatable("gui.extendedae_plus.labeled_wireless.button.refresh")); + + this.addRenderableWidget(this.newBtn); + this.addRenderableWidget(this.deleteBtn); + this.addRenderableWidget(this.setBtn); + this.addRenderableWidget(this.disconnectBtn); + + requestList(); + } + + @Override + public void render(GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) { + this.renderBackground(gfx); + super.render(gfx, mouseX, mouseY, partialTicks); + drawAllButtonText(gfx); + this.renderTooltip(gfx, mouseX, mouseY); + if (this.searchBox != null) { + this.searchBox.render(gfx, mouseX, mouseY, partialTicks); + } + } + + @Override + protected void renderLabels(GuiGraphics gfx, int mouseX, int mouseY) { + // 左上角标题 + gfx.drawString(this.font, this.title, 8, 8, 0x404040, false); + // 右侧信息区标题 + gfx.drawString(this.font, Component.translatable("gui.extendedae_plus.labeled_wireless.info"), 124, 24, 0x404040, false); + } + + @Override + protected void renderBg(GuiGraphics gfx, float partialTicks, int mouseX, int mouseY) { + RenderSystem.setShaderColor(1f, 1f, 1f, 1f); + gfx.blit(TEX, this.leftPos, this.topPos, 0, 0, this.imageWidth, this.imageHeight); + + // 占位绘制:列表和信息区内的内容框线 + // 标签列表区域 + gfx.fill(this.leftPos + 8, this.topPos + 21, this.leftPos + 107 + 1, this.topPos + 141 + 1, 0x20FFFFFF); + // 滚动条区域 + gfx.fill(this.leftPos + 111, this.topPos + 21, this.leftPos + 116 + 1, this.topPos + 141 + 1, 0x20000000); + // 当前收发器信息区域 + gfx.fill(this.leftPos + 121, this.topPos + 21, this.leftPos + 189 + 1, this.topPos + 76 + 1, 0x10FFFFFF); + + renderList(gfx); + renderScrollBar(gfx); + } + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (this.searchBox != null && this.searchBox.mouseClicked(mouseX, mouseY, button)) { + setFocused(this.searchBox); + return true; + } + if (isMouseInList(mouseX, mouseY)) { + int localY = (int) mouseY - (this.topPos + LIST_Y); + int row = localY / ROW_H; + int idx = scrollOffset + row; + if (idx >= 0 && idx < filtered.size()) { + selectedIndex = idx; + } + return true; + } + if (isMouseInScrollbar(mouseX, mouseY)) { + updateScrollByMouse((int) mouseY); + return true; + } + return super.mouseClicked(mouseX, mouseY, button); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (this.searchBox != null && this.searchBox.keyPressed(keyCode, scanCode, modifiers)) { + return true; + } + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double delta) { + if (isMouseInList(mouseX, mouseY) || isMouseInScrollbar(mouseX, mouseY)) { + int maxOffset = Math.max(0, filtered.size() - VISIBLE_ROWS); + scrollOffset = Math.max(0, Math.min(maxOffset, scrollOffset - (int) Math.signum(delta))); + return true; + } + return super.mouseScrolled(mouseX, mouseY, delta); + } + + private void renderList(GuiGraphics gfx) { + int baseX = this.leftPos + LIST_X; + int baseY = this.topPos + LIST_Y; + for (int row = 0; row < VISIBLE_ROWS; row++) { + int idx = scrollOffset + row; + if (idx >= filtered.size()) break; + int y = baseY + row * ROW_H; + if (idx == selectedIndex) { + gfx.fill(baseX, y, baseX + LIST_W, y + ROW_H, 0x40FFFFFF); + } + LabelEntry e = filtered.get(idx); + String text = this.font.plainSubstrByWidth(e.label(), LIST_W - 4); + gfx.drawString(this.font, text, baseX + 2, y + 2, 0x404040, false); + } + + // 信息显示 + int infoX = this.leftPos + 124; + int infoY = this.topPos + 36; + String labelLine = Component.translatable("gui.extendedae_plus.labeled_wireless.current_label").getString() + ": " + (currentLabel == null || currentLabel.isEmpty() ? "-" : currentLabel); + String channelLine = Component.translatable("gui.extendedae_plus.labeled_wireless.current_channel").getString() + ": " + currentChannel; + gfx.drawString(this.font, labelLine, infoX, infoY, 0x404040, false); + gfx.drawString(this.font, channelLine, infoX, infoY + 12, 0x404040, false); + } + + private void renderScrollBar(GuiGraphics gfx) { + int total = filtered.size(); + if (total <= VISIBLE_ROWS) { + // 画静态条 + gfx.fill(this.leftPos + SCROLL_X, this.topPos + SCROLL_Y, this.leftPos + SCROLL_X + SCROLL_W, this.topPos + SCROLL_Y + SCROLL_H, 0x20000000); + return; + } + int maxOffset = total - VISIBLE_ROWS; + int trackX1 = this.leftPos + SCROLL_X; + int trackY1 = this.topPos + SCROLL_Y; + int trackY2 = trackY1 + SCROLL_H; + gfx.fill(trackX1, trackY1, trackX1 + SCROLL_W, trackY2, 0x20000000); + int knobH = Math.max(10, (int) ((double) VISIBLE_ROWS / total * SCROLL_H)); + int knobY = trackY1 + (int) ((SCROLL_H - knobH) * (scrollOffset / (double) maxOffset)); + gfx.fill(trackX1, knobY, trackX1 + SCROLL_W, knobY + knobH, 0x80FFFFFF); + } + + private boolean isMouseInList(double mouseX, double mouseY) { + return mouseX >= this.leftPos + LIST_X && mouseX < this.leftPos + LIST_X + LIST_W + && mouseY >= this.topPos + LIST_Y && mouseY < this.topPos + LIST_Y + LIST_H; + } + + private boolean isMouseInScrollbar(double mouseX, double mouseY) { + return mouseX >= this.leftPos + SCROLL_X && mouseX < this.leftPos + SCROLL_X + SCROLL_W + && mouseY >= this.topPos + SCROLL_Y && mouseY < this.topPos + SCROLL_Y + SCROLL_H; + } + + private void updateScrollByMouse(int mouseY) { + int total = filtered.size(); + if (total <= VISIBLE_ROWS) return; + int maxOffset = total - VISIBLE_ROWS; + int relativeY = mouseY - (this.topPos + SCROLL_Y); + relativeY = Math.max(0, Math.min(SCROLL_H, relativeY)); + int knobH = Math.max(10, (int) ((double) VISIBLE_ROWS / total * SCROLL_H)); + double ratio = (relativeY - knobH / 2.0) / (double) (SCROLL_H - knobH); + ratio = Math.max(0.0, Math.min(1.0, ratio)); + scrollOffset = (int) Math.round(ratio * maxOffset); + } + + private void applyFilter() { + String q = searchBox.getValue() == null ? "" : searchBox.getValue().trim().toLowerCase(); + filtered.clear(); + if (q.isEmpty()) { + filtered.addAll(entries); + } else { + for (LabelEntry e : entries) { + if (e.label().toLowerCase().contains(q)) { + filtered.add(e); + } + } + } + scrollOffset = 0; + selectedIndex = filtered.isEmpty() ? -1 : Math.min(selectedIndex, filtered.size() - 1); + } + + private void requestList() { + ModNetwork.CHANNEL.sendToServer(new LabelNetworkListC2SPacket(bePos)); + } + + private void sendSet(String label) { + if (label == null) label = ""; + ModNetwork.CHANNEL.sendToServer(new LabelNetworkActionC2SPacket(bePos, label, LabelNetworkActionC2SPacket.Action.SET)); + requestList(); + } + + private void sendDelete() { + String label = getSelectedLabel(); + if (label == null || label.isEmpty()) { + label = searchBox.getValue(); + } + if (label == null) label = ""; + ModNetwork.CHANNEL.sendToServer(new LabelNetworkActionC2SPacket(bePos, label, LabelNetworkActionC2SPacket.Action.DELETE)); + requestList(); + } + + private void sendDisconnect() { + ModNetwork.CHANNEL.sendToServer(new LabelNetworkActionC2SPacket(bePos, "", LabelNetworkActionC2SPacket.Action.DISCONNECT)); + requestList(); + } + + private String getSelectedLabel() { + if (selectedIndex >= 0 && selectedIndex < filtered.size()) { + return filtered.get(selectedIndex).label(); + } + return ""; + } + + public void updateList(List list, String currentLabel, long currentChannel) { + this.entries.clear(); + for (LabelNetworkRegistry.LabelNetworkSnapshot s : list) { + this.entries.add(new LabelEntry(s.label(), s.channel())); + } + this.currentLabel = currentLabel == null ? "" : currentLabel; + this.currentChannel = currentChannel; + applyFilter(); + } + + public boolean isFor(BlockPos pos) { + return this.bePos.equals(pos); + } + + private record LabelEntry(String label, long channel) {} + + private void drawAllButtonText(GuiGraphics gfx) { + // 按钮文本(24px 内居中,避免溢出)。放在 super.render 之后,确保绘制在按钮纹理之上。 + int startX = this.leftPos + 124; + int startY = this.topPos + 91; + int hGap = 8; + int vGap = 10; + int secondColX = startX + BTN_W + hGap; + int secondRowY = startY + BTN_H + vGap; + + drawButtonText(gfx, Component.translatable("gui.extendedae_plus.labeled_wireless.button.new"), startX, startY); + drawButtonText(gfx, Component.translatable("gui.extendedae_plus.labeled_wireless.button.delete"), secondColX, startY); + drawButtonText(gfx, Component.translatable("gui.extendedae_plus.labeled_wireless.button.set"), startX, secondRowY); + drawButtonText(gfx, Component.translatable("gui.extendedae_plus.labeled_wireless.button.refresh"), secondColX, secondRowY); + } + + private void drawButtonText(GuiGraphics gfx, Component text, int x, int y) { + String s = this.font.plainSubstrByWidth(text.getString(), BTN_W - 4); + int tx = x + (BTN_W - this.font.width(s)) / 2; + int ty = y + (BTN_H - this.font.lineHeight) / 2 + 1; + gfx.drawString(this.font, s, tx, ty, 0xFFFFFF, false); + } +} diff --git a/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlock.java b/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlock.java index 51f1752..4b10d9c 100644 --- a/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlock.java +++ b/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlock.java @@ -18,6 +18,7 @@ import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.block.state.StateDefinition; import net.minecraft.world.level.block.state.properties.IntegerProperty; import net.minecraft.world.phys.BlockHitResult; +import net.minecraftforge.network.NetworkHooks; import org.jetbrains.annotations.Nullable; /** @@ -56,8 +57,15 @@ public class LabeledWirelessTransceiverBlock extends Block implements EntityBloc @Override public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { - // UI 暂未实现,直接吞掉交互,防止旧版徒手调频。 - return InteractionResult.sidedSuccess(level.isClientSide); + if (level.isClientSide) { + return InteractionResult.SUCCESS; + } + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof LabeledWirelessTransceiverBlockEntity te) { + NetworkHooks.openScreen((net.minecraft.server.level.ServerPlayer) player, te, buf -> buf.writeBlockPos(pos)); + return InteractionResult.CONSUME; + } + return InteractionResult.PASS; } @Override diff --git a/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlockEntity.java b/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlockEntity.java index 3d01d58..e4a1026 100644 --- a/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlockEntity.java +++ b/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlockEntity.java @@ -13,12 +13,18 @@ import com.extendedae_plus.ae.wireless.LabelLink; import com.extendedae_plus.ae.wireless.LabelNetworkRegistry; import com.extendedae_plus.init.ModBlockEntities; import com.extendedae_plus.init.ModItems; +import com.extendedae_plus.menu.LabeledWirelessTransceiverMenu; import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.chat.Component; import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.MenuProvider; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; import org.jetbrains.annotations.Nullable; import java.util.EnumSet; @@ -31,7 +37,7 @@ import java.util.UUID; * - 无 UI(占位),仅提供服务端逻辑与状态更新; * - 保留频率字段用于状态显示。 */ -public class LabeledWirelessTransceiverBlockEntity extends AEBaseBlockEntity implements IWirelessEndpoint, IInWorldGridNodeHost { +public class LabeledWirelessTransceiverBlockEntity extends AEBaseBlockEntity implements IWirelessEndpoint, IInWorldGridNodeHost, MenuProvider { private IManagedGridNode managedNode; @@ -87,6 +93,17 @@ public class LabeledWirelessTransceiverBlockEntity extends AEBaseBlockEntity imp return super.isRemoved(); } + /* ===================== 菜单接口 ===================== */ + @Override + public Component getDisplayName() { + return Component.translatable("block.extendedae_plus.labeled_wireless_transceiver"); + } + + @Override + public @Nullable AbstractContainerMenu createMenu(int id, Inventory inv, Player player) { + return new LabeledWirelessTransceiverMenu(id, inv, this.worldPosition); + } + /* ===================== 公共方法 ===================== */ public void setPlacerId(@Nullable UUID placerId, @Nullable String placerName) { diff --git a/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java b/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java index e440872..4a6e2a2 100644 --- a/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java +++ b/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java @@ -17,6 +17,7 @@ public final class ModCreativeTabs { .displayItems((params, output) -> { // 将本模组物品加入创造物品栏 output.accept(ModItems.WIRELESS_TRANSCEIVER.get()); + output.accept(ModItems.LABELED_WIRELESS_TRANSCEIVER.get()); output.accept(ModItems.NETWORK_PATTERN_CONTROLLER.get()); // 装配矩阵上传核心 output.accept(ModItems.ASSEMBLER_MATRIX_UPLOAD_CORE.get()); diff --git a/src/main/java/com/extendedae_plus/init/ModMenuTypes.java b/src/main/java/com/extendedae_plus/init/ModMenuTypes.java index bd1de78..d4c1027 100644 --- a/src/main/java/com/extendedae_plus/init/ModMenuTypes.java +++ b/src/main/java/com/extendedae_plus/init/ModMenuTypes.java @@ -4,6 +4,7 @@ import appeng.menu.implementations.MenuTypeBuilder; import com.extendedae_plus.ExtendedAEPlus; import com.extendedae_plus.ae.menu.EntitySpeedTickerMenu; import com.extendedae_plus.ae.parts.EntitySpeedTickerPart; +import com.extendedae_plus.menu.LabeledWirelessTransceiverMenu; import com.extendedae_plus.menu.NetworkPatternControllerMenu; import net.minecraft.world.inventory.MenuType; import net.minecraftforge.common.extensions.IForgeMenuType; @@ -22,6 +23,10 @@ public final class ModMenuTypes { MENUS.register("network_pattern_controller", () -> IForgeMenuType.create(NetworkPatternControllerMenu::new)); + public static final RegistryObject> LABELED_WIRELESS_TRANSCEIVER = + MENUS.register("labeled_wireless_transceiver", + () -> IForgeMenuType.create(LabeledWirelessTransceiverMenu::new)); + public static final RegistryObject> ENTITY_TICKER_MENU = MENUS.register("entity_speed_ticker", () -> MenuTypeBuilder diff --git a/src/main/java/com/extendedae_plus/init/ModNetwork.java b/src/main/java/com/extendedae_plus/init/ModNetwork.java index 80d5f34..dc20543 100644 --- a/src/main/java/com/extendedae_plus/init/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/init/ModNetwork.java @@ -156,6 +156,18 @@ public final class ModNetwork { .decoder(LabelNetworkActionC2SPacket::decode) .consumerNetworkThread(LabelNetworkActionC2SPacket::handle) .add(); + + CHANNEL.messageBuilder(LabelNetworkListC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(LabelNetworkListC2SPacket::encode) + .decoder(LabelNetworkListC2SPacket::decode) + .consumerNetworkThread(LabelNetworkListC2SPacket::handle) + .add(); + + CHANNEL.messageBuilder(LabelNetworkListS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT) + .encoder(LabelNetworkListS2CPacket::encode) + .decoder(LabelNetworkListS2CPacket::decode) + .consumerNetworkThread(LabelNetworkListS2CPacket::handle) + .add(); } private static int nextId() { return id++; } diff --git a/src/main/java/com/extendedae_plus/menu/LabeledWirelessTransceiverMenu.java b/src/main/java/com/extendedae_plus/menu/LabeledWirelessTransceiverMenu.java new file mode 100644 index 0000000..4487197 --- /dev/null +++ b/src/main/java/com/extendedae_plus/menu/LabeledWirelessTransceiverMenu.java @@ -0,0 +1,41 @@ +package com.extendedae_plus.menu; + +import com.extendedae_plus.init.ModMenuTypes; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.world.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; + +/** + * 标签无线收发器菜单(暂无线槽,仅用于打开客户端界面)。 + */ +public class LabeledWirelessTransceiverMenu extends AbstractContainerMenu { + private final BlockPos bePos; + + public LabeledWirelessTransceiverMenu(int id, Inventory inv, BlockPos bePos) { + super(ModMenuTypes.LABELED_WIRELESS_TRANSCEIVER.get(), id); + this.bePos = bePos; + } + + public LabeledWirelessTransceiverMenu(int id, Inventory inv, FriendlyByteBuf buf) { + this(id, inv, buf.readBlockPos()); + } + + public BlockPos getBlockEntityPos() { + return bePos; + } + + @Override + public boolean stillValid(Player player) { + return player.level() != null + && player.level().getBlockEntity(bePos) != null + && player.distanceToSqr(bePos.getX() + 0.5, bePos.getY() + 0.5, bePos.getZ() + 0.5) <= 64; + } + + @Override + public ItemStack quickMoveStack(Player player, int index) { + return ItemStack.EMPTY; + } +} diff --git a/src/main/java/com/extendedae_plus/network/LabelNetworkActionC2SPacket.java b/src/main/java/com/extendedae_plus/network/LabelNetworkActionC2SPacket.java index d1deba1..2fbf67e 100644 --- a/src/main/java/com/extendedae_plus/network/LabelNetworkActionC2SPacket.java +++ b/src/main/java/com/extendedae_plus/network/LabelNetworkActionC2SPacket.java @@ -14,7 +14,7 @@ import java.util.function.Supplier; */ public class LabelNetworkActionC2SPacket { public enum Action { - SET, DELETE, REFRESH + SET, DELETE, DISCONNECT } private final BlockPos pos; @@ -52,8 +52,7 @@ public class LabelNetworkActionC2SPacket { switch (packet.action) { case SET -> te.applyLabel(packet.label); - case DELETE -> te.clearLabel(); - case REFRESH -> te.refreshLabel(); + case DELETE, DISCONNECT -> te.clearLabel(); } }); ctx.get().setPacketHandled(true); diff --git a/src/main/java/com/extendedae_plus/network/LabelNetworkListC2SPacket.java b/src/main/java/com/extendedae_plus/network/LabelNetworkListC2SPacket.java new file mode 100644 index 0000000..b610bcd --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/LabelNetworkListC2SPacket.java @@ -0,0 +1,48 @@ +package com.extendedae_plus.network; + +import com.extendedae_plus.ae.wireless.LabelNetworkRegistry; +import com.extendedae_plus.content.wireless.LabeledWirelessTransceiverBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; +import net.minecraftforge.network.PacketDistributor; + +import java.util.function.Supplier; + +/** + * 请求标签网络列表(客户端 -> 服务端)。 + */ +public class LabelNetworkListC2SPacket { + private final BlockPos pos; + + public LabelNetworkListC2SPacket(BlockPos pos) { + this.pos = pos; + } + + public static void encode(LabelNetworkListC2SPacket pkt, FriendlyByteBuf buf) { + buf.writeBlockPos(pkt.pos); + } + + public static LabelNetworkListC2SPacket decode(FriendlyByteBuf buf) { + return new LabelNetworkListC2SPacket(buf.readBlockPos()); + } + + public static void handle(LabelNetworkListC2SPacket pkt, Supplier ctx) { + ctx.get().enqueueWork(() -> { + ServerPlayer player = ctx.get().getSender(); + if (player == null) return; + var level = player.serverLevel(); + if (!level.hasChunkAt(pkt.pos)) return; + var be = level.getBlockEntity(pkt.pos); + if (!(be instanceof LabeledWirelessTransceiverBlockEntity te)) return; + + var list = LabelNetworkRegistry.get(level).listNetworks(level, te.getPlacerId()); + String currentLabel = te.getLabelForDisplay(); + long currentChannel = te.getFrequency(); + LabelNetworkListS2CPacket rsp = new LabelNetworkListS2CPacket(pkt.pos, list, currentLabel, currentChannel); + com.extendedae_plus.init.ModNetwork.CHANNEL.send(PacketDistributor.PLAYER.with(() -> player), rsp); + }); + ctx.get().setPacketHandled(true); + } +} diff --git a/src/main/java/com/extendedae_plus/network/LabelNetworkListS2CPacket.java b/src/main/java/com/extendedae_plus/network/LabelNetworkListS2CPacket.java new file mode 100644 index 0000000..28ead66 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/LabelNetworkListS2CPacket.java @@ -0,0 +1,69 @@ +package com.extendedae_plus.network; + +import com.extendedae_plus.ae.wireless.LabelNetworkRegistry; +import com.extendedae_plus.client.screen.LabeledWirelessTransceiverScreen; +import net.minecraft.client.Minecraft; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.api.distmarker.OnlyIn; +import net.minecraftforge.network.NetworkEvent; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +/** + * 标签网络列表下发(服务端 -> 客户端)。 + */ +public class LabelNetworkListS2CPacket { + private final BlockPos pos; + private final List list; + private final String currentLabel; + private final long currentChannel; + + public LabelNetworkListS2CPacket(BlockPos pos, List list, String currentLabel, long currentChannel) { + this.pos = pos; + this.list = list; + this.currentLabel = currentLabel; + this.currentChannel = currentChannel; + } + + public static void encode(LabelNetworkListS2CPacket pkt, FriendlyByteBuf buf) { + buf.writeBlockPos(pkt.pos); + buf.writeUtf(pkt.currentLabel == null ? "" : pkt.currentLabel, 128); + buf.writeLong(pkt.currentChannel); + buf.writeVarInt(pkt.list.size()); + for (LabelNetworkRegistry.LabelNetworkSnapshot s : pkt.list) { + buf.writeUtf(s.label(), 128); + buf.writeLong(s.channel()); + } + } + + public static LabelNetworkListS2CPacket decode(FriendlyByteBuf buf) { + BlockPos pos = buf.readBlockPos(); + String curLabel = buf.readUtf(128); + long curChannel = buf.readLong(); + int size = buf.readVarInt(); + List list = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + String label = buf.readUtf(128); + long channel = buf.readLong(); + list.add(new LabelNetworkRegistry.LabelNetworkSnapshot(label, channel)); + } + return new LabelNetworkListS2CPacket(pos, list, curLabel, curChannel); + } + + public static void handle(LabelNetworkListS2CPacket pkt, Supplier ctx) { + ctx.get().enqueueWork(() -> handleClient(pkt)); + ctx.get().setPacketHandled(true); + } + + @OnlyIn(Dist.CLIENT) + private static void handleClient(LabelNetworkListS2CPacket pkt) { + Minecraft mc = Minecraft.getInstance(); + if (mc.screen instanceof LabeledWirelessTransceiverScreen screen && screen.isFor(pkt.pos)) { + screen.updateList(pkt.list, pkt.currentLabel, pkt.currentChannel); + } + } +} diff --git a/src/main/resources/assets/extendedae_plus/lang/en_us.json b/src/main/resources/assets/extendedae_plus/lang/en_us.json index b32ea21..7a4440b 100644 --- a/src/main/resources/assets/extendedae_plus/lang/en_us.json +++ b/src/main/resources/assets/extendedae_plus/lang/en_us.json @@ -32,6 +32,7 @@ "item.extendedae_plus.spatial_core": "Spatial Core", "item.extendedae_plus.oblivion_singularity": "Oblivion Singularity", "item.extendedae_plus.infinity_biginteger_cell": "§4De§cvou§6rer §eof §aCo§bsmic §dSilence", + "item.extendedae_plus.labeled_wireless_transceiver": "Labeled Wireless Transceiver", "item.extendedae_plus.basic_core.storage.1": "Basic Core·Digitized", "item.extendedae_plus.basic_core.storage.2": "Basic Core·Arrayed", "item.extendedae_plus.basic_core.storage.3": "Basic Core·Matricized", @@ -59,6 +60,7 @@ "block.extendedae_plus.assembler_matrix_upload_core.tooltip.upload_fail_not_crafting": "Only crafting patterns supported, processing patterns ignored", "block.extendedae_plus.assembler_matrix_upload_core.tooltip.upload_fail_no_matrix": "No formed Assembler Matrix found in network", "block.extendedae_plus.assembler_matrix_upload_core.tooltip.upload_fail_full": "Assembler Matrix pattern storage full or cannot insert", + "block.extendedae_plus.labeled_wireless_transceiver": "Labeled Wireless Transceiver", "block.extendedae_plus.wireless_transceiver": "Wireless Transceiver", "block.extendedae_plus.4x_crafting_accelerator": "4x Crafting Accelerator", "block.extendedae_plus.16x_crafting_accelerator": "16x Crafting Accelerator", @@ -96,6 +98,14 @@ "extendedae_plus.gui.redstone_control.enabled": "Control acceleration with redstone signal", "extendedae_plus.gui.redstone_control.disabled": "Ignore redstone signals", + "gui.extendedae_plus.labeled_wireless.info": "Transceiver Info", + "gui.extendedae_plus.labeled_wireless.button.new": "New Label", + "gui.extendedae_plus.labeled_wireless.button.delete": "Delete", + "gui.extendedae_plus.labeled_wireless.button.set": "Set Current", + "gui.extendedae_plus.labeled_wireless.button.refresh": "Disconnect", + "gui.extendedae_plus.labeled_wireless.current_label": "Current Label", + "gui.extendedae_plus.labeled_wireless.current_channel": "Current Channel", + "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", diff --git a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json index 5ca7854..c8dc70e 100644 --- a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json +++ b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json @@ -70,6 +70,7 @@ "block.extendedae_plus.assembler_matrix_speed_plus": "超级装配矩阵速度核心", "block.extendedae_plus.assembler_matrix_crafter_plus": "超级装配矩阵合成核心", "block.extendedae_plus.assembler_matrix_pattern_plus": "超级装配矩阵样板核心", + "block.extendedae_plus.labeled_wireless_transceiver": "标签无线收发器", "extendedae_plus.upload_to_matrix": "上传到装配矩阵", "extendedae_plus.upload_to_matrix.success": "样板已上传到装配矩阵", @@ -96,6 +97,14 @@ "extendedae_plus.gui.redstone_control.enabled": "使用红石信号控制加速", "extendedae_plus.gui.redstone_control.disabled": "忽略红石信号", + "gui.extendedae_plus.labeled_wireless.info": "收发器信息", + "gui.extendedae_plus.labeled_wireless.button.new": "新建标签", + "gui.extendedae_plus.labeled_wireless.button.delete": "删除", + "gui.extendedae_plus.labeled_wireless.button.set": "设为当前", + "gui.extendedae_plus.labeled_wireless.button.refresh": "断开连接", + "gui.extendedae_plus.labeled_wireless.current_label": "当前标签", + "gui.extendedae_plus.labeled_wireless.current_channel": "当前频道", + "extendedae_plus.screen.reload_mapping": "重载映射", "extendedae_plus.screen.reload_mapping_success": "重载映射成功", "extendedae_plus.screen.reload_mapping_fail": "重载映射失败: %s", diff --git a/src/main/resources/assets/extendedae_plus/textures/gui/lable_wireless_transceiver_gui.png b/src/main/resources/assets/extendedae_plus/textures/gui/lable_wireless_transceiver_gui.png new file mode 100644 index 0000000000000000000000000000000000000000..641d1e1174fc7389770496649cfa12788a806a2f GIT binary patch literal 1284 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJEiot`d^Ar*7p+}YbVB~+&2 zqw^+lhK17(F-b}9Z3vaNUYurn^VYE=0S>8&xrvDj9E5zou$eG!mUa5Wq1t<->F%MV zwVZbv*~~h6j1$w_$`M2orsVB|SpUwQf$MSQ0_2;kO|4SYC@pAfmf16+XUS&Tz z`G029R`>Y28^6jNZd&Kp|2Xshxqh4kd*RbdPYqSG{p)VryD1w`^6t&do%Q?wKX@2% z>}9a}@hfY16}%bvGA{0ByZoT5yq{ssgUSBu`IbHZeBMs(jbMZ5o?o}LnV0!6C}5z3 zMoqc5vC|k1$e8Efm1AYv&~&!<>%osD@88W0RA69#qVw~k>C*F`*L#8VCb;D(34BDr8O~r+`JkeRl_8M{1#$a) z%y_?vh2hzbUq|~FJ1`{l-QTtk=uie@9~LGd7KXGL94v+Tdw<`v=#aj?zE@L_AtK>> zzy0*8KQpJtO9d-42&myi8xDy0f7A5_nwi?q=sy)02;bk`lLVQ~!@>wO`w-ChHCBQd zk?lL53NXxg{O2h5