diff --git a/src/main/java/com/extendedae_plus/init/ModNetwork.java b/src/main/java/com/extendedae_plus/init/ModNetwork.java index 6a70eca..3480871 100644 --- a/src/main/java/com/extendedae_plus/init/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/init/ModNetwork.java @@ -72,6 +72,12 @@ public class ModNetwork { .consumerNetworkThread(SetPatternHighlightS2CPacket::handle) .add(); + CHANNEL.messageBuilder(com.extendedae_plus.network.SetBlockHighlightS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT) + .encoder(com.extendedae_plus.network.SetBlockHighlightS2CPacket::encode) + .decoder(com.extendedae_plus.network.SetBlockHighlightS2CPacket::decode) + .consumerNetworkThread(com.extendedae_plus.network.SetBlockHighlightS2CPacket::handle) + .add(); + CHANNEL.messageBuilder(SetProviderPageS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT) .encoder(SetProviderPageS2CPacket::encode) .decoder(SetProviderPageS2CPacket::decode) diff --git a/src/main/java/com/extendedae_plus/network/SetBlockHighlightS2CPacket.java b/src/main/java/com/extendedae_plus/network/SetBlockHighlightS2CPacket.java new file mode 100644 index 0000000..3b076b1 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/SetBlockHighlightS2CPacket.java @@ -0,0 +1,77 @@ +package com.extendedae_plus.network; + +import com.glodblock.github.extendedae.client.render.EAEHighlightHandler; +import com.glodblock.github.extendedae.util.FCClientUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.AABB; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +/** + * S2C: 指示客户端对某个方块位置进行高亮(仅作用于接收该包的客户端) + */ +public class SetBlockHighlightS2CPacket { + private final BlockPos pos; + private final Direction face; // 可为 null,表示方块形 + private final ResourceLocation dim; // 维度的 ResourceLocation + private final long durationMillis; // 持续时间(毫秒) + + public SetBlockHighlightS2CPacket(BlockPos pos, Direction face, ResourceLocation dim, long durationMillis) { + this.pos = pos; + this.face = face; + this.dim = dim; + this.durationMillis = durationMillis; + } + + public static void encode(SetBlockHighlightS2CPacket pkt, FriendlyByteBuf buf) { + buf.writeBlockPos(pkt.pos); + buf.writeBoolean(pkt.face != null); + if (pkt.face != null) buf.writeEnum(pkt.face); + buf.writeResourceLocation(pkt.dim); + buf.writeLong(pkt.durationMillis); + } + + public static SetBlockHighlightS2CPacket decode(FriendlyByteBuf buf) { + var pos = buf.readBlockPos(); + Direction face = null; + if (buf.readBoolean()) face = buf.readEnum(Direction.class); + var dim = buf.readResourceLocation(); + long dur = buf.readLong(); + return new SetBlockHighlightS2CPacket(pos, face, dim, dur); + } + + public static void handle(SetBlockHighlightS2CPacket msg, Supplier ctxSupplier) { + var ctx = ctxSupplier.get(); + ctx.enqueueWork(() -> { + try { + // 在客户端执行高亮 + ResourceKey dimKey = ResourceKey.create(Registries.DIMENSION, msg.dim); + long endTime = System.currentTimeMillis() + msg.durationMillis; + if (msg.face == null) { + EAEHighlightHandler.highlight(msg.pos, dimKey, endTime); + } else { + var origin = new AABB(msg.pos); + switch (msg.face) { + case WEST -> origin = FCClientUtil.rotor(origin, origin.getCenter(), Direction.Axis.Y, (float) (Math.PI / 2)); + case SOUTH -> origin = FCClientUtil.rotor(origin, origin.getCenter(), Direction.Axis.Y, (float) Math.PI); + case EAST -> origin = FCClientUtil.rotor(origin, origin.getCenter(), Direction.Axis.Y, (float) (-Math.PI / 2)); + case UP -> origin = FCClientUtil.rotor(origin, origin.getCenter(), Direction.Axis.X, (float) (-Math.PI / 2)); + case DOWN -> origin = FCClientUtil.rotor(origin, origin.getCenter(), Direction.Axis.X, (float) (Math.PI / 2)); + } + EAEHighlightHandler.highlight(msg.pos, msg.face, dimKey, endTime, origin); + } + } catch (Throwable ignored) { + } + }); + ctx.setPacketHandled(true); + } +} + + diff --git a/src/main/java/com/extendedae_plus/network/crafting/CraftingMonitorOpenProviderC2SPacket.java b/src/main/java/com/extendedae_plus/network/crafting/CraftingMonitorOpenProviderC2SPacket.java index c962b77..7fea21f 100644 --- a/src/main/java/com/extendedae_plus/network/crafting/CraftingMonitorOpenProviderC2SPacket.java +++ b/src/main/java/com/extendedae_plus/network/crafting/CraftingMonitorOpenProviderC2SPacket.java @@ -5,41 +5,34 @@ import appeng.api.networking.IGrid; import appeng.api.networking.security.IActionHost; import appeng.api.stacks.AEKey; import appeng.helpers.patternprovider.PatternProviderLogic; -import appeng.helpers.patternprovider.PatternProviderLogicHost; import appeng.me.service.CraftingService; -import appeng.menu.AEBaseMenu; import appeng.menu.locator.MenuLocators; import appeng.menu.me.crafting.CraftingCPUMenu; import appeng.parts.AEBasePart; import com.extendedae_plus.init.ModNetwork; import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor; +import com.extendedae_plus.network.SetBlockHighlightS2CPacket; import com.extendedae_plus.network.SetPatternHighlightS2CPacket; import com.extendedae_plus.network.provider.SetProviderPageS2CPacket; import com.extendedae_plus.util.PatternProviderDataUtil; -import com.glodblock.github.extendedae.util.FCClientUtil; import com.glodblock.github.glodium.util.GlodUtil; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.chat.Component; -import net.minecraft.resources.ResourceKey; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.entity.player.Player; -import net.minecraft.world.level.Level; -import net.minecraft.world.phys.AABB; import net.minecraftforge.network.NetworkDirection; import net.minecraftforge.network.NetworkEvent; import java.util.Collection; -import java.util.Objects; import java.util.function.Supplier; -import static com.glodblock.github.extendedae.client.render.EAEHighlightHandler.highlight; - /** - * 客户端从 CraftingCPUScreen 发送:鼠标下条目对应的 AEKey。 - * 服务端在当前打开的 CraftingCPUMenu 所属网络中,定位匹配该 AEKey 的样板供应器, - * 打开该供应器自身的 UI(不是目标机器的 UI)。 + * 网络包:客户端向服务器发送请求,打开与 AEKey 对应的 PatternProvider UI。 + *

+ * 流程: + * 1. 客户端发送 AEKey。 + * 2. 服务端根据当前 CraftingCPUMenu 获取对应 Grid。 + * 3. 定位所有提供该 AEKey 的 PatternProvider。 + * 4. 打开 Provider UI,并向客户端发送高亮和页码信息。 */ public class CraftingMonitorOpenProviderC2SPacket { private final AEKey what; @@ -53,118 +46,160 @@ public class CraftingMonitorOpenProviderC2SPacket { } public static CraftingMonitorOpenProviderC2SPacket decode(FriendlyByteBuf buf) { - AEKey key = AEKey.readKey(buf); - return new CraftingMonitorOpenProviderC2SPacket(key); + return new CraftingMonitorOpenProviderC2SPacket(AEKey.readKey(buf)); } public static void handle(CraftingMonitorOpenProviderC2SPacket msg, Supplier ctx) { - NetworkEvent.Context context = ctx.get(); + var context = ctx.get(); context.enqueueWork(() -> { ServerPlayer player = context.getSender(); - if (player == null) return; + if (player == null || !(player.containerMenu instanceof CraftingCPUMenu menu)) return; - // 必须在 CraftingCPU 界面内 - if (!(player.containerMenu instanceof CraftingCPUMenu menu)) { - return; - } - - // 通过菜单的 target(可能是 BlockEntity/Part/ItemHost),按 IActionHost 获取 Grid - IGrid grid = null; - Object target = ((AEBaseMenu) menu).getTarget(); - if (target instanceof IActionHost host && host.getActionableNode() != null) { - grid = host.getActionableNode().getGrid(); - } - if (grid == null) { - return; - } + // 从菜单获取 Grid + IGrid grid = GridHelper.getGridFromMenu(menu); + if (grid == null) return; + // 获取 CraftingService var cs = grid.getCraftingService(); - if (!(cs instanceof CraftingService craftingService)) { - return; - } + if (!(cs instanceof CraftingService craftingService)) return; - // 1) 根据 AEKey 找到可能的样板(pattern) + // 根据 AEKey 查找所有匹配样板 Collection patterns = craftingService.getCraftingFor(msg.what); - if (patterns.isEmpty()) { - return; - } + if (patterns.isEmpty()) return; - // 2) 遍历提供该样板的 Provider,定位 PatternProviderLogic + // 遍历所有样板,找到第一个可用 Provider 并打开 UI for (var pattern : patterns) { - var providers = craftingService.getProviders(pattern); - for (var provider : providers) { - if (provider instanceof PatternProviderLogic ppl) { - // accessor 获取 host - PatternProviderLogicHost host = ((PatternProviderLogicAccessor) ppl).eap$host(); - if (host == null) continue; - var pbe = host.getBlockEntity(); - if (pbe == null) continue; + var provider = PatternLocator.findValidProvider(craftingService, pattern, grid); + if (provider == null) continue; - // 跳过未连接到网格或不活跃的 provider(使用 util 判断并传入当前 grid) - if (!PatternProviderDataUtil.isProviderAvailable(ppl, grid)) continue; - - // 直接打开供应器自身的 UI(调用 Host 默认方法) - try { - // 部件与方块实体分别选择定位器并打开界面 - if (host instanceof AEBasePart part) { - host.openMenu(player, MenuLocators.forPart(part)); - highlightWithMessage(pbe.getBlockPos(), part.getSide(), Objects.requireNonNull(pbe.getLevel()).dimension(), 1.0, player); - } else { - host.openMenu(player, MenuLocators.forBlockEntity(pbe)); - highlightWithMessage(pbe.getBlockPos(), null, Objects.requireNonNull(pbe.getLevel()).dimension(), 1.0, player); - } - - // 高亮打开的供应器位置并发送聊天提示 - - - // 先在该 provider 中定位 pattern 的槽位索引,以便计算页码(尽量早退出,按槽位逐个解码) - int foundSlot = PatternProviderDataUtil.findSlotForPattern(ppl, pattern.getDefinition()); - if (foundSlot >= 0) { - int pageId = foundSlot / 36; - if (pageId > 0) { - // 发送 S2C 包通知客户端切换到指定页(客户端会写入 mixin 字段并重排槽位) - ModNetwork.CHANNEL.sendTo(new SetProviderPageS2CPacket(pageId), player.connection.connection, NetworkDirection.PLAY_TO_CLIENT); - } - } - - // 最后发送高亮包,保证界面已打开 - if (pattern.getOutputs() != null && pattern.getOutputs().length > 0 && pattern.getOutputs()[0] != null) { - AEKey key = pattern.getOutputs()[0].what(); - ModNetwork.CHANNEL.sendTo(new SetPatternHighlightS2CPacket(key, true), player.connection.connection, NetworkDirection.PLAY_TO_CLIENT); - } - - return; - } catch (Exception ignored) { - } - } - } + try { + ProviderUIHelper.openProviderUI(provider, pattern, player); + } catch (Exception ignored) {} } }); context.setPacketHandled(true); } - private static void highlightWithMessage(BlockPos pos, Direction face, ResourceKey dim, double multiplier, Player player) { - if (pos == null || dim == null) { - return; - } - long endTime = System.currentTimeMillis() + (long) (6000 * GlodUtil.clamp(multiplier, 1, 30)); - if (face == null) { - highlight(pos, dim, endTime); - } else { - var origin = new AABB(2 / 16D, 2 / 16D, 0, 14 / 16D, 14 / 16D, 2 / 16D).move(pos); - var center = new AABB(pos).getCenter(); - switch (face) { - case WEST -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.Y, (float) (Math.PI / 2)); - case SOUTH -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.Y, (float) Math.PI); - case EAST -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.Y, (float) (-Math.PI / 2)); - case UP -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.X, (float) (-Math.PI / 2)); - case DOWN -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.X, (float) (Math.PI / 2)); - } - highlight(pos, face, dim, endTime, origin); - } + // ===================== 内部工具类 ===================== - if (player != null) { - player.displayClientMessage(Component.translatable("chat.ex_pattern_access_terminal.pos", pos.toShortString(), dim.location().getPath()), false); + /** + * GridHelper: 从菜单中获取网格实例 + */ + public static final class GridHelper { + private GridHelper() {} + + /** + * 获取菜单对应的 Grid + * @param menu 当前 AEBaseMenu + * @return Grid 或 null + */ + public static IGrid getGridFromMenu(appeng.menu.AEBaseMenu menu) { + Object target = menu.getTarget(); + if (target instanceof IActionHost host && host.getActionableNode() != null) { + return host.getActionableNode().getGrid(); + } + return null; + } + } + + /** + * PatternLocator: 根据样板定位可用的 Provider + */ + public static final class PatternLocator { + private PatternLocator() {} + + /** + * 查找提供指定样板的可用 Provider + * @param cs CraftingService + * @param pattern 样板 + * @param grid 当前 Grid + * @return 第一个可用的 PatternProviderLogic 或 null + */ + public static PatternProviderLogic findValidProvider(CraftingService cs, IPatternDetails pattern, IGrid grid) { + var providers = cs.getProviders(pattern); + for (var provider : providers) { + if (provider instanceof PatternProviderLogic ppl) { + var host = ((PatternProviderLogicAccessor) ppl).eap$host(); + if (host == null || host.getBlockEntity() == null) continue; + if (!PatternProviderDataUtil.isProviderAvailable(ppl, grid)) continue; + return ppl; + } + } + return null; + } + } + + /** + * ProviderUIHelper: 打开 Provider UI 并发送客户端反馈 + */ + public static final class ProviderUIHelper { + private ProviderUIHelper() {} + + /** + * 打开 Provider UI + * 1. 打开菜单 + * 2. 发送高亮包 + * 3. 发送页码包 + * 4. 发送 Pattern 输出高亮包 + * + * @param provider PatternProviderLogic 实例 + * @param pattern 样板 + * @param player 玩家 + */ + public static void openProviderUI(PatternProviderLogic provider, IPatternDetails pattern, ServerPlayer player) { + var host = ((PatternProviderLogicAccessor) provider).eap$host(); + var pbe = host.getBlockEntity(); + if (pbe == null) return; + + boolean isPart = host instanceof AEBasePart part; + var locator = isPart ? MenuLocators.forPart((AEBasePart) host) : MenuLocators.forBlockEntity(pbe); + host.openMenu(player, locator); + + // 高亮显示 + ModNetwork.CHANNEL.sendTo( + new SetBlockHighlightS2CPacket( + pbe.getBlockPos(), + isPart ? ((AEBasePart) host).getSide() : null, + pbe.getLevel().dimension().location(), + (long) (6000 * GlodUtil.clamp(1.0, 1, 30)) + ), + player.connection.connection, + NetworkDirection.PLAY_TO_CLIENT + ); + + // 聊天提示 + player.displayClientMessage( + Component.translatable( + "chat.ex_pattern_access_terminal.pos", + pbe.getBlockPos().toShortString(), + pbe.getLevel().dimension().location().getPath() + ), + false + ); + + // 页码同步 + int slot = PatternProviderDataUtil.findSlotForPattern(provider, pattern.getDefinition()); + if (slot >= 0) { + int page = slot / 36; + if (page > 0) { + ModNetwork.CHANNEL.sendTo( + new SetProviderPageS2CPacket(page), + player.connection.connection, + NetworkDirection.PLAY_TO_CLIENT + ); + } + } + + // 输出高亮 + var outputs = pattern.getOutputs(); + if (outputs != null && outputs.length > 0 && outputs[0] != null) { + AEKey key = outputs[0].what(); + ModNetwork.CHANNEL.sendTo( + new SetPatternHighlightS2CPacket(key, true), + player.connection.connection, + NetworkDirection.PLAY_TO_CLIENT + ); + } } } }