为管理终端增加打开机器ui按钮功能

This commit is contained in:
GaLi 2025-08-26 18:31:01 +08:00
parent 83c52fc659
commit b6b9c14446
4 changed files with 310 additions and 7 deletions

View File

@ -102,9 +102,6 @@ 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"
}
allprojects {

View File

@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G
loom.platform = forge
# Mod properties
mod_version = 1.3.3
mod_version = 1.3.4-beta
maven_group = com.extendedae_plus
archives_name = extendedae_plus

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

@ -11,6 +11,7 @@ import appeng.client.gui.widgets.IconButton;
import appeng.menu.AEBaseMenu;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.network.chat.Component;
@ -29,9 +30,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, remap = false)
public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu> {
@Unique
@ -46,6 +51,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;
@ -181,6 +194,97 @@ 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);
long posLong = (long) pos.getClass().getMethod("asLong").invoke(pos);
Object rl = playerWorld.getClass().getMethod("location").invoke(playerWorld); // ResourceLocation
String dimStr = (String) rl.getClass().getMethod("toString").invoke(rl);
int faceOrd = -1;
if (face != null) {
faceOrd = (int) face.getClass().getMethod("ordinal").invoke(face);
}
// 发送 CGenericPacket("open_ui", [posLong, dim, face])
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("open_ui", new Object[]{posLong, dimStr, faceOrd});
Class<?> iMessage = Class.forName("com.glodblock.github.glodium.network.packet.IMessage");
Method sendToServer = EPPNetworkHandlerClass.getMethod("sendToServer", iMessage);
sendToServer.invoke(handlerInstance, packet);
if (this.minecraft != null && this.minecraft.player != null) {
EAP_LOGGER.debug("[EPlus] Sent open_ui packet: pos={}, dim={}, face={}", posLong, dimStr, faceOrd);
}
} catch (Throwable t) {
if (this.minecraft != null && this.minecraft.player != null) {
this.minecraft.player.displayClientMessage(Component.literal("❌ ExtendedAE Plus: 网络模块不可用无法发送打开UI请求"), true);
}
}
} catch (Throwable t) {
EAP_LOGGER.warn("[EPlus] eap$tryOpenProviderUI failed: {}", t.toString());
}
}
/**
* 重置当前选择的样板供应器ID
*/
@ -228,6 +332,13 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
this.addToLeftToolbar(this.eap$toggleSlotsButton);
}
@Inject(method = "init", at = @At("TAIL"), remap = false)
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 +347,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 +456,71 @@ 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();
if (!eap$debugLoggedOnce) {
EAP_LOGGER.info("[EPlus] GuiExPatternTerminalMixin.afterDrawFG fired: rows={}, currentScroll={}, visibleRows={}",
rows.size(), currentScroll, visibleRows);
eap$debugLoggedOnce = true;
}
// 先隐藏旧按钮避免残留
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;
Button btn = eap$openUIButtons.get(rowIndex);
if (btn == null) {
btn = Button.builder(Component.literal("UI"), (b) -> {
eap$tryOpenProviderUI(rowIndex);
}).size(18, 16).build();
btn.setTooltip(Tooltip.create(Component.literal("打开该供应器目标容器的界面")));
eap$openUIButtons.put(rowIndex, btn);
this.addRenderableWidget(btn);
}
btn.setPosition(bx, by);
btn.visible = true;
shownCount++;
}
if (shownCount == 0) {
EAP_LOGGER.debug("[EPlus] No GroupHeaderRow visible in current page (scroll={}, rows={})", currentScroll, rows.size());
} else {
EAP_LOGGER.debug("[EPlus] GroupHeaderRow buttons shown count: {}", shownCount);
}
} catch (Throwable ignored) {
}
// 原有的搜索高亮逻辑
// 仅当任一搜索框非空时绘制叠加层与原版行为保持一致
boolean searchActive = (this.searchOutField != null && !this.searchOutField.getValue().isEmpty())