Merge remote-tracking branch 'origin/develop/iava'

This commit is contained in:
GaLi 2025-09-03 21:00:56 +08:00
commit ef8fa756ed
15 changed files with 548 additions and 251 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<Co
super(menu, playerInventory, title, style);
}
// 移除手动挪动 Slot 坐标交由 SlotGridLayout + 原生布局控制
@Unique
@ -144,7 +145,7 @@ public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<Co
ContainerExPatternProvider menu1 = this.getMenu();
// 尝试调用 setPage
try {
java.lang.reflect.Method setPageMethod = menu1.getClass().getMethod("setPage", int.class);
Method setPageMethod = menu1.getClass().getMethod("setPage", int.class);
setPageMethod.invoke(menu1, newPage);
} catch (Throwable ignored2) {}
// 直接写入 page 字段确保生效

View File

@ -4,7 +4,6 @@ import appeng.api.crafting.PatternDetailsHelper;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.Icon;
import appeng.client.gui.me.patternaccess.PatternContainerRecord;
import appeng.client.gui.me.patternaccess.PatternSlot;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.AETextField;
import appeng.client.gui.widgets.IconButton;
@ -13,6 +12,7 @@ import com.extendedae_plus.config.ModConfigs;
import com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.OpenProviderUiC2SPacket;
import com.extendedae_plus.util.GuiUtil;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
@ -76,41 +76,6 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
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.0s: 0.0~1.0v: 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<AEBaseMenu>
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

View File

@ -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<IPatternDetails> 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<Level> 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);
}
}
}

View File

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

View File

@ -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<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
ctx.enqueueWork(() -> {
try {
ClientPatternHighlightStore.setHighlight(msg.key, msg.highlight);
} catch (Throwable ignored) {
}
});
ctx.setPacketHandled(true);
}
}

View File

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

View File

@ -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<Slot> slots, Set<ItemStack> matchedStack, Set<PatternContainerRecord> 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);
}
}

View File

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

View File

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