Merge remote-tracking branch 'origin/master' into feature/pattern_display

# Conflicts:
#	src/main/java/com/extendedae_plus/util/ExtendedAELogger.java
This commit is contained in:
C-H716 2025-08-22 15:11:35 +08:00
commit b8da83b902
11 changed files with 511 additions and 327 deletions

View File

@ -16,6 +16,15 @@ loom {
forge {
mixinConfig 'extendedae_plus.mixins.json'
}
runs {
client1 {
client()
name "Client 1"
runDir "run/client1"
programArgs "--username", "Player1"
}
}
}
repositories {

View File

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

View File

@ -0,0 +1,11 @@
package com.extendedae_plus.api;
/**
* {@code GuiExPatternProviderMixin} 实现用于从通用的 Screen Mixin 中更新按钮布局
*/
public interface ExPatternButtonsAccessor {
/**
* 在每帧调用以维护扩展样板供应器右侧按钮的可见性重注册窗口尺寸变化与定位
*/
void eap$updateButtonsLayout();
}

View File

@ -0,0 +1,8 @@
package com.extendedae_plus.api;
/**
* GuiExPatternProviderMixin 实现用于在客户端侧提供当前页号避免反射读取 AE2 内部字段失败
*/
public interface ExPatternPageAccessor {
int eap$getCurrentPage();
}

View File

@ -0,0 +1,176 @@
package com.extendedae_plus.mixin.ae2;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.TextOverride;
import appeng.client.Point;
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 com.extendedae_plus.api.ExPatternPageAccessor;
import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider;
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 org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
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;
@Mixin(AEBaseScreen.class)
public abstract class AEBaseScreenMixin {
@Unique
private ScreenStyle eap$getStyle(Object self) {
try {
var f = self.getClass().getDeclaredField("style");
f.setAccessible(true);
Object v = f.get(self);
if (v instanceof ScreenStyle s) return s;
} catch (Throwable ignored) {}
return null;
}
@Unique
private static int eap$getIntField(Object self, String name, int def) {
Class<?> c = self.getClass();
while (c != null && c != Object.class) {
try {
var f = c.getDeclaredField(name);
f.setAccessible(true);
Object v = f.get(self);
if (v instanceof Integer i) return i;
} catch (Throwable ignored) {}
c = c.getSuperclass();
}
return def;
}
@Unique
private static Font eap$getFont(Object self) {
Class<?> c = self.getClass();
while (c != null && c != Object.class) {
try {
var f = c.getDeclaredField("font");
f.setAccessible(true);
Object v = f.get(self);
if (v instanceof Font font) return font;
} catch (Throwable ignored) {}
c = c.getSuperclass();
}
return net.minecraft.client.Minecraft.getInstance().font;
}
// AEBaseScreen.drawText 完成某个文本绘制后若该文本为样板标签则紧接着绘制页码
@Inject(method = "drawText", at = @At("TAIL"), remap = false)
private void eap$appendPageAfterPatternsLabel(GuiGraphics guiGraphics,
Text text,
@Nullable TextOverride override,
CallbackInfo ci) {
Object self = this;
if (!(self instanceof GuiExPatternProvider)) {
return;
}
try {
// 解析最终用于显示的标签内容
Component content = text.getText();
if (override != null && override.getContent() != null) {
content = override.getContent().copy().withStyle(content.getStyle());
}
// 计算样板文本起点与宽度按对齐方式与缩放修正 x/y
int imageWidth = eap$getIntField(self, "imageWidth", 0);
int imageHeight = eap$getIntField(self, "imageHeight", 0);
Rect2i bounds = new Rect2i(0, 0, imageWidth, imageHeight);
Point pos = text.getPosition().resolve(bounds);
float scale = text.getScale();
Font font = eap$getFont(self);
// 只关心第一行标题类文本无换行或 maxWidth<=0
var contentLine = (text.getMaxWidth() <= 0)
? content.getVisualOrderText()
: font.split(content, text.getMaxWidth()).get(0);
int lineWidth = font.width(contentLine);
int x = pos.getX();
int y = pos.getY();
// 对齐修正
var align = text.getAlign();
if (align == TextAlignment.CENTER) {
int textPx = Math.round(lineWidth * scale);
x -= textPx / 2;
} else if (align == TextAlignment.RIGHT) {
int textPx = Math.round(lineWidth * scale);
x -= textPx;
}
// 判断是否为样板组标题多语言兼容且避免标题
boolean isPatterns = false;
// 1) 基于翻译键
var contents = content.getContents();
if (contents instanceof TranslatableContents tc) {
String key = tc.getKey();
if (key != null && key.endsWith(".patterns")) {
isPatterns = true;
}
}
// 2) 基于已知本地化键的字符串解析
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;
else if (label.equals(Component.translatable("gui.ae2.patterns").getString())) isPatterns = true;
}
}
// 3) 容错中文样板且在标题下方放宽到 y>=14或文本正好等于样板
if (!isPatterns) {
String s = content.getString();
if (s != null && ("样板".equals(s) || (s.contains("样板") && y >= 14))) {
isPatterns = true;
}
}
if (!isPatterns) return;
int cur = 1;
int max = 1;
if (self instanceof ExPatternPageAccessor accessor) {
cur = Math.max(0, accessor.eap$getCurrentPage()) + 1;
}
try {
var fMax = self.getClass().getDeclaredField("eap$maxPageLocal");
fMax.setAccessible(true);
Object v = fMax.get(self);
if (v instanceof Integer i) {
max = Math.max(1, i);
}
} catch (Throwable ignored) {}
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) {}
}
int padding = 4;
if (scale == 1.0f) {
guiGraphics.drawString(font, pageText, x + lineWidth + padding, y, color, false);
} else {
guiGraphics.pose().pushPose();
guiGraphics.pose().translate(x, y, 1);
guiGraphics.pose().scale(scale, scale, 1);
guiGraphics.drawString(font, pageText, lineWidth + padding, 0, color, false);
guiGraphics.pose().popPose();
}
} catch (Throwable ignored) {}
}
}

View File

@ -2,6 +2,7 @@ package com.extendedae_plus.mixin.ae2;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.helpers.patternprovider.PatternProviderLogicHost;
import appeng.menu.AEBaseMenu;
import appeng.menu.guisync.GuiSync;
import appeng.menu.implementations.PatternProviderMenu;
import com.extendedae_plus.api.AdvancedBlockingHolder;
@ -20,9 +21,6 @@ import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
@Mixin(PatternProviderMenu.class)
public abstract class PatternProviderMenuAdvancedMixin implements PatternProviderMenuAdvancedSync {
@Shadow
protected abstract boolean isServerSide();
@Shadow
protected PatternProviderLogic logic;
@ -33,18 +31,16 @@ public abstract class PatternProviderMenuAdvancedMixin implements PatternProvide
@Inject(method = "broadcastChanges", at = @At("HEAD"))
private void eap$syncAdvancedBlocking(CallbackInfo ci) {
if (this.isServerSide()) {
// 避免@Shadow父类方法改用公共APIAEBaseMenu#isClientSide()
if (!((AEBaseMenu) (Object) this).isClientSide()) {
var l = this.logic;
if (l instanceof AdvancedBlockingHolder holder) {
this.eap$AdvancedBlocking = holder.eap$getAdvancedBlocking();
LOGGER.debug("[EAP] Menu broadcastChanges HEAD: eap$AdvancedBlocking={}", this.eap$AdvancedBlocking);
}
}
}
@Inject(method = "broadcastChanges", at = @At("TAIL"))
private void eap$syncAdvancedBlockingTail(CallbackInfo ci) {
}
// 构造器尾注入public ctor
@Inject(method = "<init>(ILnet/minecraft/world/entity/player/Inventory;Lappeng/helpers/patternprovider/PatternProviderLogicHost;)V", at = @At("TAIL"))
private void eap$initAdvancedSync_Public(int id, Inventory playerInventory, PatternProviderLogicHost host, CallbackInfo ci) {

View File

@ -4,14 +4,15 @@ import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.Icon;
import appeng.client.gui.implementations.PatternProviderScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.ToggleButton;
import appeng.client.gui.widgets.SettingToggleButton;
import appeng.api.config.YesNo;
import appeng.api.config.Settings;
import appeng.menu.implementations.PatternProviderMenu;
import com.extendedae_plus.api.PatternProviderMenuAdvancedSync;
import com.extendedae_plus.client.ClientAdvancedBlockingState;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderMenuAdvancedAccessor;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.ToggleAdvancedBlockingC2SPacket;
import com.extendedae_plus.api.ExPatternButtonsAccessor;
import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import org.spongepowered.asm.mixin.Mixin;
@ -21,122 +22,88 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
/**
* AE2 原版样板供应器界面添加高级阻挡模式按钮仅客户端UI反馈
* AE2 原版样板供应器界面添加高级阻挡模式按钮
* - 位于左侧工具栏
* - 点击后切换图标YES/NO并切换 tooltip 提示
* - 当前不做任何网络/服务端逻辑
* - 点击仅发送 C2S 切换请求状态由 AE2 @GuiSync 回传决定
*/
@Mixin(PatternProviderScreen.class)
public abstract class PatternProviderScreenMixin<C extends PatternProviderMenu> extends AEBaseScreen<C> {
@Unique
private ToggleButton eap$AdvancedBlockingToggle;
private SettingToggleButton<YesNo> eap$AdvancedBlockingToggle;
@Unique
private boolean eap$AdvancedBlockingEnabled = false;
@Unique
private String eap$ProviderKey = null;
public PatternProviderScreenMixin(C menu, Inventory playerInventory, Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
}
@Inject(method = "<init>", at = @At("RETURN"))
private void eap$initAdvancedBlocking(C menu, Inventory playerInventory, Component title, ScreenStyle style, CallbackInfo ci) {
// 计算供应器唯一键维度ID + 方块坐标
// 使用 @GuiSync 初始化
try {
var logic = ((PatternProviderMenuAdvancedAccessor) menu).eap$logic();
var host = ((PatternProviderLogicAccessor) logic).eap$host();
var be = host.getBlockEntity();
var level = be.getLevel();
String dimId = level.dimension().location().toString();
long posLong = be.getBlockPos().asLong();
this.eap$ProviderKey = ClientAdvancedBlockingState.key(dimId, posLong);
if (menu instanceof PatternProviderMenuAdvancedSync sync) {
this.eap$AdvancedBlockingEnabled = sync.eap$getAdvancedBlockingSynced();
}
} catch (Throwable t) {
LOGGER.error("Error initializing advanced sync", t);
}
// 优先使用该供应器最近一次 S2C 状态否则回退读取 @GuiSync 初始化
if (this.eap$ProviderKey != null && ClientAdvancedBlockingState.has(this.eap$ProviderKey)) {
this.eap$AdvancedBlockingEnabled = ClientAdvancedBlockingState.get(this.eap$ProviderKey);
} else if (menu instanceof PatternProviderMenuAdvancedSync sync) {
this.eap$AdvancedBlockingEnabled = sync.eap$getAdvancedBlockingSynced();
}
// 使用 ToggleButton 以便在 YES/NO 图标与提示之间动态切换
this.eap$AdvancedBlockingToggle = new ToggleButton(
Icon.BLOCKING_MODE_YES,
Icon.BLOCKING_MODE_NO,
// 提示文本名称与说明
Component.literal("高级阻挡模式"),
Component.literal("高级阻挡模式:当开启时,执行更严格的阻挡判定"),
(state) -> {
// 客户端立即反馈切换图标/提示
this.eap$AdvancedBlockingEnabled = state;
this.eap$AdvancedBlockingToggle.setState(state);
// 发送 C2S 切换请求
// 使用 SettingToggleButton<YesNo> 的外观原版图标但自定义悬停描述为智能阻挡
this.eap$AdvancedBlockingToggle = new SettingToggleButton<>(
Settings.BLOCKING_MODE,
this.eap$AdvancedBlockingEnabled ? YesNo.YES : YesNo.NO,
(btn, backwards) -> {
// 不做本地切换点击仅发送自定义C2S显示由@GuiSync回传
LOGGER.debug("[EAP] Click advanced blocking toggle: send C2S");
ModNetwork.CHANNEL.sendToServer(new ToggleAdvancedBlockingC2SPacket());
// 可根据状态调整提示文本演示性开启/关闭不同第二行
if (state) {
this.eap$AdvancedBlockingToggle.setTooltipOn(java.util.List.of(
Component.literal("高级阻挡模式"),
Component.literal("高级阻挡模式:已开启")));
this.eap$AdvancedBlockingToggle.setTooltipOff(java.util.List.of(
Component.literal("高级阻挡模式"),
Component.literal("高级阻挡模式:已开启")));
} else {
this.eap$AdvancedBlockingToggle.setTooltipOn(java.util.List.of(
Component.literal("高级阻挡模式"),
Component.literal("高级阻挡模式:已关闭")));
this.eap$AdvancedBlockingToggle.setTooltipOff(java.util.List.of(
Component.literal("高级阻挡模式"),
Component.literal("高级阻挡模式:已关闭")));
}
}
);
this.eap$AdvancedBlockingToggle.setState(this.eap$AdvancedBlockingEnabled);
// 初始 tooltip
this.eap$AdvancedBlockingToggle.setTooltipOn(java.util.List.of(
Component.literal("高级阻挡模式"),
Component.literal(this.eap$AdvancedBlockingEnabled ? "高级阻挡模式:已开启" : "高级阻挡模式:已关闭")
));
this.eap$AdvancedBlockingToggle.setTooltipOff(java.util.List.of(
Component.literal("高级阻挡模式"),
Component.literal(this.eap$AdvancedBlockingEnabled ? "高级阻挡模式:已开启" : "高级阻挡模式:已关闭")
));
) {
@Override
public java.util.List<net.minecraft.network.chat.Component> getTooltipMessage() {
boolean enabled = eap$AdvancedBlockingEnabled;
var title = net.minecraft.network.chat.Component.literal("智能阻挡");
var line = enabled
? net.minecraft.network.chat.Component.literal("已启用YES")
: net.minecraft.network.chat.Component.literal("已禁用NO");
return java.util.List.of(title, line);
}
};
// 初始化后立刻对齐当前@GuiSync状态避免首帧显示不一致
LOGGER.debug("[EAP] Screen init: initial synced={} -> set button", this.eap$AdvancedBlockingEnabled);
this.eap$AdvancedBlockingToggle.set(this.eap$AdvancedBlockingEnabled ? YesNo.YES : YesNo.NO);
this.addToLeftToolbar(this.eap$AdvancedBlockingToggle);
}
// 每帧刷新从菜单同步布尔值保持按钮状态一致
@Inject(method = "updateBeforeRender", at = @At("TAIL"), remap = false)
// 每帧刷新从菜单(@GuiSync)同步布尔值保持按钮状态一致
@Inject(method = "updateBeforeRender", at = @At("HEAD"), remap = false)
private void eap$updateAdvancedBlocking(CallbackInfo ci) {
// 打印一条轻量 tick 日志以确认该方法被调用频繁输出可在验证后移除
// System.out.println("[EPP][CLIENT] updateBeforeRender tick, local=" + this.eppAdvancedBlockingEnabled);
if (this.eap$AdvancedBlockingToggle == null) return;
boolean desired = this.eap$AdvancedBlockingEnabled;
// 优先使用该供应器最近一次 S2C
if (this.eap$ProviderKey != null && ClientAdvancedBlockingState.has(this.eap$ProviderKey)) {
desired = ClientAdvancedBlockingState.get(this.eap$ProviderKey);
} else if (this.menu instanceof PatternProviderMenuAdvancedSync sync) {
if (this.menu instanceof PatternProviderMenuAdvancedSync sync) {
desired = sync.eap$getAdvancedBlockingSynced();
}
if (desired != this.eap$AdvancedBlockingEnabled) {
this.eap$AdvancedBlockingEnabled = desired;
this.eap$AdvancedBlockingToggle.setState(desired);
// 同步 tooltip 二行提示
this.eap$AdvancedBlockingToggle.setTooltipOn(java.util.List.of(
Component.literal("高级阻挡模式"),
Component.literal(desired ? "高级阻挡模式:已开启" : "高级阻挡模式:已关闭")
));
this.eap$AdvancedBlockingToggle.setTooltipOff(java.util.List.of(
Component.literal("高级阻挡模式"),
Component.literal(desired ? "高级阻挡模式:已开启" : "高级阻挡模式:已关闭")
));
// 与AE2一致每帧无条件对齐按钮状态至@GuiSync使用YesNo以获得原版图标与提示
LOGGER.debug("[EAP] updateBeforeRender tick: desired={}", desired);
if (this.eap$AdvancedBlockingEnabled != desired) {
LOGGER.debug("[EAP] updateBeforeRender: desired changed {} -> {}", this.eap$AdvancedBlockingEnabled, desired);
}
this.eap$AdvancedBlockingEnabled = desired;
this.eap$AdvancedBlockingToggle.set(desired ? YesNo.YES : YesNo.NO);
// 如果当前屏幕是 ExtendedAE GuiExPatternProvider则委托布局更新到 accessor
if ((Object) this instanceof GuiExPatternProvider) {
try {
((ExPatternButtonsAccessor) this).eap$updateButtonsLayout();
} catch (Throwable t) {
LOGGER.debug("[EAP] updateButtonsLayout skipped: {}", t.toString());
}
}
}
}

View File

@ -8,6 +8,7 @@ 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.CallbackInfoReturnable;
import java.lang.reflect.Field;
@Mixin(SlotGridLayout.class)
public abstract class SlotGridLayoutMixin {
@ -27,18 +28,54 @@ public abstract class SlotGridLayoutMixin {
return;
}
// 计算当前页码
int currentPage = semanticIdx / SLOTS_PER_PAGE;
// 计算在当前页中的位置
// 读取实际当前页码优先从 GUI accessor其次反射容器失败则为 0
int currentPage = 0;
try {
if (screen instanceof com.extendedae_plus.api.ExPatternPageAccessor accessor) {
currentPage = accessor.eap$getCurrentPage();
} else {
var menu = ((com.glodblock.github.extendedae.client.gui.GuiExPatternProvider) screen).getMenu();
Field fieldPage = eap$findFieldRecursive(menu.getClass(), "page");
if (fieldPage != null) {
fieldPage.setAccessible(true);
currentPage = (Integer) fieldPage.get(menu);
}
}
} catch (Throwable ignored) {
}
// 该槽位属于第几页
int slotPage = semanticIdx / SLOTS_PER_PAGE;
if (slotPage != currentPage) {
// 非当前页将其移出视野避免渲染与鼠标命中
cir.setReturnValue(new Point(-10000, -10000));
cir.cancel();
return;
}
// 当前页中的位置0..35
int slotInPage = semanticIdx % SLOTS_PER_PAGE;
int row = slotInPage / 9; // 0-3
int col = slotInPage % 9; // 0-8
// 计算目标位置始终在前4行
int targetX = x + col * 18;
int targetY = y + row * 18;
cir.setReturnValue(new Point(targetX, targetY));
cir.cancel();
}
}
@Unique
private static Field eap$findFieldRecursive(Class<?> cls, String name) {
Class<?> c = cls;
while (c != null && c != Object.class) {
try {
return c.getDeclaredField(name);
} catch (NoSuchFieldException ignored) {}
c = c.getSuperclass();
}
return null;
}
}

View File

@ -7,12 +7,12 @@ import appeng.client.gui.style.ScreenStyle;
import appeng.menu.SlotSemantics;
import com.extendedae_plus.NewIcon;
import com.glodblock.github.extendedae.client.button.ActionEPPButton;
import com.extendedae_plus.api.ExPatternButtonsAccessor;
import com.extendedae_plus.config.ModConfigs;
import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider;
import com.glodblock.github.extendedae.container.ContainerExPatternProvider;
import com.glodblock.github.extendedae.network.EPPNetworkHandler;
import com.glodblock.github.glodium.network.packet.CGenericPacket;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
@ -25,9 +25,10 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.lang.reflect.Field;
import java.util.List;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
@Mixin(GuiExPatternProvider.class)
public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<ContainerExPatternProvider> {
public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<ContainerExPatternProvider> implements ExPatternButtonsAccessor, com.extendedae_plus.api.ExPatternPageAccessor {
@Unique
ScreenStyle eap$screenStyle;
@ -41,224 +42,68 @@ public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<Co
@Unique
private static final int SLOTS_PER_PAGE = 36; // 每页显示36个槽位
@Unique
private int eap$currentPage = 0;
@Unique
private int eap$maxPageLocal = 1;
public GuiExPatternProviderMixin(ContainerExPatternProvider menu, Inventory playerInventory, Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
}
@Unique
public void render(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTicks) {
super.render(guiGraphics, mouseX, mouseY, partialTicks);
int maxSlots = this.getMenu().getSlots(SlotSemantics.ENCODED_PATTERN).size();
// 只有当槽位数超过每页显示数量时才显示翻页信息
if (maxSlots > SLOTS_PER_PAGE) {
Font fontRenderer = Minecraft.getInstance().font;
// 获取当前页码
int currentPage = getCurrentPage();
int maxPage = getMaxPage();
// 获取ae通用界面样式
int color = eap$screenStyle.getColor(PaletteColor.DEFAULT_TEXT_COLOR).toARGB();
// 调整页码显示位置"样板"文字的右边
guiGraphics.drawString(font, Component.literal("" + (currentPage + 1) + "/" + maxPage + ""),
leftPos + 8 + 50, topPos + 30, color, false);
}
}
@Unique
public void updateBeforeRender() {
super.updateBeforeRender();
try {
ContainerExPatternProvider menu1 = this.getMenu();
// 调用showPage方法
java.lang.reflect.Method showPageMethod = menu1.getClass().getMethod("showPage");
showPageMethod.invoke(menu1);
// 获取当前页码和最大页码
Field fieldPage = menu1.getClass().getDeclaredField("page");
fieldPage.setAccessible(true);
Integer page = (Integer) fieldPage.get(menu1);
Field fieldMaxPage = menu1.getClass().getDeclaredField("maxPage");
fieldMaxPage.setAccessible(true);
Integer maxPage = (Integer) fieldMaxPage.get(menu1);
// 更新按钮可见性 - 始终显示支持循环翻页
if (nextPage != null && prevPage != null) {
this.nextPage.setVisibility(true);
this.prevPage.setVisibility(true);
}
if (x2Button != null) {
this.x2Button.setVisibility(true);
}
if (divideBy2Button != null) {
this.divideBy2Button.setVisibility(true);
}
if (x10Button != null) {
this.x10Button.setVisibility(true);
}
if (divideBy10Button != null) {
this.divideBy10Button.setVisibility(true);
}
// 调整槽位位置
this.eap$adjustSlotPositions(page);
} catch (Exception e) {
// 忽略反射错误
}
// 如果屏幕尺寸发生变化窗口/GUI缩放重新注册按钮避免被 Screen.init 清空
if (this.width != eap$lastScreenWidth || this.height != eap$lastScreenHeight) {
eap$lastScreenWidth = this.width;
eap$lastScreenHeight = this.height;
try {
if (this.divideBy2Button != null) {
this.removeWidget(this.divideBy2Button);
this.addRenderableWidget(this.divideBy2Button);
}
if (this.x2Button != null) {
this.removeWidget(this.x2Button);
this.addRenderableWidget(this.x2Button);
}
if (this.divideBy5Button != null) {
this.removeWidget(this.divideBy5Button);
this.addRenderableWidget(this.divideBy5Button);
}
if (this.x5Button != null) {
this.removeWidget(this.x5Button);
this.addRenderableWidget(this.x5Button);
}
if (this.divideBy10Button != null) {
this.removeWidget(this.divideBy10Button);
this.addRenderableWidget(this.divideBy10Button);
}
if (this.x10Button != null) {
this.removeWidget(this.x10Button);
this.addRenderableWidget(this.x10Button);
}
} catch (Throwable ignored) {}
}
// 每帧定位四个按钮到 GUI 右缘外侧一点使用绝对屏幕坐标
int bx = this.leftPos + this.imageWidth + 1; // 向右平移 1px 到面板外侧
int by = this.topPos + 20;
int spacing = 22;
if (this.divideBy2Button != null) {
this.divideBy2Button.setVisibility(true);
this.divideBy2Button.setX(bx);
this.divideBy2Button.setY(by);
}
if (this.x2Button != null) {
this.x2Button.setVisibility(true);
this.x2Button.setX(bx);
this.x2Button.setY(by + spacing);
}
if (this.divideBy10Button != null) {
this.divideBy10Button.setVisibility(true);
this.divideBy10Button.setX(bx);
this.divideBy10Button.setY(by + spacing * 4);
}
if (this.x10Button != null) {
this.x10Button.setVisibility(true);
this.x10Button.setX(bx);
this.x10Button.setY(by + spacing * 5);
}
// 新增 /5 x5 的定位位于 /2x2 之后
if (this.divideBy5Button != null) {
this.divideBy5Button.setVisibility(true);
this.divideBy5Button.setX(bx);
this.divideBy5Button.setY(by + spacing * 2);
}
if (this.x5Button != null) {
this.x5Button.setVisibility(true);
this.x5Button.setX(bx);
this.x5Button.setY(by + spacing * 3);
}
}
@Unique
private void eap$adjustSlotPositions(int currentPage) {
try {
List<Slot> slots = this.getMenu().getSlots(SlotSemantics.ENCODED_PATTERN);
int totalSlots = slots.size();
if (totalSlots <= SLOTS_PER_PAGE) {
return; // 不需要翻页
}
int slot_id = 0;
for (Slot s : slots) {
int page_id = slot_id / SLOTS_PER_PAGE;
if (page_id == currentPage) {
// 当前页的槽位需要调整位置
int slotInPage = slot_id % SLOTS_PER_PAGE;
int row = slotInPage / 9; // 0-3
int col = slotInPage % 9; // 0-8
// 计算目标位置始终在前4行
int x = 8 + col * 18;
int y = 42 + row * 18;
// 使用反射设置槽位位置支持混淆环境
Field xField = null;
Field yField = null;
// 尝试不同的字段名开发环境和生产环境可能不同
String[] xFieldNames = {"x", "field_75262_c"};
String[] yFieldNames = {"y", "field_75263_d"};
for (String fieldName : xFieldNames) {
try {
xField = Slot.class.getDeclaredField(fieldName);
xField.setAccessible(true);
break;
} catch (NoSuchFieldException ignored) {}
}
for (String fieldName : yFieldNames) {
try {
yField = Slot.class.getDeclaredField(fieldName);
yField.setAccessible(true);
break;
} catch (NoSuchFieldException ignored) {}
}
if (xField != null && yField != null) {
xField.set(s, x);
yField.set(s, y);
}
}
++slot_id;
}
} catch (Exception e) {
// 忽略反射错误
}
}
// 移除手动挪动 Slot 坐标交由 SlotGridLayout + 原生布局控制
@Unique
private int getCurrentPage() {
try {
ContainerExPatternProvider menu1 = this.getMenu();
Field fieldPage = menu1.getClass().getDeclaredField("page");
fieldPage.setAccessible(true);
return (Integer) fieldPage.get(menu1);
} catch (Exception e) {
return 0;
}
// 优先使用本地 GUI 维护的页码
return Math.max(0, eap$currentPage % Math.max(1, eap$maxPageLocal));
}
@Unique
private int getMaxPage() {
// 优先使用配置倍数
try {
int cfg = ModConfigs.PAGE_MULTIPLIER.get();
if (cfg > 1) return cfg;
} catch (Throwable ignored) {}
try {
ContainerExPatternProvider menu1 = this.getMenu();
Field fieldMaxPage = menu1.getClass().getDeclaredField("maxPage");
fieldMaxPage.setAccessible(true);
return (Integer) fieldMaxPage.get(menu1);
} catch (Exception e) {
return 1;
Field fieldMaxPage = eap$findFieldRecursive(menu1.getClass(), "maxPage");
if (fieldMaxPage != null) {
fieldMaxPage.setAccessible(true);
Object v = fieldMaxPage.get(menu1);
if (v instanceof Integer i) return i;
}
} catch (Throwable ignored) {}
// 回退用槽位总数计算
try {
int totalSlots = this.getMenu().getSlots(SlotSemantics.ENCODED_PATTERN).size();
return Math.max(1, (int) Math.ceil(totalSlots / (double) SLOTS_PER_PAGE));
} catch (Throwable ignored) {}
return 1;
}
@Unique
private static Field eap$findFieldRecursive(Class<?> cls, String name) {
Class<?> c = cls;
while (c != null && c != Object.class) {
try {
return c.getDeclaredField(name);
} catch (NoSuchFieldException ignored) {}
c = c.getSuperclass();
}
return null;
}
@Unique
private static void eap$setIntFieldRecursive(Object obj, String name, int value) {
if (obj == null) return;
Field f = eap$findFieldRecursive(obj.getClass(), name);
if (f != null) {
try { f.setAccessible(true); f.set(obj, value); } catch (Throwable ignored) {}
}
}
@ -277,31 +122,78 @@ public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<Co
this.eap$screenStyle = style;
// 保留不再打印菜单类型
// 翻页按钮仅在需要时显示
// 计算并下发 maxPage配置优先其次按槽位总数计算
int totalSlots = this.getMenu().getSlots(SlotSemantics.ENCODED_PATTERN).size();
if (totalSlots > SLOTS_PER_PAGE) {
int cfgPages = 1;
try { cfgPages = Math.max(1, ModConfigs.PAGE_MULTIPLIER.get()); } catch (Throwable ignored) {}
int calcPages = Math.max(1, (int) Math.ceil(totalSlots / (double) SLOTS_PER_PAGE));
int desiredMaxPage = Math.max(cfgPages, calcPages);
LOGGER.info("[EAP] GuiExPatternProvider init: totalSlots={}, cfgPages={}, calcPages={}, desiredMaxPage={}", totalSlots, cfgPages, calcPages, desiredMaxPage);
// 更新本地最大页
this.eap$maxPageLocal = Math.max(1, desiredMaxPage);
this.eap$currentPage = 0;
try {
Field fMax = eap$findFieldRecursive(menu.getClass(), "maxPage");
if (fMax != null) { fMax.setAccessible(true); fMax.set(menu, desiredMaxPage); }
} catch (Throwable ignored) {}
// 翻页按钮当存在多页时显示支持仅由配置决定的空白页
if (desiredMaxPage > 1) {
this.prevPage = new ActionEPPButton((b) -> {
int currentPage = getCurrentPage();
int maxPage = getMaxPage();
int maxPage = Math.max(this.eap$maxPageLocal, getMaxPage());
int newPage = (currentPage - 1 + maxPage) % maxPage;
try {
ContainerExPatternProvider menu1 = this.getMenu();
java.lang.reflect.Method setPageMethod = menu1.getClass().getMethod("setPage", int.class);
setPageMethod.invoke(menu1, newPage);
// 尝试调用 setPage
try {
java.lang.reflect.Method setPageMethod = menu1.getClass().getMethod("setPage", int.class);
setPageMethod.invoke(menu1, newPage);
} catch (Throwable ignored2) {}
// 直接写入 page 字段确保生效
Field f = eap$findFieldRecursive(menu1.getClass(), "page");
if (f != null) {
f.setAccessible(true);
f.set(menu1, newPage);
}
} catch (Exception ignored) {}
// 同步到本地 GUI 页码
this.eap$currentPage = newPage;
// 日志与强制重排放在更新本地页码之后确保布局读取到新页
LOGGER.info("[EAP] PrevPage clicked: {} -> {} (max={})", currentPage, newPage, maxPage);
this.repositionSlots(SlotSemantics.ENCODED_PATTERN);
this.repositionSlots(SlotSemantics.STORAGE);
this.hoveredSlot = null;
}, Icon.ARROW_LEFT);
this.nextPage = new ActionEPPButton((b) -> {
int currentPage = getCurrentPage();
int maxPage = getMaxPage();
int maxPage = Math.max(this.eap$maxPageLocal, getMaxPage());
int newPage = (currentPage + 1) % maxPage;
try {
ContainerExPatternProvider menu1 = this.getMenu();
java.lang.reflect.Method setPageMethod = menu1.getClass().getMethod("setPage", int.class);
setPageMethod.invoke(menu1, newPage);
// 尝试调用 setPage
try {
java.lang.reflect.Method setPageMethod = menu1.getClass().getMethod("setPage", int.class);
setPageMethod.invoke(menu1, newPage);
} catch (Throwable ignored2) {}
// 直接写入 page 字段确保生效
Field f = eap$findFieldRecursive(menu1.getClass(), "page");
if (f != null) {
f.setAccessible(true);
f.set(menu1, newPage);
}
} catch (Exception ignored) {}
// 同步到本地 GUI 页码
this.eap$currentPage = newPage;
// 日志与强制重排放在更新本地页码之后确保布局读取到新页
LOGGER.info("[EAP] NextPage clicked: {} -> {} (max={})", currentPage, newPage, maxPage);
this.repositionSlots(SlotSemantics.ENCODED_PATTERN);
this.repositionSlots(SlotSemantics.STORAGE);
this.hoveredSlot = null;
}, Icon.ARROW_RIGHT);
// 恢复到 AE2 左侧工具栏
this.addToLeftToolbar(this.nextPage);
this.addToLeftToolbar(this.prevPage);
}
@ -346,8 +238,104 @@ public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<Co
this.addRenderableWidget(this.x10Button);
}
@Override
public int eap$getCurrentPage() {
return getCurrentPage();
}
// 页码文本绘制移交给 AEBaseScreenMixin.renderLabels 尾部执行
// 注意不再注入 Screen#init避免混入在某些映射情况下失败导致 TransformerError
@Override
public void eap$updateButtonsLayout() {
// 只处理按钮可见性与定位不再强制 showPage 或挪动 Slot 坐标避免与原布局/tooltip 冲突
if (nextPage != null && prevPage != null) {
this.nextPage.setVisibility(true);
this.prevPage.setVisibility(true);
}
if (x2Button != null) {
this.x2Button.setVisibility(true);
}
if (divideBy2Button != null) {
this.divideBy2Button.setVisibility(true);
}
if (x10Button != null) {
this.x10Button.setVisibility(true);
}
if (divideBy10Button != null) {
this.divideBy10Button.setVisibility(true);
}
if (divideBy5Button != null) {
this.divideBy5Button.setVisibility(true);
}
if (x5Button != null) {
this.x5Button.setVisibility(true);
}
// 如果屏幕尺寸发生变化窗口/GUI缩放重新注册右侧外列的自定义按钮翻页按钮由左侧工具栏托管
if (this.width != eap$lastScreenWidth || this.height != eap$lastScreenHeight) {
eap$lastScreenWidth = this.width;
eap$lastScreenHeight = this.height;
try {
if (this.divideBy2Button != null) {
this.removeWidget(this.divideBy2Button);
this.addRenderableWidget(this.divideBy2Button);
}
if (this.x2Button != null) {
this.removeWidget(this.x2Button);
this.addRenderableWidget(this.x2Button);
}
if (this.divideBy5Button != null) {
this.removeWidget(this.divideBy5Button);
this.addRenderableWidget(this.divideBy5Button);
}
if (this.x5Button != null) {
this.removeWidget(this.x5Button);
this.addRenderableWidget(this.x5Button);
}
if (this.divideBy10Button != null) {
this.removeWidget(this.divideBy10Button);
this.addRenderableWidget(this.divideBy10Button);
}
if (this.x10Button != null) {
this.removeWidget(this.x10Button);
this.addRenderableWidget(this.x10Button);
}
} catch (Throwable ignored) {}
}
// 定位到 GUI 右缘外侧一点使用绝对屏幕坐标
int bx = this.leftPos + this.imageWidth + 1; // 向右平移 1px 到面板外侧
int by = this.topPos + 20;
int spacing = 22;
// 翻页按钮交由左侧工具栏布局无需手动定位
if (this.divideBy2Button != null) {
this.divideBy2Button.setX(bx);
this.divideBy2Button.setY(by);
}
if (this.x2Button != null) {
this.x2Button.setX(bx);
this.x2Button.setY(by + spacing);
}
if (this.divideBy5Button != null) {
this.divideBy5Button.setX(bx);
this.divideBy5Button.setY(by + spacing * 2);
}
if (this.x5Button != null) {
this.x5Button.setX(bx);
this.x5Button.setY(by + spacing * 3);
}
if (this.divideBy10Button != null) {
this.divideBy10Button.setX(bx);
this.divideBy10Button.setY(by + spacing * 4);
}
if (this.x10Button != null) {
this.x10Button.setX(bx);
this.x10Button.setY(by + spacing * 5);
}
}
/**
* 在服务器端执行样板缩放操作单机模式
*/

View File

@ -2,11 +2,9 @@ package com.extendedae_plus.network;
import appeng.menu.implementations.PatternProviderMenu;
import com.extendedae_plus.api.AdvancedBlockingHolder;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderMenuAdvancedAccessor;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkDirection;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
@ -22,7 +20,7 @@ public class ToggleAdvancedBlockingC2SPacket {
public static ToggleAdvancedBlockingC2SPacket decode(FriendlyByteBuf buf) {
return new ToggleAdvancedBlockingC2SPacket();
}
}
public static void handle(ToggleAdvancedBlockingC2SPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
@ -38,15 +36,8 @@ public class ToggleAdvancedBlockingC2SPacket {
boolean current = holder.eap$getAdvancedBlocking();
boolean next = !current;
holder.eap$setAdvancedBlocking(next);
// 关键保存持久化触发 AE2 写入逻辑writeToNBT并由菜单 @GuiSync 同步回客户端
// 保存并触发 AE2 的菜单 @GuiSync 广播到所有观看该菜单的玩家
logic.saveChanges();
// 直接下发 S2C 强制同步带供应器标识维度+方块坐标
var host = ((PatternProviderLogicAccessor) logic).eap$host();
var be = host.getBlockEntity();
var level = be.getLevel();
String dimId = level.dimension().location().toString();
long posLong = be.getBlockPos().asLong();
ModNetwork.CHANNEL.sendTo(new AdvancedBlockingSyncS2CPacket(dimId, posLong, next), player.connection.connection, NetworkDirection.PLAY_TO_CLIENT);
}
});
ctx.setPacketHandled(true);

View File

@ -4,6 +4,7 @@
"compatibilityLevel": "JAVA_17",
"minVersion": "0.8",
"client": [
"ae2.AEBaseScreenMixin",
"PickFromWirelessMixin",
"accessor.AbstractContainerScreenAccessor",
"accessor.ScreenAccessor",