Merge branch 'master' into feature/autoPattern

This commit is contained in:
GaLicn 2025-08-29 15:10:33 +08:00 committed by GitHub
commit e21e404b6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 858 additions and 22 deletions

View File

@ -102,6 +102,7 @@ dependencies {
//jec
modCompileOnly "curse.maven:just-enough-characters-250702:6680042"
//mae2
// modRuntimeOnly "curse.maven:modern-ae2-additions-1028068:6342203"
modCompileOnly "curse.maven:modern-ae2-additions-1028068:6342203"

View File

@ -21,7 +21,7 @@ import java.util.List;
import java.util.function.Function;
/**
* 参照 MAE2 DynamicCraftingCubeModelProvider实现形成态光照模型
* 形成态光照模型
*/
public class EPlusCraftingCubeModelProvider
extends AbstractCraftingUnitModelProvider<EPlusCraftingUnitType> {
@ -29,13 +29,13 @@ public class EPlusCraftingCubeModelProvider
public static final ChunkRenderTypeSet CUTOUT = ChunkRenderTypeSet.of(RenderType.cutout());
private static final List<Material> MATERIALS = new ArrayList<>();
// MAE2 一致将环形边框与基础发光底图放在本模组命名空间
//将环形边框与基础发光底图放在本模组命名空间
protected static final Material RING_CORNER = texture(ExtendedAEPlus.MODID, "ring_corner");
protected static final Material RING_SIDE_HOR = texture(ExtendedAEPlus.MODID, "ring_side_hor");
protected static final Material RING_SIDE_VER = texture(ExtendedAEPlus.MODID, "ring_side_ver");
protected static final Material LIGHT_BASE = texture(ExtendedAEPlus.MODID, "light_base");
// 我们自己的亮面贴图formed 时使用
// 亮面贴图formed 时使用
protected static final Material ACCELERATOR_4X_LIGHT = texture(ExtendedAEPlus.MODID,
"4x_accelerator_light");
protected static final Material ACCELERATOR_16X_LIGHT = texture(ExtendedAEPlus.MODID,

View File

@ -1,14 +0,0 @@
package com.extendedae_plus.mixin.accessor;
import net.minecraft.client.gui.components.Renderable;
import net.minecraft.client.gui.components.events.GuiEventListener;
import net.minecraft.client.gui.narration.NarratableEntry;
import net.minecraft.client.gui.screens.Screen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(Screen.class)
public interface ScreenInvoker {
@Invoker("addRenderableWidget")
<W extends GuiEventListener & Renderable & NarratableEntry> W eap$invokeAddRenderableWidget(W widget);
}

View File

@ -2,21 +2,29 @@ package com.extendedae_plus.mixin.ae2;
import appeng.client.Point;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.StackWithBounds;
import appeng.client.gui.me.crafting.CraftingCPUScreen;
import appeng.client.gui.TextOverride;
import appeng.client.gui.style.PaletteColor;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.style.Text;
import appeng.client.gui.style.TextAlignment;
import appeng.api.stacks.AEKey;
import appeng.menu.slot.AppEngSlot;
import com.extendedae_plus.api.ExPatternPageAccessor;
import com.extendedae_plus.network.CraftingMonitorJumpC2SPacket;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.CraftingMonitorOpenProviderC2SPacket;
import com.extendedae_plus.util.GuiUtil;
import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider;
import com.mojang.logging.LogUtils;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.network.chat.Component;
import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.world.inventory.Slot;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@ -38,6 +46,74 @@ public abstract class AEBaseScreenMixin {
return null;
}
/**
* AEBaseScreen mouseClicked 入口拦截 CraftingCPUScreen Shift+左键
* 读取鼠标下的 AEKey 并发送 CraftingMonitorJumpC2SPacket
*/
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
private void eap$craftingCpuShiftLeftClick(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
// 仅处理 CraftingCPUScreen 实例
Object self = this;
if (!(self instanceof CraftingCPUScreen<?> screen)) {
return;
}
// 仅在 Shift + 左键 时触发
if (button != 0 || !net.minecraft.client.gui.screens.Screen.hasShiftDown()) {
return;
}
try {
StackWithBounds hovered = screen.getStackUnderMouse(mouseX, mouseY);
if (hovered == null || hovered.stack() == null) {
return;
}
AEKey key = hovered.stack().what();
if (key == null) {
return;
}
// Debug: 标记一次发送
try {
LogUtils.getLogger().info("EAP: Send CraftingMonitorJumpC2SPacket: {}", key);
} catch (Throwable ignored2) {}
ModNetwork.CHANNEL.sendToServer(new CraftingMonitorJumpC2SPacket(key));
cir.setReturnValue(true);
} catch (Throwable ignored) {
}
}
/**
* AEBaseScreen mouseClicked 入口拦截 CraftingCPUScreen Shift+右键
* 读取鼠标下的 AEKey 并发送 CraftingMonitorOpenProviderC2SPacket打开样板供应器UI
*/
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
private void eap$craftingCpuShiftRightClick(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
// 仅处理 CraftingCPUScreen 实例
Object self = this;
if (!(self instanceof CraftingCPUScreen<?> screen)) {
return;
}
// 仅在 Shift + 右键 时触发
if (button != 1 || !net.minecraft.client.gui.screens.Screen.hasShiftDown()) {
return;
}
try {
StackWithBounds hovered = screen.getStackUnderMouse(mouseX, mouseY);
if (hovered == null || hovered.stack() == null) {
return;
}
AEKey key = hovered.stack().what();
if (key == null) {
return;
}
// Debug: 标记一次发送打开供应器UI
try {
LogUtils.getLogger().info("EAP: Send CraftingMonitorOpenProviderC2SPacket: {}", key);
} catch (Throwable ignored2) {}
ModNetwork.CHANNEL.sendToServer(new CraftingMonitorOpenProviderC2SPacket(key));
cir.setReturnValue(true);
} catch (Throwable ignored) {
}
}
@Unique
private static int eap$getIntField(Object self, String name, int def) {
Class<?> c = self.getClass();

View File

@ -8,6 +8,21 @@ import com.glodblock.github.glodium.network.packet.sync.IActionHolder;
import com.glodblock.github.glodium.network.packet.sync.Paras;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.core.registries.Registries;
import net.minecraftforge.network.NetworkHooks;
import org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@ -17,11 +32,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Mixin(ContainerExPatternTerminal.class)
public abstract class ContainerExPatternTerminalMixin implements IActionHolder {
@GuiSync(11452)
@GuiSync(25564)
@Unique
public boolean eap$hidePatternSlots = false;
@ -41,13 +58,19 @@ public abstract class ContainerExPatternTerminalMixin implements IActionHolder {
}
@Unique
private final Map<String, Consumer<Paras>> eap$actions = createHolder();
private Map<String, Consumer<Paras>> eap$actions;
@Unique
private Player epp$player;
@Unique
private static final Logger EAP_LOGGER = LogManager.getLogger("ExtendedAE_Plus");
@Inject(method = "<init>*", at = @At("TAIL"))
private void init(int id, net.minecraft.world.entity.player.Inventory playerInventory, IConfigurableObject host, CallbackInfo ci) {
if (this.eap$actions == null) {
this.eap$actions = createHolder();
}
this.epp$player = playerInventory.player;
// 注册上传动作参数顺序必须与客户端 CGenericPacket 保持一致
this.eap$actions.put("upload", p -> {
@ -61,6 +84,110 @@ public abstract class ContainerExPatternTerminalMixin implements IActionHolder {
} catch (Throwable ignored) {
}
});
// 注册打开UI动作open_ui(posLong, dimensionId, faceOrdinal?)
this.eap$actions.put("open_ui", p -> {
try {
// 参数解析
Object po = p.get(0); // BlockPos as long (BlockPos#asLong)
Object do0 = p.get(1); // Dimension id string (e.g., minecraft:overworld)
Object fo;
try {
fo = p.get(2); // Optional face ordinal
} catch (Throwable __ignored) {
fo = null;
}
long posLong = (po instanceof Number) ? ((Number) po).longValue() : Long.parseLong(String.valueOf(po));
String dimStr = String.valueOf(do0);
int faceOrd = -1;
if (fo != null) {
faceOrd = (fo instanceof Number) ? ((Number) fo).intValue() : Integer.parseInt(String.valueOf(fo));
}
BlockPos pos = BlockPos.of(posLong);
ResourceLocation dimId = ResourceLocation.tryParse(dimStr);
if (dimId == null) {
EAP_LOGGER.warn("[EPlus] open_ui: invalid dim '{}'", dimStr);
return;
}
ResourceKey<Level> dimKey = ResourceKey.create(Registries.DIMENSION, dimId);
if (!(this.epp$player instanceof ServerPlayer sp)) {
EAP_LOGGER.warn("[EPlus] open_ui: not a ServerPlayer");
return;
}
ServerLevel level = sp.server.getLevel(dimKey);
if (level == null) {
EAP_LOGGER.warn("[EPlus] open_ui: level null for key {}", dimKey);
return;
}
EAP_LOGGER.debug("[EPlus] open_ui: pos={}, dim={}, faceOrd={}", pos, dimKey.location(), faceOrd);
// 目标应为供应器所面向/连接的相邻方块而非供应器自身
Direction[] tries = (faceOrd >= 0 && faceOrd < Direction.values().length)
? new Direction[]{Direction.values()[faceOrd]}
: Direction.values();
// 1) 先尝试在相邻方块直接打开 MenuProvider
for (Direction dir : tries) {
BlockPos targetPos = pos.relative(dir);
BlockEntity be = level.getBlockEntity(targetPos);
if (be instanceof MenuProvider provider) {
NetworkHooks.openScreen(sp, provider, targetPos);
EAP_LOGGER.debug("[EPlus] open_ui: opened BE MenuProvider at {} (neighbor via {})", targetPos, dir);
return;
}
var state = level.getBlockState(targetPos);
MenuProvider provider = state.getMenuProvider(level, targetPos);
if (provider != null) {
NetworkHooks.openScreen(sp, provider, targetPos);
EAP_LOGGER.debug("[EPlus] open_ui: opened State MenuProvider at {} (neighbor via {})", targetPos, dir);
return;
}
}
// 2) 兜底为避免误触发放置/覆盖仅在手上至少有一只手为空时使用 BlockState.use 进行一次徒手交互
boolean hasFace = (faceOrd >= 0 && faceOrd < Direction.values().length);
boolean anyHandEmpty = sp.getMainHandItem().isEmpty() || sp.getOffhandItem().isEmpty();
if (anyHandEmpty) {
InteractionHand hand = sp.getMainHandItem().isEmpty() ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND;
if (hasFace) {
Direction dir = Direction.values()[faceOrd];
BlockPos targetPos = pos.relative(dir);
var state2 = level.getBlockState(targetPos);
var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), dir.getOpposite(), targetPos, false);
InteractionResult r = state2.use(level, sp, hand, hit);
EAP_LOGGER.debug("[EPlus] open_ui: fallback(state.use) at {} hit {} (via {}), result={}", targetPos, dir.getOpposite(), dir, r);
} else {
// 无朝向优先尝试有方块实体的邻居否则尝试实心方块邻居各只尝试一次
Direction chosen = null;
for (Direction d : Direction.values()) {
if (level.getBlockEntity(pos.relative(d)) != null) { chosen = d; break; }
}
if (chosen == null) {
for (Direction d : Direction.values()) {
if (!level.getBlockState(pos.relative(d)).isAir()) { chosen = d; break; }
}
}
if (chosen != null) {
BlockPos targetPos = pos.relative(chosen);
var state2 = level.getBlockState(targetPos);
var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), chosen.getOpposite(), targetPos, false);
InteractionResult r = state2.use(level, sp, hand, hit);
EAP_LOGGER.debug("[EPlus] open_ui: fallback(state.use) at {} hit {} (auto via {}), result={}", targetPos, chosen.getOpposite(), chosen, r);
} else {
EAP_LOGGER.debug("[EPlus] open_ui: no neighbor candidate for fallback (faceOrd<0)");
}
}
} else {
EAP_LOGGER.debug("[EPlus] open_ui: skip fallback (hands occupied)");
}
} catch (Throwable ignored) {
}
});
}
@NotNull

View File

@ -10,13 +10,22 @@ import appeng.client.gui.widgets.AETextField;
import appeng.client.gui.widgets.IconButton;
import appeng.menu.AEBaseMenu;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.OpenProviderUiC2SPacket;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.Minecraft;
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 net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level;
import net.minecraft.resources.ResourceLocation;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
@ -29,9 +38,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Set;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@Pseudo
@Mixin(GuiExPatternTerminal.class)
@Mixin(value = GuiExPatternTerminal.class)
public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu> {
@Unique
@ -46,6 +59,14 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
private boolean eap$showSlots = false; // 默认显示槽位
@Unique
private long eap$currentlyChoicePatterProvider = -1; // 当前选择的样板供应器ID
@Unique
private final Map<Integer, Button> eap$openUIButtons = new HashMap<>();
@Unique
private static final Logger EAP_LOGGER = LogManager.getLogger("ExtendedAE_Plus");
@Unique
private boolean eap$debugLoggedOnce = false;
@Shadow(remap = false) private AETextField searchOutField;
@Shadow(remap = false) private AETextField searchInField;
@Shadow(remap = false) private Set<ItemStack> matchedStack;
@ -109,8 +130,9 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
/**
* 拦截鼠标点击事件实现Shift+左键快速上传样板功能
* 注意某些整合包的 ExtendedAE 版本不在该类中覆写 mouseClicked此处设置 require=0 以防止注入失败导致崩溃
*/
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
@Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true, require = 0)
private void onMouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) {
// 检查是否是左键点击 + Shift键
if (button == 0 && hasShiftDown()) {
@ -181,6 +203,86 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
}
}
@Unique
private int getIntConst(Class<?> cls, String name, int defVal) {
try {
var f = cls.getDeclaredField(name);
f.setAccessible(true);
return (int) f.get(null);
} catch (Throwable t) {
return defVal;
}
}
@Unique
private void eap$tryOpenProviderUI(int rowIndex) {
try {
// 使用 Accessor 获取 rows避免取到父类导致失败
com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor acc =
(com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor) (Object) this;
java.util.ArrayList<?> rows = acc.getRows();
// 找到该分组对应的第一个 PatternContainerRecord
Class<?> cls = com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal.class;
var byGroupField = cls.getDeclaredField("byGroup");
byGroupField.setAccessible(true);
Object byGroup = byGroupField.get(this); // HashMultimap<PatternContainerGroup, PatternContainerRecord>
Object headerRow = rows.get(rowIndex);
var groupField = headerRow.getClass().getDeclaredField("group");
groupField.setAccessible(true);
Object group = groupField.get(headerRow);
// 调用 byGroup.get(group)再取第一个元素
java.util.Collection<?> containers = (java.util.Collection<?>) byGroup.getClass().getMethod("get", Object.class).invoke(byGroup, group);
if (containers == null || containers.isEmpty()) {
return;
}
Object firstRecord = containers.iterator().next(); // PatternContainerRecord
long serverId = (long) firstRecord.getClass().getMethod("getServerId").invoke(firstRecord);
// 通过 infoMap 获取位置信息
var infoMapField = cls.getDeclaredField("infoMap");
infoMapField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.HashMap<Long, Object> infoMap = (java.util.HashMap<Long, Object>) infoMapField.get(this);
Object info = infoMap.get(serverId);
if (info == null) {
// 无位置信息提示
if (this.minecraft != null && this.minecraft.player != null) {
this.minecraft.player.displayClientMessage(Component.literal("未找到该供应器的位置信息无法打开UI"), true);
}
return;
}
// PatternProviderInfo record: pos(), face(), playerWorld()
Object pos = info.getClass().getMethod("pos").invoke(info);
Object face = info.getClass().getMethod("face").invoke(info); // 可能为 null方块型供应器
Object playerWorld = info.getClass().getMethod("playerWorld").invoke(info);
// 避免对 MC 类进行反射使用强制类型转换后直接调用方法 Forge 运行时重映射保证
long posLong = ((BlockPos) pos).asLong();
String dimStr = ((ResourceKey<Level>) playerWorld).location().toString();
int faceOrd = -1;
if (face != null) {
faceOrd = ((Direction) face).ordinal();
}
// 发送我们自己的 C2S OpenProviderUiC2SPacket
try {
ModNetwork.CHANNEL.sendToServer(new OpenProviderUiC2SPacket(
posLong,
new ResourceLocation(dimStr),
faceOrd
));
} catch (Throwable t) {
// 静默失败不提示玩家
}
} catch (Throwable t) {
// 静默失败不输出日志
}
}
/**
* 重置当前选择的样板供应器ID
*/
@ -228,6 +330,70 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
this.addToLeftToolbar(this.eap$toggleSlotsButton);
}
/**
* 处理屏幕缩放resize后按钮位置未更新的问题
* - 清理并移除现有的打开UI按钮
* - 尝试重置滚动条并刷新列表
* 缩放后的下一帧drawFG 会基于新的 leftPos/topPos 重建与定位按钮
*/
@Inject(method = "resize", at = @At("TAIL"), remap = false, require = 0)
private void eap$onResize(Minecraft mc, int width, int height, CallbackInfo ci) {
try {
// 移除并清理按钮避免旧位置残留
this.eap$openUIButtons.values().forEach(this::removeWidget);
this.eap$openUIButtons.clear();
// 重置一次滚动条避免可见行/偏移在缩放后与 UI 尺寸不一致
try {
Method resetScrollbarMethod = null;
try {
resetScrollbarMethod = this.getClass().getDeclaredMethod("resetScrollbar");
} catch (NoSuchMethodException e1) {
try {
resetScrollbarMethod = this.getClass().getSuperclass().getDeclaredMethod("resetScrollbar");
} catch (NoSuchMethodException e2) {
resetScrollbarMethod = null;
}
}
if (resetScrollbarMethod != null) {
resetScrollbarMethod.setAccessible(true);
resetScrollbarMethod.invoke(this);
}
} catch (Throwable ignored) {
}
// 刷新列表使 rows/visibleRows 立即以新尺寸重算
try {
Method refreshMethod = null;
try {
refreshMethod = this.getClass().getDeclaredMethod("refreshList");
} catch (NoSuchMethodException e1) {
try {
refreshMethod = this.getClass().getSuperclass().getDeclaredMethod("refreshList");
} catch (NoSuchMethodException e2) {
refreshMethod = null;
}
}
if (refreshMethod != null) {
refreshMethod.setAccessible(true);
refreshMethod.invoke(this);
}
} catch (Throwable ignored) {
}
// 下次绘制重新输出一次调试行便于确认缩放后的 rows/scroll
this.eap$debugLoggedOnce = false;
} catch (Throwable ignored) {
}
}
@Inject(method = "init", at = @At("TAIL"), remap = false, require = 0)
private void eap$onInit(CallbackInfo ci) {
// 清理旧的打开UI按钮
this.eap$openUIButtons.values().forEach(this::removeWidget);
this.eap$openUIButtons.clear();
}
@Inject(method = "refreshList", at = @At("HEAD"), remap = false)
private void onRefreshListStart(CallbackInfo ci) {
// 更新按钮图标
@ -236,6 +402,9 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
this.eap$showSlots ? "gui.expatternprovider.hide_slots" : "gui.expatternprovider.show_slots"
)));
}
// 清理旧的打开UI按钮
this.eap$openUIButtons.values().forEach(this::removeWidget);
this.eap$openUIButtons.clear();
}
@Inject(method = "refreshList", at = @At("TAIL"), remap = false)
@ -342,6 +511,63 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
@Inject(method = "drawFG", at = @At("TAIL"), remap = false)
private void eap$afterDrawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY, CallbackInfo ci) {
// 动态放置/创建每个组标题后的打开UI按钮
try {
// 使用 Accessor 获取必要的字段避免反射失败
com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor acc =
(com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor) (Object) this;
java.util.ArrayList<?> rows = acc.getRows();
int currentScroll = acc.getScrollbar().getCurrentScroll();
// 直接引用目标类以获取其静态常量
Class<?> cls = com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal.class;
int GUI_PADDING_X = getIntConst(cls, "GUI_PADDING_X", 22);
int GUI_PADDING_Y = getIntConst(cls, "GUI_PADDING_Y", 6);
int GUI_HEADER_HEIGHT = getIntConst(cls, "GUI_HEADER_HEIGHT", 51);
int ROW_HEIGHT = getIntConst(cls, "ROW_HEIGHT", 18);
int TEXT_MAX_WIDTH = getIntConst(cls, "TEXT_MAX_WIDTH", 155);
int visibleRows = acc.getVisibleRows();
// 生产环境移除调试日志
// 先隐藏旧按钮避免残留
for (Button b : this.eap$openUIButtons.values()) {
b.visible = false;
}
int shownCount = 0;
for (int i = 0; i < visibleRows; i++) {
int rowIndex = currentScroll + i;
if (rowIndex < 0 || rowIndex >= rows.size()) {
continue;
}
Object row = rows.get(rowIndex);
if (!row.getClass().getSimpleName().equals("GroupHeaderRow")) {
continue;
}
// 放置按钮位于名称文本右侧与原类 choiceButton 锚点相邻向右偏移 20px
int bx = this.leftPos + GUI_PADDING_X + TEXT_MAX_WIDTH - 40;
int by = this.topPos + GUI_PADDING_Y + GUI_HEADER_HEIGHT + i * ROW_HEIGHT - 3;
Button btn = eap$openUIButtons.get(rowIndex);
if (btn == null) {
btn = Button.builder(Component.literal("UI"), (b) -> {
eap$tryOpenProviderUI(rowIndex);
}).size(14, 12).build();
btn.setTooltip(Tooltip.create(Component.literal("打开该供应器目标容器的界面")));
eap$openUIButtons.put(rowIndex, btn);
this.addRenderableWidget(btn);
}
btn.setPosition(bx, by);
btn.visible = true;
shownCount++;
}
// 生产环境移除调试日志
} catch (Throwable ignored) {
}
// 原有的搜索高亮逻辑
// 仅当任一搜索框非空时绘制叠加层与原版行为保持一致
boolean searchActive = (this.searchOutField != null && !this.searchOutField.getValue().isEmpty())

View File

@ -0,0 +1,155 @@
package com.extendedae_plus.network;
import appeng.api.crafting.IPatternDetails;
import appeng.api.networking.IGrid;
import appeng.api.networking.crafting.ICraftingProvider;
import appeng.api.networking.security.IActionHost;
import appeng.api.stacks.AEKey;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.helpers.patternprovider.PatternProviderLogicHost;
import appeng.me.service.CraftingService;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor;
import com.mojang.logging.LogUtils;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.network.NetworkHooks;
import java.util.Collection;
import java.util.function.Supplier;
/**
* 客户端从 CraftingCPUScreen 发送鼠标下条目对应的 AEKey
* 服务端在当前打开的 CraftingCPUMenu 所属网络中定位匹配该 AEKey 的样板供应器
* 尝试打开其目标机器的 GUI
*/
public class CraftingMonitorJumpC2SPacket {
private final AEKey what;
public CraftingMonitorJumpC2SPacket(AEKey what) {
this.what = what;
}
public static void encode(CraftingMonitorJumpC2SPacket msg, FriendlyByteBuf buf) {
AEKey.writeKey(buf, msg.what);
}
public static CraftingMonitorJumpC2SPacket decode(FriendlyByteBuf buf) {
AEKey key = AEKey.readKey(buf);
return new CraftingMonitorJumpC2SPacket(key);
}
public static void handle(CraftingMonitorJumpC2SPacket msg, Supplier<NetworkEvent.Context> ctx) {
NetworkEvent.Context context = ctx.get();
context.enqueueWork(() -> {
ServerPlayer player = context.getSender();
if (player == null) return;
LogUtils.getLogger().info("EAP[S]: recv CraftingMonitorJumpC2SPacket key={} from {}", msg.what, player.getGameProfile().getName());
// 必须在 CraftingCPU 界面内
if (!(player.containerMenu instanceof appeng.menu.me.crafting.CraftingCPUMenu menu)) {
LogUtils.getLogger().info("EAP[S]: not in CraftingCPUMenu, abort");
return;
}
// 通过菜单 target可能是 BlockEntity/Part/ItemHost IActionHost 获取 Grid
IGrid grid = null;
Object target = ((appeng.menu.AEBaseMenu) menu).getTarget();
if (target instanceof IActionHost host && host.getActionableNode() != null) {
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;
}
// 2) 遍历提供该样板的 Provider优先 PatternProviderLogic
for (var pattern : patterns) {
var providers = craftingService.getProviders(pattern);
int providerCount = 0;
for (var provider : providers) {
providerCount++;
try {
LogUtils.getLogger().info("EAP[S]: provider class={}", provider.getClass().getName());
} catch (Throwable ignored) {}
if (provider instanceof PatternProviderLogic ppl) {
// 使用 accessor 获取 host受保护字段通过 accessor 访问
PatternProviderLogicHost host = ((PatternProviderLogicAccessor) ppl).eap$host();
if (host == null) continue;
var pbe = host.getBlockEntity();
ServerLevel serverLevel = player.serverLevel();
// 尝试对邻居打开 GUI复用 OpenProviderUiC2SPacket 的策略
for (Direction dir : host.getTargets()) {
BlockPos targetPos = pbe.getBlockPos().relative(dir);
var tbe = serverLevel.getBlockEntity(targetPos);
if (tbe instanceof MenuProvider provider1) {
LogUtils.getLogger().info("EAP[S]: open screen via MenuProvider at {}", targetPos);
NetworkHooks.openScreen(player, provider1, targetPos);
context.setPacketHandled(true);
return;
}
var tstate = serverLevel.getBlockState(targetPos);
var provider2 = tstate.getMenuProvider(serverLevel, targetPos);
if (provider2 != null) {
LogUtils.getLogger().info("EAP[S]: open screen via state.getMenuProvider at {}", targetPos);
NetworkHooks.openScreen(player, provider2, targetPos);
context.setPacketHandled(true);
return;
}
}
// 兜底若无 MenuProvider始终模拟一次右键优先有方块实体的一面
InteractionHand hand = player.getMainHandItem().isEmpty() ? InteractionHand.MAIN_HAND : InteractionHand.MAIN_HAND;
Direction chosen = null;
for (Direction d : host.getTargets()) {
if (serverLevel.getBlockEntity(pbe.getBlockPos().relative(d)) != null) { chosen = d; break; }
}
if (chosen == null) {
for (Direction d : host.getTargets()) {
if (!serverLevel.getBlockState(pbe.getBlockPos().relative(d)).isAir()) { chosen = d; break; }
}
}
if (chosen != null) {
BlockPos targetPos = pbe.getBlockPos().relative(chosen);
var state2 = serverLevel.getBlockState(targetPos);
var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), chosen.getOpposite(), targetPos, false);
InteractionResult r = state2.use(serverLevel, player, hand, hit);
LogUtils.getLogger().info("EAP[S]: simulated use on {}, face={}, result={}", targetPos, chosen, r);
if (r.consumesAction()) {
context.setPacketHandled(true);
return;
}
}
}
}
LogUtils.getLogger().info("EAP[S]: providers count for one pattern: {}", providerCount);
}
LogUtils.getLogger().info("EAP[S]: no target opened for key={}", msg.what);
});
context.setPacketHandled(true);
}
}

View File

@ -0,0 +1,115 @@
package com.extendedae_plus.network;
import appeng.api.crafting.IPatternDetails;
import appeng.api.networking.IGrid;
import appeng.api.networking.security.IActionHost;
import appeng.api.stacks.AEKey;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.helpers.patternprovider.PatternProviderLogicHost;
import appeng.me.service.CraftingService;
import appeng.menu.AEBaseMenu;
import appeng.menu.me.crafting.CraftingCPUMenu;
import appeng.menu.locator.MenuLocators;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor;
import com.mojang.logging.LogUtils;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
import java.util.Collection;
import java.util.function.Supplier;
/**
* 客户端从 CraftingCPUScreen 发送鼠标下条目对应的 AEKey
* 服务端在当前打开的 CraftingCPUMenu 所属网络中定位匹配该 AEKey 的样板供应器
* 打开该供应器自身的 UI不是目标机器的 UI
*/
public class CraftingMonitorOpenProviderC2SPacket {
private final AEKey what;
public CraftingMonitorOpenProviderC2SPacket(AEKey what) {
this.what = what;
}
public static void encode(CraftingMonitorOpenProviderC2SPacket msg, FriendlyByteBuf buf) {
AEKey.writeKey(buf, msg.what);
}
public static CraftingMonitorOpenProviderC2SPacket decode(FriendlyByteBuf buf) {
AEKey key = AEKey.readKey(buf);
return new CraftingMonitorOpenProviderC2SPacket(key);
}
public static void handle(CraftingMonitorOpenProviderC2SPacket msg, Supplier<NetworkEvent.Context> ctx) {
NetworkEvent.Context context = ctx.get();
context.enqueueWork(() -> {
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;
}
// 通过菜单的 target可能是 BlockEntity/Part/ItemHost IActionHost 获取 Grid
IGrid grid = null;
Object target = ((AEBaseMenu) menu).getTarget();
if (target instanceof IActionHost host && host.getActionableNode() != null) {
grid = host.getActionableNode().getGrid();
}
if (grid == null) {
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;
}
// 2) 遍历提供该样板的 Provider定位 PatternProviderLogic
for (var pattern : patterns) {
var providers = craftingService.getProviders(pattern);
for (var provider : providers) {
if (provider instanceof PatternProviderLogic ppl) {
// accessor 获取 host
PatternProviderLogicHost host = ((PatternProviderLogicAccessor) ppl).eap$host();
if (host == null) continue;
var pbe = host.getBlockEntity();
if (pbe == null) continue;
// 在服务端上下文中执行pbe 仅用于构造菜单定位器
// 直接打开供应器自身的 UI调用 Host 默认方法
try {
// 部件与方块实体分别选择定位器
if (host instanceof appeng.parts.AEBasePart part) {
host.openMenu(player, MenuLocators.forPart(part));
} else {
host.openMenu(player, MenuLocators.forBlockEntity(pbe));
}
context.setPacketHandled(true);
return;
} catch (Throwable t) {
LogUtils.getLogger().error("EAP[S]: open provider UI failed at {}", pbe.getBlockPos(), t);
}
}
}
}
LogUtils.getLogger().info("EAP[S]: no provider UI opened for key={}", msg.what);
});
context.setPacketHandled(true);
}
}

View File

@ -18,6 +18,12 @@ public class ModNetwork {
private static int id = 0;
public static void register() {
CHANNEL.messageBuilder(OpenProviderUiC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(OpenProviderUiC2SPacket::encode)
.decoder(OpenProviderUiC2SPacket::decode)
.consumerNetworkThread(OpenProviderUiC2SPacket::handle)
.add();
CHANNEL.messageBuilder(PickFromWirelessC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(PickFromWirelessC2SPacket::encode)
.decoder(PickFromWirelessC2SPacket::decode)
@ -65,6 +71,18 @@ public class ModNetwork {
.decoder(AdvancedBlockingSyncS2CPacket::decode)
.consumerNetworkThread(AdvancedBlockingSyncS2CPacket::handle)
.add();
CHANNEL.messageBuilder(CraftingMonitorJumpC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(CraftingMonitorJumpC2SPacket::encode)
.decoder(CraftingMonitorJumpC2SPacket::decode)
.consumerNetworkThread(CraftingMonitorJumpC2SPacket::handle)
.add();
CHANNEL.messageBuilder(CraftingMonitorOpenProviderC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(CraftingMonitorOpenProviderC2SPacket::encode)
.decoder(CraftingMonitorOpenProviderC2SPacket::decode)
.consumerNetworkThread(CraftingMonitorOpenProviderC2SPacket::handle)
.add();
}
private static int nextId() { return id++; }

View File

@ -0,0 +1,133 @@
package com.extendedae_plus.network;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.core.Direction;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.InteractionResult;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.Level;
import net.minecraftforge.network.NetworkEvent;
import net.minecraftforge.network.NetworkHooks;
import java.util.function.Supplier;
public class OpenProviderUiC2SPacket {
private final long posLong;
private final ResourceLocation dimId;
private final int faceOrd; // 目前保留若目标需要可用
public OpenProviderUiC2SPacket(long posLong, ResourceLocation dimId, int faceOrd) {
this.posLong = posLong;
this.dimId = dimId;
this.faceOrd = faceOrd;
}
public static void encode(OpenProviderUiC2SPacket msg, FriendlyByteBuf buf) {
buf.writeLong(msg.posLong);
buf.writeResourceLocation(msg.dimId);
buf.writeVarInt(msg.faceOrd);
}
public static OpenProviderUiC2SPacket decode(FriendlyByteBuf buf) {
long posLong = buf.readLong();
ResourceLocation dimId = buf.readResourceLocation();
int faceOrd = buf.readVarInt();
return new OpenProviderUiC2SPacket(posLong, dimId, faceOrd);
}
public static void handle(OpenProviderUiC2SPacket msg, Supplier<NetworkEvent.Context> ctx) {
NetworkEvent.Context context = ctx.get();
context.enqueueWork(() -> {
ServerPlayer player = context.getSender();
if (player == null) return;
// 校验维度与方块
ResourceKey<Level> levelKey = ResourceKey.create(Registries.DIMENSION, msg.dimId);
ServerLevel level = player.server.getLevel(levelKey);
if (level == null) {
return; // 无效维度
}
BlockPos pos = BlockPos.of(msg.posLong);
if (!level.isLoaded(pos)) {
return; // 区块未加载
}
var be = level.getBlockEntity(pos);
var stateAtPos = level.getBlockState(pos);
// 目标通常是供应器所面对/连接的相邻方块优先尝试邻居
Direction[] tries = (msg.faceOrd >= 0 && msg.faceOrd < Direction.values().length)
? new Direction[]{Direction.values()[msg.faceOrd]}
: Direction.values();
for (Direction dir : tries) {
BlockPos targetPos = pos.relative(dir);
BlockEntity tbe = level.getBlockEntity(targetPos);
if (tbe instanceof MenuProvider provider) {
NetworkHooks.openScreen(player, provider, targetPos);
return;
}
var tstate = level.getBlockState(targetPos);
MenuProvider provider2 = tstate.getMenuProvider(level, targetPos);
if (provider2 != null) {
NetworkHooks.openScreen(player, provider2, targetPos);
return;
}
}
// 如果邻居也未提供 MenuProvider则兜底尽量模拟一次徒手右键相邻方块
boolean anyHandEmpty = player.getMainHandItem().isEmpty() || player.getOffhandItem().isEmpty();
if (anyHandEmpty) {
InteractionHand hand = player.getMainHandItem().isEmpty() ? InteractionHand.MAIN_HAND : InteractionHand.OFF_HAND;
if (msg.faceOrd >= 0 && msg.faceOrd < Direction.values().length) {
Direction dir = Direction.values()[msg.faceOrd];
BlockPos targetPos = pos.relative(dir);
var state2 = level.getBlockState(targetPos);
var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), dir.getOpposite(), targetPos, false);
InteractionResult r = state2.use(level, player, hand, hit);
if (r.consumesAction()) {
return;
}
} else {
// 无明确朝向优先挑选有方块实体的邻居否则挑选非空气方块
Direction chosen = null;
for (Direction d : Direction.values()) {
if (level.getBlockEntity(pos.relative(d)) != null) { chosen = d; break; }
}
if (chosen == null) {
for (Direction d : Direction.values()) {
if (!level.getBlockState(pos.relative(d)).isAir()) { chosen = d; break; }
}
}
if (chosen != null) {
BlockPos targetPos = pos.relative(chosen);
var state2 = level.getBlockState(targetPos);
var hit = new BlockHitResult(Vec3.atCenterOf(targetPos), chosen.getOpposite(), targetPos, false);
InteractionResult r = state2.use(level, player, hand, hit);
if (r.consumesAction()) {
return;
}
} else {
// 无可选邻居
}
}
} else {
// 双手占用则跳过兜底交互
}
context.setPacketHandled(true);
});
}
}

View File

@ -7,7 +7,6 @@
"PickFromWirelessMixin",
"accessor.AbstractContainerScreenAccessor",
"accessor.ScreenAccessor",
"accessor.ScreenInvoker",
"ae2.AEBaseScreenMixin",
"ae2.PatternEncodingTermScreenMixin",
"ae2.PatternProviderScreenMixin",