Merge branch 'master' into develop/gtceu_support

This commit is contained in:
C-H716 2025-09-04 12:19:20 +08:00
commit b0815924cc
14 changed files with 628 additions and 238 deletions

View File

@ -1,119 +1,173 @@
# Changelog
## [Unreleased]
## [Unreleased] / [未发布]
## [1.4.0-fix]
### Added
### Added / 新增
- JEI 书签优先用于编码样板匹配(书签越靠前,匹配优先级越高)
- Use JEI bookmarks as primary hints for pattern encoding; earlier bookmarks have higher priority.
- 可配置:在编码样板上显示“由 <玩家名> 编写”
- Configurable: show "Encoded by <player>" on encoded patterns.
- 可配置:智能翻倍模式支持轮询发配,提高多供应器/多目标的分摊与并发效率
- Configurable: round-robin dispatch in Smart Doubling to improve distribution/concurrency across providers/targets.
- 内存卡:支持复制“智能阻挡”和“智能翻倍”相关设置
- Memory Card: can copy settings related to Smart Block and Smart Doubling.
- 添加映射后,自动将本次新增的中文名写入搜索栏并立即应用筛选
- After adding a mapping, auto-fill the newly added Chinese name into the search bar and apply filtering.
- 可配置:控制样板管理终端默认隐藏/显示槽位渲染
- Configurable: control default slot rendering visibility in the Pattern Management Terminal.
### Fixed
### Fixed / 修复
- 修复模型材质渲染异常问题
- Fixed an issue with model/texture rendering artifacts.
## [1.4.0]
### Added
### Added / 新增
- 添加智能倍增模式:一次性发配够需求量,降低后期下单卡顿
- Added Smart Doubling mode: dispatch enough items upfront to reduce late-game order lag.
- 增加模组配置 UI 界面,可配置相关属性
- Added in-game config UI for adjustable properties.
- 供应器支持启用/禁用智能倍增
- Providers can enable/disable Smart Doubling.
- 扩展样板管理终端中添加按钮,可打开样板供应器对应机器的 UI 界面
- Added a button in the Extended Pattern Management Terminal to open the provider's target machine UI.
- 无线收发器适配 Jade 显示信息
- Wireless terminals now integrate with Jade HUD.
- 合成进度界面Shift+左键跳转到对应机器 UIShift+右键跳转到对应供应器 UI
- Crafting progress view: Shift+LMB jumps to machine UI; Shift+RMB to provider UI.
- 新增样板供应器状态控制器方块,连接到 ME 网络后可全局调整所有样板供应器三个模式的启用状态
- Added a Provider State Controller block to toggle three modes globally when connected to the ME network.
## [1.3.3]
### Added
### Added / 新增
- ME 扩展样板管理终端:搜索命中槽位增加 18x18 边框与彩虹流转高亮
- Added 18x18 border and rainbow highlight for search-hit slots in the Extended Pattern Terminal.
- 装配矩阵:新增锻造台与切石机配方上传
- Assembler Matrix: added upload support for Smithing Table and Stonecutter recipes.
- 在 AE2 与 ExtendedAE 的终端与供应器中显示样板生产数量
- Show pattern output amounts in AE2 and ExtendedAE terminals/providers.
- 增加并行处理单元系列,最高 1024 并行
- New Parallel Processing Units, up to 1024-way parallelism.
- 新增JEI 书签界面按 F 键自动搜索物品到 AE 终端
- New: press F on JEI bookmarks to auto-search the item in the AE terminal.
### Fixed
### Fixed / 修复
- 修复同方块多样板供应器贴片误选供应器问题
- Fixed provider mis-selection on blocks with multiple provider attachments.
- 修复多人游戏下智能阻挡按钮属性值显示不同步
- Fixed desync of Smart Block button state in multiplayer.
### Docs
### Docs / 文档
- 重要:与 modern ae2 additions 模组不兼容(加装后智能阻挡无效)
- Important: incompatible with "modern ae2 additions" (Smart Block becomes ineffective when installed together).
## [1.3.2]
### Added
### Added / 新增
- 增加映射修改便捷功能(输入名称即可删除并重新添加)
- Added quick mapping edit: re-add by typing the name to replace.
- 供应器新增高级阻挡模式:相同配方不再阻挡,直至该配方完成清空容器后再发配下一种(支持输入总成;总成内电路不受影响;催化剂槽内不可放置任意一个样板的输入物品)
- Advanced Block mode for providers: do not block identical recipes; dispatch the next recipe only after the current one completes and the container is cleared (supports input assemblies; internal circuits unaffected; catalyst slot cannot accept inputs for any pattern).
### Fixed
### Fixed / 修复
- 修复石英切割刀 Shift+右键复制名称功能,且支持格雷大型机器不同配方类型子名称
- Fixed Quartz Cutting Knife Shift+RMB name-copying; supports sub-names for different GT large machine recipe types.
### Docs
### Docs / 文档
- 与 modernae 模组存在兼容性问题:同时安装不报错,但高级阻挡模式会失效
- Compatibility issue with "modernae": no crash together, but Advanced Block mode becomes ineffective.
- 高级阻挡模式需同时开启“原版阻挡模式”和“高级阻挡模式”方可生效
- Advanced Block mode requires both Vanilla Block Mode and Advanced Block Mode to be enabled.
## [1.3.1]
### Added
### Added / 新增
- 无线收发器支持:放下后默认从端、频率 1支持镐子挖掘支持 AE2 扳手 Shift+右键快速拆除;扳手右键可锁定/解锁收发器(锁定后无法更改主从与频道)
- Wireless Repeater support: default slave and channel 1 when placed; mineable by pickaxe; AE2 wrench Shift+RMB to quickly break; wrench RMB to lock/unlock (locking prevents changing role/channel).
- 石英切割刀Shift+右键实体可复制其名称到剪切板,便于命名样板供应器
- Quartz Cutting Knife: Shift+RMB an entity to copy its name to clipboard for naming pattern providers.
### Fixed
### Fixed / 修复
- 修复 JEI 作弊模式下 Shift+左键无法获取物品的问题
- Fixed inability to obtain items with Shift+LMB in JEI cheat mode.
- 修复同名供应器中其中一个满了不会上传到其他的问题(建议打开“未满且可见”模式)
- Fixed issue where uploads wouldn't go to other providers if one with the same name was full (recommend enabling "Not Full & Visible" mode).
### Changed
### Changed / 变更
- 无线收发器默认行为与交互方式调整(见 Added
- Adjusted default behavior and interactions for the Wireless Repeater (see Added).
## [1.3.0]
### Added
### Added / 新增
- 为处理样板添加快速上传功能
- Added quick upload for processing patterns.
- 自动搜索改为采用映射表实现
- Auto-search now implemented via a mapping table.
### Docs
### Docs / 文档
- 可在 `/config/extendedae-plus/recipe_type_names.json` 修改映射名称
- You can edit mapping names at `/config/extendedae-plus/recipe_type_names.json`.
## [1.2.2]
### Added
### Added / 新增
- JEI 中 Shift+左键自动拉取物品到背包;若 AE 中没有但可自动化合成,则跳转到下单界面
- In JEI, Shift+LMB auto-pulls items to inventory; if not stored but craftable, jumps to the order screen.
- 上传样板到装配矩阵:新增已有样板检测;已有则不再上传,并在左下角提示玩家
- Uploading patterns to the Assembler Matrix now checks existing ones; skips upload and notifies the player if already present.
### Fixed
### Fixed / 修复
- 修复上传样板到装配矩阵偶发失败的问题
- Fixed occasional failures when uploading patterns to the Assembler Matrix.
- 修复一个可能引发偶发崩端的问题
- Fixed an issue that could occasionally cause client crashes.
## [1.2.1-fix]
### Added
### Added / 新增
- 编码样板终端:编写合成样板时自动上传到装配矩阵(网络需有装配矩阵)
- Pattern Encoding Terminal: auto-upload crafting patterns to the Assembler Matrix (requires it on the network).
- 鼠标中键点击某方块:若 AE 网络有库存,自动拉取到手上
- Middle-click a block to pull it to hand if the AE network has stock.
- JEI 中对着物品鼠标中键:若网络有该物品的自动化方案,自动跳转到下单界面
- In JEI, middle-clicking an item jumps to the order screen if a crafting plan exists.
- 支持在 Curios 槽位使用通用终端;量子卡可随时随地进行下单与拉取
- Universal Terminal works in Curios slots; Quantum Card enables ordering/pulling anywhere.
### Changed
### Changed / 变更
- 优化交互逻辑(减少奇怪的交互问题)
- Improved interaction logic (reduces odd interaction issues).
### Docs
### Docs / 文档
- 上述“中键拉取/跳转”功能需携带无线终端(放饰品槽也支持)
- The middle-click pull/jump features require carrying a Wireless Terminal (Curios slot supported).
## [1.2.0-a]
### Added
### Added / 新增
- 新增无线收发器方块:可无线传输 ME 频道,支持跨维度(主方块区块需设置强加载)
- Added Wireless Repeater block: transmits ME channels wirelessly across dimensions (keep the main block chunk-loaded).
- 为无线收发器添加抗卸载机制(重启后无需手动更新主端状态)
- Added unload-resilience for the Wireless Repeater (no manual master-state refresh after restart).
### Changed
### Changed / 变更
- 更改扩展样板管理终端:打开 UI 时默认隐藏所有样板
- Extended Pattern Terminal: hide all patterns by default when opening the UI.
### Fixed
### Fixed / 修复
- 修复 JEI 无法正常搜索到物品的问题
- Fixed an issue where JEI could not properly search for items.
### Performance
### Performance / 性能
- 提升网络稳定性:无线收发器抗卸载机制
- Improved network stability via the Wireless Repeater's unload-resilience.
### Docs
### Docs / 文档
- 操作方法空手右键切换主从Shift+右键频道+1Shift+左键频道-1主端发信从端收信
- How-to: bare-hand RMB to toggle master/slave; Shift+RMB channel +1; Shift+LMB channel -1; master transmits, slave receives.
## [1.1.3-b]
### Added
### Added / 新增
- 配置项 `pageMultiplier`
- Config option `pageMultiplier`.
- 打开样板编码终端时自动填充空白样板
- Auto-fill blank patterns when opening the Pattern Encoding Terminal.
### Fixed
### Fixed / 修复
- 修复其他 AE 物品 UI 显示异常
- Fixed display issues in other AE item UIs.

View File

@ -0,0 +1,34 @@
package com.extendedae_plus.content;
import appeng.api.stacks.AEKey;
import appeng.api.crafting.IPatternDetails;
import appeng.client.gui.me.patternaccess.PatternContainerRecord;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* 客户端本地的高亮存储仅作用于接收该客户端
* 使用 AEKey 作为标识渲染时可通过 AEKey 与本地解码的 IPatternDetails 比对
*/
public final class ClientPatternHighlightStore {
private static final Set<AEKey> HIGHLIGHTS = Collections.synchronizedSet(new HashSet<>());
private ClientPatternHighlightStore() {}
public static void setHighlight(AEKey key, boolean highlight) {
if (key == null) return;
if (highlight) HIGHLIGHTS.add(key);
else HIGHLIGHTS.remove(key);
}
public static boolean hasHighlight(AEKey key) {
if (key == null) return false;
return HIGHLIGHTS.contains(key);
}
public static void clearAll() { HIGHLIGHTS.clear(); }
}

View File

@ -1,5 +1,6 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.api.networking.IManagedGridNode;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.helpers.patternprovider.PatternProviderLogicHost;
import org.spongepowered.asm.mixin.Mixin;
@ -9,4 +10,7 @@ import org.spongepowered.asm.mixin.gen.Accessor;
public interface PatternProviderLogicAccessor {
@Accessor("host")
PatternProviderLogicHost eap$host();
@Accessor("mainNode")
IManagedGridNode eap$mainNode();
}

View File

@ -1,10 +1,12 @@
package com.extendedae_plus.mixin.ae2.client.gui;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.stacks.AEKey;
import appeng.client.Point;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.StackWithBounds;
import appeng.client.gui.TextOverride;
import appeng.client.gui.implementations.PatternProviderScreen;
import appeng.client.gui.me.crafting.CraftingCPUScreen;
import appeng.client.gui.style.PaletteColor;
import appeng.client.gui.style.ScreenStyle;
@ -12,12 +14,14 @@ import appeng.client.gui.style.Text;
import appeng.client.gui.style.TextAlignment;
import appeng.menu.slot.AppEngSlot;
import com.extendedae_plus.api.ExPatternPageAccessor;
import com.extendedae_plus.content.ClientPatternHighlightStore;
import com.extendedae_plus.network.CraftingMonitorJumpC2SPacket;
import com.extendedae_plus.network.CraftingMonitorOpenProviderC2SPacket;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.util.GuiUtil;
import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider;
import com.mojang.logging.LogUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.renderer.Rect2i;
@ -26,6 +30,7 @@ import net.minecraft.network.chat.contents.TranslatableContents;
import net.minecraft.world.inventory.Slot;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@ -150,40 +155,54 @@ public abstract class AEBaseScreenMixin {
@Inject(method = "renderSlot", at = @At("TAIL"))
private void eap$renderSlotAmounts(GuiGraphics guiGraphics, Slot s, CallbackInfo ci) {
Object self = this;
// 只处理AppEngSlot类型的槽位
if (!(s instanceof AppEngSlot appEngSlot)) {
return;
}
// 检查槽位是否可见且有效
if (!appEngSlot.isActive() || !appEngSlot.isSlotEnabled()) {
return;
}
// 获取槽位中的物品
var itemStack = appEngSlot.getItem();
if (itemStack.isEmpty()) {
return;
}
// 使用GuiUtil的格式化方法获取数量文本
String amountText = GuiUtil.getPatternOutputText(itemStack);
if (amountText.isEmpty()) {
return;
}
// 在槽位右下角绘制数量文本
Font font = eap$getFont(self);
GuiUtil.drawAmountText(guiGraphics, font, amountText, appEngSlot.x, appEngSlot.y, 0.6f);
try {
var details = PatternDetailsHelper.decodePattern(itemStack, Minecraft.getInstance().level, false);
try {
if (details != null && details.getOutputs() != null && details.getOutputs().length > 0) {
AEKey key = details.getOutputs()[0].what();
if (key != null && ClientPatternHighlightStore.hasHighlight(key)) {
try {
GuiUtil.drawSlotRainbowHighlight(guiGraphics, s.x, s.y);
} catch (Throwable ignored) {}
}
}
} catch (Throwable ignore) {}
} catch (Throwable ignore) {}
}
// AEBaseScreen.drawText 完成某个文本绘制后若该文本为样板标签则紧接着绘制页码
@Inject(method = "drawText", at = @At("TAIL"), remap = false)
private void eap$appendPageAfterPatternsLabel(GuiGraphics guiGraphics,
Text text,
@Nullable TextOverride override,
CallbackInfo ci) {
Text text,
@Nullable TextOverride override,
CallbackInfo ci) {
Object self = this;
if (!(self instanceof GuiExPatternProvider)) {
return;
@ -237,8 +256,10 @@ public abstract class AEBaseScreenMixin {
if (!isPatterns) {
String label = content.getString();
if (label != null) {
if (label.equals(Component.translatable("gui.pattern_provider.patterns").getString())) isPatterns = true;
else if (label.equals(Component.translatable("gui.extendedae.patterns").getString())) isPatterns = true;
if (label.equals(Component.translatable("gui.pattern_provider.patterns").getString()))
isPatterns = true;
else if (label.equals(Component.translatable("gui.extendedae.patterns").getString()))
isPatterns = true;
else if (label.equals(Component.translatable("gui.ae2.patterns").getString())) isPatterns = true;
}
}
@ -263,16 +284,18 @@ public abstract class AEBaseScreenMixin {
if (v instanceof Integer i) {
max = Math.max(1, i);
}
} catch (Throwable ignored) {}
} catch (Throwable ignored) {
}
String pageText = ""+cur+"" + "/" + max + "";
String pageText = "" + cur + "" + "/" + max + "";
ScreenStyle style = eap$getStyle(self);
int color = 0xFFFFFFFF;
if (style != null) {
try {
color = style.getColor(PaletteColor.DEFAULT_TEXT_COLOR).toARGB();
} catch (Throwable ignored) {}
} catch (Throwable ignored) {
}
}
int padding = 4;
if (scale == 1.0f) {
@ -284,6 +307,24 @@ public abstract class AEBaseScreenMixin {
guiGraphics.drawString(font, pageText, lineWidth + padding, 0, color, false);
guiGraphics.pose().popPose();
}
} catch (Throwable ignored) {
}
}
@Shadow
protected void setTextContent(String id, Component content) {};
@Inject(method = "updateBeforeRender", at = @At("RETURN"), remap = false)
private void onUpdateBeforeRender(CallbackInfo ci) {
try {
AEBaseScreen<?> self = (AEBaseScreen<?>) (Object) this;
if (self instanceof PatternProviderScreen screen){
Component t = screen.getTitle();
if (t != null && !t.getString().isEmpty()) {
this.setTextContent(AEBaseScreen.TEXT_ID_DIALOG_TITLE, t);
}
}
} catch (Throwable ignored) {}
}
}

View File

@ -0,0 +1,28 @@
package com.extendedae_plus.mixin.ae2.client.gui;
import appeng.client.gui.implementations.PatternProviderScreen;
import com.extendedae_plus.content.ClientPatternHighlightStore;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.world.inventory.AbstractContainerMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = AbstractContainerScreen.class, remap = false)
public class PatternProviderCloseMixin {
@Shadow
protected AbstractContainerMenu menu;
@Inject(method = "removed", at = @At("HEAD"))
private void onRemoved(CallbackInfo ci) {
try {
if (((Object) this) instanceof PatternProviderScreen) {
ClientPatternHighlightStore.clearAll();
}
} catch (Throwable ignored) {
}
}
}

View File

@ -21,6 +21,7 @@ import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
@ -49,8 +50,8 @@ public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<Co
super(menu, playerInventory, title, style);
}
// 移除手动挪动 Slot 坐标交由 SlotGridLayout + 原生布局控制
@Unique
@ -144,7 +145,7 @@ public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<Co
ContainerExPatternProvider menu1 = this.getMenu();
// 尝试调用 setPage
try {
java.lang.reflect.Method setPageMethod = menu1.getClass().getMethod("setPage", int.class);
Method setPageMethod = menu1.getClass().getMethod("setPage", int.class);
setPageMethod.invoke(menu1, newPage);
} catch (Throwable ignored2) {}
// 直接写入 page 字段确保生效

View File

@ -4,7 +4,6 @@ import appeng.api.crafting.PatternDetailsHelper;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.Icon;
import appeng.client.gui.me.patternaccess.PatternContainerRecord;
import appeng.client.gui.me.patternaccess.PatternSlot;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.AETextField;
import appeng.client.gui.widgets.IconButton;
@ -13,6 +12,7 @@ import com.extendedae_plus.config.ModConfigs;
import com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.OpenProviderUiC2SPacket;
import com.extendedae_plus.util.GuiUtil;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
@ -76,41 +76,6 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
super(menu, playerInventory, title, style);
}
@Unique
private static int eap$withAlpha(int rgb, int alpha255) {
return ((alpha255 & 0xFF) << 24) | (rgb & 0x00FFFFFF);
}
/**
* HSV 转换为 RGB返回 0xRRGGBB不含 alpha
* h: 0.0~1.0s: 0.0~1.0v: 0.0~1.0
*/
@Unique
private static int eap$hsvToRgb(float h, float s, float v) {
if (s <= 0.0f) {
int g = Math.round(v * 255.0f);
return (g << 16) | (g << 8) | g;
}
float hh = (h - (float) Math.floor(h)) * 6.0f;
int sector = (int) Math.floor(hh);
float f = hh - sector;
float p = v * (1.0f - s);
float q = v * (1.0f - s * f);
float t = v * (1.0f - s * (1.0f - f));
float r, g, b;
switch (sector) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
default: r = v; g = p; b = q; break;
}
int ri = Math.round(r * 255.0f);
int gi = Math.round(g * 255.0f);
int bi = Math.round(b * 255.0f);
return (ri << 16) | (gi << 8) | bi;
}
/**
* 获取当前选择的样板供应器ID
@ -579,53 +544,9 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu>
return;
}
// 彩虹色的流转基于时间在 HSV 色环上循环4 秒为一周期
long now = System.currentTimeMillis();
final long rainbowPeriodMs = 4000L;
float hue = (now % rainbowPeriodMs) / (float) rainbowPeriodMs; // 0.0 ~ 1.0
int rainbowRgb = eap$hsvToRgb(hue, 1.0f, 1.0f);
// 使用 GuiUtil 的通用绘制方法绘制槽位高亮包含彩虹流转效果
GuiUtil.drawPatternSlotHighlights(guiGraphics, this.menu.slots, this.matchedStack, this.matchedProvider);
for (Slot slot : this.menu.slots) {
if (!(slot instanceof PatternSlot ps)) {
continue;
}
int sx = slot.x;
int sy = slot.y;
boolean isMatchedSlot = this.matchedStack != null && this.matchedStack.contains(slot.getItem());
boolean isMatchedProvider = false;
try {
PatternContainerRecord container = ps.getMachineInv();
isMatchedProvider = this.matchedProvider != null && this.matchedProvider.contains(container);
} catch (Throwable ignored) {
}
// 依据命中状态选择颜色方案
int borderColor;
int backgroundColor;
if (isMatchedSlot) {
// 命中槽位使用彩虹色边框与浅底色固定透明度呈现色相流转效果
borderColor = eap$withAlpha(rainbowRgb, 0xA0);
backgroundColor = eap$withAlpha(rainbowRgb, 0x3C);
} else if (!isMatchedProvider) {
borderColor = eap$withAlpha(0xFFFFFF, 0x40);
backgroundColor = eap$withAlpha(0x000000, 0x18);
} else {
borderColor = eap$withAlpha(0xFFFFFF, 0x30);
backgroundColor = eap$withAlpha(0xFFFFFF, 0x14);
}
// 绘制 18x18 边框1px
eap$fill(guiGraphics, new Rect2i(sx - 1, sy - 1, 18, 1), borderColor);
eap$fill(guiGraphics, new Rect2i(sx - 1, sy + 16, 18, 1), borderColor);
eap$fill(guiGraphics, new Rect2i(sx - 1, sy, 1, 16), borderColor);
eap$fill(guiGraphics, new Rect2i(sx + 16, sy, 1, 16), borderColor);
// 绘制 16x16 浅底色半透明叠加在槽位上方
eap$fill(guiGraphics, new Rect2i(sx, sy, 16, 16), backgroundColor);
}
}
@Unique

View File

@ -12,14 +12,27 @@ import appeng.menu.locator.MenuLocators;
import appeng.menu.me.crafting.CraftingCPUMenu;
import appeng.parts.AEBasePart;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor;
import com.mojang.logging.LogUtils;
import com.extendedae_plus.util.PatternProviderDataUtil;
import com.glodblock.github.extendedae.util.FCClientUtil;
import com.glodblock.github.glodium.util.GlodUtil;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.AABB;
import net.minecraftforge.network.NetworkDirection;
import net.minecraftforge.network.NetworkEvent;
import java.util.Collection;
import java.util.Objects;
import java.util.function.Supplier;
import static com.glodblock.github.extendedae.client.render.EAEHighlightHandler.highlight;
/**
* 客户端从 CraftingCPUScreen 发送鼠标下条目对应的 AEKey
* 服务端在当前打开的 CraftingCPUMenu 所属网络中定位匹配该 AEKey 的样板供应器
@ -47,11 +60,8 @@ public class CraftingMonitorOpenProviderC2SPacket {
ServerPlayer player = context.getSender();
if (player == null) return;
LogUtils.getLogger().info("EAP[S]: recv CraftingMonitorOpenProviderC2SPacket key={} from {}", msg.what, player.getGameProfile().getName());
// 必须在 CraftingCPU 界面内
if (!(player.containerMenu instanceof CraftingCPUMenu menu)) {
LogUtils.getLogger().info("EAP[S]: not in CraftingCPUMenu, abort");
return;
}
@ -62,19 +72,16 @@ public class CraftingMonitorOpenProviderC2SPacket {
grid = host.getActionableNode().getGrid();
}
if (grid == null) {
LogUtils.getLogger().info("EAP[S]: grid is null, abort");
return;
}
var cs = grid.getCraftingService();
if (!(cs instanceof CraftingService craftingService)) {
LogUtils.getLogger().info("EAP[S]: craftingService is null/unsupported, abort");
return;
}
// 1) 根据 AEKey 找到可能的样板pattern
Collection<IPatternDetails> patterns = craftingService.getCraftingFor(msg.what);
LogUtils.getLogger().info("EAP[S]: patterns found={} for key={}", patterns.size(), msg.what);
if (patterns.isEmpty()) {
return;
}
@ -89,29 +96,72 @@ public class CraftingMonitorOpenProviderC2SPacket {
if (host == null) continue;
var pbe = host.getBlockEntity();
if (pbe == null) continue;
// 在服务端上下文中执行pbe 仅用于构造菜单定位器
// 跳过未连接到网格或不活跃的 provider使用 util 判断并传入当前 grid
if (!PatternProviderDataUtil.isProviderAvailable(ppl, grid)) continue;
// 直接打开供应器自身的 UI调用 Host 默认方法
try {
// 部件与方块实体分别选择定位器
// 部件与方块实体分别选择定位器并打开界面
if (host instanceof AEBasePart part) {
host.openMenu(player, MenuLocators.forPart(part));
highlightWithMessage(pbe.getBlockPos(), part.getSide(), Objects.requireNonNull(pbe.getLevel()).dimension(), 1.0, player);
} else {
host.openMenu(player, MenuLocators.forBlockEntity(pbe));
highlightWithMessage(pbe.getBlockPos(), null, Objects.requireNonNull(pbe.getLevel()).dimension(), 1.0, player);
}
context.setPacketHandled(true);
// 高亮打开的供应器位置并发送聊天提示
// 先在该 provider 中定位 pattern 的槽位索引以便计算页码尽量早退出按槽位逐个解码
int foundSlot = PatternProviderDataUtil.findSlotForPattern(ppl, pattern.getDefinition());
if (foundSlot >= 0) {
int pageId = foundSlot / 36;
if (pageId > 0) {
// 发送 S2C 包通知客户端切换到指定页客户端会写入 mixin 字段并重排槽位
ModNetwork.CHANNEL.sendTo(new SetProviderPageS2CPacket(pageId), player.connection.connection, NetworkDirection.PLAY_TO_CLIENT);
}
}
// 最后发送高亮包保证界面已打开
if (pattern.getOutputs() != null && pattern.getOutputs().length > 0 && pattern.getOutputs()[0] != null) {
AEKey key = pattern.getOutputs()[0].what();
ModNetwork.CHANNEL.sendTo(new SetPatternHighlightS2CPacket(key, true), player.connection.connection, NetworkDirection.PLAY_TO_CLIENT);
}
return;
} catch (Throwable t) {
LogUtils.getLogger().error("EAP[S]: open provider UI failed at {}", pbe.getBlockPos(), t);
} catch (Exception ignored) {
}
}
}
}
LogUtils.getLogger().info("EAP[S]: no provider UI opened for key={}", msg.what);
});
context.setPacketHandled(true);
}
private static void highlightWithMessage(BlockPos pos, Direction face, ResourceKey<Level> dim, double multiplier, Player player) {
if (pos == null || dim == null) {
return;
}
long endTime = System.currentTimeMillis() + (long) (6000 * GlodUtil.clamp(multiplier, 1, 30));
if (face == null) {
highlight(pos, dim, endTime);
} else {
var origin = new AABB(2 / 16D, 2 / 16D, 0, 14 / 16D, 14 / 16D, 2 / 16D).move(pos);
var center = new AABB(pos).getCenter();
switch (face) {
case WEST -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.Y, (float) (Math.PI / 2));
case SOUTH -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.Y, (float) Math.PI);
case EAST -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.Y, (float) (-Math.PI / 2));
case UP -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.X, (float) (-Math.PI / 2));
case DOWN -> origin = FCClientUtil.rotor(origin, center, Direction.Axis.X, (float) (Math.PI / 2));
}
highlight(pos, face, dim, endTime, origin);
}
if (player != null) {
player.displayClientMessage(Component.translatable("chat.ex_pattern_access_terminal.pos", pos.toShortString(), dim.location().getPath()), false);
}
}
}

View File

@ -60,6 +60,18 @@ public class ModNetwork {
.consumerNetworkThread(ProvidersListS2CPacket::handle)
.add();
CHANNEL.messageBuilder(SetPatternHighlightS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT)
.encoder(SetPatternHighlightS2CPacket::encode)
.decoder(SetPatternHighlightS2CPacket::decode)
.consumerNetworkThread(SetPatternHighlightS2CPacket::handle)
.add();
CHANNEL.messageBuilder(SetProviderPageS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT)
.encoder(SetProviderPageS2CPacket::encode)
.decoder(SetProviderPageS2CPacket::decode)
.consumerNetworkThread(SetProviderPageS2CPacket::handle)
.add();
CHANNEL.messageBuilder(ToggleAdvancedBlockingC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(ToggleAdvancedBlockingC2SPacket::encode)
.decoder(ToggleAdvancedBlockingC2SPacket::decode)

View File

@ -0,0 +1,45 @@
package com.extendedae_plus.network;
import appeng.api.stacks.AEKey;
import com.extendedae_plus.content.ClientPatternHighlightStore;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
/**
* S2C: 指示客户端对某个 AEKey 的样板进行高亮/取消高亮仅作用于接收该包的客户端
*/
public class SetPatternHighlightS2CPacket {
private final AEKey key;
private final boolean highlight;
public SetPatternHighlightS2CPacket(AEKey key, boolean highlight) {
this.key = key;
this.highlight = highlight;
}
public static void encode(SetPatternHighlightS2CPacket msg, FriendlyByteBuf buf) {
AEKey.writeKey(buf, msg.key);
buf.writeBoolean(msg.highlight);
}
public static SetPatternHighlightS2CPacket decode(FriendlyByteBuf buf) {
AEKey key = AEKey.readKey(buf);
boolean h = buf.readBoolean();
return new SetPatternHighlightS2CPacket(key, h);
}
public static void handle(SetPatternHighlightS2CPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
ctx.enqueueWork(() -> {
try {
ClientPatternHighlightStore.setHighlight(msg.key, msg.highlight);
} catch (Throwable ignored) {
}
});
ctx.setPacketHandled(true);
}
}

View File

@ -0,0 +1,58 @@
package com.extendedae_plus.network;
import appeng.menu.SlotSemantics;
import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
import java.lang.reflect.Field;
import java.util.function.Supplier;
/**
* S2C: 指示客户端在已打开的样板供应器界面切换到指定页
*/
public class SetProviderPageS2CPacket {
private final int page;
public SetProviderPageS2CPacket(int page) {
this.page = page;
}
public static void encode(SetProviderPageS2CPacket msg, FriendlyByteBuf buf) {
buf.writeVarInt(msg.page);
}
public static SetProviderPageS2CPacket decode(FriendlyByteBuf buf) {
int p = buf.readVarInt();
return new SetProviderPageS2CPacket(p);
}
public static void handle(SetProviderPageS2CPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
ctx.enqueueWork(() -> {
try {
Screen screen = Minecraft.getInstance().screen;
if (screen instanceof GuiExPatternProvider guiExPatternProvider) {
Field currentPage = screen.getClass().getDeclaredField("eap$currentPage");
currentPage.setAccessible(true);
currentPage.setInt(guiExPatternProvider, msg.page);
guiExPatternProvider.repositionSlots(SlotSemantics.ENCODED_PATTERN);
guiExPatternProvider.repositionSlots(SlotSemantics.STORAGE);
Field hs = screen.getClass().getDeclaredField("hoveredSlot");
hs.setAccessible(true);
hs.set(screen, null);
}
} catch (Throwable ignored) {
}
}
);
ctx.setPacketHandled(true);
}
}

View File

@ -2,11 +2,17 @@ package com.extendedae_plus.util;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.stacks.GenericStack;
import appeng.client.gui.me.patternaccess.PatternContainerRecord;
import appeng.client.gui.me.patternaccess.PatternSlot;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.Font;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import java.util.List;
import java.util.Set;
/**
* GUI工具类提供样板获取绘制等通用功能
@ -77,4 +83,105 @@ public class GuiUtil {
guiGraphics.drawString(font, text, (int)(textX / scale), (int)(textY / scale), 0xFFFFFFFF, true);
guiGraphics.pose().popPose();
}
// Helper: add alpha channel to RGB (rgb is 0xRRGGBB)
private static int withAlpha(int rgb, int alpha255) {
return ((alpha255 & 0xFF) << 24) | (rgb & 0x00FFFFFF);
}
// HSV -> RGB (returns 0xRRGGBB)
private static int hsvToRgb(float h, float s, float v) {
if (s <= 0.0f) {
int g = Math.round(v * 255.0f);
return (g << 16) | (g << 8) | g;
}
float hh = (h - (float) Math.floor(h)) * 6.0f;
int sector = (int) Math.floor(hh);
float f = hh - sector;
float p = v * (1.0f - s);
float q = v * (1.0f - s * f);
float t = v * (1.0f - s * (1.0f - f));
float r, g, b;
switch (sector) {
case 0: r = v; g = t; b = p; break;
case 1: r = q; g = v; b = p; break;
case 2: r = p; g = v; b = t; break;
case 3: r = p; g = q; b = v; break;
case 4: r = t; g = p; b = v; break;
default: r = v; g = p; b = q; break;
}
int ri = Math.round(r * 255.0f);
int gi = Math.round(g * 255.0f);
int bi = Math.round(b * 255.0f);
return (ri << 16) | (gi << 8) | bi;
}
// 返回当前时间对应的彩虹色0xRRGGBB周期固定为 4000ms
private static int getRainbowRgb() {
long now = System.currentTimeMillis();
final long rainbowPeriodMs = 4000L;
float hue = (now % rainbowPeriodMs) / (float) rainbowPeriodMs; // 0.0 ~ 1.0
return hsvToRgb(hue, 1.0f, 1.0f);
}
// 在给定槽位坐标绘制 1px 边框18x18 16x16 半透明背景
private static void drawSlotBox(GuiGraphics guiGraphics, int sx, int sy, int borderColor, int backgroundColor) {
guiGraphics.fill(sx - 1, sy - 1, sx + 17, sy, borderColor);
guiGraphics.fill(sx - 1, sy + 16, sx + 17, sy + 17, borderColor);
guiGraphics.fill(sx - 1, sy, sx, sy + 16, borderColor);
guiGraphics.fill(sx + 16, sy, sx + 17, sy + 16, borderColor);
guiGraphics.fill(sx, sy, sx + 16, sy + 16, backgroundColor);
}
/**
* 在槽位上绘制彩色流转的高亮和浅底色
*/
public static void drawPatternSlotHighlights(GuiGraphics guiGraphics, List<Slot> slots, Set<ItemStack> matchedStack, Set<PatternContainerRecord> matchedProvider) {
if (slots == null) return;
int rainbowRgb = getRainbowRgb();
for (Slot slot : slots) {
if (!(slot instanceof PatternSlot ps)) {
continue;
}
int sx = slot.x;
int sy = slot.y;
boolean isMatchedSlot = matchedStack != null && matchedStack.contains(slot.getItem());
boolean isMatchedProvider = false;
try {
PatternContainerRecord container = ps.getMachineInv();
isMatchedProvider = matchedProvider != null && matchedProvider.contains(container);
} catch (Throwable ignored) {
}
int borderColor;
int backgroundColor;
if (isMatchedSlot) {
borderColor = withAlpha(rainbowRgb, 0xA0);
backgroundColor = withAlpha(rainbowRgb, 0x3C);
} else if (!isMatchedProvider) {
borderColor = withAlpha(0xFFFFFF, 0x40);
backgroundColor = withAlpha(0x000000, 0x18);
} else {
borderColor = withAlpha(0xFFFFFF, 0x30);
backgroundColor = withAlpha(0xFFFFFF, 0x14);
}
drawSlotBox(guiGraphics, sx, sy, borderColor, backgroundColor);
}
}
/**
* 在指定槽位坐标绘制彩虹流转的边框与浅底色用于非 PatternSlot 的高亮场景
*/
public static void drawSlotRainbowHighlight(GuiGraphics guiGraphics, int sx, int sy) {
int rainbowRgb = getRainbowRgb();
int borderColor = withAlpha(rainbowRgb, 0xA0);
int backgroundColor = withAlpha(rainbowRgb, 0x3C);
drawSlotBox(guiGraphics, sx, sy, borderColor, backgroundColor);
}
}

View File

@ -3,11 +3,15 @@ package com.extendedae_plus.util;
import appeng.api.crafting.IPatternDetails;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.inventories.InternalInventory;
import appeng.api.networking.IGrid;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.helpers.patternprovider.PatternProviderLogic;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor;
import com.mojang.logging.LogUtils;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import java.util.ArrayList;
import java.util.HashMap;
@ -230,26 +234,8 @@ public class PatternProviderDataUtil {
if (patternInventory == null) {
return patternDataList;
}
// 通过反射安全地访问host字段获取Level
Level level = null;
try {
var hostField = patternProvider.getClass().getDeclaredField("host");
hostField.setAccessible(true);
var host = hostField.get(patternProvider);
if (host != null) {
var getBlockEntityMethod = host.getClass().getMethod("getBlockEntity");
var blockEntity = getBlockEntityMethod.invoke(host);
if (blockEntity != null) {
var getLevelMethod = blockEntity.getClass().getMethod("getLevel");
level = (Level) getLevelMethod.invoke(blockEntity);
}
}
} catch (Exception e) {
// 如果反射失败返回空列表
return patternDataList;
}
// 获取 Level使用 mixin accessor 替代反射
Level level = getPatternProviderLevel(patternProvider);
if (level == null) {
return patternDataList;
}
@ -257,12 +243,15 @@ public class PatternProviderDataUtil {
// 遍历所有样板槽位
for (int i = 0; i < patternInventory.size(); i++) {
ItemStack patternStack = patternInventory.getStackInSlot(i);
if (!patternStack.isEmpty()) {
if (patternStack.isEmpty()) continue;
try {
// 解码样板
IPatternDetails patternDetails = PatternDetailsHelper.decodePattern(patternStack, level);
if (patternDetails != null) {
patternDataList.add(new PatternData(patternDetails, patternStack, i));
}
} catch (Exception e) {
if (DEBUG) LogUtils.getLogger().debug("Pattern decode failed at slot {}: {}", i, e.toString());
}
}
@ -353,24 +342,7 @@ public class PatternProviderDataUtil {
return null;
}
// 通过反射安全地访问host字段获取Level
Level level = null;
try {
var hostField = patternProvider.getClass().getDeclaredField("host");
hostField.setAccessible(true);
var host = hostField.get(patternProvider);
if (host != null) {
var getBlockEntityMethod = host.getClass().getMethod("getBlockEntity");
var blockEntity = getBlockEntityMethod.invoke(host);
if (blockEntity != null) {
var getLevelMethod = blockEntity.getClass().getMethod("getLevel");
level = (Level) getLevelMethod.invoke(blockEntity);
}
}
} catch (Exception e) {
return null;
}
Level level = getPatternProviderLevel(patternProvider);
if (level == null) {
return null;
}
@ -429,6 +401,59 @@ public class PatternProviderDataUtil {
return patternProvider.getGrid() != null;
}
/**
* 判断 provider 是否可用并属于指定网格在线且有频道/处于活跃状态
*/
public static boolean isProviderAvailable(PatternProviderLogic provider, IGrid expectedGrid) {
if (provider == null || expectedGrid == null) return false;
try {
var grid = provider.getGrid();
if (grid == null || !grid.equals(expectedGrid)) return false;
// 使用 accessor 获取 mainNode再调用 isActive
if (provider instanceof PatternProviderLogicAccessor accessor) {
var mainNode = accessor.eap$mainNode();
if (mainNode == null) return false;
try {
var isActiveMethod = mainNode.getClass().getMethod("isActive");
Object active = isActiveMethod.invoke(mainNode);
if (active instanceof Boolean && !((Boolean) active)) return false;
} catch (NoSuchMethodException nsme) {
// 没有 isActive 方法时退回到检查 channels
try {
var getChannels = mainNode.getClass().getMethod("getChannels");
Object channels = getChannels.invoke(mainNode);
if (channels instanceof java.util.Collection) {
if (((java.util.Collection<?>) channels).isEmpty()) return false;
}
} catch (Exception ignored) {
// 无法判断 channels 认为不可用
return false;
}
}
} else {
// 没有 accessor 的情况尽量通过反射判断 mainNode.channels
try {
var mainNodeField = provider.getClass().getDeclaredField("mainNode");
mainNodeField.setAccessible(true);
var mainNode = mainNodeField.get(provider);
if (mainNode == null) return false;
var getChannelsMethod = mainNode.getClass().getMethod("getChannels");
Object channels = getChannelsMethod.invoke(mainNode);
if (channels instanceof java.util.Collection) {
return !((java.util.Collection<?>) channels).isEmpty();
}
} catch (Exception e) {
return false;
}
}
return true;
} catch (Exception e) {
return false;
}
}
/**
* 检查样板供应器是否处于活跃状态
*
@ -443,18 +468,21 @@ public class PatternProviderDataUtil {
if (grid == null) {
return false;
}
// 检查网格节点是否活跃
// 检查网格节点是否活跃使用 accessor 代替反射
try {
// 使用反射安全地访问mainNode字段
var mainNodeField = patternProvider.getClass().getDeclaredField("mainNode");
mainNodeField.setAccessible(true);
var mainNode = mainNodeField.get(patternProvider);
if (mainNode != null) {
var isActiveMethod = mainNode.getClass().getMethod("isActive");
return (Boolean) isActiveMethod.invoke(mainNode);
if (patternProvider instanceof PatternProviderLogicAccessor accessor) {
var mainNode = accessor.eap$mainNode();
if (mainNode != null) {
try {
var isActiveMethod = mainNode.getClass().getMethod("isActive");
return (Boolean) isActiveMethod.invoke(mainNode);
} catch (Exception e) {
// 无法调用 isActive 认为活跃
return true;
}
}
}
} catch (Exception e) {
// 如果反射失败假设是活跃的
} catch (Exception ignored) {
}
return true;
}
@ -522,6 +550,34 @@ public class PatternProviderDataUtil {
return scalePatternAmountsExtendedAEStyle(patternProvider, multiplier, true);
}
/**
* 查找 provider 中匹配给定定义的样板槽位轻量按需解码并早退出
* @param patternProvider 要搜索的 provider
* @param targetDefinition pattern.getDefinition() 返回的对象用于 equals 比较
* @return 找到的槽位索引未找到返回 -1
*/
public static int findSlotForPattern(PatternProviderLogic patternProvider, Object targetDefinition) {
if (patternProvider == null || targetDefinition == null) return -1;
InternalInventory inv = patternProvider.getPatternInv();
if (inv == null) return -1;
Level level = getPatternProviderLevel(patternProvider);
if (level == null) return -1;
for (int i = 0; i < inv.size(); i++) {
ItemStack s = inv.getStackInSlot(i);
if (s.isEmpty()) continue;
try {
IPatternDetails d = PatternDetailsHelper.decodePattern(s, level);
if (d != null && d.getDefinition().equals(targetDefinition)) {
return i;
}
} catch (Exception e) {
if (DEBUG) LogUtils.getLogger().debug("findSlotForPattern decode failed at {}: {}", i, e.toString());
}
}
return -1;
}
/**
* ExtendedAE风格的样板复制倍增
* 支持更精确的样板处理和错误恢复
@ -862,50 +918,30 @@ public class PatternProviderDataUtil {
// 1. 设置物品到库存
patternInventory.setItemDirect(slot, newPattern);
// 2. 标记数据为脏数据确保保存到磁盘
// 2. 标记数据为脏数据确保保存到磁盘尝试使用 mixin accessor 替代反射
try {
// 通过反射获取host并标记为脏数据
var hostField = patternProvider.getClass().getDeclaredField("host");
hostField.setAccessible(true);
var host = hostField.get(patternProvider);
if (host != null) {
// 获取BlockEntity并标记为脏数据
var getBlockEntityMethod = host.getClass().getMethod("getBlockEntity");
var blockEntity = getBlockEntityMethod.invoke(host);
if (blockEntity != null) {
// 调用setChanged()方法标记为脏数据
var setChangedMethod = blockEntity.getClass().getMethod("setChanged");
setChangedMethod.invoke(blockEntity);
// 尝试触发网络同步
try {
var levelField = blockEntity.getClass().getSuperclass().getDeclaredField("level");
levelField.setAccessible(true);
Level level = (Level) levelField.get(blockEntity);
if (level != null && !level.isClientSide()) {
// 服务器端强制同步到客户端
var getBlockPosMethod = blockEntity.getClass().getMethod("getBlockPos");
var blockPos = getBlockPosMethod.invoke(blockEntity);
if (blockPos != null) {
// 通知客户端方块状态变更
var getBlockStateMethod = blockEntity.getClass().getMethod("getBlockState");
var blockState = getBlockStateMethod.invoke(blockEntity);
level.sendBlockUpdated((net.minecraft.core.BlockPos) blockPos,
(net.minecraft.world.level.block.state.BlockState) blockState,
(net.minecraft.world.level.block.state.BlockState) blockState, 3);
}
if (patternProvider instanceof PatternProviderLogicAccessor accessor) {
var host = accessor.eap$host();
if (host != null) {
BlockEntity be = host.getBlockEntity();
if (be != null) {
try {
be.setChanged();
} catch (Exception ignored) {
}
try {
Level level = be.getLevel();
if (level != null && !level.isClientSide()) {
var pos = be.getBlockPos();
var state = be.getBlockState();
level.sendBlockUpdated(pos, state, state, 3);
}
} catch (Exception ignored) {
}
} catch (Exception syncException) {
// 网络同步失败不影响主要功能
}
}
}
} catch (Exception e) {
// 如果反射失败使用备用方案
} catch (Exception ignored) {
}
// 3. 强制更新样板缓存
@ -920,20 +956,18 @@ public class PatternProviderDataUtil {
* ExtendedAE风格安全获取样板供应器的Level对象
*/
private static Level getPatternProviderLevel(PatternProviderLogic patternProvider) {
if (patternProvider == null) return null;
try {
var hostField = patternProvider.getClass().getDeclaredField("host");
hostField.setAccessible(true);
var host = hostField.get(patternProvider);
if (host != null) {
var getBlockEntityMethod = host.getClass().getMethod("getBlockEntity");
var blockEntity = getBlockEntityMethod.invoke(host);
if (blockEntity != null) {
var getLevelMethod = blockEntity.getClass().getMethod("getLevel");
return (Level) getLevelMethod.invoke(blockEntity);
if (patternProvider instanceof PatternProviderLogicAccessor accessor) {
var host = accessor.eap$host();
if (host != null) {
BlockEntity be = host.getBlockEntity();
if (be != null) {
return be.getLevel();
}
}
}
} catch (Exception e) {
// 静默处理异常返回null让调用者处理
} catch (Exception ignored) {
}
return null;
}

View File

@ -15,6 +15,7 @@
"ae2.accessor.PatternAccessTermScreenSlotsRowAccessor",
"ae2.client.gui.AEBaseScreenMixin",
"ae2.client.gui.PatternEncodingTermScreenMixin",
"ae2.client.gui.PatternProviderCloseMixin",
"ae2.client.gui.PatternProviderScreenMixin",
"ae2.client.gui.SlotGridLayoutMixin",
"extendedae.accessor.GuiExPatternTerminalAccessor",