From 1c7291b86545fd094f6c46a8b9b1b4b57da20400 Mon Sep 17 00:00:00 2001 From: GaLicn <133291877+GaLicn@users.noreply.github.com> Date: Fri, 12 Dec 2025 13:33:51 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=87=E7=AD=BE=E6=97=A0=E7=BA=BF=E6=94=B6?= =?UTF-8?q?=E5=8F=91=E5=99=A8=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ae/wireless/LabelLink.java | 101 ++++ .../ae/wireless/LabelNetworkRegistry.java | 349 +++++++++++++ .../extendedae_plus/client/ClientProxy.java | 41 +- .../LabeledWirelessTransceiverScreen.java | 476 ++++++++++++++++++ .../LabeledWirelessTransceiverBlock.java | 92 ++++ ...LabeledWirelessTransceiverBlockEntity.java | 311 ++++++++++++ .../init/ModBlockEntities.java | 6 + .../com/extendedae_plus/init/ModBlocks.java | 11 + .../extendedae_plus/init/ModCreativeTabs.java | 1 + .../com/extendedae_plus/init/ModItems.java | 4 + .../extendedae_plus/init/ModMenuTypes.java | 5 + .../com/extendedae_plus/init/ModNetwork.java | 11 + .../LabeledWirelessTransceiverProvider.java | 68 +++ .../jade/WirelessTransceiverJadePlugin.java | 8 + .../menu/LabeledWirelessTransceiverMenu.java | 41 ++ .../network/LabelNetworkActionC2SPacket.java | 57 +++ .../network/LabelNetworkListC2SPacket.java | 73 +++ .../network/LabelNetworkListS2CPacket.java | 79 +++ .../labeled_wireless_transceiver.json | 10 + .../assets/extendedae_plus/lang/en_us.json | 17 + .../assets/extendedae_plus/lang/zh_cn.json | 20 + .../lable_wireless_transceiver_off.json | 7 + .../lable_wireless_transceiver_on.json | 7 + .../item/labeled_wireless_transceiver.json | 3 + .../lable_wireless_transceiver_off.png | Bin 0 -> 409 bytes .../lable_wireless_transceiver_on.png | Bin 0 -> 499 bytes .../gui/lable_wireless_transceiver_gui.png | Bin 0 -> 1452 bytes 27 files changed, 1772 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/extendedae_plus/ae/wireless/LabelLink.java create mode 100644 src/main/java/com/extendedae_plus/ae/wireless/LabelNetworkRegistry.java create mode 100644 src/main/java/com/extendedae_plus/client/screen/LabeledWirelessTransceiverScreen.java create mode 100644 src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlock.java create mode 100644 src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlockEntity.java create mode 100644 src/main/java/com/extendedae_plus/integration/jade/LabeledWirelessTransceiverProvider.java create mode 100644 src/main/java/com/extendedae_plus/menu/LabeledWirelessTransceiverMenu.java create mode 100644 src/main/java/com/extendedae_plus/network/LabelNetworkActionC2SPacket.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/blockstates/labeled_wireless_transceiver.json create mode 100644 src/main/resources/assets/extendedae_plus/models/block/wirelesstransceiver/lable_wireless_transceiver_off.json create mode 100644 src/main/resources/assets/extendedae_plus/models/block/wirelesstransceiver/lable_wireless_transceiver_on.json create mode 100644 src/main/resources/assets/extendedae_plus/models/item/labeled_wireless_transceiver.json create mode 100644 src/main/resources/assets/extendedae_plus/textures/block/wireless_transceiver/lable_wireless_transceiver_off.png create mode 100644 src/main/resources/assets/extendedae_plus/textures/block/wireless_transceiver/lable_wireless_transceiver_on.png create mode 100644 src/main/resources/assets/extendedae_plus/textures/gui/lable_wireless_transceiver_gui.png diff --git a/src/main/java/com/extendedae_plus/ae/wireless/LabelLink.java b/src/main/java/com/extendedae_plus/ae/wireless/LabelLink.java new file mode 100644 index 0000000..18a975b --- /dev/null +++ b/src/main/java/com/extendedae_plus/ae/wireless/LabelLink.java @@ -0,0 +1,101 @@ +package com.extendedae_plus.ae.wireless; + +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGridNode; +import appeng.me.service.helpers.ConnectionWrapper; +import net.minecraft.resources.ResourceKey; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import org.jetbrains.annotations.Nullable; + +/** + * 标签无线收发器的连接器。 + * - 将 BE 的 in-world 节点连接到标签网络的虚拟节点。 + * - 只维护物理连接,不负责注册/反注册标签。 + */ +public class LabelLink { + private final IWirelessEndpoint host; + private final ConnectionWrapper connection = new ConnectionWrapper(null); + + @Nullable + private LabelNetworkRegistry.LabelNetwork target; + + public LabelLink(IWirelessEndpoint host) { + this.host = host; + } + + public void setTarget(@Nullable LabelNetworkRegistry.LabelNetwork target) { + this.target = target; + updateStatus(); + } + + public void clearTarget() { + setTarget(null); + } + + public boolean isConnected() { + return connection.getConnection() != null; + } + + /** + * 在 serverTick 或标签变化时调用。 + */ + public void updateStatus() { + if (host.isEndpointRemoved()) { + destroyConnection(); + return; + } + if (target == null) { + destroyConnection(); + return; + } + + final ServerLevel hostLevel = host.getServerLevel(); + if (hostLevel == null) { + destroyConnection(); + return; + } + + // 维度校验:未开启跨维且维度不匹配则断开 + ResourceKey targetDim = target.dim(); + if (targetDim != null && targetDim != hostLevel.dimension()) { + destroyConnection(); + return; + } + + IGridNode hostNode = host.getGridNode(); + IGridNode targetNode = target.node(); + if (hostNode == null || targetNode == null) { + destroyConnection(); + return; + } + + try { + var current = connection.getConnection(); + if (current != null) { + var a = current.a(); + var b = current.b(); + if ((a == hostNode || b == hostNode) && (a == targetNode || b == targetNode)) { + return; // 已连接 + } + current.destroy(); + connection.setConnection(null); + } + connection.setConnection(GridHelper.createConnection(hostNode, targetNode)); + } catch (IllegalStateException ignore) { + destroyConnection(); + } + } + + public void onUnloadOrRemove() { + destroyConnection(); + } + + private void destroyConnection() { + var current = connection.getConnection(); + if (current != null) { + current.destroy(); + connection.setConnection(null); + } + } +} diff --git a/src/main/java/com/extendedae_plus/ae/wireless/LabelNetworkRegistry.java b/src/main/java/com/extendedae_plus/ae/wireless/LabelNetworkRegistry.java new file mode 100644 index 0000000..0fc5e55 --- /dev/null +++ b/src/main/java/com/extendedae_plus/ae/wireless/LabelNetworkRegistry.java @@ -0,0 +1,349 @@ +package com.extendedae_plus.ae.wireless; + +import appeng.api.networking.GridFlags; +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IGridNodeListener; +import appeng.api.networking.IInWorldGridNodeHost; +import appeng.api.networking.IManagedGridNode; +import com.extendedae_plus.ExtendedAEPlus; +import com.extendedae_plus.config.ModConfigs; +import com.extendedae_plus.init.ModItems; +import com.extendedae_plus.util.wireless.WirelessTeamUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.registries.Registries; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.saveddata.SavedData; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +/** + * 标签无线网络注册中心(SavedData)。 + * - 负责标签到频道(频率)的分配与复用; + * - 创建/销毁虚拟节点,所有收发器连接到同一虚拟节点; + * - 记录在线端点集合,端点卸载后需调用 unregister。 + */ +public class LabelNetworkRegistry extends SavedData { + public static final String SAVE_ID = ExtendedAEPlus.MODID + "_label_networks"; + private static final Factory FACTORY = new Factory<>(LabelNetworkRegistry::new, LabelNetworkRegistry::load); + private static final long CHANNEL_START = 1_000_000L; + private static final UUID PUBLIC_NETWORK_UUID = new UUID(0, 0); + + private final Map networks = new HashMap<>(); + private long nextChannel = CHANNEL_START; + + /* 入口 API */ + public static LabelNetworkRegistry get(MinecraftServer server) { + ServerLevel level = server.getLevel(ServerLevel.OVERWORLD); + return level.getDataStorage().computeIfAbsent(FACTORY, SAVE_ID); + } + + public record LabelNetworkSnapshot(String label, long channel) {} + + public static LabelNetworkRegistry get(ServerLevel level) { + return get(level.getServer()); + } + + /** + * 规范化标签:trim;保持大小写敏感。 + */ + public static String normalizeLabel(String raw) { + if (raw == null) return null; + String t = raw.trim(); + if (t.isEmpty()) return null; + if (t.length() > 64) { + t = t.substring(0, 64); + } + for (int i = 0; i < t.length(); i++) { + char c = t.charAt(i); + if (!(Character.isLetterOrDigit(c) || c == '_' || c == '-')) { + return null; + } + } + return t; + } + + /** + * 注册/切换标签。 + */ + public synchronized LabelNetwork register(ServerLevel beLevel, String rawLabel, @Nullable UUID placerId, IWirelessEndpoint endpoint) { + String label = normalizeLabel(rawLabel); + if (label == null) return null; + + UUID owner = placerId == null ? PUBLIC_NETWORK_UUID : WirelessTeamUtil.getNetworkOwnerUUID(beLevel, placerId); + ResourceKey dimKey = ModConfigs.WIRELESS_CROSS_DIM_ENABLE.get() ? null : beLevel.dimension(); + Key key = new Key(dimKey, label, owner); + + LabelNetwork network = networks.get(key); + if (network == null) { + long channel = allocateChannel(); + network = new LabelNetwork(dimKey, label, owner, channel); + if (!network.ensureVirtualNode(beLevel)) { + return null; + } + networks.put(key, network); + setDirty(); + } else { + network.ensureVirtualNode(beLevel); + } + + network.endpoints.add(new EndpointRef(dimKey, endpoint.getBlockPos())); + setDirty(); + return network; + } + + /** + * 注销端点;不自动删除网络,网络的移除需显式调用 removeNetwork。 + */ + public synchronized void unregister(IWirelessEndpoint endpoint) { + ServerLevel level = endpoint.getServerLevel(); + if (level == null) return; + ResourceKey dimKey = ModConfigs.WIRELESS_CROSS_DIM_ENABLE.get() ? null : level.dimension(); + BlockPos pos = endpoint.getBlockPos(); + for (LabelNetwork net : networks.values()) { + net.endpoints.removeIf(ref -> ref.matches(dimKey, pos)); + } + setDirty(); + } + + public synchronized LabelNetwork getNetwork(ServerLevel level, String rawLabel, @Nullable UUID placerId) { + String label = normalizeLabel(rawLabel); + if (label == null) return null; + UUID owner = placerId == null ? PUBLIC_NETWORK_UUID : WirelessTeamUtil.getNetworkOwnerUUID(level, placerId); + ResourceKey dimKey = ModConfigs.WIRELESS_CROSS_DIM_ENABLE.get() ? null : level.dimension(); + Key key = new Key(dimKey, label, owner); + return networks.get(key); + } + + /** + * 显式删除一个网络(销毁虚拟节点),仅在 UI “删除” 时调用。 + */ + public synchronized boolean removeNetwork(ServerLevel level, String rawLabel, @Nullable UUID placerId) { + String label = normalizeLabel(rawLabel); + if (label == null) return false; + UUID owner = placerId == null ? PUBLIC_NETWORK_UUID : WirelessTeamUtil.getNetworkOwnerUUID(level, placerId); + ResourceKey dimKey = ModConfigs.WIRELESS_CROSS_DIM_ENABLE.get() ? null : level.dimension(); + Key key = new Key(dimKey, label, owner); + LabelNetwork net = networks.remove(key); + if (net != null) { + net.destroyVirtualNode(); + setDirty(); + return true; + } + return false; + } + + /** + * 获取当前玩家所属网络列表(按标签排序)。 + */ + public synchronized List listNetworks(ServerLevel level, @Nullable UUID placerId) { + UUID owner = placerId == null ? PUBLIC_NETWORK_UUID : WirelessTeamUtil.getNetworkOwnerUUID(level, placerId); + ResourceKey dimKey = ModConfigs.WIRELESS_CROSS_DIM_ENABLE.get() ? 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.comparingLong(LabelNetworkSnapshot::channel)); + return list; + } + + /* 序列化 */ + @Override + public @NotNull CompoundTag save(@NotNull CompoundTag tag, HolderLookup.Provider registries) { + tag.putLong("nextChannel", nextChannel); + ListTag list = new ListTag(); + networks.forEach((k, v) -> { + CompoundTag nbt = new CompoundTag(); + nbt.putString("label", k.label()); + if (k.dim() != null) { + nbt.putString("dim", k.dim().location().toString()); + } + nbt.putUUID("owner", k.owner()); + nbt.putLong("channel", v.channel); + nbt.put("endpoints", v.saveEndpoints()); + list.add(nbt); + }); + tag.put("networks", list); + return tag; + } + + public static LabelNetworkRegistry load(CompoundTag tag, HolderLookup.Provider registries) { + LabelNetworkRegistry reg = new LabelNetworkRegistry(); + reg.nextChannel = tag.getLong("nextChannel"); + ListTag list = tag.getList("networks", CompoundTag.TAG_COMPOUND); + for (int i = 0; i < list.size(); i++) { + CompoundTag nbt = list.getCompound(i); + String label = nbt.getString("label"); + ResourceKey dim = nbt.contains("dim") ? ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(nbt.getString("dim"))) : null; + UUID owner = nbt.getUUID("owner"); + long channel = nbt.getLong("channel"); + LabelNetwork net = new LabelNetwork(dim, label, owner, channel); + net.loadEndpoints(nbt.getList("endpoints", CompoundTag.TAG_COMPOUND)); + reg.networks.put(new Key(dim, label, owner), net); + } + return reg; + } + + private long allocateChannel() { + if (nextChannel < CHANNEL_START) { + nextChannel = CHANNEL_START; + } + long ch = nextChannel++; + return ch; + } + + /* 内部类型 */ + public record Key(@Nullable ResourceKey dim, String label, UUID owner) {} + + public static class LabelNetwork { + private final ResourceKey dim; // null 表示跨维共用 + private final String label; + private final UUID owner; + private final long channel; + private final Set endpoints = new HashSet<>(); + + @Nullable + private IManagedGridNode managedNode; + @Nullable + private VirtualLabelNodeHost virtualHost; + + LabelNetwork(@Nullable ResourceKey dim, String label, UUID owner, long channel) { + this.dim = dim; + this.label = label; + this.owner = owner; + this.channel = channel; + } + + public long channel() { + return channel; + } + + public ResourceKey dim() { + return dim; + } + + public @Nullable IGridNode node() { + return managedNode == null ? null : managedNode.getNode(); + } + + /** + * 确保虚拟节点存在;若已存在则复用。 + */ + public boolean ensureVirtualNode(ServerLevel level) { + if (managedNode != null && managedNode.getNode() != null) { + return true; + } + ServerLevel hostLevel = dim == null ? level.getServer().getLevel(ServerLevel.OVERWORLD) : level.getServer().getLevel(dim); + if (hostLevel == null) return false; + + this.virtualHost = new VirtualLabelNodeHost(); + this.managedNode = GridHelper.createManagedNode(virtualHost, NodeListener.INSTANCE); + this.virtualHost.setManagedNode(this.managedNode); + this.managedNode.setFlags(GridFlags.DENSE_CAPACITY); + this.managedNode.setIdlePowerUsage(0.0); + this.managedNode.setInWorldNode(false); + this.managedNode.setVisualRepresentation(ModItems.LABELED_WIRELESS_TRANSCEIVER.get().getDefaultInstance()); + this.managedNode.setTagName("label_net_" + label); + this.managedNode.create(hostLevel, null); + return true; + } + + public void destroyVirtualNode() { + if (managedNode != null) { + managedNode.destroy(); + } + managedNode = null; + virtualHost = null; + } + + public ListTag saveEndpoints() { + ListTag list = new ListTag(); + for (EndpointRef ref : endpoints) { + list.add(ref.save()); + } + return list; + } + + public void loadEndpoints(ListTag list) { + endpoints.clear(); + for (int i = 0; i < list.size(); i++) { + endpoints.add(EndpointRef.load(list.getCompound(i))); + } + } + + public int endpointCount() { + return endpoints.size(); + } + } + + public record EndpointRef(@Nullable ResourceKey dim, BlockPos pos) { + public boolean matches(@Nullable ResourceKey currentDim, BlockPos currentPos) { + if (!Objects.equals(dim, currentDim)) return false; + return pos.equals(currentPos); + } + + public CompoundTag save() { + CompoundTag tag = new CompoundTag(); + if (dim != null) { + tag.putString("dim", dim.location().toString()); + } + tag.putLong("pos", pos.asLong()); + return tag; + } + + public static EndpointRef load(CompoundTag tag) { + ResourceKey d = tag.contains("dim") + ? ResourceKey.create(Registries.DIMENSION, ResourceLocation.parse(tag.getString("dim"))) + : null; + BlockPos p = BlockPos.of(tag.getLong("pos")); + return new EndpointRef(d, p); + } + } + + enum NodeListener implements IGridNodeListener { + INSTANCE; + + @Override + public void onSaveChanges(VirtualLabelNodeHost host, IGridNode node) {} + + @Override + public void onStateChanged(VirtualLabelNodeHost host, IGridNode node, IGridNodeListener.State state) {} + + @Override + public void onInWorldConnectionChanged(VirtualLabelNodeHost host, IGridNode node) {} + + @Override + public void onGridChanged(VirtualLabelNodeHost host, IGridNode node) {} + + @Override + public void onOwnerChanged(VirtualLabelNodeHost host, IGridNode node) {} + } + + /** + * 虚拟标签网络节点宿主,不在世界中放置实体。 + */ + static class VirtualLabelNodeHost implements IInWorldGridNodeHost { + private IManagedGridNode managedNode; + + void setManagedNode(IManagedGridNode managedNode) { + this.managedNode = managedNode; + } + + @Override + public @Nullable IGridNode getGridNode(@Nullable net.minecraft.core.Direction dir) { + return managedNode == null ? null : managedNode.getNode(); + } + } +} diff --git a/src/main/java/com/extendedae_plus/client/ClientProxy.java b/src/main/java/com/extendedae_plus/client/ClientProxy.java index c65b771..9c6e3cc 100644 --- a/src/main/java/com/extendedae_plus/client/ClientProxy.java +++ b/src/main/java/com/extendedae_plus/client/ClientProxy.java @@ -4,12 +4,11 @@ import appeng.client.render.crafting.CraftingCubeModel; import appeng.init.client.InitScreens; import com.extendedae_plus.ExtendedAEPlus; import com.extendedae_plus.ae.screen.EntitySpeedTickerScreen; -import com.extendedae_plus.client.render.crafting.EPlusCraftingCubeModelProvider; -import com.extendedae_plus.content.crafting.EPlusCraftingUnitType; -import com.extendedae_plus.hooks.BuiltInModelHooks; import com.extendedae_plus.init.ModItems; import com.extendedae_plus.init.ModMenuTypes; import com.extendedae_plus.items.materials.EntitySpeedCardItem; +import com.extendedae_plus.client.screen.LabeledWirelessTransceiverScreen; +import com.extendedae_plus.menu.LabeledWirelessTransceiverMenu; import net.minecraft.client.renderer.item.ItemProperties; import net.neoforged.api.distmarker.Dist; import net.neoforged.bus.api.SubscribeEvent; @@ -28,31 +27,9 @@ public final class ClientProxy { public static void init() { if (REGISTERED) return; REGISTERED = true; - // 注册 Item property,用于根据 ItemStack 的 NBT exponent 切换模型 + // 注册 Item property ItemProperties.register(ModItems.ENTITY_SPEED_CARD.get(), ExtendedAEPlus.id("mult"), (stack, world, entity, seed) -> (float) EntitySpeedCardItem.readMultiplier(stack)); - - // 注册四种形成态模型为内置模型 - BuiltInModelHooks.addBuiltInModel( - ExtendedAEPlus.id("block/crafting/4x_accelerator_formed_v2"), - new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_4x))); - - BuiltInModelHooks.addBuiltInModel( - ExtendedAEPlus.id("block/crafting/16x_accelerator_formed_v2"), - new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_16x))); - - BuiltInModelHooks.addBuiltInModel( - ExtendedAEPlus.id("block/crafting/64x_accelerator_formed_v2"), - new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_64x))); - - BuiltInModelHooks.addBuiltInModel( - ExtendedAEPlus.id("block/crafting/256x_accelerator_formed_v2"), - new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_256x))); - - BuiltInModelHooks.addBuiltInModel( - ExtendedAEPlus.id("block/crafting/1024x_accelerator_formed_v2"), - new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_1024x))); - } @SubscribeEvent @@ -73,6 +50,18 @@ public final class ClientProxy { } ); + event.register( + ModMenuTypes.LABELED_WIRELESS_TRANSCEIVER.get(), + new net.minecraft.client.gui.screens.MenuScreens.ScreenConstructor< + LabeledWirelessTransceiverMenu, + LabeledWirelessTransceiverScreen>() { + @Override + public LabeledWirelessTransceiverScreen create(LabeledWirelessTransceiverMenu menu, net.minecraft.world.entity.player.Inventory inv, net.minecraft.network.chat.Component title) { + return new LabeledWirelessTransceiverScreen(menu, inv, title); + } + } + ); + /** * 注册由 AE2 InitScreens 所需的屏幕资源映射(用于内置 JSON 屏幕注册) */ 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..e848e20 --- /dev/null +++ b/src/main/java/com/extendedae_plus/client/screen/LabeledWirelessTransceiverScreen.java @@ -0,0 +1,476 @@ +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.Minecraft; +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.core.BlockPos; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Inventory; +import org.lwjgl.glfw.GLFW; +import java.util.ArrayList; +import java.util.List; + +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 = 2; + private static final int BTN_V = 159; + 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 = 9; + private static final int LIST_Y = 27; + private static final int LIST_W = 110; // 118-9+1 + private static final int LIST_H = 114; // 140-27+1 + private static final int ROW_H = 11; // 10px text height + 1px 分隔 + private static final int VISIBLE_ROWS = LIST_H / ROW_H; // 10 + private static final int SCROLL_X = 123; + private static final int SCROLL_Y = 21; + private static final int SCROLL_W = 6; + private static final int SCROLL_H = 121; // 141-21+1 + private static final int INFO_MAX_WIDTH = 116; // 信息区实际宽度(249-134+1=116) + + 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 lastSelectedLabel = ""; + private String currentLabel = ""; + private String currentOwner = ""; + private int onlineCount = 0; + private int usedChannels = 0; + private int maxChannels = 0; + + public LabeledWirelessTransceiverScreen(LabeledWirelessTransceiverMenu menu, Inventory inv, Component title) { + super(menu, inv, title); + this.imageWidth = 256; + this.imageHeight = 156; + this.inventoryLabelY = this.imageHeight; // 不显示玩家物品栏标签 + this.bePos = menu.getBlockEntityPos(); + } + + @Override + protected void init() { + super.init(); + // 搜索框:起点(134,23) 终点(249,31) => 宽116 高9(取整为9) + int sx = this.leftPos + 134; + int sy = this.topPos + 23; + this.searchBox = new EditBox(this.font, sx, sy, 116, 9, 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 + 145; + int startY = this.topPos + 101; + int hGap = 30; + int vGap = 8; + int secondColX = startX + BTN_W + hGap; + int secondRowY = startY + BTN_H + vGap; + + this.newBtn = new StateImageButton(startX, startY, BTN_W, BTN_H, BTN_U, BTN_V, 2, 177, 2, 195, TEX, TEX_W, TEX_H, + b -> sendSet(searchBox.getValue()), Component.translatable("gui.extendedae_plus.labeled_wireless.button.new")); + this.deleteBtn = new StateImageButton(secondColX, startY, BTN_W, BTN_H, BTN_U, BTN_V, 2, 177, 2, 195, TEX, TEX_W, TEX_H, + b -> sendDelete(), Component.translatable("gui.extendedae_plus.labeled_wireless.button.delete")); + this.setBtn = new StateImageButton(startX, secondRowY, BTN_W, BTN_H, BTN_U, BTN_V, 2, 177, 2, 195, TEX, TEX_W, TEX_H, + b -> sendSet(getSelectedLabel()), Component.translatable("gui.extendedae_plus.labeled_wireless.button.set")); + this.disconnectBtn = new StateImageButton(secondColX, secondRowY, BTN_W, BTN_H, BTN_U, BTN_V, 2, 177, 2, 195, 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) { + float titleScale = getTitleScale(); + var pose = gfx.pose(); + pose.pushPose(); + pose.translate(8, 8, 0); + pose.scale(titleScale, titleScale, 1.0f); + gfx.drawString(this.font, this.title, 0, 0, 0x404040, false); + pose.popPose(); + + pose.pushPose(); + pose.translate(134, 8, 0); + pose.scale(titleScale, titleScale, 1.0f); + gfx.drawString(this.font, Component.translatable("gui.extendedae_plus.labeled_wireless.info"), 0, 0, 0x404040, false); + pose.popPose(); + } + + @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 + 9, this.topPos + 27, this.leftPos + 118 + 1, this.topPos + 140 + 1, 0x20FFFFFF); + // 滚动条区域 + gfx.fill(this.leftPos + 123, this.topPos + 21, this.leftPos + 128 + 1, this.topPos + 141 + 1, 0x20000000); + // 当前收发器信息区域 + gfx.fill(this.leftPos + 134, this.topPos + 41, this.leftPos + 249 + 1, this.topPos + 92 + 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; + lastSelectedLabel = filtered.get(idx).label(); + } + 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 - 2); + int ty = y + (ROW_H - this.font.lineHeight) / 2; + gfx.drawString(this.font, text, baseX + 2, ty, 0x404040, false); + } + + // 信息显示 + int infoX = this.leftPos + 134; + int infoY = this.topPos + 41; + float infoScale = getInfoScale(); + String labelLine = Component.translatable("gui.extendedae_plus.labeled_wireless.current_label").getString() + ": " + (currentLabel == null || currentLabel.isEmpty() ? "-" : currentLabel); + String ownerLine = Component.translatable("gui.extendedae_plus.labeled_wireless.current_owner").getString() + ": " + (currentOwner == null || currentOwner.isEmpty() ? Component.translatable("extendedae_plus.jade.owner.public").getString() : currentOwner); + String onlineLine = Component.translatable("gui.extendedae_plus.labeled_wireless.online_count").getString() + ": " + onlineCount; + Component channelComp = maxChannels <= 0 + ? Component.translatable("extendedae_plus.jade.channels", usedChannels) + : Component.translatable("extendedae_plus.jade.channels_of", usedChannels, maxChannels); + drawInfoLine(gfx, labelLine, infoX, infoY, infoScale); + drawInfoLine(gfx, ownerLine, infoX, infoY + 12, infoScale); + drawInfoLine(gfx, onlineLine, infoX, infoY + 24, infoScale); + drawInfoLine(gfx, channelComp.getString(), infoX, infoY + 36, infoScale); + } + + 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 prevSelected = lastSelectedLabel; + String q = searchBox.getValue() == null ? "" : searchBox.getValue().trim(); + filtered.clear(); + if (q.isEmpty()) { + filtered.addAll(entries); + } else { + for (LabelEntry e : entries) { + if (e.label().contains(q)) { + filtered.add(e); + } + } + } + scrollOffset = 0; + selectedIndex = -1; + if (prevSelected != null && !prevSelected.isEmpty()) { + for (int i = 0; i < filtered.size(); i++) { + if (filtered.get(i).label().equals(prevSelected)) { + selectedIndex = i; + ensureSelectionVisible(); + break; + } + } + } + } + + 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)); + this.lastSelectedLabel = label; + this.searchBox.setValue(""); + 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)); + this.lastSelectedLabel = ""; + requestList(); + } + + private void sendDisconnect() { + ModNetwork.CHANNEL.sendToServer(new LabelNetworkActionC2SPacket(bePos, "", LabelNetworkActionC2SPacket.Action.DISCONNECT)); + this.lastSelectedLabel = ""; + requestList(); + } + + private String getSelectedLabel() { + if (selectedIndex >= 0 && selectedIndex < filtered.size()) { + return filtered.get(selectedIndex).label(); + } + return ""; + } + + public void updateList(List list, String currentLabel, String ownerName, int usedChannels, int maxChannels, int onlineCount) { + String prevSelected = getSelectedLabel(); + this.entries.clear(); + for (LabelNetworkRegistry.LabelNetworkSnapshot s : list) { + this.entries.add(new LabelEntry(s.label(), s.channel())); + } + this.currentLabel = currentLabel == null ? "" : currentLabel; + this.currentOwner = ownerName == null ? "" : ownerName; + this.onlineCount = onlineCount; + this.usedChannels = usedChannels; + this.maxChannels = maxChannels; + + if (prevSelected != null && !prevSelected.isEmpty()) { + this.lastSelectedLabel = prevSelected; + } else if (this.currentLabel != null && !this.currentLabel.isEmpty()) { + this.lastSelectedLabel = this.currentLabel; + } else { + this.lastSelectedLabel = ""; + } + 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 + 145; + int startY = this.topPos + 101; + int hGap = 30; + int vGap = 8; + 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); + } + + private void ensureSelectionVisible() { + if (selectedIndex < 0) return; + int maxOffset = Math.max(0, filtered.size() - VISIBLE_ROWS); + int targetRow = selectedIndex; + if (targetRow < scrollOffset) { + scrollOffset = targetRow; + } else if (targetRow >= scrollOffset + VISIBLE_ROWS) { + scrollOffset = Math.min(maxOffset, targetRow - VISIBLE_ROWS + 1); + } + } + + private String trimInfo(String text, float scale) { + if (text == null) return ""; + int maxWidth = (int) (INFO_MAX_WIDTH / Math.max(0.0001f, scale)); + return this.font.plainSubstrByWidth(text, maxWidth); + } + + private void drawInfoLine(GuiGraphics gfx, String text, int x, int y, float scale) { + String trimmed = trimInfo(text, scale); + var pose = gfx.pose(); + pose.pushPose(); + pose.translate(x, y, 0); + pose.scale(scale, scale, 1.0f); + gfx.drawString(this.font, trimmed, 0, 0, 0x404040, false); + pose.popPose(); + } + + private boolean isEnglish() { + Minecraft mc = Minecraft.getInstance(); + var lang = mc.getLanguageManager().getSelected(); + return lang != null && lang.equalsIgnoreCase("en_us"); + } + + private float getInfoScale() { + return isEnglish() ? 0.75f : 1.0f; + } + + private float getTitleScale() { + return isEnglish() ? 0.75f : 1.0f; + } + + private static class StateImageButton extends ImageButton { + private final ResourceLocation tex; + private final int texW; + private final int texH; + private final int baseU; + private final int baseV; + private final int hoverU; + private final int hoverV; + private final int pressU; + private final int pressV; + private boolean pressedVisual = false; + + public StateImageButton(int x, int y, int w, int h, int baseU, int baseV, int hoverU, int hoverV, int pressU, int pressV, + ResourceLocation tex, int texW, int texH, OnPress onPress, Component tooltip) { + super(x, y, w, h, baseU, baseV, 0, tex, texW, texH, onPress, tooltip); + this.tex = tex; + this.texW = texW; + this.texH = texH; + this.baseU = baseU; + this.baseV = baseV; + this.hoverU = hoverU; + this.hoverV = hoverV; + this.pressU = pressU; + this.pressV = pressV; + } + + @Override + public void renderWidget(GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) { + boolean hovered = this.isMouseOver(mouseX, mouseY); + boolean pressed = pressedVisual || (hovered && GLFW.glfwGetMouseButton(Minecraft.getInstance().getWindow().getWindow(), GLFW.GLFW_MOUSE_BUTTON_1) == GLFW.GLFW_PRESS); + int u = baseU; + int v = baseV; + if (pressed) { + u = pressU; + v = pressV; + } else if (hovered) { + u = hoverU; + v = hoverV; + } + gfx.blit(tex, this.getX(), this.getY(), u, v, this.width, this.height, texW, texH); + } + + @Override + public void onClick(double mouseX, double mouseY) { + this.pressedVisual = true; + super.onClick(mouseX, mouseY); + this.setFocused(false); + } + + @Override + public void onRelease(double mouseX, double mouseY) { + this.pressedVisual = false; + super.onRelease(mouseX, mouseY); + this.setFocused(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 new file mode 100644 index 0000000..3692b48 --- /dev/null +++ b/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlock.java @@ -0,0 +1,92 @@ +package com.extendedae_plus.content.wireless; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.ItemInteractionResult; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.EntityBlock; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityTicker; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.StateDefinition; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.phys.BlockHitResult; +import net.neoforged.neoforge.network.MenuHooks; +import org.jetbrains.annotations.Nullable; + +import com.extendedae_plus.init.ModBlockEntities; +import com.extendedae_plus.menu.LabeledWirelessTransceiverMenu; + +/** + * 标签无线收发器方块。 + */ +public class LabeledWirelessTransceiverBlock extends Block implements EntityBlock { + public static final BooleanProperty STATE = BooleanProperty.create("state"); + + public LabeledWirelessTransceiverBlock(Properties props) { + super(props); + this.registerDefaultState(this.stateDefinition.any().setValue(STATE, Boolean.FALSE)); + } + + @Override + protected void createBlockStateDefinition(StateDefinition.Builder builder) { + builder.add(STATE); + } + + @Nullable + @Override + public BlockState getStateForPlacement(BlockPlaceContext ctx) { + return this.defaultBlockState(); + } + + @Nullable + @Override + public BlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new LabeledWirelessTransceiverBlockEntity(pos, state); + } + + @Nullable + @Override + public BlockEntityTicker getTicker(Level level, BlockState state, BlockEntityType type) { + if (level.isClientSide) return null; + return type == ModBlockEntities.LABELED_WIRELESS_TRANSCEIVER_BE.get() + ? (lvl, pos, st, be) -> LabeledWirelessTransceiverBlockEntity.serverTick(lvl, pos, st, (LabeledWirelessTransceiverBlockEntity) be) + : null; + } + + @Override + protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hit) { + if (level.isClientSide) { + return InteractionResult.SUCCESS; + } + BlockEntity be = level.getBlockEntity(pos); + if (!(be instanceof LabeledWirelessTransceiverBlockEntity te)) { + return InteractionResult.PASS; + } + MenuHooks.openMenu((net.minecraft.server.level.ServerPlayer) player, te, pos); + return InteractionResult.CONSUME; + } + + @Override + protected ItemInteractionResult useItemOn(net.minecraft.world.item.ItemStack heldItem, BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) { + // 委托空手交互逻辑(统一入口) + InteractionResult r = this.useWithoutItem(state, level, pos, player, hit); + return r.consumesAction() ? ItemInteractionResult.SUCCESS : ItemInteractionResult.PASS_TO_DEFAULT_BLOCK_INTERACTION; + } + + @Override + public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) { + if (!state.is(newState.getBlock())) { + BlockEntity be = level.getBlockEntity(pos); + if (be instanceof LabeledWirelessTransceiverBlockEntity te) { + te.onRemoved(); + } + } + super.onRemove(state, level, pos, newState, isMoving); + } +} diff --git a/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlockEntity.java b/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlockEntity.java new file mode 100644 index 0000000..4faee47 --- /dev/null +++ b/src/main/java/com/extendedae_plus/content/wireless/LabeledWirelessTransceiverBlockEntity.java @@ -0,0 +1,311 @@ +package com.extendedae_plus.content.wireless; + +import appeng.api.networking.GridFlags; +import appeng.api.networking.GridHelper; +import appeng.api.networking.IGridNode; +import appeng.api.networking.IGridNodeListener; +import appeng.api.networking.IInWorldGridNodeHost; +import appeng.api.networking.IManagedGridNode; +import appeng.api.util.AECableType; +import appeng.blockentity.AEBaseBlockEntity; +import com.extendedae_plus.ae.wireless.IWirelessEndpoint; +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.core.HolderLookup; +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.entity.player.Inventory; +import net.minecraft.world.entity.player.Player; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.Nullable; + +import java.util.EnumSet; +import java.util.Objects; +import java.util.UUID; + +public class LabeledWirelessTransceiverBlockEntity extends AEBaseBlockEntity implements IWirelessEndpoint, IInWorldGridNodeHost, MenuProvider { + + private IManagedGridNode managedNode; + + private long frequency = 0L; + @Nullable + private String labelForDisplay; + private boolean beingRemoved = false; + + @Nullable + private UUID placerId; + @Nullable + private String placerName; + + private final LabelLink labelLink = new LabelLink(this); + + public LabeledWirelessTransceiverBlockEntity(BlockPos pos, BlockState state) { + super(ModBlockEntities.LABELED_WIRELESS_TRANSCEIVER_BE.get(), pos, state); + this.managedNode = GridHelper.createManagedNode(this, NodeListener.INSTANCE) + .setFlags(GridFlags.DENSE_CAPACITY); + this.managedNode.setIdlePowerUsage(1.0); + this.managedNode.setTagName("labeled_wireless_node"); + this.managedNode.setInWorldNode(true); + this.managedNode.setExposedOnSides(EnumSet.allOf(Direction.class)); + this.managedNode.setVisualRepresentation(ModItems.LABELED_WIRELESS_TRANSCEIVER.get().getDefaultInstance()); + } + + @Override + public @Nullable IGridNode getGridNode(Direction dir) { + return getGridNode(); + } + + @Override + public ServerLevel getServerLevel() { + Level lvl = super.getLevel(); + return lvl instanceof ServerLevel sl ? sl : null; + } + + @Override + public BlockPos getBlockPos() { + return this.worldPosition; + } + + @Override + public IGridNode getGridNode() { + return managedNode == null ? null : managedNode.getNode(); + } + + @Override + public boolean isEndpointRemoved() { + 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) { + this.placerId = placerId; + this.placerName = placerName; + setChanged(); + } + + @Nullable + public UUID getPlacerId() { + return placerId; + } + + @Nullable + public String getPlacerName() { + return placerName; + } + + public long getFrequency() { + return frequency; + } + + @Nullable + public String getLabelForDisplay() { + return labelForDisplay; + } + + public void applyLabel(@Nullable String rawLabel) { + ServerLevel sl = getServerLevel(); + if (sl == null) return; + + LabelNetworkRegistry.get(sl).unregister(this); + + var network = LabelNetworkRegistry.get(sl).register(sl, rawLabel, placerId, this); + if (network == null) { + clearLabel(); + return; + } + + this.labelForDisplay = rawLabel; + this.frequency = network.channel(); + this.labelLink.setTarget(network); + updateState(); + setChanged(); + } + + public void clearLabel() { + ServerLevel sl = getServerLevel(); + if (sl != null) { + LabelNetworkRegistry.get(sl).unregister(this); + } + this.labelForDisplay = null; + this.frequency = 0L; + this.labelLink.clearTarget(); + updateState(); + setChanged(); + } + + public void refreshLabel(boolean ensureRegister) { + ServerLevel sl = getServerLevel(); + if (sl == null) return; + if (labelForDisplay == null || labelForDisplay.isEmpty()) { + this.frequency = 0L; + this.labelLink.clearTarget(); + updateState(); + return; + } + var registry = LabelNetworkRegistry.get(sl); + var network = registry.getNetwork(sl, labelForDisplay, placerId); + if (network == null && ensureRegister) { + network = registry.register(sl, labelForDisplay, placerId, this); + } + if (network == null) { + this.frequency = 0L; + this.labelLink.clearTarget(); + } else { + network.ensureVirtualNode(sl); + this.frequency = network.channel(); + this.labelLink.setTarget(network); + } + updateState(); + setChanged(); + } + + public void refreshLabel() { + refreshLabel(false); + } + + public void onRemoved() { + this.beingRemoved = true; + labelLink.onUnloadOrRemove(); + ServerLevel sl = getServerLevel(); + if (sl != null) { + LabelNetworkRegistry.get(sl).unregister(this); + } + if (managedNode != null) { + managedNode.destroy(); + } + } + + public static void serverTick(Level level, BlockPos pos, BlockState state, LabeledWirelessTransceiverBlockEntity be) { + if (!(level instanceof ServerLevel)) return; + be.labelLink.updateStatus(); + be.updateState(); + } + + private void updateState() { + if (this.level == null || this.level.isClientSide) return; + if (this.beingRemoved || this.isRemoved()) return; + BlockState currentState = this.getBlockState(); + if (!(currentState.getBlock() instanceof LabeledWirelessTransceiverBlock)) { + return; + } + + IGridNode node = this.getGridNode(); + boolean online = false; + if (node != null && node.isActive()) { + try { + var grid = node.getGrid(); + online = grid != null && grid.getEnergyService().isNetworkPowered(); + } catch (Throwable ignored) { + online = false; + } + } + + if (currentState.getValue(LabeledWirelessTransceiverBlock.STATE) != online) { + this.level.setBlock(this.worldPosition, currentState.setValue(LabeledWirelessTransceiverBlock.STATE, online), 3); + } + } + + @Override + public AECableType getCableConnectionType(Direction dir) { + if (this.level == null) return AECableType.GLASS; + var adjacentPos = this.worldPosition.relative(dir); + if (!Objects.requireNonNull(this.getLevel()).hasChunkAt(adjacentPos)) return AECableType.GLASS; + var adjacentHost = GridHelper.getNodeHost(this.getLevel(), adjacentPos); + if (adjacentHost != null) { + var t = adjacentHost.getCableConnectionType(dir.getOpposite()); + if (t != null) return t; + } + return AECableType.GLASS; + } + + @Override + public void onLoad() { + super.onLoad(); + ServerLevel sl = getServerLevel(); + if (sl == null) return; + GridHelper.onFirstTick(this, be -> { + be.managedNode.create(be.getLevel(), be.getBlockPos()); + be.refreshLabel(true); + be.labelLink.updateStatus(); + be.updateState(); + }); + } + + @Override + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) { + super.saveAdditional(tag, registries); + tag.putLong("frequency", frequency); + if (labelForDisplay != null) { + tag.putString("label", labelForDisplay); + } + if (placerId != null) { + tag.putUUID("placerId", placerId); + } + if (placerName != null) { + tag.putString("placerName", placerName); + } + if (managedNode != null) { + managedNode.saveToNBT(tag); + } + } + + @Override + public void loadTag(CompoundTag tag, HolderLookup.Provider registries) { + super.loadTag(tag, registries); + this.frequency = tag.getLong("frequency"); + if (tag.contains("label")) { + this.labelForDisplay = tag.getString("label"); + } else { + this.labelForDisplay = null; + } + if (tag.hasUUID("placerId")) { + this.placerId = tag.getUUID("placerId"); + } + if (tag.contains("placerName")) { + this.placerName = tag.getString("placerName"); + } + if (managedNode != null) { + managedNode.loadFromNBT(tag); + } + } + + enum NodeListener implements IGridNodeListener { + INSTANCE; + @Override + public void onSaveChanges(LabeledWirelessTransceiverBlockEntity host, IGridNode node) { + host.setChanged(); + } + @Override + public void onStateChanged(LabeledWirelessTransceiverBlockEntity host, IGridNode node, State state) { + host.updateState(); + } + @Override + public void onInWorldConnectionChanged(LabeledWirelessTransceiverBlockEntity host, IGridNode node) { + host.updateState(); + } + @Override + public void onGridChanged(LabeledWirelessTransceiverBlockEntity host, IGridNode node) { + host.updateState(); + } + @Override + public void onOwnerChanged(LabeledWirelessTransceiverBlockEntity host, IGridNode node) {} + } +} diff --git a/src/main/java/com/extendedae_plus/init/ModBlockEntities.java b/src/main/java/com/extendedae_plus/init/ModBlockEntities.java index 8215833..069aed6 100644 --- a/src/main/java/com/extendedae_plus/init/ModBlockEntities.java +++ b/src/main/java/com/extendedae_plus/init/ModBlockEntities.java @@ -5,6 +5,7 @@ import com.extendedae_plus.content.matrix.CrafterCorePlusBlockEntity; import com.extendedae_plus.content.matrix.PatternCorePlusBlockEntity; import com.extendedae_plus.content.matrix.SpeedCorePlusBlockEntity; import com.extendedae_plus.content.wireless.WirelessTransceiverBlockEntity; +import com.extendedae_plus.content.wireless.LabeledWirelessTransceiverBlockEntity; import com.extendedae_plus.content.controller.NetworkPatternControllerBlockEntity; import com.extendedae_plus.content.matrix.UploadCoreBlockEntity; import appeng.blockentity.crafting.CraftingBlockEntity; @@ -25,6 +26,11 @@ public final class ModBlockEntities { () -> BlockEntityType.Builder.of(WirelessTransceiverBlockEntity::new, ModBlocks.WIRELESS_TRANSCEIVER.get()).build(null)); + public static final DeferredHolder, BlockEntityType> LABELED_WIRELESS_TRANSCEIVER_BE = + BLOCK_ENTITY_TYPES.register("labeled_wireless_transceiver", + () -> BlockEntityType.Builder.of(LabeledWirelessTransceiverBlockEntity::new, + ModBlocks.LABELED_WIRELESS_TRANSCEIVER.get()).build(null)); + public static final DeferredHolder, BlockEntityType> NETWORK_PATTERN_CONTROLLER_BE = BLOCK_ENTITY_TYPES.register("network_pattern_controller", () -> BlockEntityType.Builder.of(NetworkPatternControllerBlockEntity::new, diff --git a/src/main/java/com/extendedae_plus/init/ModBlocks.java b/src/main/java/com/extendedae_plus/init/ModBlocks.java index 4f97723..51a275b 100644 --- a/src/main/java/com/extendedae_plus/init/ModBlocks.java +++ b/src/main/java/com/extendedae_plus/init/ModBlocks.java @@ -6,6 +6,7 @@ import com.extendedae_plus.content.matrix.CrafterCorePlusBlock; import com.extendedae_plus.content.matrix.PatternCorePlusBlock; import com.extendedae_plus.content.matrix.SpeedCorePlusBlock; import com.extendedae_plus.content.matrix.UploadCoreBlock; +import com.extendedae_plus.content.wireless.LabeledWirelessTransceiverBlock; import com.extendedae_plus.content.wireless.WirelessTransceiverBlock; import com.extendedae_plus.content.crafting.EPlusCraftingUnitType; import appeng.block.crafting.CraftingUnitBlock; @@ -32,6 +33,16 @@ public final class ModBlocks { ) ); + public static final DeferredBlock LABELED_WIRELESS_TRANSCEIVER = BLOCKS.register( + "labeled_wireless_transceiver", + () -> new LabeledWirelessTransceiverBlock( + BlockBehaviour.Properties.of() + .mapColor(MapColor.METAL) + .strength(1.5F, 6.0F) + .requiresCorrectToolForDrops() + ) + ); + // AE2 网络模式控制器方块 public static final DeferredBlock NETWORK_PATTERN_CONTROLLER = BLOCKS.register( "network_pattern_controller", diff --git a/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java b/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java index 08056d2..1eb9320 100644 --- a/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java +++ b/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java @@ -20,6 +20,7 @@ public final class ModCreativeTabs { .displayItems((params, output) -> { List.of( ModItems.WIRELESS_TRANSCEIVER.get().getDefaultInstance(), + ModItems.LABELED_WIRELESS_TRANSCEIVER.get().getDefaultInstance(), ModItems.NETWORK_PATTERN_CONTROLLER.get().getDefaultInstance(), ModItems.ACCELERATOR_4x.get().getDefaultInstance(), ModItems.ACCELERATOR_16x.get().getDefaultInstance(), diff --git a/src/main/java/com/extendedae_plus/init/ModItems.java b/src/main/java/com/extendedae_plus/init/ModItems.java index f757230..4739b30 100644 --- a/src/main/java/com/extendedae_plus/init/ModItems.java +++ b/src/main/java/com/extendedae_plus/init/ModItems.java @@ -18,6 +18,10 @@ public final class ModItems { "wireless_transceiver", () -> new BlockItem(ModBlocks.WIRELESS_TRANSCEIVER.get(), new Item.Properties()) ); + public static final DeferredItem LABELED_WIRELESS_TRANSCEIVER = ITEMS.register( + "labeled_wireless_transceiver", + () -> new BlockItem(ModBlocks.LABELED_WIRELESS_TRANSCEIVER.get(), new Item.Properties()) + ); // Crafting Accelerators public static final DeferredItem ACCELERATOR_4x = ITEMS.register( "4x_crafting_accelerator", diff --git a/src/main/java/com/extendedae_plus/init/ModMenuTypes.java b/src/main/java/com/extendedae_plus/init/ModMenuTypes.java index c42986b..f499d05 100644 --- a/src/main/java/com/extendedae_plus/init/ModMenuTypes.java +++ b/src/main/java/com/extendedae_plus/init/ModMenuTypes.java @@ -5,6 +5,7 @@ import com.extendedae_plus.ExtendedAEPlus; import com.extendedae_plus.ae.menu.EntitySpeedTickerMenu; import com.extendedae_plus.ae.parts.EntitySpeedTickerPart; import com.extendedae_plus.menu.NetworkPatternControllerMenu; +import com.extendedae_plus.menu.LabeledWirelessTransceiverMenu; import net.minecraft.core.registries.Registries; import net.minecraft.world.inventory.MenuType; import net.neoforged.neoforge.common.extensions.IMenuTypeExtension; @@ -21,6 +22,10 @@ public final class ModMenuTypes { MENUS.register("network_pattern_controller", () -> IMenuTypeExtension.create(NetworkPatternControllerMenu::new)); + public static final DeferredHolder, MenuType> LABELED_WIRELESS_TRANSCEIVER = + MENUS.register("labeled_wireless_transceiver", + () -> IMenuTypeExtension.create(LabeledWirelessTransceiverMenu::new)); + public static final DeferredHolder, MenuType> 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 913f6be..d589e85 100644 --- a/src/main/java/com/extendedae_plus/init/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/init/ModNetwork.java @@ -42,6 +42,17 @@ public class ModNetwork { com.extendedae_plus.network.SetWirelessFrequencyC2SPacket.STREAM_CODEC, com.extendedae_plus.network.SetWirelessFrequencyC2SPacket::handle); + // 标签无线收发器:请求列表 / 操作 / 列表下发 + registrar.playToServer(com.extendedae_plus.network.LabelNetworkListC2SPacket.TYPE, + com.extendedae_plus.network.LabelNetworkListC2SPacket.STREAM_CODEC, + com.extendedae_plus.network.LabelNetworkListC2SPacket::handle); + registrar.playToServer(com.extendedae_plus.network.LabelNetworkActionC2SPacket.TYPE, + com.extendedae_plus.network.LabelNetworkActionC2SPacket.STREAM_CODEC, + com.extendedae_plus.network.LabelNetworkActionC2SPacket::handle); + registrar.playToClient(com.extendedae_plus.network.LabelNetworkListS2CPacket.TYPE, + com.extendedae_plus.network.LabelNetworkListS2CPacket.STREAM_CODEC, + com.extendedae_plus.network.LabelNetworkListS2CPacket::handle); + registrar.playToServer(EAPConfigButtonPacket.TYPE, EAPConfigButtonPacket.STREAM_CODEC, EAPConfigButtonPacket::handleOnServer); } } diff --git a/src/main/java/com/extendedae_plus/integration/jade/LabeledWirelessTransceiverProvider.java b/src/main/java/com/extendedae_plus/integration/jade/LabeledWirelessTransceiverProvider.java new file mode 100644 index 0000000..de496f7 --- /dev/null +++ b/src/main/java/com/extendedae_plus/integration/jade/LabeledWirelessTransceiverProvider.java @@ -0,0 +1,68 @@ +package com.extendedae_plus.integration.jade; + +import appeng.api.networking.IGrid; +import appeng.api.networking.IGridNode; +import com.extendedae_plus.content.wireless.LabeledWirelessTransceiverBlockEntity; +import com.extendedae_plus.util.wireless.WirelessTeamUtil; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.resources.ResourceLocation; +import snownee.jade.api.BlockAccessor; +import snownee.jade.api.IServerDataProvider; + +/** + * 标签无线收发器:服务端数据同步(无主从)。 + */ +public enum LabeledWirelessTransceiverProvider implements IServerDataProvider { + INSTANCE; + + private static final ResourceLocation UID = new ResourceLocation("extendedae_plus", "labeled_wireless_info"); + + @Override + public ResourceLocation getUid() { + return UID; + } + + @Override + public void appendServerData(CompoundTag data, BlockAccessor accessor) { + if (!(accessor.getBlockEntity() instanceof LabeledWirelessTransceiverBlockEntity be)) return; + String label = be.getLabelForDisplay(); + if (label != null) { + data.putString("label", label); + } + // 所有者 + var placerId = be.getPlacerId(); + if (placerId != null && be.getServerLevel() != null) { + data.putUUID("placerId", placerId); + data.putString("ownerName", WirelessTeamUtil.getNetworkOwnerName(be.getServerLevel(), placerId).getString()); + } + + IGridNode node = be.getGridNode(); + IGrid grid = node == null ? null : node.getGrid(); + boolean networkUsable = false; + int usedChannels = 0; + int maxChannels = 0; + if (node != null && node.isActive()) { + for (var connection : node.getConnections()) { + usedChannels = Math.max(connection.getUsedChannels(), usedChannels); + } + if (node instanceof appeng.me.GridNode gridNode) { + var channelMode = gridNode.getGrid().getPathingService().getChannelMode(); + if (channelMode == appeng.api.networking.pathing.ChannelMode.INFINITE) { + maxChannels = -1; + } else { + maxChannels = gridNode.getMaxChannels(); + } + } + } + if (grid != null) { + try { + networkUsable = grid.getEnergyService().isNetworkPowered(); + } catch (Throwable ignored) { + networkUsable = false; + } + } + data.putInt("usedChannels", usedChannels); + data.putInt("maxChannels", maxChannels); + data.putBoolean("networkUsable", networkUsable); + } +} diff --git a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePlugin.java b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePlugin.java index 34159cd..23703f6 100644 --- a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePlugin.java +++ b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePlugin.java @@ -2,6 +2,12 @@ package com.extendedae_plus.integration.jade; import com.extendedae_plus.content.wireless.WirelessTransceiverBlock; import com.extendedae_plus.content.wireless.WirelessTransceiverBlockEntity; +import com.extendedae_plus.content.wireless.LabeledWirelessTransceiverBlock; +import com.extendedae_plus.content.wireless.LabeledWirelessTransceiverBlockEntity; +import com.extendedae_plus.integration.jade.LabeledWirelessTransceiverProvider; +import com.extendedae_plus.integration.jade.LabeledWirelessTransceiverComponents; +import com.extendedae_plus.integration.jade.WirelessTransceiverProvider; +import com.extendedae_plus.integration.jade.WirelessTransceiverJadePluginComponents; import snownee.jade.api.IWailaClientRegistration; import snownee.jade.api.IWailaCommonRegistration; import snownee.jade.api.IWailaPlugin; @@ -14,6 +20,7 @@ public class WirelessTransceiverJadePlugin implements IWailaPlugin { public void register(IWailaCommonRegistration registration) { // 注册服务端数据提供者(用于同步数据) registration.registerBlockDataProvider(WirelessTransceiverProvider.INSTANCE, WirelessTransceiverBlockEntity.class); + registration.registerBlockDataProvider(LabeledWirelessTransceiverProvider.INSTANCE, LabeledWirelessTransceiverBlockEntity.class); } @Override @@ -22,5 +29,6 @@ public class WirelessTransceiverJadePlugin implements IWailaPlugin { for (var component : WirelessTransceiverJadePluginComponents.values()) { registration.registerBlockComponent(component, WirelessTransceiverBlock.class); } + registration.registerBlockComponent(LabeledWirelessTransceiverComponents.LABEL_AND_CHANNEL, LabeledWirelessTransceiverBlock.class); } } \ No newline at end of file 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..7c5fe6c --- /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.RegistryFriendlyByteBuf; +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, RegistryFriendlyByteBuf 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 new file mode 100644 index 0000000..2b24048 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/LabelNetworkActionC2SPacket.java @@ -0,0 +1,57 @@ +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.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +/** + * C2S:标签无线收发器操作(设置/删除/断开)。 + */ +public record LabelNetworkActionC2SPacket(BlockPos pos, String label, Action action) implements CustomPacketPayload { + public enum Action { SET, DELETE, DISCONNECT } + + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(com.extendedae_plus.ExtendedAEPlus.MODID, "label_network_action")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeBlockPos(pkt.pos); + buf.writeUtf(pkt.label == null ? "" : pkt.label, 128); + buf.writeEnum(pkt.action); + }, + buf -> new LabelNetworkActionC2SPacket(buf.readBlockPos(), buf.readUtf(128), buf.readEnum(Action.class)) + ); + + @Override + public Type type() { + return TYPE; + } + + public static void handle(LabelNetworkActionC2SPacket packet, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + var player = ctx.player(); + if (player == null) return; + var level = player.serverLevel(); + if (!level.hasChunkAt(packet.pos)) return; + var be = level.getBlockEntity(packet.pos); + if (!(be instanceof LabeledWirelessTransceiverBlockEntity te)) return; + + switch (packet.action) { + case SET -> te.applyLabel(packet.label); + case DELETE -> { + String target = (packet.label == null || packet.label.isEmpty()) ? te.getLabelForDisplay() : packet.label; + if (target != null && !target.isEmpty()) { + LabelNetworkRegistry.get(level).removeNetwork(level, target, te.getPlacerId()); + } + te.clearLabel(); + } + case DISCONNECT -> te.clearLabel(); + } + }); + } +} 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..1492b33 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/LabelNetworkListC2SPacket.java @@ -0,0 +1,73 @@ +package com.extendedae_plus.network; + +import appeng.api.networking.pathing.ChannelMode; +import appeng.me.GridNode; +import com.extendedae_plus.ae.wireless.LabelNetworkRegistry; +import com.extendedae_plus.content.wireless.LabeledWirelessTransceiverBlockEntity; +import com.extendedae_plus.util.wireless.WirelessTeamUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.network.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +/** + * C2S:请求标签网络列表及当前标签信息。 + */ +public record LabelNetworkListC2SPacket(BlockPos pos) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(com.extendedae_plus.ExtendedAEPlus.MODID, "label_network_list")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> buf.writeBlockPos(pkt.pos), + buf -> new LabelNetworkListC2SPacket(buf.readBlockPos()) + ); + + @Override + public Type type() { + return TYPE; + } + + public static void handle(LabelNetworkListC2SPacket pkt, IPayloadContext ctx) { + ctx.enqueueWork(() -> { + var player = ctx.player(); + 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(); + String ownerName = te.getPlacerId() != null ? WirelessTeamUtil.getNetworkOwnerName(level, te.getPlacerId()).getString() : ""; + + int onlineCount = 0; + if (currentLabel != null && !currentLabel.isEmpty()) { + var network = LabelNetworkRegistry.get(level).getNetwork(level, currentLabel, te.getPlacerId()); + if (network != null) { + onlineCount = network.endpointCount(); + } + } + + int usedChannels = 0; + int maxChannels = 0; + var node = te.getGridNode(); + if (node != null && node.isActive()) { + for (var connection : node.getConnections()) { + usedChannels = Math.max(connection.getUsedChannels(), usedChannels); + } + if (node instanceof GridNode gridNode) { + var channelMode = gridNode.getGrid().getPathingService().getChannelMode(); + if (channelMode == ChannelMode.INFINITE) { + maxChannels = -1; + } else { + maxChannels = gridNode.getMaxChannels(); + } + } + } + + ctx.reply(new LabelNetworkListS2CPacket(pkt.pos, list, currentLabel == null ? "" : currentLabel, ownerName, usedChannels, maxChannels, onlineCount)); + }); + } +} 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..08fc16f --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/LabelNetworkListS2CPacket.java @@ -0,0 +1,79 @@ +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.RegistryFriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.api.distmarker.Dist; +import net.neoforged.api.distmarker.OnlyIn; +import net.neoforged.neoforge.network.handling.IPayloadContext; + +import java.util.ArrayList; +import java.util.List; + +/** + * S2C:标签网络列表下发。 + */ +public record LabelNetworkListS2CPacket(BlockPos pos, + List list, + String currentLabel, + String ownerName, + int usedChannels, + int maxChannels, + int onlineCount) implements CustomPacketPayload { + public static final Type TYPE = new Type<>( + ResourceLocation.fromNamespaceAndPath(com.extendedae_plus.ExtendedAEPlus.MODID, "label_network_list_s2c")); + + public static final StreamCodec STREAM_CODEC = StreamCodec.of( + (buf, pkt) -> { + buf.writeBlockPos(pkt.pos); + buf.writeUtf(pkt.currentLabel == null ? "" : pkt.currentLabel, 128); + buf.writeUtf(pkt.ownerName == null ? "" : pkt.ownerName, 128); + buf.writeVarInt(pkt.usedChannels); + buf.writeVarInt(pkt.maxChannels); + buf.writeVarInt(pkt.onlineCount); + buf.writeVarInt(pkt.list.size()); + for (var s : pkt.list) { + buf.writeUtf(s.label(), 128); + buf.writeLong(s.channel()); + } + }, + buf -> { + BlockPos pos = buf.readBlockPos(); + String curLabel = buf.readUtf(128); + String ownerName = buf.readUtf(128); + int usedChannels = buf.readVarInt(); + int maxChannels = buf.readVarInt(); + int onlineCount = buf.readVarInt(); + 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, ownerName, usedChannels, maxChannels, onlineCount); + } + ); + + @Override + public Type type() { + return TYPE; + } + + public static void handle(LabelNetworkListS2CPacket pkt, IPayloadContext ctx) { + ctx.enqueueWork(() -> handleClient(pkt)); + } + + @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.ownerName, pkt.usedChannels, pkt.maxChannels, pkt.onlineCount); + } + } +} diff --git a/src/main/resources/assets/extendedae_plus/blockstates/labeled_wireless_transceiver.json b/src/main/resources/assets/extendedae_plus/blockstates/labeled_wireless_transceiver.json new file mode 100644 index 0000000..8fa674d --- /dev/null +++ b/src/main/resources/assets/extendedae_plus/blockstates/labeled_wireless_transceiver.json @@ -0,0 +1,10 @@ +{ + "variants": { + "state=false": { + "model": "extendedae_plus:block/wirelesstransceiver/lable_wireless_transceiver_off" + }, + "state=true": { + "model": "extendedae_plus:block/wirelesstransceiver/lable_wireless_transceiver_on" + } + } +} 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 903726b..ac13263 100644 --- a/src/main/resources/assets/extendedae_plus/lang/en_us.json +++ b/src/main/resources/assets/extendedae_plus/lang/en_us.json @@ -117,6 +117,23 @@ "extendedae_plus.configuration.state_on": "On", "extendedae_plus.configuration.state_off": "Off", + "block.extendedae_plus.labeled_wireless_transceiver": "Labeled Wireless Transceiver", + "item.extendedae_plus.labeled_wireless_transceiver": "Labeled Wireless Transceiver", + + "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_owner": "Owner", + "gui.extendedae_plus.labeled_wireless.online_count": "Online", + + "extendedae_plus.jade.label": "Label: %s", + "extendedae_plus.jade.owner.public": "Owner: Public", + "extendedae_plus.jade.channels": "Channels: %s", + "extendedae_plus.jade.channels_of": "Channels: %s/%s", + "extendedae_plus.tooltip.owner": "Owner: %s", "extendedae_plus.tooltip.owner.public": "Owner: Public", "extendedae_plus.tooltip.channels": "Channels: %d", 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 94cf9d3..7df6a57 100644 --- a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json +++ b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json @@ -114,6 +114,26 @@ "extendedae_plus.configuration.state_on": "开", "extendedae_plus.configuration.state_off": "关", + "block.extendedae_plus.labeled_wireless_transceiver": "标签无线收发器", + "item.extendedae_plus.labeled_wireless_transceiver": "标签无线收发器", + + "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_owner": "所有者", + "gui.extendedae_plus.labeled_wireless.online_count": "在线数", + + "extendedae_plus.jade.label": "标签: %s", + "extendedae_plus.jade.owner": "所有者: %s", + "extendedae_plus.jade.owner.public": "所有者: 公共", + "extendedae_plus.jade.channels": "频道: %s", + "extendedae_plus.jade.channels_of": "频道: %s/%s", + "extendedae_plus.jade.online": "设备在线", + "extendedae_plus.jade.offline": "设备离线", + "extendedae_plus.tooltip.owner": "所有者: %s", "extendedae_plus.tooltip.owner.public": "所有者: 公共", "extendedae_plus.tooltip.channels": "频道: %d", diff --git a/src/main/resources/assets/extendedae_plus/models/block/wirelesstransceiver/lable_wireless_transceiver_off.json b/src/main/resources/assets/extendedae_plus/models/block/wirelesstransceiver/lable_wireless_transceiver_off.json new file mode 100644 index 0000000..c7f095b --- /dev/null +++ b/src/main/resources/assets/extendedae_plus/models/block/wirelesstransceiver/lable_wireless_transceiver_off.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "extendedae_plus:block/wireless_transceiver/lable_wireless_transceiver_off" + } +} + diff --git a/src/main/resources/assets/extendedae_plus/models/block/wirelesstransceiver/lable_wireless_transceiver_on.json b/src/main/resources/assets/extendedae_plus/models/block/wirelesstransceiver/lable_wireless_transceiver_on.json new file mode 100644 index 0000000..601d4c4 --- /dev/null +++ b/src/main/resources/assets/extendedae_plus/models/block/wirelesstransceiver/lable_wireless_transceiver_on.json @@ -0,0 +1,7 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "extendedae_plus:block/wireless_transceiver/lable_wireless_transceiver_on" + } +} + diff --git a/src/main/resources/assets/extendedae_plus/models/item/labeled_wireless_transceiver.json b/src/main/resources/assets/extendedae_plus/models/item/labeled_wireless_transceiver.json new file mode 100644 index 0000000..704d1b0 --- /dev/null +++ b/src/main/resources/assets/extendedae_plus/models/item/labeled_wireless_transceiver.json @@ -0,0 +1,3 @@ +{ + "parent": "extendedae_plus:block/wirelesstransceiver/lable_wireless_transceiver_off" +} diff --git a/src/main/resources/assets/extendedae_plus/textures/block/wireless_transceiver/lable_wireless_transceiver_off.png b/src/main/resources/assets/extendedae_plus/textures/block/wireless_transceiver/lable_wireless_transceiver_off.png new file mode 100644 index 0000000000000000000000000000000000000000..701f0fcbb3161c92e3a18f1693e4dea3a21d3fd4 GIT binary patch literal 409 zcmV;K0cQS*P)Y5QbM-6FtL$no0|~F2BXv?ixYU8Z6S8Dt0NtS=bzYG}qY)`7PsDu$Wdp0Cl5|clJcaAKp>!xGR{nE4c6Eg9 z5?9G&ECP^fbfi@95|qKUs^w1e|8S)YdZoZB*A%Q$%BmC;;Pve(=o|bgxlMafO{x4G zN$9fmiQa|p+Xz#w#P36VBG%Bx)E#0$wT9iSSv-iV*l@(R^m;&t6}I@TwHB3cz=OC7 zpQLGD71V_UEZ9_&{{jF2|Nl@czW@LL00v1!K~w_(fO5l>8~d`m00000NkvXXu0mjf DK~J`D literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/extendedae_plus/textures/block/wireless_transceiver/lable_wireless_transceiver_on.png b/src/main/resources/assets/extendedae_plus/textures/block/wireless_transceiver/lable_wireless_transceiver_on.png new file mode 100644 index 0000000000000000000000000000000000000000..906f82d536f594c5e6514fdd9e010a862bd069e4 GIT binary patch literal 499 zcmVRO7clOuGD@WioQ8U*N-TDdXA+7t z7Eqt_zb_pP(QH%=`1&8I_h+FJy2y@%?-j`$W+NXwZ3Od#Kg5KVTvZ1o>%i z4C~(S4qhDjM9-p5=8pjtN!m5AGPEVJYYaW7(`K{Y?v${ z)GQP~@^N!L13)3wsb0(0@VwW6I2XyS^`e3R{Cy1vQ=`LSO7S1M!cq z^Wa`2?7{hzvn`{{r-*1)%Ak*T(K5=$q6vdr#l_%0@B=3-E~Lw*Q4?8qr;t+1#9(d} p7lX6iXs9RsNfcFl1R*k$DZd{_(V0|?i^Ko`002ovPDHLkV1lKU;{yNy literal 0 HcmV?d00001 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..4a586ba0b3c01e62a5d55fd477853477839e3591 GIT binary patch literal 1452 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K5893O0R7}x|GzJFNL{AsTkcv5P?%H?9gvvBL z+&qDQLBwn)N8Ow+%s-ZI6c$yExXq!oh$HI8l8pfzoQiBBTlgdbJ(qK4c}R*lcc}<; zN3=hYUX$n4H2J1M_szL?cfS8Da`ViF=oq%RcRT+)n^UWASD#SJ;Q8C^-_MU%%$YMP zpFZ85$DVNbsrGfY3m={c|IYt=$Gky@vB7{uw4s%8$L3Oj)i3u)@4fqe^PX~Ri}^oI za<{#`KkXd@-=3fM628u{DJ*%nSNQ(ixs`vPy*{7&YrA}XQH_D@{MujVmc5T=+ORTx zUTu{@EfYf`5Jhtxc-(Jq+VJvb5W^bw#92Cw3O)>QbmPijHI|uTj0t^S(y@L5!Tr#Rp!$PF}P2 z?!W)X`oC_ik6(AcuKMxQx%Y4XnEUnX!DeHwGfHlaif4pizJq%c&UnLRljznMc|^lt zwopHtH^Z}-eMPw!FukY7V6&m$&c0$D}vB|A*F82oGZ)bKg zfV{Mm;dZwFTyBQwhcOLrH$Fed%CO865&ejeJfrcUMYlRUA?*A#rh>1pmNNSE18qLG zlVPtP#FU2euXH9mu*B#Nap2!fHm_+zNviF%4I@8Zb;>A0zG4e11|jJ})vj z+_=kN^Yh3*#w~Za4v5FkmvR8w(6V;#k3WrH8CBNuDx3z!&^xw-o8FD_b=EK6F_+yu zBYR-C{2u#h4{tnFX1-Bd?A`F%Jg;wV$W1G$u*P)Mq=%Lg&BPfN@48?p+yD$m-UZ2O z4Ab@I$UC&Zf8so&y+I(&U4divPUat&+Vl62G{p4H;>Y`{-o0deBRG!{M@)fZ z@J0+n&nu_nJ`8>GzbYAIjTwJ@z7o#-;=|k;j?V%Dhu(0oBpR19=7QtapsN