This commit is contained in:
GaLicn 2025-12-11 20:31:37 +08:00
parent 29d07b1f90
commit 0b4443773d
16 changed files with 654 additions and 6 deletions

View File

@ -0,0 +1,75 @@
# Labeled Wireless Transceiver 设计方案(标签无线收发器)
## 目标与命名
- 新方块/物品/BELabeled 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<EndpointRef>` 端点集合、`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 设置标签。

View File

@ -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<LabelNetworkSnapshot> listNetworks(ServerLevel level, @Nullable UUID placerId) {
UUID owner = placerId == null ? WirelessMasterRegistry.PUBLIC_NETWORK_UUID : WirelessTeamUtil.getNetworkOwnerUUID(level, placerId);
ResourceKey<Level> dimKey = ModConfig.INSTANCE.wirelessCrossDimEnable ? null : level.dimension();
List<LabelNetworkSnapshot> list = new ArrayList<>();
for (Map.Entry<Key, LabelNetwork> 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

View File

@ -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);
}
/**

View File

@ -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<LabeledWirelessTransceiverMenu> {
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<LabelEntry> entries = new ArrayList<>();
private final List<LabelEntry> 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<LabelNetworkRegistry.LabelNetworkSnapshot> 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);
}
}

View File

@ -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

View File

@ -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) {

View File

@ -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());

View File

@ -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<MenuType<LabeledWirelessTransceiverMenu>> LABELED_WIRELESS_TRANSCEIVER =
MENUS.register("labeled_wireless_transceiver",
() -> IForgeMenuType.create(LabeledWirelessTransceiverMenu::new));
public static final RegistryObject<MenuType<EntitySpeedTickerMenu>> ENTITY_TICKER_MENU =
MENUS.register("entity_speed_ticker",
() -> MenuTypeBuilder

View File

@ -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++; }

View File

@ -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;
}
}

View File

@ -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);

View File

@ -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<NetworkEvent.Context> 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);
}
}

View File

@ -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<LabelNetworkRegistry.LabelNetworkSnapshot> list;
private final String currentLabel;
private final long currentChannel;
public LabelNetworkListS2CPacket(BlockPos pos, List<LabelNetworkRegistry.LabelNetworkSnapshot> 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<LabelNetworkRegistry.LabelNetworkSnapshot> 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<NetworkEvent.Context> 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);
}
}
}

View File

@ -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",

View File

@ -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",

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB