一、使用 pos+face 精确匹配并判空,修复同方块多贴片误选供应器问题

二、为ME扩展样板终端命中槽位加入18x18 边框+彩虹流转高亮槽位叠加
三、装配矩阵添加锻造台/切石机配方上传功能
四、对GuiExPatternTerminalMixin进行软依赖处理
五、添加工具类
六、对ae2和extendedae样板管理终端添加显示样板制作数量功能
This commit is contained in:
C-H716 2025-08-22 11:35:13 +08:00
parent 0db35edb8f
commit 3ef34c106d
15 changed files with 634 additions and 132 deletions

1
.gitignore vendored
View File

@ -3,6 +3,7 @@
build/
out/
classes/
source/
# Eclipse
*.tmp

View File

@ -94,12 +94,9 @@ dependencies {
modCompileOnly "curse.maven:just-enough-characters-250702:6680042"
}
//
gradle.projectsEvaluated {
tasks.withType(JavaCompile).tap {
configureEach {
options.compilerArgs << "-Xlint:-deprecation"
}
allprojects {
tasks.withType(JavaCompile).configureEach {
options.compilerArgs << "-Xlint:-deprecation"
}
}

View File

@ -20,7 +20,6 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
import java.util.function.Consumer;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
/**
* AE2 PatternEncodingTermMenu 增加一个通用动作持有者实现接收 EPP CGenericPacket 动作
* 注册动作 "upload_to_matrix"仅上传合成图样 ExtendedAE 装配矩阵
@ -53,8 +52,7 @@ public abstract class ContainerPatternEncodingTermMenuMixin implements IActionHo
eap$scheduleUploadWithRetry(sp, menu, attemptsLeft - 1);
}
}
} catch (Throwable t) {
LOGGER.error("Error uploading pattern to matrix", t);
} catch (Throwable ignored) {
}
});
}
@ -87,8 +85,10 @@ public abstract class ContainerPatternEncodingTermMenuMixin implements IActionHo
return; // 仅服务器执行
}
var menu = (PatternEncodingTermMenu) (Object) this;
if (menu.getMode() != EncodingMode.CRAFTING) {
return; // 只处理合成样板
if (menu.getMode() != EncodingMode.CRAFTING
&& menu.getMode() != EncodingMode.SMITHING_TABLE
&& menu.getMode() != EncodingMode.STONECUTTING) {
return; // 只处理合成/锻造台/切石机样板
}
if (this.encodedPatternSlot == null) {
return;
@ -107,8 +107,7 @@ public abstract class ContainerPatternEncodingTermMenuMixin implements IActionHo
} catch (Throwable ignored) {
}
});
} catch (Throwable t) {
LOGGER.error("Error uploading pattern to matrix", t);
} catch (Throwable ignored) {
}
}
}

View File

@ -0,0 +1,24 @@
package com.extendedae_plus.mixin.ae2;
import appeng.client.gui.me.patternaccess.PatternAccessTermScreen;
import com.extendedae_plus.util.GuiUtil;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@OnlyIn(Dist.CLIENT)
@Mixin(PatternAccessTermScreen.class)
public class PatternAccessTermScreenMixin {
// 在绘制前景的最后阶段叠加显示样板输出数量
@Inject(method = "drawFG", at = @At("TAIL"), remap = false)
private void injectDrawCraftingAmount(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY, CallbackInfo ci) {
PatternAccessTermScreen<?> screen = (PatternAccessTermScreen<?>)(Object) this;
// 调用GuiUtil的通用渲染方法
GuiUtil.renderPatternAmounts(guiGraphics, screen);
}
}

View File

@ -0,0 +1,23 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.client.gui.me.patternaccess.PatternAccessTermScreen;
import appeng.client.gui.widgets.Scrollbar;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.ArrayList;
@OnlyIn(Dist.CLIENT)
@Mixin(value = PatternAccessTermScreen.class, remap = false)
public interface PatternAccessTermScreenAccessor {
@Accessor("scrollbar")
Scrollbar getScrollbar();
@Accessor("visibleRows")
int getVisibleRows();
@Accessor("rows")
ArrayList<?> getRows();
}

View File

@ -0,0 +1,20 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.client.gui.me.patternaccess.PatternContainerRecord;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@OnlyIn(Dist.CLIENT)
@Mixin(targets = "appeng.client.gui.me.patternaccess.PatternAccessTermScreen$SlotsRow", remap = false)
public interface PatternAccessTermScreenSlotsRowAccessor {
@Accessor("container")
PatternContainerRecord getContainer();
@Accessor("offset")
int getOffset();
@Accessor("slots")
int getSlots();
}

View File

@ -3,65 +3,111 @@ package com.extendedae_plus.mixin.extendedae;
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;
import appeng.menu.AEBaseMenu;
import com.extendedae_plus.util.GuiUtil;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import com.glodblock.github.extendedae.container.ContainerExPatternTerminal;
import com.glodblock.github.extendedae.network.EPPNetworkHandler;
import com.glodblock.github.glodium.network.packet.CGenericPacket;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Set;
@Pseudo
@Mixin(GuiExPatternTerminal.class)
public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<ContainerExPatternTerminal> {
public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu> {
@Unique
private IconButton toggleSlotsButton;
@Unique
private boolean showSlots = false; // 默认显示槽位
@Unique
private long currentlychooicepatterprovider = -1; // 当前选择的样板供应器ID
@Unique
private static final String UPLOAD_SUCCESS_MESSAGE = "✅ ExtendedAE Plus: 样板快速上传成功!";
@Unique
private static final String UPLOAD_FAILED_MESSAGE = "❌ ExtendedAE Plus: 样板上传失败,请检查供应器状态";
@Unique
private static final String NO_PROVIDER_MESSAGE = "ExtendedAE Plus: 请先选择一个样板供应器点击GroupHeader旁的按钮";
@Unique
private IconButton eap$toggleSlotsButton;
@Unique
private boolean eap$showSlots = false; // 默认显示槽位
@Unique
private long eap$currentlyChoicePatterProvider = -1; // 当前选择的样板供应器ID
@Shadow(remap = false) private AETextField searchOutField;
@Shadow(remap = false) private AETextField searchInField;
@Shadow(remap = false) private Set<ItemStack> matchedStack;
@Shadow(remap = false) private Set<PatternContainerRecord> matchedProvider;
public GuiExPatternTerminalMixin(ContainerExPatternTerminal menu, Inventory playerInventory, Component title, ScreenStyle style) {
public GuiExPatternTerminalMixin(AEBaseMenu menu, Inventory playerInventory, Component title, ScreenStyle style) {
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
*/
@Unique
public long getCurrentlyChoicePatternProvider() {
return currentlychooicepatterprovider;
return eap$currentlyChoicePatterProvider;
}
/**
* 设置当前选择的样板供应器ID
*/
@Unique
public void setCurrentlyChoicePatternProvider(long id) {
this.currentlychooicepatterprovider = id;
this.eap$currentlyChoicePatterProvider = id;
}
/**
* 拦截鼠标点击事件实现Shift+左键快速上传样板功能
*/
@ -74,22 +120,22 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<ContainerEx
if (hoveredSlot != null && hoveredSlot.container == this.minecraft.player.getInventory()) {
// 点击的是玩家背包槽位
ItemStack clickedItem = hoveredSlot.getItem();
// 检查是否是有效的编码样板
if (!clickedItem.isEmpty() && PatternDetailsHelper.isEncodedPattern(clickedItem)) {
// 检查是否选择了样板供应器
if (currentlychooicepatterprovider != -1) {
if (eap$currentlyChoicePatterProvider != -1) {
// 执行快速上传
this.quickUploadPattern(hoveredSlot.getSlotIndex());
this.eap$quickUploadPattern(hoveredSlot.getSlotIndex());
// 取消默认的点击行为
cir.setReturnValue(true);
} else {
// 显示提示消息请先选择一个样板供应器
if (this.minecraft.player != null) {
this.minecraft.player.displayClientMessage(
Component.literal("ExtendedAE Plus: 请先选择一个样板供应器点击GroupHeader旁的按钮"),
true
Component.literal("ExtendedAE Plus: 请先选择一个样板供应器点击GroupHeader旁的按钮"),
true
);
}
}
@ -97,44 +143,59 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<ContainerEx
}
}
}
/**
* 快速上传样板到当前选择的供应器
*/
@Unique
private void quickUploadPattern(int playerSlotIndex) {
private void eap$quickUploadPattern(int playerSlotIndex) {
if (this.minecraft.player != null) {
// 获取要上传的物品
ItemStack itemToUpload = this.minecraft.player.getInventory().getItem(playerSlotIndex);
if (!itemToUpload.isEmpty() && PatternDetailsHelper.isEncodedPattern(itemToUpload)) {
// 通过 ExtendedAE 内置网络系统发送通用动作到服务端
// 动作: "upload"参数: 槽位索引(int)供应器ID(long)
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("upload", playerSlotIndex, currentlychooicepatterprovider));
// 通过反射调用 ExtendedAE 的网络发送软依赖
try {
Class<?> EPPNetworkHandlerClass = Class.forName("com.glodblock.github.extendedae.network.EPPNetworkHandler");
Object handlerInstance = EPPNetworkHandlerClass.getField("INSTANCE").get(null);
Class<?> packetClass = Class.forName("com.glodblock.github.glodium.network.packet.CGenericPacket");
Constructor<?> constructor = packetClass.getConstructor(String.class, Object[].class);
Object packet = constructor.newInstance("upload", new Object[]{playerSlotIndex, eap$currentlyChoicePatterProvider});
Class<?> iMessage = Class.forName("com.glodblock.github.glodium.network.packet.IMessage");
Method sendToServer = EPPNetworkHandlerClass.getMethod("sendToServer", iMessage);
sendToServer.invoke(handlerInstance, packet);
} catch (Throwable t) {
this.minecraft.player.displayClientMessage(
Component.literal("❌ ExtendedAE Plus: 未找到 ExtendedAE 网络支持(可能未安装或版本不兼容)"),
true
);
}
} else {
this.minecraft.player.displayClientMessage(
Component.literal("❌ ExtendedAE Plus: 无效的样板物品"),
true
Component.literal("❌ ExtendedAE Plus: 无效的样板物品"),
true
);
}
}
}
/**
* 重置当前选择的样板供应器ID
*/
@Unique
public void resetCurrentlyChoicePatternProvider() {
this.currentlychooicepatterprovider = -1;
this.eap$currentlyChoicePatterProvider = -1;
}
@Inject(method = "<init>", at = @At("TAIL"), remap = false)
private void injectConstructor(ContainerExPatternTerminal menu, Inventory playerInventory, Component title, ScreenStyle style, CallbackInfo ci) {
private void injectConstructor(CallbackInfo ci) {
// 创建切换槽位显示的按钮
this.toggleSlotsButton = new IconButton((b) -> {
this.showSlots = !this.showSlots; // 开关状态
this.eap$toggleSlotsButton = new IconButton((b) -> {
this.eap$showSlots = !this.eap$showSlots; // 开关状态
// 通过反射调用refreshList方法 - 先尝试当前类失败后尝试父类
try {
java.lang.reflect.Method refreshMethod = null;
@ -149,41 +210,40 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<ContainerEx
throw e2;
}
}
refreshMethod.setAccessible(true);
refreshMethod.invoke(this);
} catch (Exception e) {
} catch (Exception ignored) {
}
}) {
@Override
protected Icon getIcon() {
return showSlots ? Icon.PATTERN_ACCESS_HIDE : Icon.PATTERN_ACCESS_SHOW;
return eap$showSlots ? Icon.PATTERN_ACCESS_HIDE : Icon.PATTERN_ACCESS_SHOW;
}
};
// 设置按钮提示文本
this.toggleSlotsButton.setTooltip(Tooltip.create(Component.translatable("gui.expatternprovider.toggle_slots")));
this.eap$toggleSlotsButton.setTooltip(Tooltip.create(Component.translatable("gui.expatternprovider.toggle_slots")));
// 添加到左侧工具栏
this.addToLeftToolbar(this.toggleSlotsButton);
this.addToLeftToolbar(this.eap$toggleSlotsButton);
}
@Inject(method = "refreshList", at = @At("HEAD"), remap = false)
private void onRefreshListStart(CallbackInfo ci) {
// 更新按钮图标
if (this.toggleSlotsButton != null) {
this.toggleSlotsButton.setTooltip(Tooltip.create(Component.translatable(
this.showSlots ? "gui.expatternprovider.hide_slots" : "gui.expatternprovider.show_slots"
if (this.eap$toggleSlotsButton != null) {
this.eap$toggleSlotsButton.setTooltip(Tooltip.create(Component.translatable(
this.eap$showSlots ? "gui.expatternprovider.hide_slots" : "gui.expatternprovider.show_slots"
)));
}
}
@Inject(method = "refreshList", at = @At("TAIL"), remap = false)
private void onRefreshListEnd(CallbackInfo ci) {
// 在refreshList结束后根据showSlots状态过滤SlotsRow
if (!this.showSlots) {
if (!this.eap$showSlots) {
try {
// 通过反射访问rows字段 - 先尝试当前类失败后尝试父类
java.lang.reflect.Field rowsField = null;
@ -200,7 +260,7 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<ContainerEx
}
rowsField.setAccessible(true);
java.util.ArrayList<?> rows = (java.util.ArrayList<?>) rowsField.get(this);
// 通过反射访问highlightBtns字段
java.lang.reflect.Field highlightBtnsField = null;
try {
@ -217,22 +277,22 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<ContainerEx
highlightBtnsField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.HashMap<Integer, Object> highlightBtns = (java.util.HashMap<Integer, Object>) highlightBtnsField.get(this);
// 创建新的索引映射
java.util.HashMap<Integer, Object> newHighlightBtns = new java.util.HashMap<>();
int newIndex = 0;
// 移除所有SlotsRow只保留GroupHeaderRow同时重新映射高亮按钮索引
for (int i = 0; i < rows.size(); i++) {
Object row = rows.get(i);
String className = row.getClass().getSimpleName();
if (className.equals("GroupHeaderRow")) {
// 保留GroupHeaderRow并重新映射对应的高亮按钮
@SuppressWarnings("unchecked")
java.util.ArrayList<Object> typedRows = (java.util.ArrayList<Object>) rows;
typedRows.set(newIndex, row);
// 查找原来在这个位置的高亮按钮
// 原始代码中高亮按钮的索引是在添加GroupHeaderRow之后添加第一个SlotsRow之前设置的
// 所以按钮的索引指向的是第一个SlotsRow的位置
@ -241,25 +301,25 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<ContainerEx
Object button = highlightBtns.get(i + 1);
newHighlightBtns.put(newIndex, button);
}
newIndex++;
} else if (className.equals("SlotsRow")) {
// 不保留SlotsRow也不增加newIndex
}
}
// 移除多余的行
while (rows.size() > newIndex) {
rows.remove(rows.size() - 1);
}
// 更新highlightBtns
highlightBtns.clear();
highlightBtns.putAll(newHighlightBtns);
// 强制刷新滚动条
try {
java.lang.reflect.Method resetScrollbarMethod = null;
Method resetScrollbarMethod = null;
try {
// 先尝试在当前类中查找
resetScrollbarMethod = this.getClass().getDeclaredMethod("resetScrollbar");
@ -271,14 +331,80 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<ContainerEx
throw e2;
}
}
resetScrollbarMethod.setAccessible(true);
resetScrollbarMethod.invoke(this);
} catch (Exception e) {
} catch (Exception ignored) {
}
} catch (Exception e) {
} catch (Exception ignored) {
}
} else {
}
}
@Inject(method = "drawFG", at = @At("TAIL"), remap = false)
private void eap$afterDrawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY, CallbackInfo ci) {
// 调用GuiUtil的通用渲染方法显示样板数量
GuiUtil.renderPatternAmounts(guiGraphics, this);
// 原有的搜索高亮逻辑
// 仅当任一搜索框非空时绘制叠加层与原版行为保持一致
boolean searchActive = (this.searchOutField != null && !this.searchOutField.getValue().isEmpty())
|| (this.searchInField != null && !this.searchInField.getValue().isEmpty());
if (!searchActive) {
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);
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
private void eap$fill(GuiGraphics guiGraphics, Rect2i rect, int argb) {
this.fillRect(guiGraphics, rect, argb);
}
}

View File

@ -3,61 +3,70 @@ package com.extendedae_plus.mixin.extendedae;
import com.glodblock.github.extendedae.client.button.HighlightButton;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import net.minecraft.client.gui.components.Button;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import java.util.Objects;
@Mixin(value = HighlightButton.class, priority = 1000)
public abstract class HighlightButtonMixin {
@Shadow(remap = false)
private static void highlight(Button btn) {}
@Shadow(remap = false)
private static void highlight(Button btn) {}
private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAEPlus");
@Inject(method = "highlight", at = @At("TAIL"), remap = false)
private static void onHighlight(Button btn, CallbackInfo ci) {
if (btn instanceof HighlightButton hb) {
// 获取当前打开的GUI屏幕
var minecraft = net.minecraft.client.Minecraft.getInstance();
if (minecraft.screen instanceof GuiExPatternTerminal<?> terminal) {
// 通过反射获取HighlightButton的serverId信息
try {
// 获取HighlightButton的pos字段用于标识对应的样板供应器
var posField = HighlightButton.class.getDeclaredField("pos");
posField.setAccessible(true);
var pos = posField.get(hb);
if (pos != null) {
// 通过反射访问infoMap字段
var infoMapField = GuiExPatternTerminal.class.getDeclaredField("infoMap");
infoMapField.setAccessible(true);
@SuppressWarnings("unchecked")
var infoMap = (java.util.Map<Long, Object>) infoMapField.get(terminal);
// 查找对应的样板供应器ID
for (var entry : infoMap.entrySet()) {
var info = entry.getValue();
// 通过反射调用pos()方法
var posMethod = info.getClass().getMethod("pos");
var infoPos = posMethod.invoke(info);
if (pos.equals(infoPos)) {
long serverId = entry.getKey();
// 通过反射调用setter方法
try {
var setMethod = terminal.getClass().getMethod("setCurrentlyChoicePatternProvider", long.class);
setMethod.invoke(terminal, serverId);
} catch (Exception ignored) {
}
break;
}
}
}
} catch (Exception ignored) {
}
}
}
}
@Inject(method = "highlight", at = @At("TAIL"), remap = false)
private static void onHighlight(Button btn, CallbackInfo ci) {
if (btn instanceof HighlightButton hb) {
var minecraft = net.minecraft.client.Minecraft.getInstance();
if (minecraft.screen instanceof GuiExPatternTerminal<?> terminal) {
try {
var fPos = HighlightButton.class.getDeclaredField("pos");
fPos.setAccessible(true);
Object btnPos = fPos.get(hb);
if (btnPos == null) {
return;
}
var fFace = HighlightButton.class.getDeclaredField("face");
fFace.setAccessible(true);
Object btnFace = fFace.get(hb); // 允许为 null方块形
var infoMapField = GuiExPatternTerminal.class.getDeclaredField("infoMap");
infoMapField.setAccessible(true);
@SuppressWarnings("unchecked")
var infoMap = (java.util.Map<Long, Object>) infoMapField.get(terminal);
for (var entry : infoMap.entrySet()) {
var info = entry.getValue();
var mPos = info.getClass().getMethod("pos");
mPos.setAccessible(true);
Object infoPos = mPos.invoke(info);
var mFace = info.getClass().getMethod("face");
mFace.setAccessible(true);
Object infoFace = mFace.invoke(info); // 允许为 null方块形
// 匹配规则pos 必须相等face 允许为 nullnull 仅与 null 匹配
boolean posEqual = Objects.equals(btnPos, infoPos);
boolean faceEqual = (btnFace == null && infoFace == null) || Objects.equals(btnFace, infoFace);
if (posEqual && faceEqual) {
long serverId = entry.getKey();
var setMethod = terminal.getClass().getMethod("setCurrentlyChoicePatternProvider", long.class);
setMethod.setAccessible(true);
setMethod.invoke(terminal, serverId);
break;
}
}
} catch (Throwable t) {
LOGGER.warn("HighlightButton onHighlight 处理异常", t);
}
}
}
}
}

View File

@ -0,0 +1,23 @@
package com.extendedae_plus.mixin.extendedae.accessor;
import appeng.client.gui.widgets.Scrollbar;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.ArrayList;
@OnlyIn(Dist.CLIENT)
@Mixin(value = GuiExPatternTerminal.class, remap = false)
public interface GuiExPatternTerminalAccessor {
@Accessor("scrollbar")
Scrollbar getScrollbar();
@Accessor("visibleRows")
int getVisibleRows();
@Accessor("rows")
ArrayList<?> getRows();
}

View File

@ -0,0 +1,20 @@
package com.extendedae_plus.mixin.extendedae.accessor;
import appeng.client.gui.me.patternaccess.PatternContainerRecord;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@OnlyIn(Dist.CLIENT)
@Mixin(targets = "com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal$SlotsRow", remap = false)
public interface GuiExPatternTerminalSlotsRowAccessor {
@Accessor("container")
PatternContainerRecord getContainer();
@Accessor("offset")
int getOffset();
@Accessor("slots")
int getSlots();
}

View File

@ -0,0 +1,13 @@
package com.extendedae_plus.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Global logger utility for ExtendedAE Plus mod
*/
public class ExtendedAELogger {
public static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAEPlus");
private ExtendedAELogger() {throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");}
}

View File

@ -7,6 +7,8 @@ import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNode;
import appeng.core.definitions.AEItems;
import appeng.crafting.pattern.AECraftingPattern;
import appeng.crafting.pattern.AESmithingTablePattern;
import appeng.crafting.pattern.AEStonecuttingPattern;
import appeng.helpers.patternprovider.PatternContainer;
import appeng.menu.implementations.PatternAccessTermMenu;
import appeng.menu.me.items.PatternEncodingTermMenu;
@ -433,10 +435,12 @@ public class ExtendedAEPatternUploadUtil {
return false;
}
// 仅允许合成图样
// 仅允许合成/锻造台/切石机图样
IPatternDetails details = PatternDetailsHelper.decodePattern(stack, player.level());
if (!(details instanceof AECraftingPattern)) {
sendMessage(player, "extendedae_plus.upload_to_matrix.fail_not_crafting");
if (!(details instanceof AECraftingPattern
|| details instanceof AESmithingTablePattern
|| details instanceof AEStonecuttingPattern)) {
sendMessage(player, "extendedae_plus.upload_to_matrix.fail");
return false;
}

View File

@ -0,0 +1,176 @@
package com.extendedae_plus.util;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.stacks.GenericStack;
import appeng.util.inv.AppEngInternalInventory;
import com.extendedae_plus.mixin.ae2.accessor.PatternAccessTermScreenAccessor;
import com.extendedae_plus.mixin.ae2.accessor.PatternAccessTermScreenSlotsRowAccessor;
import com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor;
import com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalSlotsRowAccessor;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.world.item.ItemStack;
import java.util.ArrayList;
/**
* GUI工具类提供样板获取绘制等通用功能
*/
public class GuiUtil {
private GuiUtil() {throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");}
/**
* 从样板中获取输出数量文本
*
* @param pattern 样板物品
* @return 格式化后的数量文本
*/
public static String getPatternOutputText(ItemStack pattern) {
if (pattern.isEmpty()) {
return "";
}
var details = PatternDetailsHelper.decodePattern(pattern, Minecraft.getInstance().level, false);
if (details == null || details.getOutputs().length == 0) {
return "";
}
GenericStack out = details.getOutputs()[0];
long amount = out.amount();
long perUnit = out.what().getAmountPerUnit();
if (amount <= 0 || perUnit <= 0) {
return "";
}
// 计算实际单位数量支持小数
double units = (double) amount / perUnit;
if (units <= 0) {
return "";
}
// 自动判断是否为流体避免重复后缀
String autoSuffix = "";
if (perUnit > 1) {
// 如果每单位数量大于1说明是流体如1000mB = 1B
autoSuffix = "B";
}
return NumberFormatUtil.formatNumberWithDecimal(units) + autoSuffix;
}
/**
* 在槽位右下角绘制数量文本
* @param guiGraphics GUI图形上下文
* @param font 字体
* @param text 要绘制的文本
* @param slotX 槽位X坐标
* @param slotY 槽位Y坐标
* @param scale 缩放比例
*/
public static void drawAmountText(GuiGraphics guiGraphics, Font font, String text, int slotX, int slotY, float scale) {
if (text.isEmpty()) {
return;
}
// 计算缩放后的字体宽度确保右对齐
int scaledWidth = (int)(font.width(text) * scale);
int textX = slotX + 16 - scaledWidth;
int textY = slotY + 11; // 右下角显示
guiGraphics.pose().pushPose();
guiGraphics.pose().translate(0, 0, 300); // 提升 Z确保在最上层
guiGraphics.pose().scale(scale, scale, 1.0f); // 缩小字体
guiGraphics.drawString(font, text, (int)(textX / scale), (int)(textY / scale), 0xFFFFFFFF, true);
guiGraphics.pose().popPose();
}
/**
* 渲染样板管理终端的数量显示
* @param guiGraphics GUI图形上下文
* @param screen 屏幕对象
*/
public static void renderPatternAmounts(GuiGraphics guiGraphics, Object screen) {
int scrollLevel;
int visibleRows;
ArrayList<?> rowsList;
if (screen instanceof PatternAccessTermScreenAccessor aeAccessor) {
var scrollbar = aeAccessor.getScrollbar();
if (scrollbar == null) return;
scrollLevel = scrollbar.getCurrentScroll();
visibleRows = aeAccessor.getVisibleRows();
if (visibleRows <= 0) return;
rowsList = aeAccessor.getRows();
if (rowsList == null || rowsList.isEmpty()) return;
} else if (screen instanceof GuiExPatternTerminalAccessor exAccessor) {
var scrollbar = exAccessor.getScrollbar();
if (scrollbar == null) return;
scrollLevel = scrollbar.getCurrentScroll();
visibleRows = exAccessor.getVisibleRows();
if (visibleRows <= 0) return;
rowsList = exAccessor.getRows();
if (rowsList == null || rowsList.isEmpty()) return;
} else {
return;
}
// 判断是否为ExtendedAE终端
boolean isExtendedAE = screen instanceof GuiExPatternTerminalAccessor;
// 根据终端类型使用不同的常量 AE2/ExtendedAE 源码保持一致
final int SLOT_SIZE = 18; // ROW_HEIGHT == 18, SLOT_SIZE == ROW_HEIGHT
final int GUI_PADDING_X = isExtendedAE ? 22 : 8; // ExtendedAE使用22AE2使用8
final int SLOT_Y_OFFSET = isExtendedAE ? 34 : 0; // ExtendedAE需要额外的Y偏移
var font = Minecraft.getInstance().font;
for (int i = 0; i < visibleRows; ++i) {
int rowIdx = scrollLevel + i;
if (rowIdx < 0 || rowIdx >= rowsList.size()) {
continue;
}
Object row = rowsList.get(rowIdx);
if (row instanceof PatternAccessTermScreenSlotsRowAccessor slotsRow) {
var container = slotsRow.getContainer();
var inventory = container.getInventory();
drawRowAmounts(guiGraphics, font, inventory, slotsRow.getOffset(), slotsRow.getSlots(), i, SLOT_SIZE, GUI_PADDING_X, SLOT_Y_OFFSET);
continue;
}
if (row instanceof GuiExPatternTerminalSlotsRowAccessor exSlotsRow) {
var container = exSlotsRow.getContainer();
var inventory = container.getInventory();
drawRowAmounts(guiGraphics, font, inventory, exSlotsRow.getOffset(), exSlotsRow.getSlots(), i, SLOT_SIZE, GUI_PADDING_X, SLOT_Y_OFFSET);
}
}
}
private static void drawRowAmounts(
GuiGraphics guiGraphics,
Font font,
AppEngInternalInventory inventory,
int offset,
int slots,
int visibleRowIndex,
int slotSize,
int guiPaddingX,
int slotYOffset
) {
for (int col = 0; col < slots; col++) {
int index = offset + col;
var pattern = inventory.getStackInSlot(index);
if (pattern == null || pattern.isEmpty()) {
continue;
}
String amountText = getPatternOutputText(pattern);
if (amountText.isEmpty()) {
continue;
}
int slotX = col * slotSize + guiPaddingX;
int slotY = (visibleRowIndex + 1) * slotSize + slotYOffset;
drawAmountText(guiGraphics, font, amountText, slotX, slotY, 0.6f);
}
}
}

View File

@ -0,0 +1,62 @@
package com.extendedae_plus.util;
/**
* 数字格式化工具类提供大数字和小数的格式化功能
*/
public class NumberFormatUtil {
private NumberFormatUtil() {throw new UnsupportedOperationException("This is a utility class and cannot be instantiated");}
/**
* 格式化数字将大数字转换为km等格式
* 支持小数显示小数点后为0则不显示0
* @param number 要格式化的数字
* @return 格式化后的字符串
*/
public static String formatNumber(long number) {
if (number < 1000) {
return String.valueOf(number);
} else if (number < 1000000) {
double value = number / 1000.0;
return formatDecimal(value, "k");
} else {
double value = number / 1000000.0;
return formatDecimal(value, "m");
}
}
/**
* 格式化带小数的数字支持流体等需要显示小数的场景
* @param value 小数值
* @return 格式化后的字符串
*/
public static String formatNumberWithDecimal(double value) {
if (value < 1000) {
if (value == (long) value) {
return String.valueOf((long) value);
} else {
return String.format("%.1f", value).replaceAll("\\.0$", "");
}
} else if (value < 1000000) {
return formatDecimal(value / 1000.0, "k");
} else {
return formatDecimal(value / 1000000.0, "m");
}
}
/**
* 格式化小数如果小数点后为0则不显示0
* @param value 小数值
* @param suffix 后缀kmg等
* @return 格式化后的字符串
*/
private static String formatDecimal(double value, String suffix) {
// 对于接近整数的值使用整数显示
if (Math.abs(value - Math.round(value)) < 0.001) {
return String.valueOf(Math.round(value)) + suffix;
} else {
// 修复重复后缀问题先格式化数字再添加后缀
String formatted = String.format("%.1f", value).replaceAll("\\.0$", "");
return formatted + suffix;
}
}
}

View File

@ -8,6 +8,7 @@
"accessor.AbstractContainerScreenAccessor",
"accessor.ScreenAccessor",
"accessor.ScreenInvoker",
"ae2.PatternAccessTermScreenMixin",
"ae2.PatternEncodingTermScreenMixin",
"ae2.PatternProviderScreenMixin",
"ae2.QuartzCuttingKnifeItemMixin",
@ -15,9 +16,13 @@
"ae2.accessor.AEBaseScreenAccessor",
"ae2.accessor.AEBaseScreenInvoker",
"ae2.accessor.MEStorageScreenAccessor",
"ae2.accessor.PatternAccessTermScreenAccessor",
"ae2.accessor.PatternAccessTermScreenSlotsRowAccessor",
"extendedae.GuiExPatternProviderMixin",
"extendedae.GuiExPatternTerminalMixin",
"extendedae.HighlightButtonMixin",
"extendedae.accessor.GuiExPatternTerminalAccessor",
"extendedae.accessor.GuiExPatternTerminalSlotsRowAccessor",
"jei.EncodePatternTransferHandlerMixin"
],
"mixins": [