diff --git a/src/main/java/com/extendedae_plus/content/ClientPatternHighlightStore.java b/src/main/java/com/extendedae_plus/content/ClientPatternHighlightStore.java new file mode 100644 index 0000000..2e9f0c4 --- /dev/null +++ b/src/main/java/com/extendedae_plus/content/ClientPatternHighlightStore.java @@ -0,0 +1,34 @@ +package com.extendedae_plus.content; + +import appeng.api.stacks.AEKey; +import appeng.api.crafting.IPatternDetails; +import appeng.client.gui.me.patternaccess.PatternContainerRecord; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +/** + * 客户端本地的高亮存储,仅作用于接收该客户端。 + * 使用 AEKey 作为标识,渲染时可通过 AEKey 与本地解码的 IPatternDetails 比对。 + */ +public final class ClientPatternHighlightStore { + private static final Set HIGHLIGHTS = Collections.synchronizedSet(new HashSet<>()); + + private ClientPatternHighlightStore() {} + + public static void setHighlight(AEKey key, boolean highlight) { + if (key == null) return; + if (highlight) HIGHLIGHTS.add(key); + else HIGHLIGHTS.remove(key); + } + + public static boolean hasHighlight(AEKey key) { + if (key == null) return false; + return HIGHLIGHTS.contains(key); + } + + public static void clearAll() { HIGHLIGHTS.clear(); } +} + + diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/AEProcessingPatternMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/AEProcessingPatternMixin.java index 4381601..28030b2 100644 --- a/src/main/java/com/extendedae_plus/mixin/ae2/AEProcessingPatternMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/ae2/AEProcessingPatternMixin.java @@ -8,7 +8,7 @@ import org.spongepowered.asm.mixin.Unique; @Mixin(value = AEProcessingPattern.class, remap = false) public class AEProcessingPatternMixin implements SmartDoublingAwarePattern { @Unique - private boolean eap$allowScaling = true; // 默认允许缩放 + private boolean eap$allowScaling = false; // 默认不允许缩放 @Override public boolean eap$allowScaling() { diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/accessor/PatternProviderLogicAccessor.java b/src/main/java/com/extendedae_plus/mixin/ae2/accessor/PatternProviderLogicAccessor.java index 7cba84b..1c93b75 100644 --- a/src/main/java/com/extendedae_plus/mixin/ae2/accessor/PatternProviderLogicAccessor.java +++ b/src/main/java/com/extendedae_plus/mixin/ae2/accessor/PatternProviderLogicAccessor.java @@ -1,5 +1,6 @@ package com.extendedae_plus.mixin.ae2.accessor; +import appeng.api.networking.IManagedGridNode; import appeng.helpers.patternprovider.PatternProviderLogic; import appeng.helpers.patternprovider.PatternProviderLogicHost; import org.spongepowered.asm.mixin.Mixin; @@ -9,4 +10,7 @@ import org.spongepowered.asm.mixin.gen.Accessor; public interface PatternProviderLogicAccessor { @Accessor("host") PatternProviderLogicHost eap$host(); + + @Accessor("mainNode") + IManagedGridNode eap$mainNode(); } diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/autopattern/adaptation/AdvPatternProviderLogicContainsRedirectMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/autopattern/adaptation/AdvPatternProviderLogicContainsRedirectMixin.java deleted file mode 100644 index 1ff3e78..0000000 --- a/src/main/java/com/extendedae_plus/mixin/ae2/autopattern/adaptation/AdvPatternProviderLogicContainsRedirectMixin.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.extendedae_plus.mixin.ae2.autopattern.adaptation; - -import appeng.api.crafting.IPatternDetails; -import com.extendedae_plus.content.ScaledProcessingPattern; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Pseudo; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Redirect; - -import java.util.List; - -/**适配 - * Redirect PatternProviderLogic.pushPattern 中对 List.contains 的调用, - * 在遇到缩放样板时回退匹配到原始样板实例。 - */ -@Pseudo -@Mixin(targets = "net.pedroksl.advanced_ae.common.logic.AdvPatternProviderLogic", remap = false) -public class AdvPatternProviderLogicContainsRedirectMixin { - - @Redirect(method = "pushPattern", - at = @At( - value = "INVOKE", - target = "Ljava/util/List;contains(Ljava/lang/Object;)Z") - ) - private boolean eap$patternsContains(List list, Object o) { - try { - if (o instanceof ScaledProcessingPattern scaled) { - IPatternDetails base = scaled.getOriginal(); - if (base != null && list.indexOf(base) != -1) { - return true; - } - } - // 使用 indexOf 避免再次触发对 List.contains 的 redirect(防止递归) - return list.indexOf(o) != -1; - } catch (Throwable t) { - return list.indexOf(o) != -1; - } - } -} \ No newline at end of file diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/AEBaseScreenMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/AEBaseScreenMixin.java index 880b6c0..b741be3 100644 --- a/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/AEBaseScreenMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/AEBaseScreenMixin.java @@ -1,10 +1,12 @@ package com.extendedae_plus.mixin.ae2.client.gui; +import appeng.api.crafting.PatternDetailsHelper; import appeng.api.stacks.AEKey; import appeng.client.Point; import appeng.client.gui.AEBaseScreen; import appeng.client.gui.StackWithBounds; import appeng.client.gui.TextOverride; +import appeng.client.gui.implementations.PatternProviderScreen; import appeng.client.gui.me.crafting.CraftingCPUScreen; import appeng.client.gui.style.PaletteColor; import appeng.client.gui.style.ScreenStyle; @@ -12,12 +14,14 @@ import appeng.client.gui.style.Text; import appeng.client.gui.style.TextAlignment; import appeng.menu.slot.AppEngSlot; import com.extendedae_plus.api.ExPatternPageAccessor; +import com.extendedae_plus.content.ClientPatternHighlightStore; import com.extendedae_plus.network.CraftingMonitorJumpC2SPacket; import com.extendedae_plus.network.CraftingMonitorOpenProviderC2SPacket; import com.extendedae_plus.network.ModNetwork; import com.extendedae_plus.util.GuiUtil; import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider; import com.mojang.logging.LogUtils; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.renderer.Rect2i; @@ -26,6 +30,7 @@ import net.minecraft.network.chat.contents.TranslatableContents; import net.minecraft.world.inventory.Slot; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -150,40 +155,54 @@ public abstract class AEBaseScreenMixin { @Inject(method = "renderSlot", at = @At("TAIL")) private void eap$renderSlotAmounts(GuiGraphics guiGraphics, Slot s, CallbackInfo ci) { Object self = this; - + // 只处理AppEngSlot类型的槽位 if (!(s instanceof AppEngSlot appEngSlot)) { return; } - + // 检查槽位是否可见且有效 if (!appEngSlot.isActive() || !appEngSlot.isSlotEnabled()) { return; } - + // 获取槽位中的物品 var itemStack = appEngSlot.getItem(); if (itemStack.isEmpty()) { return; } - + // 使用GuiUtil的格式化方法获取数量文本 String amountText = GuiUtil.getPatternOutputText(itemStack); if (amountText.isEmpty()) { return; } - + // 在槽位右下角绘制数量文本 Font font = eap$getFont(self); GuiUtil.drawAmountText(guiGraphics, font, amountText, appEngSlot.x, appEngSlot.y, 0.6f); + + try { + var details = PatternDetailsHelper.decodePattern(itemStack, Minecraft.getInstance().level, false); + try { + if (details != null && details.getOutputs() != null && details.getOutputs().length > 0) { + AEKey key = details.getOutputs()[0].what(); + if (key != null && ClientPatternHighlightStore.hasHighlight(key)) { + try { + GuiUtil.drawSlotRainbowHighlight(guiGraphics, s.x, s.y); + } catch (Throwable ignored) {} + } + } + } catch (Throwable ignore) {} + } catch (Throwable ignore) {} } // 在 AEBaseScreen.drawText 完成某个文本绘制后,若该文本为“样板”标签,则紧接着绘制页码。 @Inject(method = "drawText", at = @At("TAIL"), remap = false) private void eap$appendPageAfterPatternsLabel(GuiGraphics guiGraphics, - Text text, - @Nullable TextOverride override, - CallbackInfo ci) { + Text text, + @Nullable TextOverride override, + CallbackInfo ci) { Object self = this; if (!(self instanceof GuiExPatternProvider)) { return; @@ -237,8 +256,10 @@ public abstract class AEBaseScreenMixin { if (!isPatterns) { String label = content.getString(); if (label != null) { - if (label.equals(Component.translatable("gui.pattern_provider.patterns").getString())) isPatterns = true; - else if (label.equals(Component.translatable("gui.extendedae.patterns").getString())) isPatterns = true; + if (label.equals(Component.translatable("gui.pattern_provider.patterns").getString())) + isPatterns = true; + else if (label.equals(Component.translatable("gui.extendedae.patterns").getString())) + isPatterns = true; else if (label.equals(Component.translatable("gui.ae2.patterns").getString())) isPatterns = true; } } @@ -263,16 +284,18 @@ public abstract class AEBaseScreenMixin { if (v instanceof Integer i) { max = Math.max(1, i); } - } catch (Throwable ignored) {} + } catch (Throwable ignored) { + } - String pageText = "第"+cur+"页" + "/" + max + "页"; + String pageText = "第" + cur + "页" + "/" + max + "页"; ScreenStyle style = eap$getStyle(self); int color = 0xFFFFFFFF; if (style != null) { try { color = style.getColor(PaletteColor.DEFAULT_TEXT_COLOR).toARGB(); - } catch (Throwable ignored) {} + } catch (Throwable ignored) { + } } int padding = 4; if (scale == 1.0f) { @@ -284,6 +307,24 @@ public abstract class AEBaseScreenMixin { guiGraphics.drawString(font, pageText, lineWidth + padding, 0, color, false); guiGraphics.pose().popPose(); } + } catch (Throwable ignored) { + } + } + + + @Shadow + protected void setTextContent(String id, Component content) {}; + + @Inject(method = "updateBeforeRender", at = @At("RETURN"), remap = false) + private void onUpdateBeforeRender(CallbackInfo ci) { + try { + AEBaseScreen self = (AEBaseScreen) (Object) this; + if (self instanceof PatternProviderScreen screen){ + Component t = screen.getTitle(); + if (t != null && !t.getString().isEmpty()) { + this.setTextContent(AEBaseScreen.TEXT_ID_DIALOG_TITLE, t); + } + } } catch (Throwable ignored) {} } } diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/PatternProviderCloseMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/PatternProviderCloseMixin.java new file mode 100644 index 0000000..767e888 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/client/gui/PatternProviderCloseMixin.java @@ -0,0 +1,28 @@ +package com.extendedae_plus.mixin.ae2.client.gui; + +import appeng.client.gui.implementations.PatternProviderScreen; +import com.extendedae_plus.content.ClientPatternHighlightStore; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.world.inventory.AbstractContainerMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(value = AbstractContainerScreen.class, remap = false) +public class PatternProviderCloseMixin { + + @Shadow + protected AbstractContainerMenu menu; + + @Inject(method = "removed", at = @At("HEAD")) + private void onRemoved(CallbackInfo ci) { + try { + if (((Object) this) instanceof PatternProviderScreen) { + ClientPatternHighlightStore.clearAll(); + } + } catch (Throwable ignored) { + } + } +} \ No newline at end of file diff --git a/src/main/java/com/extendedae_plus/mixin/extendedae/client/gui/GuiExPatternProviderMixin.java b/src/main/java/com/extendedae_plus/mixin/extendedae/client/gui/GuiExPatternProviderMixin.java index c4286d4..42b1ce2 100644 --- a/src/main/java/com/extendedae_plus/mixin/extendedae/client/gui/GuiExPatternProviderMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/extendedae/client/gui/GuiExPatternProviderMixin.java @@ -21,6 +21,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.lang.reflect.Field; +import java.lang.reflect.Method; import static com.extendedae_plus.util.ExtendedAELogger.LOGGER; @@ -49,8 +50,8 @@ public abstract class GuiExPatternProviderMixin extends PatternProviderScreen super(menu, playerInventory, title, style); } - @Unique - private static int eap$withAlpha(int rgb, int alpha255) { - return ((alpha255 & 0xFF) << 24) | (rgb & 0x00FFFFFF); - } - - /** - * 将 HSV 转换为 RGB(返回 0xRRGGBB,不含 alpha)。 - * h: 0.0~1.0,s: 0.0~1.0,v: 0.0~1.0 - */ - @Unique - private static int eap$hsvToRgb(float h, float s, float v) { - if (s <= 0.0f) { - int g = Math.round(v * 255.0f); - return (g << 16) | (g << 8) | g; - } - float hh = (h - (float) Math.floor(h)) * 6.0f; - int sector = (int) Math.floor(hh); - float f = hh - sector; - float p = v * (1.0f - s); - float q = v * (1.0f - s * f); - float t = v * (1.0f - s * (1.0f - f)); - float r, g, b; - switch (sector) { - case 0: r = v; g = t; b = p; break; - case 1: r = q; g = v; b = p; break; - case 2: r = p; g = v; b = t; break; - case 3: r = p; g = q; b = v; break; - case 4: r = t; g = p; b = v; break; - default: r = v; g = p; b = q; break; - } - int ri = Math.round(r * 255.0f); - int gi = Math.round(g * 255.0f); - int bi = Math.round(b * 255.0f); - return (ri << 16) | (gi << 8) | bi; - } /** * 获取当前选择的样板供应器ID @@ -579,53 +544,9 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen return; } - // 彩虹色的流转:基于时间在 HSV 色环上循环(4 秒为一周期) - long now = System.currentTimeMillis(); - final long rainbowPeriodMs = 4000L; - float hue = (now % rainbowPeriodMs) / (float) rainbowPeriodMs; // 0.0 ~ 1.0 - int rainbowRgb = eap$hsvToRgb(hue, 1.0f, 1.0f); + // 使用 GuiUtil 的通用绘制方法绘制槽位高亮(包含彩虹流转效果) + GuiUtil.drawPatternSlotHighlights(guiGraphics, this.menu.slots, this.matchedStack, this.matchedProvider); - for (Slot slot : this.menu.slots) { - if (!(slot instanceof PatternSlot ps)) { - continue; - } - - int sx = slot.x; - int sy = slot.y; - - boolean isMatchedSlot = this.matchedStack != null && this.matchedStack.contains(slot.getItem()); - boolean isMatchedProvider = false; - try { - PatternContainerRecord container = ps.getMachineInv(); - isMatchedProvider = this.matchedProvider != null && this.matchedProvider.contains(container); - } catch (Throwable ignored) { - } - - // 依据命中状态选择颜色方案 - int borderColor; - int backgroundColor; - - if (isMatchedSlot) { - // 命中槽位:使用彩虹色边框与浅底色(固定透明度,呈现色相流转效果) - borderColor = eap$withAlpha(rainbowRgb, 0xA0); - backgroundColor = eap$withAlpha(rainbowRgb, 0x3C); - } else if (!isMatchedProvider) { - borderColor = eap$withAlpha(0xFFFFFF, 0x40); - backgroundColor = eap$withAlpha(0x000000, 0x18); - } else { - borderColor = eap$withAlpha(0xFFFFFF, 0x30); - backgroundColor = eap$withAlpha(0xFFFFFF, 0x14); - } - - // 绘制 18x18 边框(1px 宽) - eap$fill(guiGraphics, new Rect2i(sx - 1, sy - 1, 18, 1), borderColor); - eap$fill(guiGraphics, new Rect2i(sx - 1, sy + 16, 18, 1), borderColor); - eap$fill(guiGraphics, new Rect2i(sx - 1, sy, 1, 16), borderColor); - eap$fill(guiGraphics, new Rect2i(sx + 16, sy, 1, 16), borderColor); - - // 绘制 16x16 浅底色(半透明,叠加在槽位上方) - eap$fill(guiGraphics, new Rect2i(sx, sy, 16, 16), backgroundColor); - } } @Unique diff --git a/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java b/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java index b1329fe..6680f2f 100644 --- a/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java +++ b/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java @@ -12,14 +12,27 @@ import appeng.menu.locator.MenuLocators; import appeng.menu.me.crafting.CraftingCPUMenu; import appeng.parts.AEBasePart; import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor; -import com.mojang.logging.LogUtils; +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 的样板供应器, @@ -47,11 +60,8 @@ public class CraftingMonitorOpenProviderC2SPacket { ServerPlayer player = context.getSender(); if (player == null) return; - LogUtils.getLogger().info("EAP[S]: recv CraftingMonitorOpenProviderC2SPacket key={} from {}", msg.what, player.getGameProfile().getName()); - // 必须在 CraftingCPU 界面内 if (!(player.containerMenu instanceof CraftingCPUMenu menu)) { - LogUtils.getLogger().info("EAP[S]: not in CraftingCPUMenu, abort"); return; } @@ -62,19 +72,16 @@ public class CraftingMonitorOpenProviderC2SPacket { grid = host.getActionableNode().getGrid(); } if (grid == null) { - LogUtils.getLogger().info("EAP[S]: grid is null, abort"); return; } var cs = grid.getCraftingService(); if (!(cs instanceof CraftingService craftingService)) { - LogUtils.getLogger().info("EAP[S]: craftingService is null/unsupported, abort"); return; } // 1) 根据 AEKey 找到可能的样板(pattern) Collection patterns = craftingService.getCraftingFor(msg.what); - LogUtils.getLogger().info("EAP[S]: patterns found={} for key={}", patterns.size(), msg.what); if (patterns.isEmpty()) { return; } @@ -89,29 +96,72 @@ public class CraftingMonitorOpenProviderC2SPacket { if (host == null) continue; var pbe = host.getBlockEntity(); if (pbe == null) continue; - // 在服务端上下文中执行,pbe 仅用于构造菜单定位器 + + // 跳过未连接到网格或不活跃的 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); } - context.setPacketHandled(true); + + // 高亮打开的供应器位置并发送聊天提示 + + + // 先在该 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 (Throwable t) { - LogUtils.getLogger().error("EAP[S]: open provider UI failed at {}", pbe.getBlockPos(), t); + } catch (Exception ignored) { } } } } - - LogUtils.getLogger().info("EAP[S]: no provider UI opened for key={}", msg.what); }); 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); + } + } } diff --git a/src/main/java/com/extendedae_plus/network/ModNetwork.java b/src/main/java/com/extendedae_plus/network/ModNetwork.java index a869cfe..78e3adb 100644 --- a/src/main/java/com/extendedae_plus/network/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/network/ModNetwork.java @@ -60,6 +60,18 @@ public class ModNetwork { .consumerNetworkThread(ProvidersListS2CPacket::handle) .add(); + CHANNEL.messageBuilder(SetPatternHighlightS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT) + .encoder(SetPatternHighlightS2CPacket::encode) + .decoder(SetPatternHighlightS2CPacket::decode) + .consumerNetworkThread(SetPatternHighlightS2CPacket::handle) + .add(); + + CHANNEL.messageBuilder(SetProviderPageS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT) + .encoder(SetProviderPageS2CPacket::encode) + .decoder(SetProviderPageS2CPacket::decode) + .consumerNetworkThread(SetProviderPageS2CPacket::handle) + .add(); + CHANNEL.messageBuilder(ToggleAdvancedBlockingC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) .encoder(ToggleAdvancedBlockingC2SPacket::encode) .decoder(ToggleAdvancedBlockingC2SPacket::decode) diff --git a/src/main/java/com/extendedae_plus/network/SetPatternHighlightS2CPacket.java b/src/main/java/com/extendedae_plus/network/SetPatternHighlightS2CPacket.java new file mode 100644 index 0000000..6da5fda --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/SetPatternHighlightS2CPacket.java @@ -0,0 +1,45 @@ +package com.extendedae_plus.network; + +import appeng.api.stacks.AEKey; +import com.extendedae_plus.content.ClientPatternHighlightStore; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +/** + * S2C: 指示客户端对某个 AEKey 的样板进行高亮/取消高亮(仅作用于接收该包的客户端) + */ +public class SetPatternHighlightS2CPacket { + private final AEKey key; + private final boolean highlight; + + public SetPatternHighlightS2CPacket(AEKey key, boolean highlight) { + this.key = key; + this.highlight = highlight; + } + + public static void encode(SetPatternHighlightS2CPacket msg, FriendlyByteBuf buf) { + AEKey.writeKey(buf, msg.key); + buf.writeBoolean(msg.highlight); + } + + public static SetPatternHighlightS2CPacket decode(FriendlyByteBuf buf) { + AEKey key = AEKey.readKey(buf); + boolean h = buf.readBoolean(); + return new SetPatternHighlightS2CPacket(key, h); + } + + public static void handle(SetPatternHighlightS2CPacket msg, Supplier ctxSupplier) { + var ctx = ctxSupplier.get(); + ctx.enqueueWork(() -> { + try { + ClientPatternHighlightStore.setHighlight(msg.key, msg.highlight); + } catch (Throwable ignored) { + } + }); + ctx.setPacketHandled(true); + } +} + + diff --git a/src/main/java/com/extendedae_plus/network/SetProviderPageS2CPacket.java b/src/main/java/com/extendedae_plus/network/SetProviderPageS2CPacket.java new file mode 100644 index 0000000..1c57644 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/SetProviderPageS2CPacket.java @@ -0,0 +1,58 @@ +package com.extendedae_plus.network; + +import appeng.menu.SlotSemantics; +import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraftforge.network.NetworkEvent; + +import java.lang.reflect.Field; +import java.util.function.Supplier; + +/** + * S2C: 指示客户端在已打开的样板供应器界面切换到指定页 + */ +public class SetProviderPageS2CPacket { + private final int page; + + public SetProviderPageS2CPacket(int page) { + this.page = page; + } + + public static void encode(SetProviderPageS2CPacket msg, FriendlyByteBuf buf) { + buf.writeVarInt(msg.page); + } + + public static SetProviderPageS2CPacket decode(FriendlyByteBuf buf) { + int p = buf.readVarInt(); + return new SetProviderPageS2CPacket(p); + } + + public static void handle(SetProviderPageS2CPacket msg, Supplier ctxSupplier) { + var ctx = ctxSupplier.get(); + ctx.enqueueWork(() -> { + try { + Screen screen = Minecraft.getInstance().screen; + if (screen instanceof GuiExPatternProvider guiExPatternProvider) { + Field currentPage = screen.getClass().getDeclaredField("eap$currentPage"); + currentPage.setAccessible(true); + currentPage.setInt(guiExPatternProvider, msg.page); + + + guiExPatternProvider.repositionSlots(SlotSemantics.ENCODED_PATTERN); + guiExPatternProvider.repositionSlots(SlotSemantics.STORAGE); + + Field hs = screen.getClass().getDeclaredField("hoveredSlot"); + hs.setAccessible(true); + hs.set(screen, null); + } + } catch (Throwable ignored) { + } + } + ); + ctx.setPacketHandled(true); + } +} + + diff --git a/src/main/java/com/extendedae_plus/util/GuiUtil.java b/src/main/java/com/extendedae_plus/util/GuiUtil.java index 2320665..1c804d9 100644 --- a/src/main/java/com/extendedae_plus/util/GuiUtil.java +++ b/src/main/java/com/extendedae_plus/util/GuiUtil.java @@ -2,11 +2,17 @@ package com.extendedae_plus.util; import appeng.api.crafting.PatternDetailsHelper; import appeng.api.stacks.GenericStack; +import appeng.client.gui.me.patternaccess.PatternContainerRecord; +import appeng.client.gui.me.patternaccess.PatternSlot; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.Font; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; +import java.util.List; +import java.util.Set; + /** * GUI工具类,提供样板获取、绘制等通用功能 @@ -77,4 +83,105 @@ public class GuiUtil { guiGraphics.drawString(font, text, (int)(textX / scale), (int)(textY / scale), 0xFFFFFFFF, true); guiGraphics.pose().popPose(); } + + // Helper: add alpha channel to RGB (rgb is 0xRRGGBB) + private static int withAlpha(int rgb, int alpha255) { + return ((alpha255 & 0xFF) << 24) | (rgb & 0x00FFFFFF); + } + + // HSV -> RGB (returns 0xRRGGBB) + private static int hsvToRgb(float h, float s, float v) { + if (s <= 0.0f) { + int g = Math.round(v * 255.0f); + return (g << 16) | (g << 8) | g; + } + float hh = (h - (float) Math.floor(h)) * 6.0f; + int sector = (int) Math.floor(hh); + float f = hh - sector; + float p = v * (1.0f - s); + float q = v * (1.0f - s * f); + float t = v * (1.0f - s * (1.0f - f)); + float r, g, b; + switch (sector) { + case 0: r = v; g = t; b = p; break; + case 1: r = q; g = v; b = p; break; + case 2: r = p; g = v; b = t; break; + case 3: r = p; g = q; b = v; break; + case 4: r = t; g = p; b = v; break; + default: r = v; g = p; b = q; break; + } + int ri = Math.round(r * 255.0f); + int gi = Math.round(g * 255.0f); + int bi = Math.round(b * 255.0f); + return (ri << 16) | (gi << 8) | bi; + } + + // 返回当前时间对应的彩虹色(0xRRGGBB),周期固定为 4000ms + private static int getRainbowRgb() { + long now = System.currentTimeMillis(); + final long rainbowPeriodMs = 4000L; + float hue = (now % rainbowPeriodMs) / (float) rainbowPeriodMs; // 0.0 ~ 1.0 + return hsvToRgb(hue, 1.0f, 1.0f); + } + + // 在给定槽位坐标绘制 1px 边框(18x18)和 16x16 半透明背景 + private static void drawSlotBox(GuiGraphics guiGraphics, int sx, int sy, int borderColor, int backgroundColor) { + guiGraphics.fill(sx - 1, sy - 1, sx + 17, sy, borderColor); + guiGraphics.fill(sx - 1, sy + 16, sx + 17, sy + 17, borderColor); + guiGraphics.fill(sx - 1, sy, sx, sy + 16, borderColor); + guiGraphics.fill(sx + 16, sy, sx + 17, sy + 16, borderColor); + guiGraphics.fill(sx, sy, sx + 16, sy + 16, backgroundColor); + } + + /** + * 在槽位上绘制彩色流转的高亮和浅底色 + */ + public static void drawPatternSlotHighlights(GuiGraphics guiGraphics, List slots, Set matchedStack, Set matchedProvider) { + if (slots == null) return; + + int rainbowRgb = getRainbowRgb(); + + for (Slot slot : slots) { + if (!(slot instanceof PatternSlot ps)) { + continue; + } + + int sx = slot.x; + int sy = slot.y; + + boolean isMatchedSlot = matchedStack != null && matchedStack.contains(slot.getItem()); + boolean isMatchedProvider = false; + try { + PatternContainerRecord container = ps.getMachineInv(); + isMatchedProvider = matchedProvider != null && matchedProvider.contains(container); + } catch (Throwable ignored) { + } + + int borderColor; + int backgroundColor; + + if (isMatchedSlot) { + borderColor = withAlpha(rainbowRgb, 0xA0); + backgroundColor = withAlpha(rainbowRgb, 0x3C); + } else if (!isMatchedProvider) { + borderColor = withAlpha(0xFFFFFF, 0x40); + backgroundColor = withAlpha(0x000000, 0x18); + } else { + borderColor = withAlpha(0xFFFFFF, 0x30); + backgroundColor = withAlpha(0xFFFFFF, 0x14); + } + + drawSlotBox(guiGraphics, sx, sy, borderColor, backgroundColor); + } + } + + /** + * 在指定槽位坐标绘制彩虹流转的边框与浅底色(用于非 PatternSlot 的高亮场景) + */ + public static void drawSlotRainbowHighlight(GuiGraphics guiGraphics, int sx, int sy) { + int rainbowRgb = getRainbowRgb(); + int borderColor = withAlpha(rainbowRgb, 0xA0); + int backgroundColor = withAlpha(rainbowRgb, 0x3C); + drawSlotBox(guiGraphics, sx, sy, borderColor, backgroundColor); + } } \ No newline at end of file diff --git a/src/main/java/com/extendedae_plus/util/PatternProviderDataUtil.java b/src/main/java/com/extendedae_plus/util/PatternProviderDataUtil.java index 15b0ad7..9209f3e 100644 --- a/src/main/java/com/extendedae_plus/util/PatternProviderDataUtil.java +++ b/src/main/java/com/extendedae_plus/util/PatternProviderDataUtil.java @@ -3,11 +3,15 @@ package com.extendedae_plus.util; import appeng.api.crafting.IPatternDetails; import appeng.api.crafting.PatternDetailsHelper; import appeng.api.inventories.InternalInventory; +import appeng.api.networking.IGrid; import appeng.api.stacks.AEKey; import appeng.api.stacks.GenericStack; import appeng.helpers.patternprovider.PatternProviderLogic; +import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor; +import com.mojang.logging.LogUtils; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; import java.util.ArrayList; import java.util.HashMap; @@ -230,26 +234,8 @@ public class PatternProviderDataUtil { if (patternInventory == null) { return patternDataList; } - - // 通过反射安全地访问host字段获取Level - Level level = null; - try { - var hostField = patternProvider.getClass().getDeclaredField("host"); - hostField.setAccessible(true); - var host = hostField.get(patternProvider); - if (host != null) { - var getBlockEntityMethod = host.getClass().getMethod("getBlockEntity"); - var blockEntity = getBlockEntityMethod.invoke(host); - if (blockEntity != null) { - var getLevelMethod = blockEntity.getClass().getMethod("getLevel"); - level = (Level) getLevelMethod.invoke(blockEntity); - } - } - } catch (Exception e) { - // 如果反射失败,返回空列表 - return patternDataList; - } - + // 获取 Level(使用 mixin accessor 替代反射) + Level level = getPatternProviderLevel(patternProvider); if (level == null) { return patternDataList; } @@ -257,12 +243,15 @@ public class PatternProviderDataUtil { // 遍历所有样板槽位 for (int i = 0; i < patternInventory.size(); i++) { ItemStack patternStack = patternInventory.getStackInSlot(i); - if (!patternStack.isEmpty()) { + if (patternStack.isEmpty()) continue; + try { // 解码样板 IPatternDetails patternDetails = PatternDetailsHelper.decodePattern(patternStack, level); if (patternDetails != null) { patternDataList.add(new PatternData(patternDetails, patternStack, i)); } + } catch (Exception e) { + if (DEBUG) LogUtils.getLogger().debug("Pattern decode failed at slot {}: {}", i, e.toString()); } } @@ -353,24 +342,7 @@ public class PatternProviderDataUtil { return null; } - // 通过反射安全地访问host字段获取Level - Level level = null; - try { - var hostField = patternProvider.getClass().getDeclaredField("host"); - hostField.setAccessible(true); - var host = hostField.get(patternProvider); - if (host != null) { - var getBlockEntityMethod = host.getClass().getMethod("getBlockEntity"); - var blockEntity = getBlockEntityMethod.invoke(host); - if (blockEntity != null) { - var getLevelMethod = blockEntity.getClass().getMethod("getLevel"); - level = (Level) getLevelMethod.invoke(blockEntity); - } - } - } catch (Exception e) { - return null; - } - + Level level = getPatternProviderLevel(patternProvider); if (level == null) { return null; } @@ -429,6 +401,59 @@ public class PatternProviderDataUtil { return patternProvider.getGrid() != null; } + /** + * 判断 provider 是否可用并属于指定网格(在线且有频道/处于活跃状态) + */ + public static boolean isProviderAvailable(PatternProviderLogic provider, IGrid expectedGrid) { + if (provider == null || expectedGrid == null) return false; + try { + var grid = provider.getGrid(); + if (grid == null || !grid.equals(expectedGrid)) return false; + + // 使用 accessor 获取 mainNode,再调用 isActive + if (provider instanceof PatternProviderLogicAccessor accessor) { + var mainNode = accessor.eap$mainNode(); + if (mainNode == null) return false; + try { + var isActiveMethod = mainNode.getClass().getMethod("isActive"); + Object active = isActiveMethod.invoke(mainNode); + if (active instanceof Boolean && !((Boolean) active)) return false; + } catch (NoSuchMethodException nsme) { + // 没有 isActive 方法时,退回到检查 channels + try { + var getChannels = mainNode.getClass().getMethod("getChannels"); + Object channels = getChannels.invoke(mainNode); + if (channels instanceof java.util.Collection) { + if (((java.util.Collection) channels).isEmpty()) return false; + } + } catch (Exception ignored) { + // 无法判断 channels 时,认为不可用 + return false; + } + } + } else { + // 没有 accessor 的情况,尽量通过反射判断 mainNode.channels + try { + var mainNodeField = provider.getClass().getDeclaredField("mainNode"); + mainNodeField.setAccessible(true); + var mainNode = mainNodeField.get(provider); + if (mainNode == null) return false; + var getChannelsMethod = mainNode.getClass().getMethod("getChannels"); + Object channels = getChannelsMethod.invoke(mainNode); + if (channels instanceof java.util.Collection) { + return !((java.util.Collection) channels).isEmpty(); + } + } catch (Exception e) { + return false; + } + } + + return true; + } catch (Exception e) { + return false; + } + } + /** * 检查样板供应器是否处于活跃状态 * @@ -443,18 +468,21 @@ public class PatternProviderDataUtil { if (grid == null) { return false; } - // 检查网格节点是否活跃 + // 检查网格节点是否活跃(使用 accessor 代替反射) try { - // 使用反射安全地访问mainNode字段 - var mainNodeField = patternProvider.getClass().getDeclaredField("mainNode"); - mainNodeField.setAccessible(true); - var mainNode = mainNodeField.get(patternProvider); - if (mainNode != null) { - var isActiveMethod = mainNode.getClass().getMethod("isActive"); - return (Boolean) isActiveMethod.invoke(mainNode); + if (patternProvider instanceof PatternProviderLogicAccessor accessor) { + var mainNode = accessor.eap$mainNode(); + if (mainNode != null) { + try { + var isActiveMethod = mainNode.getClass().getMethod("isActive"); + return (Boolean) isActiveMethod.invoke(mainNode); + } catch (Exception e) { + // 无法调用 isActive 时,认为活跃 + return true; + } + } } - } catch (Exception e) { - // 如果反射失败,假设是活跃的 + } catch (Exception ignored) { } return true; } @@ -522,6 +550,34 @@ public class PatternProviderDataUtil { return scalePatternAmountsExtendedAEStyle(patternProvider, multiplier, true); } + /** + * 查找 provider 中匹配给定定义的样板槽位(轻量、按需解码并早退出) + * @param patternProvider 要搜索的 provider + * @param targetDefinition pattern.getDefinition() 返回的对象(用于 equals 比较) + * @return 找到的槽位索引,未找到返回 -1 + */ + public static int findSlotForPattern(PatternProviderLogic patternProvider, Object targetDefinition) { + if (patternProvider == null || targetDefinition == null) return -1; + InternalInventory inv = patternProvider.getPatternInv(); + if (inv == null) return -1; + Level level = getPatternProviderLevel(patternProvider); + if (level == null) return -1; + + for (int i = 0; i < inv.size(); i++) { + ItemStack s = inv.getStackInSlot(i); + if (s.isEmpty()) continue; + try { + IPatternDetails d = PatternDetailsHelper.decodePattern(s, level); + if (d != null && d.getDefinition().equals(targetDefinition)) { + return i; + } + } catch (Exception e) { + if (DEBUG) LogUtils.getLogger().debug("findSlotForPattern decode failed at {}: {}", i, e.toString()); + } + } + return -1; + } + /** * ExtendedAE风格的样板复制倍增 * 支持更精确的样板处理和错误恢复 @@ -862,50 +918,30 @@ public class PatternProviderDataUtil { // 1. 设置物品到库存 patternInventory.setItemDirect(slot, newPattern); - // 2. 标记数据为脏数据,确保保存到磁盘 + // 2. 标记数据为脏数据,确保保存到磁盘(尝试使用 mixin accessor 替代反射) try { - // 通过反射获取host并标记为脏数据 - var hostField = patternProvider.getClass().getDeclaredField("host"); - hostField.setAccessible(true); - var host = hostField.get(patternProvider); - - if (host != null) { - // 获取BlockEntity并标记为脏数据 - var getBlockEntityMethod = host.getClass().getMethod("getBlockEntity"); - var blockEntity = getBlockEntityMethod.invoke(host); - - if (blockEntity != null) { - // 调用setChanged()方法标记为脏数据 - var setChangedMethod = blockEntity.getClass().getMethod("setChanged"); - setChangedMethod.invoke(blockEntity); - - // 尝试触发网络同步 - try { - var levelField = blockEntity.getClass().getSuperclass().getDeclaredField("level"); - levelField.setAccessible(true); - Level level = (Level) levelField.get(blockEntity); - - if (level != null && !level.isClientSide()) { - // 服务器端:强制同步到客户端 - var getBlockPosMethod = blockEntity.getClass().getMethod("getBlockPos"); - var blockPos = getBlockPosMethod.invoke(blockEntity); - - if (blockPos != null) { - // 通知客户端方块状态变更 - var getBlockStateMethod = blockEntity.getClass().getMethod("getBlockState"); - var blockState = getBlockStateMethod.invoke(blockEntity); - level.sendBlockUpdated((net.minecraft.core.BlockPos) blockPos, - (net.minecraft.world.level.block.state.BlockState) blockState, - (net.minecraft.world.level.block.state.BlockState) blockState, 3); - } + if (patternProvider instanceof PatternProviderLogicAccessor accessor) { + var host = accessor.eap$host(); + if (host != null) { + BlockEntity be = host.getBlockEntity(); + if (be != null) { + try { + be.setChanged(); + } catch (Exception ignored) { + } + try { + Level level = be.getLevel(); + if (level != null && !level.isClientSide()) { + var pos = be.getBlockPos(); + var state = be.getBlockState(); + level.sendBlockUpdated(pos, state, state, 3); + } + } catch (Exception ignored) { } - } catch (Exception syncException) { - // 网络同步失败不影响主要功能 } } } - } catch (Exception e) { - // 如果反射失败,使用备用方案 + } catch (Exception ignored) { } // 3. 强制更新样板缓存 @@ -920,20 +956,18 @@ public class PatternProviderDataUtil { * ExtendedAE风格:安全获取样板供应器的Level对象 */ private static Level getPatternProviderLevel(PatternProviderLogic patternProvider) { + if (patternProvider == null) return null; try { - var hostField = patternProvider.getClass().getDeclaredField("host"); - hostField.setAccessible(true); - var host = hostField.get(patternProvider); - if (host != null) { - var getBlockEntityMethod = host.getClass().getMethod("getBlockEntity"); - var blockEntity = getBlockEntityMethod.invoke(host); - if (blockEntity != null) { - var getLevelMethod = blockEntity.getClass().getMethod("getLevel"); - return (Level) getLevelMethod.invoke(blockEntity); + if (patternProvider instanceof PatternProviderLogicAccessor accessor) { + var host = accessor.eap$host(); + if (host != null) { + BlockEntity be = host.getBlockEntity(); + if (be != null) { + return be.getLevel(); + } } } - } catch (Exception e) { - // 静默处理异常,返回null让调用者处理 + } catch (Exception ignored) { } return null; } diff --git a/src/main/resources/extendedae_plus.mixins.json b/src/main/resources/extendedae_plus.mixins.json index e001c30..6e1c457 100644 --- a/src/main/resources/extendedae_plus.mixins.json +++ b/src/main/resources/extendedae_plus.mixins.json @@ -15,6 +15,7 @@ "ae2.accessor.PatternAccessTermScreenSlotsRowAccessor", "ae2.client.gui.AEBaseScreenMixin", "ae2.client.gui.PatternEncodingTermScreenMixin", + "ae2.client.gui.PatternProviderCloseMixin", "ae2.client.gui.PatternProviderScreenMixin", "ae2.client.gui.SlotGridLayoutMixin", "extendedae.accessor.GuiExPatternTerminalAccessor",