初步更改

This commit is contained in:
GaLicn 2025-09-05 23:28:46 +08:00
parent 31e04d9415
commit 701affe217
162 changed files with 12075 additions and 11 deletions

View File

@ -125,6 +125,47 @@ neoForge {
// Include resources generated by data generators.
sourceSets.main.resources { srcDir 'src/generated/resources' }
//
sourceSets.main.java {
// 广ExtendedAEPlusExtendedAEPlusClientConfig
exclude 'com/extendedae_plus/NewIcon.java'
// api/** include
include 'com/extendedae_plus/api/**'
exclude 'com/extendedae_plus/client/**'
//
include 'com/extendedae_plus/config/**'
exclude 'com/extendedae_plus/content/**'
exclude 'com/extendedae_plus/hooks/**'
exclude 'com/extendedae_plus/init/**'
exclude 'com/extendedae_plus/integration/**'
exclude 'com/extendedae_plus/menu/**'
// Mod
include 'com/extendedae_plus/ExtendedAEPlus.java'
// mixin accessor
// accessor
include 'com/extendedae_plus/mixin/ae2/accessor/**'
// GUI A GUI
include 'com/extendedae_plus/mixin/ae2/client/gui/PatternProviderScreenMixin.java'
include 'com/extendedae_plus/mixin/ae2/menu/PatternProviderMenuAdvancedMixin.java'
include 'com/extendedae_plus/mixin/ae2/menu/PatternProviderMenuDoublingMixin.java'
// network/** include
// util
include 'com/extendedae_plus/util/PatternProviderDataUtil.java'
include 'com/extendedae_plus/util/PatternProviderUIHelper.java'
// GUI
// api
include 'com/extendedae_plus/api/**'
include 'com/extendedae_plus/util/ExtendedAELogger.java'
include 'com/extendedae_plus/network/ModNetwork.java'
include 'com/extendedae_plus/network/ToggleAdvancedBlockingC2SPacket.java'
include 'com/extendedae_plus/network/ToggleSmartDoublingC2SPacket.java'
// include util util/**
include 'com/extendedae_plus/util/PatternProviderDataUtil.java'
include 'com/extendedae_plus/util/PatternProviderUIHelper.java'
include 'com/extendedae_plus/util/ExtendedAELogger.java'
exclude 'com/extendedae_plus/wireless/**'
}
// Sets up a dependency configuration called 'localRuntime'.
// This configuration should be used instead of 'runtimeOnly' to declare
// a dependency that will be present for runtime testing but that is
@ -163,6 +204,7 @@ dependencies {
// jarJar configuration not set in this build; use implementation for API for now
implementation "de.mari_023:ae2wtlib_api:19.2.0"
implementation "curse.maven:ex-pattern-provider-892005:6863556"
implementation "curse.maven:curios-309927:6529130"
compileOnly "curse.maven:applied-flux-965012:5614830"
compileOnly "dev.emi:emi-neoforge:1.1.10+1.21"

View File

@ -3,7 +3,10 @@ package com.extendedae_plus;
import org.slf4j.Logger;
import com.mojang.logging.LogUtils;
import com.extendedae_plus.config.ModConfigs;
import com.extendedae_plus.network.ModNetwork;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
@ -56,7 +59,7 @@ public class ExtendedAEPlus {
// Creates a creative tab with the id "extendedaeplus:example_tab" for the example item, that is placed after the combat tab
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> EXAMPLE_TAB = CREATIVE_MODE_TABS.register("example_tab", () -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup.extendedaeplus")) //The language key for the title of your CreativeModeTab
.title(Component.translatable("itemGroup.extendedae_plus")) //The language key for the title of your CreativeModeTab
.withTabsBefore(CreativeModeTabs.COMBAT)
.icon(() -> EXAMPLE_ITEM.get().getDefaultInstance())
.displayItems((parameters, output) -> {
@ -84,21 +87,23 @@ public class ExtendedAEPlus {
// Register the item to a creative tab
modEventBus.addListener(this::addCreative);
// Register our mod's ModConfigSpec so that FML can create and load the config file for us
modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
// 注册配置接入自定义的 ModConfigs
modContainer.registerConfig(ModConfig.Type.COMMON, ModConfigs.COMMON_SPEC);
}
// 便捷 ResourceLocation 工具
public static ResourceLocation id(String path) {
return ResourceLocation.fromNamespaceAndPath(MODID, path);
}
private void commonSetup(FMLCommonSetupEvent event) {
// Some common setup code
LOGGER.info("HELLO FROM COMMON SETUP");
// 示例日志避免引用不存在的模板 Config 字段
LOGGER.info("DIRT BLOCK >> {}", BuiltInRegistries.BLOCK.getKey(Blocks.DIRT));
if (Config.LOG_DIRT_BLOCK.getAsBoolean()) {
LOGGER.info("DIRT BLOCK >> {}", BuiltInRegistries.BLOCK.getKey(Blocks.DIRT));
}
LOGGER.info("{}{}", Config.MAGIC_NUMBER_INTRODUCTION.get(), Config.MAGIC_NUMBER.getAsInt());
Config.ITEM_STRINGS.get().forEach((item) -> LOGGER.info("ITEM >> {}", item));
// 注册网络通道
event.enqueueWork(ModNetwork::register);
}
// Add the example block item to the building blocks tab
@ -115,3 +120,4 @@ public class ExtendedAEPlus {
LOGGER.info("HELLO from server starting");
}
}

View File

@ -0,0 +1,28 @@
package com.extendedae_plus;
import appeng.client.gui.style.Blitter;
import net.minecraft.resources.ResourceLocation;
public class NewIcon {
@SuppressWarnings("all")
private static final ResourceLocation TEXTURE = new ResourceLocation(ExtendedAEPlus.MODID,"textures/gui/nicons.png");
public static final Blitter MULTIPLY2;
public static final Blitter DIVIDE2;
public static final Blitter MULTIPLY5;
public static final Blitter DIVIDE5;
public static final Blitter MULTIPLY10;
public static final Blitter DIVIDE10;
static {
MULTIPLY2 = Blitter.texture(TEXTURE, 64, 64).src(32, 0, 16, 16);
DIVIDE2 = Blitter.texture(TEXTURE, 64, 64).src(48, 0, 16, 16);
MULTIPLY5 = Blitter.texture(TEXTURE, 64, 64).src(0, 0, 16, 16);
DIVIDE5 = Blitter.texture(TEXTURE, 64, 64).src(16, 0, 16, 16);
MULTIPLY10 = Blitter.texture(TEXTURE, 64, 64).src(0, 16, 16, 16);
DIVIDE10 = Blitter.texture(TEXTURE, 64, 64).src(16, 16, 16, 16);
}
}

View File

@ -0,0 +1,7 @@
package com.extendedae_plus.api;
public interface AdvancedBlockingHolder {
boolean eap$getAdvancedBlocking();
void eap$setAdvancedBlocking(boolean value);
}

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,5 @@
package com.extendedae_plus.api;
public interface PatternProviderMenuAdvancedSync {
boolean eap$getAdvancedBlockingSynced();
}

View File

@ -0,0 +1,5 @@
package com.extendedae_plus.api;
public interface PatternProviderMenuDoublingSync {
boolean eap$getSmartDoublingSynced();
}

View File

@ -0,0 +1,6 @@
package com.extendedae_plus.api;
public interface SmartDoublingAwarePattern {
boolean eap$allowScaling();
void eap$setAllowScaling(boolean allow);
}

View File

@ -0,0 +1,6 @@
package com.extendedae_plus.api;
public interface SmartDoublingHolder {
boolean eap$getSmartDoubling();
void eap$setSmartDoubling(boolean value);
}

View File

@ -0,0 +1,26 @@
package com.extendedae_plus.client;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public final class ClientAdvancedBlockingState {
private static final Map<String, Boolean> states = new ConcurrentHashMap<>();
private ClientAdvancedBlockingState() {}
public static String key(String dimension, long blockPosLong) {
return dimension + "@" + blockPosLong;
}
public static void set(String key, boolean v) {
states.put(key, v);
}
public static boolean has(String key) {
return states.containsKey(key);
}
public static boolean get(String key) {
return states.getOrDefault(key, false);
}
}

View File

@ -0,0 +1,27 @@
package com.extendedae_plus.client;
import com.extendedae_plus.ExtendedAEPlus;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.ModelEvent;
/**
* 确保在模型烘焙/资源重载期间也会注册内置模型避免在刷新资源后丢失内置模型映射
*/
@EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT, bus = EventBusSubscriber.Bus.MOD)
public final class ClientModelEvents {
private ClientModelEvents() {}
@SubscribeEvent
public static void onRegisterAdditional(ModelEvent.RegisterAdditional event) {
// 在每次模型重载开始时确保内置模型已注册
// 先显式登记这些模型ID使其在首次加载阶段被请求从而触发我们的内置模型拦截
event.register(ExtendedAEPlus.id("block/crafting/4x_accelerator_formed_v2"));
event.register(ExtendedAEPlus.id("block/crafting/16x_accelerator_formed_v2"));
event.register(ExtendedAEPlus.id("block/crafting/64x_accelerator_formed_v2"));
event.register(ExtendedAEPlus.id("block/crafting/256x_accelerator_formed_v2"));
event.register(ExtendedAEPlus.id("block/crafting/1024x_accelerator_formed_v2"));
ClientProxy.init();
}
}

View File

@ -0,0 +1,57 @@
package com.extendedae_plus.client;
import appeng.client.render.crafting.CraftingCubeModel;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.client.render.crafting.EPlusCraftingCubeModelProvider;
import com.extendedae_plus.client.screen.GlobalProviderModesScreen;
import com.extendedae_plus.init.ModMenuTypes;
import com.extendedae_plus.content.crafting.EPlusCraftingUnitType;
import com.extendedae_plus.hooks.BuiltInModelHooks;
import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent;
import net.minecraft.client.gui.screens.MenuScreens;
/**
* 客户端模型注册 formed 模型注册为内置模型
*/
public final class ClientProxy {
private ClientProxy() {}
private static boolean REGISTERED = false;
public static void init() {
if (REGISTERED) return;
REGISTERED = true;
// 注册四种形成态模型为内置模型
BuiltInModelHooks.addBuiltInModel(
ExtendedAEPlus.id("block/crafting/4x_accelerator_formed_v2"),
new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_4x)));
BuiltInModelHooks.addBuiltInModel(
ExtendedAEPlus.id("block/crafting/16x_accelerator_formed_v2"),
new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_16x)));
BuiltInModelHooks.addBuiltInModel(
ExtendedAEPlus.id("block/crafting/64x_accelerator_formed_v2"),
new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_64x)));
BuiltInModelHooks.addBuiltInModel(
ExtendedAEPlus.id("block/crafting/256x_accelerator_formed_v2"),
new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_256x)));
BuiltInModelHooks.addBuiltInModel(
ExtendedAEPlus.id("block/crafting/1024x_accelerator_formed_v2"),
new CraftingCubeModel(new EPlusCraftingCubeModelProvider(EPlusCraftingUnitType.ACCELERATOR_1024x)));
}
/**
* 客户端设置阶段延迟执行需要访问注册对象的客户端注册
*/
public static void onClientSetup(final FMLClientSetupEvent event) {
event.enqueueWork(() -> {
// 确保在首次资源加载前完成内置模型注册REGISTERED 保护避免重复
init();
// 菜单 -> 屏幕 绑定
MenuScreens.register(ModMenuTypes.NETWORK_PATTERN_CONTROLLER.get(), GlobalProviderModesScreen::new);
});
}
}

View File

@ -0,0 +1,126 @@
package com.extendedae_plus.client;
import appeng.api.stacks.GenericStack;
import appeng.client.gui.me.common.MEStorageScreen;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.integration.jei.JeiRuntimeProxy;
import com.extendedae_plus.mixin.ae2.accessor.MEStorageScreenAccessor;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.OpenCraftFromJeiC2SPacket;
import com.extendedae_plus.network.PullFromJeiOrCraftC2SPacket;
import mezz.jei.api.ingredients.ITypedIngredient;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.world.item.ItemStack;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.ScreenEvent;
import org.lwjgl.glfw.GLFW;
import java.util.Optional;
@EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT)
public final class InputEvents {
private InputEvents() {}
// 临时适配在缺少 AE2 JEI 辅助类时仅尝试从 JEI 提供的原生 ItemStack 获取否则不处理
private static GenericStack toGenericStack(ITypedIngredient<?> typed) {
try {
Optional<ItemStack> maybe = typed.getItemStack();
if (maybe.isPresent()) {
ItemStack is = maybe.get();
// 尝试使用 AE2 的通用构造若不可用则返回 null
try {
return GenericStack.fromItemStack(is);
} catch (Throwable ignored) {
return null;
}
}
} catch (Throwable ignored) {
}
return null;
}
@SubscribeEvent
public static void onMouseButtonPre(ScreenEvent.MouseButtonPressed.Pre event) {
// 优先处理Shift + 左键拉取或下单
if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_LEFT && Screen.hasShiftDown()) {
double mouseX = event.getMouseX();
double mouseY = event.getMouseY();
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse(mouseX, mouseY);
if (hovered.isEmpty()) {
hovered = JeiRuntimeProxy.getIngredientUnderMouse();
}
if (hovered.isPresent()) {
// JEI 作弊模式开启则放行给 JEI 处理Shift+左键=一组
if (JeiRuntimeProxy.isJeiCheatModeEnabled()) {
return;
}
ITypedIngredient<?> typed = hovered.get();
GenericStack stack = toGenericStack(typed);
if (stack != null) {
// 发送到服务端若网络有库存则拉取一组到空槽否则若可合成则打开下单界面
ModNetwork.CHANNEL.sendToServer(new PullFromJeiOrCraftC2SPacket(stack));
// 消费此次点击避免 JEI/原版对左键的其它处理
event.setCanceled(true);
return;
}
}
}
// 中键打开 AE 下单界面保持原有功能
if (event.getButton() == GLFW.GLFW_MOUSE_BUTTON_MIDDLE) {
// 优先在 JEI 配方界面基于坐标获取若无再从覆盖层/书签获取
double mouseX = event.getMouseX();
double mouseY = event.getMouseY();
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse(mouseX, mouseY);
if (hovered.isEmpty()) {
hovered = JeiRuntimeProxy.getIngredientUnderMouse();
}
if (hovered.isEmpty()) return;
ITypedIngredient<?> typed = hovered.get();
// JEI 作弊模式开启则放行给 JEI 处理中键=一组
if (JeiRuntimeProxy.isJeiCheatModeEnabled()) {
return;
}
GenericStack stack = toGenericStack(typed);
if (stack == null) return;
// 发送到服务端让其验证并打开 CraftAmountMenu
ModNetwork.CHANNEL.sendToServer(new OpenCraftFromJeiC2SPacket(stack));
// 消费此次点击避免 JEI/原版对中键的其它处理
event.setCanceled(true);
}
}
@SubscribeEvent
public static void onKeyPressedPre(ScreenEvent.KeyPressed.Pre event) {
if (event.getKeyCode() != GLFW.GLFW_KEY_F) return;
// 仅当鼠标确实悬停在 JEI 配料上时触发
Optional<ITypedIngredient<?>> hovered = JeiRuntimeProxy.getIngredientUnderMouse();
if (hovered.isEmpty()) return;
ITypedIngredient<?> typed = hovered.get();
// 通用获取显示名称兼容物品/流体等
String name = JeiRuntimeProxy.getTypedIngredientDisplayName(typed);
if (name == null || name.isEmpty()) return;
// 写入 AE2 终端的搜索框
var screen = Minecraft.getInstance().screen;
if (screen instanceof MEStorageScreen<?> me) {
try {
MEStorageScreenAccessor acc = (MEStorageScreenAccessor) (Object) me;
acc.eap$getSearchField().setValue(name);
acc.eap$setSearchText(name); // 同步到 Repo 并刷新
event.setCanceled(true);
return;
} catch (Throwable ignored) {
}
}
}
}

View File

@ -0,0 +1,161 @@
package com.extendedae_plus.client;
import com.extendedae_plus.config.ModConfigs;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.CycleButton;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
public class ModConfigScreen extends Screen {
private final Screen parent;
// 输入控件
private EditBox pageMultiplierBox;
private EditBox wirelessMaxRangeBox;
private CycleButton<Boolean> crossDimToggle;
private CycleButton<Boolean> providerRoundRobinToggle;
private EditBox smartScalingMaxMulBox;
private CycleButton<Boolean> showEncoderToggle;
private CycleButton<Boolean> patternTerminalShowSlotsToggle;
public ModConfigScreen(Screen parent) {
super(Component.translatable("screen.extendedae_plus.title"));
this.parent = parent;
}
@Override
protected void init() {
int centerX = this.width / 2;
int y = this.height / 6 + 24; // 起始高度整体更上方
int row = 0;
int rowHeight = 26;
int boxWidth = 150;
// 左右两列左侧标签起点右侧输入控件起点
int leftX = centerX - 170;
int rightX = centerX + 20;
// pageMultiplier: Int 1-64
pageMultiplierBox = new EditBox(this.font, rightX, y + row * rowHeight, boxWidth, 20, Component.translatable("config.extendedae_plus.pageMultiplier"));
pageMultiplierBox.setValue(String.valueOf(ModConfigs.PAGE_MULTIPLIER.get()));
pageMultiplierBox.setFilter(s -> s.matches("\\d*") && parseIntOrDefault(s, 1) >= 1 && parseIntOrDefault(s, 64) <= 64);
this.addRenderableWidget(pageMultiplierBox);
row++;
// wirelessMaxRange: Double 1-4096
wirelessMaxRangeBox = new EditBox(this.font, rightX, y + row * rowHeight, boxWidth, 20, Component.translatable("config.extendedae_plus.wirelessMaxRange"));
wirelessMaxRangeBox.setValue(String.valueOf(ModConfigs.WIRELESS_MAX_RANGE.get()));
wirelessMaxRangeBox.setFilter(s -> s.isEmpty() || s.matches("\\d*(\\.\\d*)?"));
this.addRenderableWidget(wirelessMaxRangeBox);
row++;
// cross dim toggle
crossDimToggle = this.addRenderableWidget(createToggle(rightX, y + row * rowHeight, boxWidth, 20, ModConfigs.WIRELESS_CROSS_DIM_ENABLE.get()));
row++;
// provider round-robin toggle (smart doubling)
providerRoundRobinToggle = this.addRenderableWidget(createToggle(rightX, y + row * rowHeight, boxWidth, 20, ModConfigs.PROVIDER_ROUND_ROBIN_ENABLE.get()));
row++;
// smartScalingMaxMultiplier: Int 0-1048576 (0 means unlimited)
smartScalingMaxMulBox = new EditBox(this.font, rightX, y + row * rowHeight, boxWidth, 20, Component.translatable("config.extendedae_plus.smartScalingMaxMultiplier"));
smartScalingMaxMulBox.setValue(String.valueOf(ModConfigs.SMART_SCALING_MAX_MULTIPLIER.get()));
smartScalingMaxMulBox.setFilter(s -> s.matches("\\d*") && parseIntOrDefault(s, 0) >= 0 && parseIntOrDefault(s, 1048576) <= 1048576);
this.addRenderableWidget(smartScalingMaxMulBox);
row++;
// show encoder pattern player toggle
showEncoderToggle = this.addRenderableWidget(createToggle(rightX, y + row * rowHeight, boxWidth, 20, ModConfigs.SHOW_ENCOD_PATTERN_PLAYER.get()));
row++;
// pattern terminal show slots default toggle
patternTerminalShowSlotsToggle = this.addRenderableWidget(createToggle(rightX, y + row * rowHeight, boxWidth, 20, ModConfigs.PATTERN_TERMINAL_SHOW_SLOTS_DEFAULT.get()));
row++;
// 按钮保存返回
int btnW = 100;
int gap = 8;
int buttonsY = y + row * rowHeight + 18;
this.addRenderableWidget(Button.builder(Component.translatable("gui.done"), b -> saveAndClose())
.pos(centerX - btnW - gap/2, buttonsY)
.size(btnW, 20)
.build());
this.addRenderableWidget(Button.builder(Component.translatable("gui.cancel"), b -> onClose())
.pos(centerX + gap/2, buttonsY)
.size(btnW, 20)
.build());
}
private void saveAndClose() {
// 读取与校验
int pageMul = clamp(parseIntOrDefault(pageMultiplierBox.getValue(), ModConfigs.PAGE_MULTIPLIER.get()), 1, 64);
double maxRange = clamp(parseDoubleOrDefault(wirelessMaxRangeBox.getValue(), ModConfigs.WIRELESS_MAX_RANGE.get()), 1.0, 4096.0);
boolean crossDim = crossDimToggle.getValue();
boolean providerRoundRobin = providerRoundRobinToggle.getValue();
int smartMaxMul = clamp(parseIntOrDefault(smartScalingMaxMulBox.getValue(), ModConfigs.SMART_SCALING_MAX_MULTIPLIER.get()), 0, 1048576);
boolean showEncoder = showEncoderToggle.getValue();
boolean patternShowSlots = patternTerminalShowSlotsToggle.getValue();
// 应用到 Forge 配置值
ModConfigs.PAGE_MULTIPLIER.set(pageMul);
ModConfigs.WIRELESS_MAX_RANGE.set(maxRange);
ModConfigs.WIRELESS_CROSS_DIM_ENABLE.set(crossDim);
ModConfigs.PROVIDER_ROUND_ROBIN_ENABLE.set(providerRoundRobin);
ModConfigs.SMART_SCALING_MAX_MULTIPLIER.set(smartMaxMul);
ModConfigs.SHOW_ENCOD_PATTERN_PLAYER.set(showEncoder);
ModConfigs.PATTERN_TERMINAL_SHOW_SLOTS_DEFAULT.set(patternShowSlots);
// Forge 会在合适的时机写回到配置文件部分改动可能需要重启游戏或世界才完全生效
onClose();
}
// Helper to create a boolean on/off CycleButton which shows localized on/off text
private CycleButton<Boolean> createToggle(int x, int y, int width, int height, boolean initial) {
CycleButton<Boolean> btn = CycleButton.onOffBuilder(initial)
.create(x, y, width, height, Component.empty(), (b, v) -> b.setMessage(Component.translatable(v ? "config.extendedae_plus.state_on" : "config.extendedae_plus.state_off")));
btn.setMessage(Component.translatable(initial ? "config.extendedae_plus.state_on" : "config.extendedae_plus.state_off"));
return btn;
}
@Override
public void onClose() {
Minecraft.getInstance().setScreen(parent);
}
@Override
public void render(GuiGraphics g, int mouseX, int mouseY, float partialTick) {
this.renderBackground(g);
super.render(g, mouseX, mouseY, partialTick);
int centerX = this.width / 2;
int y = this.height / 6 + 24;
int rowHeight = 26;
int labelColor = 0xFFFFFF;
int leftX = centerX - 170; // 标签左列位置
// 标题
g.drawCenteredString(this.font, this.title, centerX, y - 28, 0xFFFFFF);
// 每行标签
g.drawString(this.font, Component.translatable("config.extendedae_plus.pageMultiplier_with_range"), leftX, y + 0 * rowHeight + 6, labelColor, false);
g.drawString(this.font, Component.translatable("config.extendedae_plus.wirelessMaxRange_with_range"), leftX, y + 1 * rowHeight + 6, labelColor, false);
g.drawString(this.font, Component.translatable("config.extendedae_plus.wirelessCrossDimEnable"), leftX, y + 2 * rowHeight + 6, labelColor, false);
g.drawString(this.font, Component.translatable("config.extendedae_plus.providerRoundRobinEnable"), leftX, y + 3 * rowHeight + 6, labelColor, false);
g.drawString(this.font, Component.translatable("config.extendedae_plus.smartScalingMaxMultiplier_with_range"), leftX, y + 4 * rowHeight + 6, labelColor, false);
g.drawString(this.font, Component.translatable("config.extendedae_plus.showEncoderPatternPlayer"), leftX, y + 5 * rowHeight + 6, labelColor, false);
g.drawString(this.font, Component.translatable("config.extendedae_plus.patternTerminalShowSlotsDefault"), leftX, y + 6 * rowHeight + 6, labelColor, false);
}
private static int parseIntOrDefault(String s, int def) {
try { return Integer.parseInt(s); } catch (Exception e) { return def; }
}
private static double parseDoubleOrDefault(String s, double def) {
try { return Double.parseDouble(s); } catch (Exception e) { return def; }
}
private static int clamp(int v, int min, int max) { return Math.max(min, Math.min(max, v)); }
private static double clamp(double v, double min, double max) { return Math.max(min, Math.min(max, v)); }
}

View File

@ -0,0 +1,89 @@
package com.extendedae_plus.client.render.crafting;
import appeng.client.render.crafting.AbstractCraftingUnitModelProvider;
import appeng.client.render.crafting.LightBakedModel;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.content.crafting.EPlusCraftingUnitType;
import net.minecraft.client.renderer.RenderType;
import net.minecraft.client.renderer.texture.TextureAtlas;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.Material;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.neoforge.client.ChunkRenderTypeSet;
import net.neoforged.neoforge.client.model.data.ModelData;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
/**
* 形成态光照模型
*/
public class EPlusCraftingCubeModelProvider
extends AbstractCraftingUnitModelProvider<EPlusCraftingUnitType> {
public static final ChunkRenderTypeSet CUTOUT = ChunkRenderTypeSet.of(RenderType.cutout());
private static final List<Material> MATERIALS = new ArrayList<>();
//将环形边框与基础发光底图放在本模组命名空间
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 时使用
protected static final Material ACCELERATOR_4X_LIGHT = texture(ExtendedAEPlus.MODID,
"4x_accelerator_light");
protected static final Material ACCELERATOR_16X_LIGHT = texture(ExtendedAEPlus.MODID,
"16x_accelerator_light");
protected static final Material ACCELERATOR_64X_LIGHT = texture(ExtendedAEPlus.MODID,
"64x_accelerator_light");
protected static final Material ACCELERATOR_256X_LIGHT = texture(ExtendedAEPlus.MODID,
"256x_accelerator_light");
protected static final Material ACCELERATOR_1024X_LIGHT = texture(ExtendedAEPlus.MODID,
"1024x_accelerator_light");
public EPlusCraftingCubeModelProvider(EPlusCraftingUnitType type) {
super(type);
}
@Override
public List<Material> getMaterials() {
return Collections.unmodifiableList(MATERIALS);
}
@Override
public BakedModel getBakedModel(Function<Material, TextureAtlasSprite> spriteGetter) {
TextureAtlasSprite ringCorner = spriteGetter.apply(RING_CORNER);
TextureAtlasSprite ringSideHor = spriteGetter.apply(RING_SIDE_HOR);
TextureAtlasSprite ringSideVer = spriteGetter.apply(RING_SIDE_VER);
return new LightBakedModel(ringCorner, ringSideHor, ringSideVer,
spriteGetter.apply(LIGHT_BASE), this.getLightMaterial(spriteGetter)) {
public ChunkRenderTypeSet getRenderTypes(BlockState state, RandomSource rand, ModelData data) {
return CUTOUT;
}
};
}
private TextureAtlasSprite getLightMaterial(Function<Material, TextureAtlasSprite> textureGetter) {
return switch (this.type) {
case ACCELERATOR_4x -> textureGetter.apply(ACCELERATOR_4X_LIGHT);
case ACCELERATOR_16x -> textureGetter.apply(ACCELERATOR_16X_LIGHT);
case ACCELERATOR_64x -> textureGetter.apply(ACCELERATOR_64X_LIGHT);
case ACCELERATOR_256x -> textureGetter.apply(ACCELERATOR_256X_LIGHT);
case ACCELERATOR_1024x -> textureGetter.apply(ACCELERATOR_1024X_LIGHT);
};
}
private static Material texture(String namespace, String name) {
var mat = new Material(TextureAtlas.LOCATION_BLOCKS,
new ResourceLocation(namespace, "block/crafting/" + name));
MATERIALS.add(mat);
return mat;
}
}

View File

@ -0,0 +1,108 @@
package com.extendedae_plus.client.screen;
import com.extendedae_plus.menu.NetworkPatternControllerMenu;
import com.extendedae_plus.network.GlobalToggleProviderModesC2SPacket;
import com.extendedae_plus.network.ModNetwork;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
public class GlobalProviderModesScreen extends AbstractContainerScreen<NetworkPatternControllerMenu> {
private static final Component CUSTOM_TITLE = Component.literal("样板供应器状态控制器");
public GlobalProviderModesScreen(NetworkPatternControllerMenu menu, Inventory inv, Component title) {
super(menu, inv, title);
this.imageWidth = 240;
this.imageHeight = 140;
}
@Override
protected void init() {
super.init();
int w = 70; // 按钮宽
int h = 20; // 按钮高
int s = 8; // 按钮间距
int y = this.topPos + 28; // 第一行 Y
// 计算三列按钮的左侧起点使其在面板内水平居中
int totalW3 = w * 3 + s * 2;
int x = this.leftPos + (this.imageWidth - totalW3) / 2;
// 行1三个单项切换
addRenderableWidget(Button.builder(Component.translatable("gui.extendedae_plus.global.toggle_blocking"), b ->
ModNetwork.CHANNEL.sendToServer(new GlobalToggleProviderModesC2SPacket(
GlobalToggleProviderModesC2SPacket.Op.TOGGLE,
GlobalToggleProviderModesC2SPacket.Op.NOOP,
GlobalToggleProviderModesC2SPacket.Op.NOOP,
this.menu.getBlockEntityPos()
))).bounds(x, y, w, h).build());
addRenderableWidget(Button.builder(Component.translatable("gui.extendedae_plus.global.toggle_adv_blocking"), b ->
ModNetwork.CHANNEL.sendToServer(new GlobalToggleProviderModesC2SPacket(
GlobalToggleProviderModesC2SPacket.Op.NOOP,
GlobalToggleProviderModesC2SPacket.Op.TOGGLE,
GlobalToggleProviderModesC2SPacket.Op.NOOP,
this.menu.getBlockEntityPos()
))).bounds(x + w + s, y, w, h).build());
addRenderableWidget(Button.builder(Component.translatable("gui.extendedae_plus.global.toggle_smart_doubling"), b ->
ModNetwork.CHANNEL.sendToServer(new GlobalToggleProviderModesC2SPacket(
GlobalToggleProviderModesC2SPacket.Op.NOOP,
GlobalToggleProviderModesC2SPacket.Op.NOOP,
GlobalToggleProviderModesC2SPacket.Op.TOGGLE,
this.menu.getBlockEntityPos()
))).bounds(x + (w + s) * 2, y, w, h).build());
// 行2一键全开/全关
int y2 = y + h + 12;
// 第二行两列按钮总宽并居中
int totalW2 = w * 2 + s;
int x2 = this.leftPos + (this.imageWidth - totalW2) / 2;
addRenderableWidget(Button.builder(Component.translatable("gui.extendedae_plus.global.all_on"), b ->
ModNetwork.CHANNEL.sendToServer(new GlobalToggleProviderModesC2SPacket(
GlobalToggleProviderModesC2SPacket.Op.SET_TRUE,
GlobalToggleProviderModesC2SPacket.Op.SET_TRUE,
GlobalToggleProviderModesC2SPacket.Op.SET_TRUE,
this.menu.getBlockEntityPos()
))).bounds(x2, y2, w, h).build());
addRenderableWidget(Button.builder(Component.translatable("gui.extendedae_plus.global.all_off"), b ->
ModNetwork.CHANNEL.sendToServer(new GlobalToggleProviderModesC2SPacket(
GlobalToggleProviderModesC2SPacket.Op.SET_FALSE,
GlobalToggleProviderModesC2SPacket.Op.SET_FALSE,
GlobalToggleProviderModesC2SPacket.Op.SET_FALSE,
this.menu.getBlockEntityPos()
))).bounds(x2 + w + s, y2, w, h).build());
}
@Override
protected void renderBg(net.minecraft.client.gui.GuiGraphics gfx, float partialTicks, int mouseX, int mouseY) {
// 半透明全屏遮罩避免底层 HUD准星/物品栏文字透出
gfx.fill(0, 0, this.width, this.height, 0xC0000000);
// 在按钮区域绘制一个半透明面板提升可读性
int pad = 6;
int panelLeft = this.leftPos - pad;
int panelTop = this.topPos - pad;
int panelRight = this.leftPos + this.imageWidth + pad;
int panelBottom = this.topPos + this.imageHeight + pad;
gfx.fill(panelLeft, panelTop, panelRight, panelBottom, 0xA01E1E1E);
// 边框
gfx.fill(panelLeft, panelTop, panelRight, panelTop + 1, 0x80FFFFFF);
gfx.fill(panelLeft, panelBottom - 1, panelRight, panelBottom, 0x80000000);
gfx.fill(panelLeft, panelTop, panelLeft + 1, panelBottom, 0x80FFFFFF);
gfx.fill(panelRight - 1, panelTop, panelRight, panelBottom, 0x80000000);
}
@Override
public void render(net.minecraft.client.gui.GuiGraphics gfx, int mouseX, int mouseY, float partialTicks) {
this.renderBackground(gfx);
super.render(gfx, mouseX, mouseY, partialTicks);
gfx.drawString(this.font, CUSTOM_TITLE, this.leftPos + 10, this.topPos + 8, 0xFFFFFF, false);
}
@Override
protected void renderLabels(net.minecraft.client.gui.GuiGraphics gfx, int mouseX, int mouseY) {
// 不绘制默认的玩家物品栏标题例如物品栏避免与自定义面板重叠
// 标题已在 render() 中手动绘制
}
}

View File

@ -0,0 +1,405 @@
package com.extendedae_plus.client.ui;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.UploadEncodedPatternToProviderC2SPacket;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.EditBox;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import java.util.*;
/**
* 简单的供应器选择弹窗
* 展示若干个可点击的供应器条目点击后发送带 providerId 的上传请求
*/
public class ProviderSelectScreen extends Screen {
private final Screen parent;
// 原始数据
private final List<Long> ids;
private final List<String> names;
private final List<Integer> emptySlots;
// 分组后的数据同名合并
private final List<Long> gIds = new ArrayList<>(); // 代表条目使用的 providerId选择空位数最多的那个
private final List<String> gNames = new ArrayList<>(); // 分组名供应器名称
private final List<Integer> gTotalSlots = new ArrayList<>(); // 该名称下供应器空位总和
private final List<Integer> gCount = new ArrayList<>(); // 该名称下供应器数量
// 过滤后的数据由查询生成
private final List<Long> fIds = new ArrayList<>();
private final List<String> fNames = new ArrayList<>();
private final List<Integer> fTotalSlots = new ArrayList<>();
private final List<Integer> fCount = new ArrayList<>();
// 搜索框
private EditBox searchBox;
// 中文名输入框用于添加映射
private EditBox cnInput;
private String query = "";
private boolean needsRefresh = false;
private int page = 0;
private static final int PAGE_SIZE = 6;
private final List<Button> entryButtons = new ArrayList<>();
public ProviderSelectScreen(Screen parent, List<Long> ids, List<String> names, List<Integer> emptySlots) {
super(Component.translatable("extendedae_plus.screen.choose_provider.title"));
this.parent = parent;
this.ids = ids;
this.names = names;
this.emptySlots = emptySlots;
// 如果有来自 JEI 的最近处理名称则作为初始查询
try {
String recent = com.extendedae_plus.util.ExtendedAEPatternUploadUtil.lastProcessingName;
if (recent != null && !recent.isBlank()) {
this.query = recent;
// 用后即清空避免污染下次
com.extendedae_plus.util.ExtendedAEPatternUploadUtil.lastProcessingName = null;
}
} catch (Throwable ignored) {}
buildGroups();
applyFilter();
}
@Override
protected void init() {
this.clearWidgets();
entryButtons.clear();
int centerX = this.width / 2;
int startY = this.height / 2 - 70;
// 搜索框置于条目上方
if (searchBox == null) {
searchBox = new EditBox(this.font, centerX - 120, startY - 25, 240, 18, Component.translatable("extendedae_plus.screen.search"));
} else {
// 重新定位保持输入值
searchBox.setX(centerX - 120);
searchBox.setY(startY - 25);
searchBox.setWidth(240);
}
searchBox.setValue(query);
searchBox.setResponder(text -> {
// 只有当输入真正发生变化时才重置页码与过滤
if (Objects.equals(text, query)) return;
query = text;
page = 0;
applyFilter();
// 避免在回调中直接重建 UI延迟到下一次 tick
needsRefresh = true;
});
this.addRenderableWidget(searchBox);
int start = page * PAGE_SIZE;
int end = Math.min(start + PAGE_SIZE, fIds.size());
int buttonWidth = 240;
int buttonHeight = 20;
int gap = 5;
for (int i = start; i < end; i++) {
int idx = i;
String label = buildLabel(idx);
Button btn = Button.builder(Component.literal(label), b -> onChoose(idx))
.bounds(centerX - buttonWidth / 2, startY + (i - start) * (buttonHeight + gap), buttonWidth, buttonHeight)
.build();
entryButtons.add(btn);
this.addRenderableWidget(btn);
}
// 分页按钮
int navY = startY + PAGE_SIZE * (buttonHeight + gap) + 10;
Button prev = Button.builder(Component.literal("<"), b -> changePage(-1))
.bounds(centerX - 60, navY, 20, 20)
.build();
Button next = Button.builder(Component.literal(">"), b -> changePage(1))
.bounds(centerX + 40, navY, 20, 20)
.build();
prev.active = page > 0;
next.active = (page + 1) * PAGE_SIZE < fIds.size();
this.addRenderableWidget(prev);
this.addRenderableWidget(next);
// 重载映射按钮热重载 recipe_type_names.json移至下一行与关闭按钮并排
Button reload = Button.builder(Component.translatable("extendedae_plus.screen.reload_mapping"), b -> reloadMapping())
.bounds(centerX - 130, navY + 30, 80, 20)
.build();
this.addRenderableWidget(reload);
// 中文名输入框用于新增映射的值
if (cnInput == null) {
cnInput = new EditBox(this.font, centerX + 50, navY + 30, 120, 20, Component.translatable("extendedae_plus.screen.cn_name"));
} else {
cnInput.setX(centerX + 50);
cnInput.setY(navY + 30);
cnInput.setWidth(120);
}
this.addRenderableWidget(cnInput);
// 增加映射按钮使用当前搜索关键字 -> 中文
Button addMap = Button.builder(Component.translatable("extendedae_plus.screen.add_mapping"), b -> addMappingFromUI())
.bounds(centerX + 175, navY + 30, 60, 20)
.build();
this.addRenderableWidget(addMap);
// 删除映射按中文值精确匹配删除按钮
Button delByCn = Button.builder(Component.literal("删除映射"), b -> deleteMappingByCnFromUI())
.bounds(centerX + 240, navY + 30, 60, 20)
.build();
this.addRenderableWidget(delByCn);
// 关闭按钮
Button close = Button.builder(Component.translatable("gui.cancel"), b -> onClose())
.bounds(centerX - 40, navY + 30, 80, 20)
.build();
this.addRenderableWidget(close);
}
private void changePage(int delta) {
int newPage = page + delta;
if (newPage < 0) return;
if (newPage * PAGE_SIZE >= fIds.size()) return;
page = newPage;
// 避免在回调中直接重建 UI改为下帧刷新
needsRefresh = true;
}
private void reloadMapping() {
try {
com.extendedae_plus.util.ExtendedAEPatternUploadUtil.loadRecipeTypeNames();
var player = Minecraft.getInstance().player;
if (player != null) {
player.sendSystemMessage(Component.literal("ExtendedAE Plus: 已重载映射表"));
}
// 重载后不强制刷新筛选但如需立即应用到名称匹配可手动编辑搜索框或翻页
} catch (Throwable t) {
var player = Minecraft.getInstance().player;
if (player != null) {
player.sendSystemMessage(Component.literal("ExtendedAE Plus: 重载映射表失败: " + t.getClass().getSimpleName()));
}
}
}
private String buildLabel(int idx) {
String name = fNames.get(idx);
int totalSlots = fTotalSlots.get(idx);
int count = fCount.get(idx);
// 不显示具体 id显示合并统计名称总空位x数量
return name + " (" + totalSlots + ") x" + count;
}
private void onChoose(int idx) {
if (idx < 0 || idx >= fIds.size()) return;
long providerId = fIds.get(idx);
ModNetwork.CHANNEL.sendToServer(new UploadEncodedPatternToProviderC2SPacket(providerId));
this.onClose();
}
@Override
public void onClose() {
Minecraft.getInstance().setScreen(parent);
}
@Override
public boolean isPauseScreen() {
return false;
}
private void buildGroups() {
// 使用 LinkedHashMap 保持首次出现顺序
Map<String, Group> map = new LinkedHashMap<>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
long id = ids.get(i);
int slots = emptySlots.get(i);
Group g = map.computeIfAbsent(name, k -> new Group());
g.count++;
g.totalSlots += Math.max(0, slots);
// 挑选空位最多的作为代表 id若并列保留先到者
if (slots > g.bestSlots) {
g.bestSlots = slots;
g.bestId = id;
}
}
for (Map.Entry<String, Group> e : map.entrySet()) {
String name = e.getKey();
Group g = e.getValue();
gNames.add(name);
gIds.add(g.bestId);
gTotalSlots.add(g.totalSlots);
gCount.add(g.count);
}
}
private static class Group {
long bestId = Long.MIN_VALUE;
int bestSlots = Integer.MIN_VALUE;
int totalSlots = 0;
int count = 0;
}
private void applyFilter() {
fIds.clear();
fNames.clear();
fTotalSlots.clear();
fCount.clear();
String q = query == null ? "" : query.trim();
for (int i = 0; i < gIds.size(); i++) {
String name = gNames.get(i);
if (q.isEmpty() || nameMatches(name, q)) {
fIds.add(gIds.get(i));
fNames.add(name);
fTotalSlots.add(gTotalSlots.get(i));
fCount.add(gCount.get(i));
}
}
// 若查询不为空但没有任何匹配则回退为显示全部避免空列表误导用户
if (!q.isEmpty() && fIds.isEmpty()) {
for (int i = 0; i < gIds.size(); i++) {
fIds.add(gIds.get(i));
fNames.add(gNames.get(i));
fTotalSlots.add(gTotalSlots.get(i));
fCount.add(gCount.get(i));
}
}
}
// 优先使用 JEC 的拼音匹配否则回退到大小写不敏感子串匹配
private static Boolean JEC_AVAILABLE = null;
private static java.lang.reflect.Method JEC_CONTAINS = null;
private static boolean nameMatches(String name, String key) {
if (name == null) return false;
if (key == null || key.isEmpty()) return true;
try {
if (JEC_AVAILABLE == null) {
try {
Class<?> cls = Class.forName("me.towdium.jecharacters.utils.Match");
// 使用 contains(CharSequence, CharSequence)
JEC_CONTAINS = cls.getMethod("contains", CharSequence.class, CharSequence.class);
JEC_AVAILABLE = true;
} catch (Throwable t) {
JEC_AVAILABLE = false;
}
}
if (Boolean.TRUE.equals(JEC_AVAILABLE) && JEC_CONTAINS != null) {
Object r = JEC_CONTAINS.invoke(null, name, key);
if (r instanceof Boolean && (Boolean) r) return true;
// 再尝试大小写不敏感双方转为小写重新匹配
String nL = name.toLowerCase();
String kL = key.toLowerCase();
Object r2 = JEC_CONTAINS.invoke(null, nL, kL);
if (r2 instanceof Boolean && (Boolean) r2) return true;
}
} catch (Throwable ignored) {
// 回退
}
// 默认大小写不敏感子串
return name.toLowerCase().contains(key.toLowerCase());
}
@Override
public boolean keyPressed(int keyCode, int scanCode, int modifiers) {
if (searchBox != null && searchBox.keyPressed(keyCode, scanCode, modifiers)) {
return true;
}
return super.keyPressed(keyCode, scanCode, modifiers);
}
@Override
public boolean charTyped(char codePoint, int modifiers) {
if (searchBox != null && searchBox.charTyped(codePoint, modifiers)) {
return true;
}
return super.charTyped(codePoint, modifiers);
}
@Override
public boolean mouseClicked(double mouseX, double mouseY, int button) {
// 右键点击搜索框区域时清空搜索框内容并刷新
if (button == 1 && this.searchBox != null) {
int x = this.searchBox.getX();
int y = this.searchBox.getY();
int w = this.searchBox.getWidth();
int h = this.searchBox.getHeight();
if (mouseX >= x && mouseX <= x + w && mouseY >= y && mouseY <= y + h) {
if (!this.searchBox.getValue().isEmpty()) {
this.searchBox.setValue("");
}
this.query = "";
this.page = 0;
applyFilter();
this.needsRefresh = true;
return true;
}
}
return super.mouseClicked(mouseX, mouseY, button);
}
@Override
public void tick() {
super.tick();
if (searchBox != null) {
searchBox.tick();
}
if (cnInput != null) {
cnInput.tick();
}
if (needsRefresh) {
needsRefresh = false;
// 重新构建当前屏幕内容
init();
}
}
private void addMappingFromUI() {
String key = query == null ? "" : query.trim();
String val = cnInput == null ? "" : cnInput.getValue().trim();
var player = Minecraft.getInstance().player;
if (key.isEmpty()) {
if (player != null) player.sendSystemMessage(Component.literal("请输入搜索关键字后再添加映射"));
return;
}
if (val.isEmpty()) {
if (player != null) player.sendSystemMessage(Component.literal("请输入中文名称"));
return;
}
boolean ok = com.extendedae_plus.util.ExtendedAEPatternUploadUtil.addOrUpdateAliasMapping(key, val);
if (ok) {
if (player != null) player.sendSystemMessage(Component.literal("已添加/更新映射: " + key + " -> " + val));
// 将刚添加的中文名写入搜索框作为当前查询
this.query = val;
if (this.searchBox != null) {
this.searchBox.setValue(val);
}
// 更新本地过滤显示若名称包含中文可被搜索
applyFilter();
// 回到第一页以展示最新筛选结果
page = 0;
needsRefresh = true;
} else {
if (player != null) player.sendSystemMessage(Component.literal("写入映射失败"));
}
}
// 使用中文值精确匹配删除映射
private void deleteMappingByCnFromUI() {
String val = cnInput == null ? "" : cnInput.getValue().trim();
var player = Minecraft.getInstance().player;
if (val.isEmpty()) {
if (player != null) player.sendSystemMessage(Component.literal("请输入中文名称后再删除映射"));
return;
}
int removed = com.extendedae_plus.util.ExtendedAEPatternUploadUtil.removeMappingsByCnValue(val);
if (removed > 0) {
if (player != null) player.sendSystemMessage(Component.literal("已删除 " + removed + " 条映射,中文= " + val));
applyFilter();
needsRefresh = true;
} else {
if (player != null) player.sendSystemMessage(Component.literal("未找到中文为 '" + val + "' 的映射"));
}
}
}

View File

@ -0,0 +1,77 @@
package com.extendedae_plus.config;
import net.neoforged.neoforge.common.ModConfigSpec;
public final class ModConfigs {
public static final ModConfigSpec COMMON_SPEC;
public static final ModConfigSpec.IntValue PAGE_MULTIPLIER;
public static final ModConfigSpec.DoubleValue WIRELESS_MAX_RANGE;
public static final ModConfigSpec.BooleanValue WIRELESS_CROSS_DIM_ENABLE;
public static final ModConfigSpec.BooleanValue SHOW_ENCOD_PATTERN_PLAYER;
public static final ModConfigSpec.BooleanValue PROVIDER_ROUND_ROBIN_ENABLE;
public static final ModConfigSpec.BooleanValue PATTERN_TERMINAL_SHOW_SLOTS_DEFAULT;
public static final ModConfigSpec.IntValue SMART_SCALING_MAX_MULTIPLIER;
static {
ModConfigSpec.Builder builder = new ModConfigSpec.Builder();
builder.push("extendedae_plus");
PAGE_MULTIPLIER = builder
.comment(
"扩展样板供应器总槽位容量的倍率。",
"基础为36每页仍显示36格倍率会增加总页数/总容量。",
"建议范围 1-16")
.defineInRange("pageMultiplier", 1, 1, 64);
// 无线收发器最大连接距离单位方块
// 一对多从端连接主端时将以该值作为范围限制
WIRELESS_MAX_RANGE = builder
.comment(
"无线收发器最大连接距离(单位:方块)",
"从端与主端的直线距离需小于等于该值才会建立连接。")
.defineInRange("wirelessMaxRange", 256.0D, 1.0D, 4096.0D);
// 是否允许跨维度连接忽略维度差异进行频道传输
WIRELESS_CROSS_DIM_ENABLE = builder
.comment(
"是否允许无线收发器跨维度建立连接",
"开启后,从端可连接到不同维度的主端(忽略距离限制)")
.define("wirelessCrossDimEnable", true);
SHOW_ENCOD_PATTERN_PLAYER = builder
.comment(
"是否显示样板编码玩家",
"开启后将在样板 HoverText 上添加样板的编码玩家"
)
.define("showEncoderPatternPlayer", true);
// 智能倍增后是否在样板供应器间轮询分配请求量开启 provider 均分关闭不拆分
PROVIDER_ROUND_ROBIN_ENABLE = builder
.comment(
"智能倍增时是否对样板供应器轮询分配",
"仅多个供应器有相同样板时生效,开启后请求会均分到所有可用供应器,关闭则全部分配给单一供应器",
"注意:所有相关供应器需开启智能倍增,否则可能失效",
"默认: true")
.define("providerRoundRobinEnable", true);
// 智能倍增的最大倍数以单次样板产出为单位
// 0 表示不限制>0 表示最大倍增倍数上限例如 64 表示最多放大到 64
SMART_SCALING_MAX_MULTIPLIER = builder
.comment(
"智能倍增的最大倍数0 表示不限制)",
"此倍数是针对单次样板产出的放大倍数上限,用于限制一次推送中按倍增缩放的规模")
.defineInRange("smartScalingMaxMultiplier", 0, 0, 1048576);
// 模式访问终端ExtendedAE 图样终端默认是否显示槽位渲染SlotsRow
// true: 默认显示可通过界面按钮临时隐藏false: 默认隐藏可通过按钮显示
PATTERN_TERMINAL_SHOW_SLOTS_DEFAULT = builder
.comment(
"样板终端默认是否显示槽位",
"影响进入界面时SlotsRow的默认可见性仅影响客户端显示"
)
.define("patternTerminalShowSlotsDefault", true);
builder.pop();
COMMON_SPEC = builder.build();
}
private ModConfigs() {}
}

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

@ -0,0 +1,136 @@
package com.extendedae_plus.content;
import appeng.api.crafting.IPatternDetails;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.api.stacks.KeyCounter;
import appeng.crafting.pattern.AEProcessingPattern;
import net.minecraft.world.level.Level;
import org.jetbrains.annotations.Nullable;
import java.util.Objects;
/**
* 缩放后的处理样板结构完全模拟 AEProcessingPattern
* 保持 sparse/condensed/inputs 的一致性同时保存原始样板
*/
public final class ScaledProcessingPattern implements IPatternDetails {
private final AEProcessingPattern original; // 原始样板引用
private final AEItemKey definition; // 样板物品
private final GenericStack[] sparseInputs; // 缩放后的稀疏输入
private final GenericStack[] sparseOutputs; // 缩放后的稀疏输出
private final IInput[] inputs; // 缩放后的压缩输入
private final GenericStack[] condensedOutputs; // 缩放后的压缩输出
public ScaledProcessingPattern(
AEProcessingPattern original,
AEItemKey definition,
GenericStack[] sparseInputs,
GenericStack[] sparseOutputs,
IInput[] inputs,
GenericStack[] condensedOutputs
) {
this.original = Objects.requireNonNull(original);
this.definition = Objects.requireNonNull(definition);
this.sparseInputs = Objects.requireNonNull(sparseInputs);
this.sparseOutputs = Objects.requireNonNull(sparseOutputs);
this.inputs = Objects.requireNonNull(inputs);
this.condensedOutputs = Objects.requireNonNull(condensedOutputs);
}
/* -------------------- API 实现 -------------------- */
public AEProcessingPattern getOriginal() {
return original;
}
@Override
public AEItemKey getDefinition() {
return definition;
}
@Override
public IInput[] getInputs() {
return inputs;
}
@Override
public GenericStack[] getOutputs() {
return condensedOutputs;
}
public GenericStack[] getSparseInputs() {
return sparseInputs;
}
public GenericStack[] getSparseOutputs() {
return sparseOutputs;
}
@Override
public GenericStack getPrimaryOutput() {
if (condensedOutputs.length > 0) return condensedOutputs[0];
return original.getPrimaryOutput();
}
@Override
public boolean supportsPushInputsToExternalInventory() {
return original.supportsPushInputsToExternalInventory();
}
@Override
public void pushInputsToExternalInventory(KeyCounter[] inputHolder, PatternInputSink inputSink) {
// 保持和 AEProcessingPattern 一致 sparseInputs 驱动
if (sparseInputs.length == inputs.length) {
IPatternDetails.super.pushInputsToExternalInventory(inputHolder, inputSink);
} else {
KeyCounter allInputs = new KeyCounter();
for (KeyCounter counter : inputHolder) {
allInputs.addAll(counter);
}
for (GenericStack sparseInput : sparseInputs) {
if (sparseInput != null) {
AEKey key = sparseInput.what();
long amount = sparseInput.amount();
long available = allInputs.get(key);
if (available < amount) {
throw new RuntimeException("Expected at least %d of %s when pushing scaled pattern, but only %d available"
.formatted(amount, key, available));
}
inputSink.pushInput(key, amount);
allInputs.remove(key, amount);
}
}
}
}
/* -------------------- 缩放输入代理 -------------------- */
public static final class Input implements IPatternDetails.IInput {
private final GenericStack[] template;
private final long multiplier;
public Input(GenericStack[] template, long multiplier) {
this.template = template;
this.multiplier = multiplier;
}
public GenericStack[] getPossibleInputs() {
return this.template;
}
public long getMultiplier() {
return this.multiplier;
}
public boolean isValid(AEKey input, Level level) {
return input.matches(this.template[0]);
}
public @Nullable AEKey getRemainingKey(AEKey template) {
return null;
}
}
}

View File

@ -0,0 +1,38 @@
package com.extendedae_plus.content.controller;
import net.minecraft.core.BlockPos;
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.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
public class NetworkPatternControllerBlock extends Block implements EntityBlock {
public NetworkPatternControllerBlock(Properties props) {
super(props);
}
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new NetworkPatternControllerBlockEntity(pos, state);
}
@Override
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (!level.isClientSide) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof MenuProvider provider && player instanceof ServerPlayer sp) {
// 使用原生 API 打开界面如需自定义数据传输请在 Menu 构造中使用 BlockPos/Access 读取
sp.openMenu(provider);
}
}
return InteractionResult.sidedSuccess(level.isClientSide);
}
}

View File

@ -0,0 +1,92 @@
package com.extendedae_plus.content.controller;
import appeng.api.networking.GridHelper;
import appeng.api.networking.GridFlags;
import appeng.api.networking.IGridNode;
import appeng.api.networking.IGridNodeListener;
import appeng.api.networking.IInWorldGridNodeHost;
import appeng.api.networking.IManagedGridNode;
import com.extendedae_plus.init.ModBlockEntities;
import com.extendedae_plus.init.ModMenuTypes;
import com.extendedae_plus.menu.NetworkPatternControllerMenu;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
public class NetworkPatternControllerBlockEntity extends BlockEntity implements IInWorldGridNodeHost, MenuProvider {
private final IManagedGridNode managedNode;
public NetworkPatternControllerBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.NETWORK_PATTERN_CONTROLLER_BE.get(), pos, state);
this.managedNode = GridHelper.createManagedNode(this, NodeListener.INSTANCE);
this.managedNode.setIdlePowerUsage(1.0);
this.managedNode.setInWorldNode(true);
this.managedNode.setFlags(GridFlags.REQUIRE_CHANNEL);
this.managedNode.setTagName("network_pattern_controller");
}
@Override
public @Nullable IGridNode getGridNode(@Nullable Direction dir) {
return managedNode == null ? null : managedNode.getNode();
}
@Override
public void onLoad() {
super.onLoad();
if (this.level != null && !this.level.isClientSide) {
GridHelper.onFirstTick(this, be -> be.managedNode.create(be.getLevel(), be.getBlockPos()));
}
}
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
this.managedNode.saveToNBT(tag);
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
this.managedNode.loadFromNBT(tag);
}
@Override
public void onChunkUnloaded() {
super.onChunkUnloaded();
this.managedNode.destroy();
}
@Override
public void setRemoved() {
super.setRemoved();
this.managedNode.destroy();
}
@Override
public Component getDisplayName() {
return Component.translatable("block.extendedae_plus.network_pattern_controller");
}
@Override
public AbstractContainerMenu createMenu(int id, Inventory inv, Player player) {
return new NetworkPatternControllerMenu(id, inv, this.worldPosition);
}
enum NodeListener implements IGridNodeListener<NetworkPatternControllerBlockEntity> {
INSTANCE;
@Override
public void onSaveChanges(NetworkPatternControllerBlockEntity host, IGridNode node) {
host.setChanged();
}
}
}

View File

@ -0,0 +1,44 @@
package com.extendedae_plus.content.crafting;
import appeng.block.crafting.ICraftingUnitType;
import net.minecraft.world.item.Item;
import com.extendedae_plus.init.ModItems;
public enum EPlusCraftingUnitType implements ICraftingUnitType {
ACCELERATOR_4x(0, 4),
ACCELERATOR_16x(0, 16),
ACCELERATOR_64x(0, 64),
ACCELERATOR_256x(0, 256),
ACCELERATOR_1024x(0, 1024);
private final long storage;
private final int threads;
EPlusCraftingUnitType(long storage, int threads) {
this.storage = storage;
this.threads = threads;
}
@Override
public long getStorageBytes() {
return this.storage;
}
@Override
public int getAcceleratorThreads() {
// 返回真实线程值单块可能超过 16上限校验已由 mixin 绕过
return this.threads;
}
@Override
public Item getItemFromType() {
return switch (this) {
case ACCELERATOR_4x -> ModItems.ACCELERATOR_4x.get();
case ACCELERATOR_16x -> ModItems.ACCELERATOR_16x.get();
case ACCELERATOR_64x -> ModItems.ACCELERATOR_64x.get();
case ACCELERATOR_256x -> ModItems.ACCELERATOR_256x.get();
case ACCELERATOR_1024x -> ModItems.ACCELERATOR_1024x.get();
};
}
}

View File

@ -0,0 +1,112 @@
package com.extendedae_plus.content.wireless;
import com.extendedae_plus.init.ModBlockEntities;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.EntityBlock;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityTicker;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
public class WirelessTransceiverBlock extends Block implements EntityBlock {
public WirelessTransceiverBlock(Properties props) {
super(props);
}
@Override
public BlockEntity newBlockEntity(BlockPos pos, BlockState state) {
return new WirelessTransceiverBlockEntity(pos, state);
}
@Override
public void attack(BlockState state, Level level, BlockPos pos, Player player) {
// 潜行左键减频-1 -10
if (!level.isClientSide && player.isShiftKeyDown()) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof WirelessTransceiverBlockEntity te) {
if (te.isLocked()) {
player.displayClientMessage(Component.literal("收发器已锁定,无法修改频道"), true);
super.attack(state, level, pos, player);
return;
}
int step = 1;
if (player.getMainHandItem().is(Items.REDSTONE_TORCH)) step = 10;
if (player.getMainHandItem().is(Items.STICK)) step = 10;
long f = te.getFrequency();
f -= step;
if (f < 0) f = 0;
te.setFrequency(f);
player.displayClientMessage(Component.literal("频道:" + te.getFrequency()), true);
}
}
super.attack(state, level, pos, player);
}
@Override
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
if (level.isClientSide) {
return InteractionResult.SUCCESS;
}
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof WirelessTransceiverBlockEntity te) {
boolean sneaking = player.isShiftKeyDown();
if (sneaking) {
if (te.isLocked()) {
player.displayClientMessage(Component.literal("收发器已锁定,无法修改频道"), true);
return InteractionResult.CONSUME;
}
// 频率调节主手 +1 +10副手 -1 -10
int step = 1;
// 手持红石火把加10手持木棍减10仅改变步长不改变加/减方向
if (player.getItemInHand(hand).is(Items.REDSTONE_TORCH)) step = 10;
if (player.getItemInHand(hand).is(Items.STICK)) step = 10;
long f = te.getFrequency();
if (hand == InteractionHand.MAIN_HAND) {
f += step;
} else {
f -= step;
if (f < 0) f = 0;
}
te.setFrequency(f);
player.displayClientMessage(Component.literal("频道:" + te.getFrequency()), true);
} else {
if (te.isLocked()) {
player.displayClientMessage(Component.literal("收发器已锁定,无法切换模式"), true);
return InteractionResult.CONSUME;
}
te.setMasterMode(!te.isMasterMode());
player.displayClientMessage(Component.literal(te.isMasterMode() ? "模式:主端" : "模式:从端"), true);
}
return InteractionResult.CONSUME;
}
return InteractionResult.PASS;
}
@Override
public void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean isMoving) {
if (!state.is(newState.getBlock())) {
BlockEntity be = level.getBlockEntity(pos);
if (be instanceof WirelessTransceiverBlockEntity te) {
te.onRemoved();
}
}
super.onRemove(state, level, pos, newState, isMoving);
}
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
if (level.isClientSide) return null;
return type == ModBlockEntities.WIRELESS_TRANSCEIVER_BE.get()
? (lvl, pos, st, be) -> WirelessTransceiverBlockEntity.serverTick(lvl, pos, st, (WirelessTransceiverBlockEntity) be)
: null;
}
}

View File

@ -0,0 +1,218 @@
package com.extendedae_plus.content.wireless;
import appeng.api.networking.*;
import com.extendedae_plus.init.ModBlockEntities;
import com.extendedae_plus.init.ModItems;
import com.extendedae_plus.wireless.IWirelessEndpoint;
import com.extendedae_plus.wireless.WirelessMasterLink;
import com.extendedae_plus.wireless.WirelessSlaveLink;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import org.jetbrains.annotations.Nullable;
import java.util.EnumSet;
/**
* 无线收发器方块实体骨架
* - /从模式切换
* - 频率设置
* - 集成 AE2 节点
* - 集成无线主/从逻辑
*/
public class WirelessTransceiverBlockEntity extends BlockEntity implements IWirelessEndpoint, IInWorldGridNodeHost {
private IManagedGridNode managedNode;
private long frequency = 1L;
private boolean masterMode = false;
private boolean locked = false;
private WirelessMasterLink masterLink;
private WirelessSlaveLink slaveLink;
public WirelessTransceiverBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.WIRELESS_TRANSCEIVER_BE.get(), pos, state);
// 创建 AE2 管理节点
this.managedNode = GridHelper.createManagedNode(this, NodeListener.INSTANCE);
this.managedNode.setIdlePowerUsage(1.0); // 可按需调整基础待机功耗
this.managedNode.setTagName("wireless_node");
this.managedNode.setInWorldNode(true);
this.managedNode.setExposedOnSides(EnumSet.allOf(Direction.class));
// 可见表示方便在 AE2 界面中识别可选
this.managedNode.setVisualRepresentation(ModItems.WIRELESS_TRANSCEIVER.get().getDefaultInstance());
// 初始化无线逻辑
this.masterLink = new WirelessMasterLink(this);
this.slaveLink = new WirelessSlaveLink(this);
}
/* ===================== IInWorldGridNodeHost ===================== */
@Override
public @Nullable IGridNode getGridNode(Direction dir) {
return getGridNode();
}
/* ===================== IWirelessEndpoint ===================== */
@Override
public ServerLevel getServerLevel() {
Level lvl = super.getLevel();
return lvl instanceof ServerLevel sl ? sl : null;
}
@Override
public BlockPos getBlockPos() {
return this.worldPosition;
}
@Override
public IGridNode getGridNode() {
return managedNode == null ? null : managedNode.getNode();
}
@Override
public boolean isEndpointRemoved() {
return super.isRemoved();
}
/* ===================== 公共方法(交互调用) ===================== */
public long getFrequency() {
return frequency;
}
public void setFrequency(long frequency) {
if (this.locked) return;
if (this.frequency == frequency) return;
this.frequency = frequency;
if (isMasterMode()) {
masterLink.setFrequency(frequency);
} else {
slaveLink.setFrequency(frequency);
}
setChanged();
}
public boolean isMasterMode() {
return masterMode;
}
public void setMasterMode(boolean masterMode) {
if (this.locked) return;
if (this.masterMode == masterMode) return;
// 切换前清理原模式状态
if (this.masterMode) {
masterLink.onUnloadOrRemove();
} else {
slaveLink.onUnloadOrRemove();
}
this.masterMode = masterMode;
// 切换后应用频率
if (this.masterMode) {
masterLink.setFrequency(frequency);
} else {
slaveLink.setFrequency(frequency);
}
setChanged();
}
public boolean isLocked() {
return locked;
}
public void setLocked(boolean locked) {
if (this.locked == locked) return;
this.locked = locked;
setChanged();
}
public void onRemoved() {
if (this.masterMode) {
masterLink.onUnloadOrRemove();
} else {
slaveLink.onUnloadOrRemove();
}
if (managedNode != null) {
managedNode.destroy();
}
}
/* ===================== Tick ===================== */
public static void serverTick(Level level, BlockPos pos, BlockState state, WirelessTransceiverBlockEntity be) {
if (!(level instanceof ServerLevel)) return;
if (!be.masterMode) {
// 从端需要周期检查与维护连接
be.slaveLink.updateStatus();
}
}
@Override
public void onLoad() {
super.onLoad();
// 仅服务端创建节点
ServerLevel sl = getServerLevel();
if (sl == null) return;
// 在首个 tick 创建以保证区块已就绪
GridHelper.onFirstTick(this, be -> {
be.managedNode.create(be.getLevel(), be.getBlockPos());
// 节点创建后重新应用当前模式与频率确保
// - 主端在重载后完成注册
// - 从端在重载后开始维护连接
if (be.masterMode) {
be.masterLink.setFrequency(be.frequency);
} else {
be.slaveLink.setFrequency(be.frequency);
}
});
}
/* ===================== NBT ===================== */
@Override
protected void saveAdditional(CompoundTag tag) {
super.saveAdditional(tag);
tag.putLong("frequency", frequency);
tag.putBoolean("master", masterMode);
tag.putBoolean("locked", locked);
if (managedNode != null) {
managedNode.saveToNBT(tag);
}
}
@Override
public void load(CompoundTag tag) {
super.load(tag);
this.frequency = tag.getLong("frequency");
this.masterMode = tag.getBoolean("master");
this.locked = tag.getBoolean("locked");
if (managedNode != null) {
managedNode.loadFromNBT(tag);
}
// 应用到链接器
if (masterMode) {
masterLink.setFrequency(frequency);
} else {
slaveLink.setFrequency(frequency);
}
}
/* ===================== AE2 节点监听 ===================== */
enum NodeListener implements IGridNodeListener<WirelessTransceiverBlockEntity> {
INSTANCE;
@Override
public void onSaveChanges(WirelessTransceiverBlockEntity host, IGridNode node) {
host.setChanged();
}
@Override
public void onStateChanged(WirelessTransceiverBlockEntity host, IGridNode node, State state) {
// 可在此响应 POWER/CHANNEL 等变化刷新显示等
}
@Override
public void onInWorldConnectionChanged(WirelessTransceiverBlockEntity host, IGridNode node) {}
@Override
public void onGridChanged(WirelessTransceiverBlockEntity host, IGridNode node) {}
@Override
public void onOwnerChanged(WirelessTransceiverBlockEntity host, IGridNode node) {}
}
}

View File

@ -0,0 +1,27 @@
package com.extendedae_plus.hooks;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 复刻 Fabric 的内置模型注册能力 AE2/MAE2 相同实现
*/
public final class BuiltInModelHooks {
private static final Map<ResourceLocation, UnbakedModel> BUILTIN_MODELS = new ConcurrentHashMap<>();
private BuiltInModelHooks() {}
public static void addBuiltInModel(ResourceLocation id, UnbakedModel model) {
var prev = BUILTIN_MODELS.putIfAbsent(id, model);
if (prev != null) {
throw new IllegalStateException("Duplicate built-in model ID: " + id);
}
}
public static UnbakedModel getBuiltInModel(ResourceLocation id) {
return BUILTIN_MODELS.get(id);
}
}

View File

@ -0,0 +1,104 @@
package com.extendedae_plus.hooks;
import appeng.util.InteractionUtil;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.content.wireless.WirelessTransceiverBlockEntity;
import appeng.block.crafting.CraftingUnitBlock;
import appeng.blockentity.crafting.CraftingBlockEntity;
import net.minecraft.network.chat.Component;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.sounds.SoundSource;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.entity.player.PlayerInteractEvent;
@EventBusSubscriber(modid = ExtendedAEPlus.MODID)
public final class WrenchHook {
private WrenchHook() {}
@SubscribeEvent
public static void onPlayerUseBlockEvent(PlayerInteractEvent.RightClickBlock event) {
if (event.getUseBlock() == Event.Result.DENY) {
return;
}
var player = event.getEntity();
var level = event.getLevel();
var hand = event.getHand();
var hit = event.getHitVec();
// 仅主手非旁观者
if (player.isSpectator() || hand != InteractionHand.MAIN_HAND) {
return;
}
ItemStack stack = player.getItemInHand(hand);
// 潜行且为扳手拆解
if (InteractionUtil.isInAlternateUseMode(player) && InteractionUtil.canWrenchDisassemble(stack)) {
BlockEntity be = level.getBlockEntity(hit.getBlockPos());
if (be instanceof WirelessTransceiverBlockEntity te) {
var pos = hit.getBlockPos();
BlockState state = level.getBlockState(pos);
var block = state.getBlock();
if (!level.isClientSide) {
var drops = Block.getDrops(state, (net.minecraft.server.level.ServerLevel) level, pos, te, player, stack);
for (var item : drops) {
player.getInventory().placeItemBackInInventory(item);
}
}
level.playSound(player, hit.getBlockPos(), SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 0.7F, 1.0F);
block.playerWillDestroy(level, hit.getBlockPos(), state, player);
level.removeBlock(hit.getBlockPos(), false);
block.destroy(level, hit.getBlockPos(), state);
event.setCanceled(true);
event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide));
}
// AE2 并行处理器系列CraftingUnitBlock潜行扳手拆除直接入背包
else {
var pos = hit.getBlockPos();
BlockState state = level.getBlockState(pos);
if (state.getBlock() instanceof CraftingUnitBlock) {
if (!level.isClientSide) {
var drops = Block.getDrops(state, (net.minecraft.server.level.ServerLevel) level, pos, level.getBlockEntity(pos), player, stack);
for (var item : drops) {
player.getInventory().placeItemBackInInventory(item);
}
}
level.playSound(player, hit.getBlockPos(), SoundEvents.ITEM_FRAME_REMOVE_ITEM, SoundSource.BLOCKS, 0.7F, 1.0F);
state.getBlock().playerWillDestroy(level, pos, state, player);
level.removeBlock(pos, false);
state.getBlock().destroy(level, pos, state);
event.setCanceled(true);
event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide));
}
}
} else if (!InteractionUtil.isInAlternateUseMode(player) && InteractionUtil.canWrenchRotate(stack)) {
// 未潜行 + 扳手切换锁定状态
BlockEntity be = level.getBlockEntity(hit.getBlockPos());
if (be instanceof WirelessTransceiverBlockEntity te) {
boolean newLocked = !te.isLocked();
te.setLocked(newLocked);
// 提示玩家
player.displayClientMessage(Component.literal(newLocked ? "已锁定收发器" : "已解锁收发器"), true);
// 轻微反馈音效
level.playSound(player, hit.getBlockPos(), SoundEvents.LEVER_CLICK, SoundSource.BLOCKS, 0.5F, newLocked ? 0.6F : 0.9F);
event.setCanceled(true);
event.setCancellationResult(InteractionResult.sidedSuccess(level.isClientSide));
}
}
}
}

View File

@ -0,0 +1,26 @@
package com.extendedae_plus.init;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.content.wireless.WirelessTransceiverBlockEntity;
import com.extendedae_plus.content.controller.NetworkPatternControllerBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.core.registries.Registries;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
public final class ModBlockEntities {
private ModBlockEntities() {}
public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITY_TYPES =
DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, ExtendedAEPlus.MODID);
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<WirelessTransceiverBlockEntity>> WIRELESS_TRANSCEIVER_BE =
BLOCK_ENTITY_TYPES.register("wireless_transceiver",
() -> BlockEntityType.Builder.of(WirelessTransceiverBlockEntity::new,
ModBlocks.WIRELESS_TRANSCEIVER.get()).build(null));
public static final DeferredHolder<BlockEntityType<?>, BlockEntityType<NetworkPatternControllerBlockEntity>> NETWORK_PATTERN_CONTROLLER_BE =
BLOCK_ENTITY_TYPES.register("network_pattern_controller",
() -> BlockEntityType.Builder.of(NetworkPatternControllerBlockEntity::new,
ModBlocks.NETWORK_PATTERN_CONTROLLER.get()).build(null));
}

View File

@ -0,0 +1,86 @@
package com.extendedae_plus.init;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.content.wireless.WirelessTransceiverBlock;
import com.extendedae_plus.content.crafting.EPlusCraftingUnitType;
import appeng.block.crafting.CraftingUnitBlock;
import appeng.blockentity.crafting.CraftingBlockEntity;
import appeng.core.definitions.AEBlockEntities;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockBehaviour;
import net.minecraft.world.level.material.MapColor;
import net.neoforged.neoforge.registries.DeferredBlock;
import net.neoforged.neoforge.registries.DeferredRegister;
public final class ModBlocks {
private ModBlocks() {}
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(ExtendedAEPlus.MODID);
public static final DeferredBlock<Block> WIRELESS_TRANSCEIVER = BLOCKS.register(
"wireless_transceiver",
() -> new WirelessTransceiverBlock(
BlockBehaviour.Properties.of()
.mapColor(MapColor.METAL)
.strength(1.5F, 6.0F)
.requiresCorrectToolForDrops()
)
);
// AE2 网络模式控制器方块
public static final DeferredBlock<Block> NETWORK_PATTERN_CONTROLLER = BLOCKS.register(
"network_pattern_controller",
() -> new com.extendedae_plus.content.controller.NetworkPatternControllerBlock(
BlockBehaviour.Properties.of()
.mapColor(MapColor.METAL)
.strength(1.5F, 6.0F)
.requiresCorrectToolForDrops()
)
);
// Crafting Accelerators (reuse MAE2 textures/models)
public static final DeferredBlock<CraftingUnitBlock> ACCELERATOR_4x = BLOCKS.register(
"4x_crafting_accelerator",
() -> {
var b = new CraftingUnitBlock(EPlusCraftingUnitType.ACCELERATOR_4x);
b.setBlockEntity(CraftingBlockEntity.class, AEBlockEntities.CRAFTING_UNIT, null, null);
return b;
}
);
public static final DeferredBlock<CraftingUnitBlock> ACCELERATOR_16x = BLOCKS.register(
"16x_crafting_accelerator",
() -> {
var b = new CraftingUnitBlock(EPlusCraftingUnitType.ACCELERATOR_16x);
b.setBlockEntity(CraftingBlockEntity.class, AEBlockEntities.CRAFTING_UNIT, null, null);
return b;
}
);
public static final DeferredBlock<CraftingUnitBlock> ACCELERATOR_64x = BLOCKS.register(
"64x_crafting_accelerator",
() -> {
var b = new CraftingUnitBlock(EPlusCraftingUnitType.ACCELERATOR_64x);
b.setBlockEntity(CraftingBlockEntity.class, AEBlockEntities.CRAFTING_UNIT, null, null);
return b;
}
);
public static final DeferredBlock<CraftingUnitBlock> ACCELERATOR_256x = BLOCKS.register(
"256x_crafting_accelerator",
() -> {
var b = new CraftingUnitBlock(EPlusCraftingUnitType.ACCELERATOR_256x);
b.setBlockEntity(CraftingBlockEntity.class, AEBlockEntities.CRAFTING_UNIT, null, null);
return b;
}
);
public static final DeferredBlock<CraftingUnitBlock> ACCELERATOR_1024x = BLOCKS.register(
"1024x_crafting_accelerator",
() -> {
var b = new CraftingUnitBlock(EPlusCraftingUnitType.ACCELERATOR_1024x);
b.setBlockEntity(CraftingBlockEntity.class, AEBlockEntities.CRAFTING_UNIT, null, null);
return b;
}
);
}

View File

@ -0,0 +1,31 @@
package com.extendedae_plus.init;
import com.extendedae_plus.ExtendedAEPlus;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.CreativeModeTab;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
public final class ModCreativeTabs {
private ModCreativeTabs() {}
public static final DeferredRegister<CreativeModeTab> TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, ExtendedAEPlus.MODID);
public static final DeferredHolder<CreativeModeTab, CreativeModeTab> MAIN = TABS.register("main",
() -> CreativeModeTab.builder()
.title(Component.translatable("itemGroup." + ExtendedAEPlus.MODID + ".main"))
.icon(() -> ModItems.WIRELESS_TRANSCEIVER.get().getDefaultInstance())
.displayItems((params, output) -> {
// 将本模组物品加入创造物品栏
output.accept(ModItems.WIRELESS_TRANSCEIVER.get());
output.accept(ModItems.NETWORK_PATTERN_CONTROLLER.get());
output.accept(ModItems.ACCELERATOR_4x.get());
output.accept(ModItems.ACCELERATOR_16x.get());
output.accept(ModItems.ACCELERATOR_64x.get());
output.accept(ModItems.ACCELERATOR_256x.get());
output.accept(ModItems.ACCELERATOR_1024x.get());
})
.build());
}

View File

@ -0,0 +1,49 @@
package com.extendedae_plus.init;
import com.extendedae_plus.ExtendedAEPlus;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.neoforged.neoforge.registries.DeferredItem;
import net.neoforged.neoforge.registries.DeferredRegister;
public final class ModItems {
private ModItems() {}
public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(ExtendedAEPlus.MODID);
public static final DeferredItem<Item> WIRELESS_TRANSCEIVER = ITEMS.register(
"wireless_transceiver",
() -> new BlockItem(ModBlocks.WIRELESS_TRANSCEIVER.get(), new Item.Properties())
);
public static final DeferredItem<Item> NETWORK_PATTERN_CONTROLLER = ITEMS.register(
"network_pattern_controller",
() -> new BlockItem(ModBlocks.NETWORK_PATTERN_CONTROLLER.get(), new Item.Properties())
);
// Crafting Accelerators
public static final DeferredItem<Item> ACCELERATOR_4x = ITEMS.register(
"4x_crafting_accelerator",
() -> new BlockItem(ModBlocks.ACCELERATOR_4x.get(), new Item.Properties())
);
public static final DeferredItem<Item> ACCELERATOR_16x = ITEMS.register(
"16x_crafting_accelerator",
() -> new BlockItem(ModBlocks.ACCELERATOR_16x.get(), new Item.Properties())
);
public static final DeferredItem<Item> ACCELERATOR_64x = ITEMS.register(
"64x_crafting_accelerator",
() -> new BlockItem(ModBlocks.ACCELERATOR_64x.get(), new Item.Properties())
);
public static final DeferredItem<Item> ACCELERATOR_256x = ITEMS.register(
"256x_crafting_accelerator",
() -> new BlockItem(ModBlocks.ACCELERATOR_256x.get(), new Item.Properties())
);
public static final DeferredItem<Item> ACCELERATOR_1024x = ITEMS.register(
"1024x_crafting_accelerator",
() -> new BlockItem(ModBlocks.ACCELERATOR_1024x.get(), new Item.Properties())
);
}

View File

@ -0,0 +1,19 @@
package com.extendedae_plus.init;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.menu.NetworkPatternControllerMenu;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.core.registries.Registries;
import net.neoforged.neoforge.registries.DeferredHolder;
import net.neoforged.neoforge.registries.DeferredRegister;
public final class ModMenuTypes {
private ModMenuTypes() {}
public static final DeferredRegister<MenuType<?>> MENUS =
DeferredRegister.create(Registries.MENU, ExtendedAEPlus.MODID);
public static final DeferredHolder<MenuType<?>, MenuType<NetworkPatternControllerMenu>> NETWORK_PATTERN_CONTROLLER =
MENUS.register("network_pattern_controller",
() -> new MenuType<>(NetworkPatternControllerMenu::new));
}

View File

@ -0,0 +1,26 @@
package com.extendedae_plus.integration.jade;
import com.extendedae_plus.content.wireless.WirelessTransceiverBlock;
import com.extendedae_plus.content.wireless.WirelessTransceiverBlockEntity;
import snownee.jade.api.IWailaClientRegistration;
import snownee.jade.api.IWailaCommonRegistration;
import snownee.jade.api.IWailaPlugin;
import snownee.jade.api.WailaPlugin;
@WailaPlugin("extendedae_plus") // 你的 mod ID
public class WirelessTransceiverJadePlugin implements IWailaPlugin {
@Override
public void register(IWailaCommonRegistration registration) {
// 注册服务端数据提供者用于同步数据
registration.registerBlockDataProvider(WirelessTransceiverProvider.INSTANCE, WirelessTransceiverBlockEntity.class);
}
@Override
public void registerClient(IWailaClientRegistration registration) {
// 遍历组件常量逐一注册
for (var component : WirelessTransceiverJadePluginComponents.values()) {
registration.registerBlockComponent(component, WirelessTransceiverBlock.class);
}
}
}

View File

@ -0,0 +1,87 @@
package com.extendedae_plus.integration.jade;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IBlockComponentProvider;
import snownee.jade.api.ITooltip;
import snownee.jade.api.config.IPluginConfig;
/**
* 单文件聚合的 Jade 组件提供者包含五个子组件常量分别对应五个独立的开关/UID
*/
public enum WirelessTransceiverJadePluginComponents implements IBlockComponentProvider {
FREQUENCY("wt_frequency") {
@Override
protected void add(BlockAccessor accessor, ITooltip tooltip, IPluginConfig config, CompoundTag data) {
if (data.contains("frequency")) {
long frequency = data.getLong("frequency");
tooltip.add(Component.translatable("extendedae_plus.tooltip.frequency", frequency));
}
}
},
MODE("wt_master_mode") {
@Override
protected void add(BlockAccessor accessor, ITooltip tooltip, IPluginConfig config, CompoundTag data) {
if (data.contains("masterMode")) {
boolean masterMode = data.getBoolean("masterMode");
tooltip.add(Component.translatable("extendedae_plus.tooltip.master_mode", masterMode ? "主模式" : "从模式"));
}
}
},
MASTER_LOCATION("wt_master_location") {
@Override
protected void add(BlockAccessor accessor, ITooltip tooltip, IPluginConfig config, CompoundTag data) {
if (data.contains("masterMode") && !data.getBoolean("masterMode") && data.contains("masterPos")) {
BlockPos pos = BlockPos.of(data.getLong("masterPos"));
String dim = data.contains("masterDim") ? data.getString("masterDim") : "";
tooltip.add(Component.literal("主节点位置: (" + pos.getX() + ", " + pos.getY() + ", " + pos.getZ() + ")"));
if (!dim.isEmpty()) {
tooltip.add(Component.literal("维度: " + dim));
}
}
}
},
LOCKED("wt_locked") {
@Override
protected void add(BlockAccessor accessor, ITooltip tooltip, IPluginConfig config, CompoundTag data) {
if (data.contains("locked")) {
boolean locked = data.getBoolean("locked");
tooltip.add(Component.translatable("extendedae_plus.tooltip.locked", locked ? "已锁定" : "未锁定"));
}
}
},
NETWORK_USABLE("wt_network_usable") {
@Override
protected void add(BlockAccessor accessor, ITooltip tooltip, IPluginConfig config, CompoundTag data) {
if (data.contains("networkUsable")) {
boolean usable = data.getBoolean("networkUsable");
tooltip.add(Component.literal((usable ? "设备在线" : "设备离线")));
}
}
};
private final ResourceLocation uid;
WirelessTransceiverJadePluginComponents(String path) {
this.uid = new ResourceLocation("extendedae_plus", path);
}
@Override
public ResourceLocation getUid() {
return uid;
}
@Override
public final void appendTooltip(ITooltip tooltip, BlockAccessor accessor, IPluginConfig config) {
CompoundTag data = accessor.getServerData();
if (data == null) return;
add(accessor, tooltip, config, data);
}
protected abstract void add(BlockAccessor accessor, ITooltip tooltip, IPluginConfig config, CompoundTag data);
}

View File

@ -0,0 +1,60 @@
package com.extendedae_plus.integration.jade;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNode;
import com.extendedae_plus.content.wireless.WirelessTransceiverBlockEntity;
import com.extendedae_plus.wireless.IWirelessEndpoint;
import com.extendedae_plus.wireless.WirelessMasterRegistry;
import net.minecraft.core.BlockPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import snownee.jade.api.BlockAccessor;
import snownee.jade.api.IServerDataProvider;
public enum WirelessTransceiverProvider implements IServerDataProvider<BlockAccessor> {
INSTANCE;
private static final ResourceLocation UID = new ResourceLocation("extendedae_plus", "wireless_transceiver_info");
// 此类仅用于同步服务端数据不再包含客户端选项键
@Override
public ResourceLocation getUid() {
return UID;
}
@Override
public void appendServerData(CompoundTag data, BlockAccessor accessor) {
if (accessor.getBlockEntity() instanceof WirelessTransceiverBlockEntity blockEntity) {
data.putLong("frequency", blockEntity.getFrequency());
data.putBoolean("masterMode", blockEntity.isMasterMode());
data.putBoolean("locked", blockEntity.isLocked());
// 判断 AE 网络是否可用节点存在加入网路且网络通电
IGridNode node = blockEntity.getGridNode();
IGrid grid = node == null ? null : node.getGrid();
boolean networkUsable = false;
if (grid != null) {
try {
networkUsable = grid.getEnergyService().isNetworkPowered();
} catch (Throwable ignored) {
networkUsable = false;
}
}
data.putBoolean("networkUsable", networkUsable);
// 如果是从模式查询主节点位置与维度
if (!blockEntity.isMasterMode()) {
var level = blockEntity.getServerLevel();
long freq = blockEntity.getFrequency();
IWirelessEndpoint master = WirelessMasterRegistry.get(level, freq);
if (master != null && !master.isEndpointRemoved()) {
BlockPos pos = master.getBlockPos();
if (pos != null) {
data.putLong("masterPos", pos.asLong());
}
if (master.getServerLevel() != null) {
data.putString("masterDim", master.getServerLevel().dimension().location().toString());
}
}
}
}
}
}

View File

@ -0,0 +1,22 @@
package com.extendedae_plus.integration.jei;
import com.extendedae_plus.ExtendedAEPlus;
import mezz.jei.api.IModPlugin;
import mezz.jei.api.JeiPlugin;
import mezz.jei.api.runtime.IJeiRuntime;
import net.minecraft.resources.ResourceLocation;
@JeiPlugin
public class ExtendedAEJeiPlugin implements IModPlugin {
private static final ResourceLocation UID = new ResourceLocation(ExtendedAEPlus.MODID, "jei_plugin");
@Override
public ResourceLocation getPluginUid() {
return UID;
}
@Override
public void onRuntimeAvailable(IJeiRuntime jeiRuntime) {
JeiRuntimeProxy.setRuntime(jeiRuntime);
}
}

View File

@ -0,0 +1,135 @@
package com.extendedae_plus.integration.jei;
import com.extendedae_plus.mixin.jei.accessor.BookmarkOverlayAccessor;
import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.ingredients.ITypedIngredient;
import mezz.jei.api.runtime.IBookmarkOverlay;
import mezz.jei.api.runtime.IIngredientListOverlay;
import mezz.jei.api.runtime.IJeiRuntime;
import mezz.jei.gui.bookmarks.BookmarkList;
import mezz.jei.gui.overlay.elements.IElement;
import javax.annotation.Nullable;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
/**
* 线程安全地缓存并访问 JEI Runtime
*/
public final class JeiRuntimeProxy {
private static volatile IJeiRuntime RUNTIME;
private JeiRuntimeProxy() {}
static void setRuntime(IJeiRuntime runtime) {
RUNTIME = runtime;
}
@Nullable
public static IJeiRuntime get() {
return RUNTIME;
}
public static Optional<ITypedIngredient<?>> getIngredientUnderMouse() {
IJeiRuntime rt = RUNTIME;
if (rt == null) return Optional.empty();
IIngredientListOverlay list = rt.getIngredientListOverlay();
if (list != null) {
var ing = list.getIngredientUnderMouse();
if (ing.isPresent()) return ing.map(i -> (ITypedIngredient<?>) i);
}
IBookmarkOverlay bm = rt.getBookmarkOverlay();
if (bm != null) {
var ing = bm.getIngredientUnderMouse();
if (ing.isPresent()) return ing.map(i -> (ITypedIngredient<?>) i);
}
return Optional.empty();
}
/**
* JEI 配方界面区域内基于屏幕坐标查询鼠标下的配料优先物品其次流体
*/
public static Optional<ITypedIngredient<?>> getIngredientUnderMouse(double mouseX, double mouseY) {
IJeiRuntime rt = RUNTIME;
if (rt == null || rt.getRecipesGui() == null) return Optional.empty();
var ingredientManager = rt.getIngredientManager();
// 支持物品通用且所有版本可用如需流体可后续按版本判断再扩展
var item = rt.getRecipesGui().getIngredientUnderMouse(VanillaTypes.ITEM_STACK)
.flatMap(v -> ingredientManager.createTypedIngredient(VanillaTypes.ITEM_STACK, v))
.map(x -> (ITypedIngredient<?>) x);
if (item.isPresent()) return Optional.of(item.get());
return Optional.empty();
}
/**
* 检测 JEI 是否开启了作弊模式给物品
* 使用 JEI 内部开关 JEI 未初始化或异常则返回 false
*/
public static boolean isJeiCheatModeEnabled() {
try {
// 使用完全限定名以避免在源码缺失时的编译依赖问题
return mezz.jei.common.Internal.getClientToggleState().isCheatItemsEnabled();
} catch (Throwable t) {
return false;
}
}
/**
* 将文本写入 JEI 的搜索过滤框
* JEI runtime 不可用则静默返回
*/
public static void setIngredientFilterText(String text) {
IJeiRuntime rt = RUNTIME;
if (rt == null) return;
try {
rt.getIngredientFilter().setFilterText(text == null ? "" : text);
} catch (Throwable ignored) {
// 兼容不同 JEI 版本或在启动阶段尚未就绪
}
}
/**
* 通用获取 JEI 悬浮配料的本地化显示名称适配物品/流体等
* 若无法安全获取则返回空字符串
*/
public static <T> String getTypedIngredientDisplayName(ITypedIngredient<T> typed) {
IJeiRuntime rt = RUNTIME;
if (rt == null || typed == null) return "";
try {
var manager = rt.getIngredientManager();
var helper = manager.getIngredientHelper(typed.getType());
// JEI IIngredientHelper#getDisplayName 返回 Component新版本 String旧版本
// 统一转为字符串使用 toString() 兜底
Object display = helper.getDisplayName(typed.getIngredient());
if (display == null) return "";
// 新版net.minecraft.network.chat.Component
if (display instanceof net.minecraft.network.chat.Component comp) {
String s = comp.getString();
return s == null ? "" : s;
}
String s = display.toString();
return s == null ? "" : s;
} catch (Throwable ignored) {
}
return "";
}
/**
* 获取JEI书签列表
*/
public static List<? extends ITypedIngredient<?>> getBookmarkList() {
IJeiRuntime rt = RUNTIME;
if (rt == null) return Collections.emptyList();
IBookmarkOverlay bookmarkOverlay = rt.getBookmarkOverlay();
if (bookmarkOverlay instanceof BookmarkOverlayAccessor accessor) {
BookmarkList bookmarkList = accessor.eap$getBookmarkList();
return bookmarkList.getElements().stream().map(IElement::getTypedIngredient).toList();
}
return Collections.emptyList();
}
}

View File

@ -0,0 +1,33 @@
package com.extendedae_plus.menu;
import com.extendedae_plus.init.ModMenuTypes;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
public class NetworkPatternControllerMenu extends AbstractContainerMenu {
private final BlockPos bePos;
public NetworkPatternControllerMenu(int id, Inventory inv, BlockPos bePos) {
super(ModMenuTypes.NETWORK_PATTERN_CONTROLLER.get(), id);
this.bePos = bePos;
}
public NetworkPatternControllerMenu(int id, Inventory inv, FriendlyByteBuf buf) {
this(id, inv, buf.readBlockPos());
}
public BlockPos getBlockEntityPos() { return bePos; }
@Override
public boolean stillValid(Player player) { return true; }
@Override
public ItemStack quickMoveStack(Player player, int index) {
// 无物品槽的容器直接返回空堆以禁用快速转移
return ItemStack.EMPTY;
}
}

View File

@ -0,0 +1,57 @@
package com.extendedae_plus.menu.host;
import appeng.menu.ISubMenu;
import de.mari_023.ae2wtlib.terminal.WTMenuHost;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler;
import java.util.function.BiConsumer;
/**
* 针对 Curios 槽位的 ae2wtlib WTMenuHost 适配器
* - 复用 wtlib 的量子卡跨维/跨距逻辑rangeCheck/isQuantumLinked
* - 覆写槽位校验与回写改为使用 Curios 实际槽位避免 wtlib Trinkets 平台判断失效
*/
public class CuriosWTMenuHost extends WTMenuHost {
private final ICurioStacksHandler curiosHandler;
private final int curiosIndex;
public CuriosWTMenuHost(Player player,
@Nullable Integer inventorySlot,
ItemStack is,
ICurioStacksHandler curiosHandler,
int curiosIndex,
BiConsumer<Player, ISubMenu> returnToMainMenu) {
super(player, inventorySlot, is, returnToMainMenu);
this.curiosHandler = curiosHandler;
this.curiosIndex = curiosIndex;
// 初始化内部库存含奇点槽以便量子桥判定能够读取到频率等 NBT
try {
super.readFromNbt();
} catch (Throwable ignored) {
}
}
@Override
protected boolean ensureItemStillInSlot() {
try {
ItemStack cur = curiosHandler.getStacks().getStackInSlot(curiosIndex);
return !cur.isEmpty();
} catch (Throwable ignored) {
return false;
}
}
@Override
public boolean onBroadcastChanges(AbstractContainerMenu menu) {
try {
ItemStack current = getItemStack();
curiosHandler.getStacks().setStackInSlot(curiosIndex, current);
} catch (Throwable ignored) {
}
return super.onBroadcastChanges(menu);
}
}

View File

@ -0,0 +1,40 @@
package com.extendedae_plus.menu.host;
import appeng.api.storage.ISubMenuHost;
import appeng.helpers.WirelessTerminalMenuHost;
import appeng.menu.ISubMenu;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.inventory.AbstractContainerMenu;
import net.minecraft.world.item.ItemStack;
import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler;
/**
* 针对 Curios 槽位的无线终端菜单宿主
* 关键点 onBroadcastChanges 周期性把 getItemStack() 回写到 Curios 槽位
* 以持久化能量消耗等 NBT 变化
*/
public class CuriosWirelessTerminalMenuHost extends WirelessTerminalMenuHost implements ISubMenuHost {
private final ICurioStacksHandler curiosHandler;
private final int curiosIndex;
public CuriosWirelessTerminalMenuHost(Player player,
ItemStack itemStack,
ICurioStacksHandler curiosHandler,
int curiosIndex,
java.util.function.BiConsumer<Player, ISubMenu> returnToMainMenu) {
super(player, null, itemStack, returnToMainMenu);
this.curiosHandler = curiosHandler;
this.curiosIndex = curiosIndex;
}
@Override
public boolean onBroadcastChanges(AbstractContainerMenu menu) {
// 将当前 ItemStack 写回 Curios 槽位保证 NBT 改动如耗电持久化
try {
ItemStack current = getItemStack();
curiosHandler.getStacks().setStackInSlot(curiosIndex, current);
} catch (Throwable ignored) {
}
return super.onBroadcastChanges(menu);
}
}

View File

@ -0,0 +1,93 @@
package com.extendedae_plus.menu.locator;
import appeng.api.implementations.menuobjects.IMenuItem;
import appeng.api.implementations.menuobjects.ItemMenuHost;
import appeng.helpers.WirelessTerminalMenuHost;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.menu.MenuOpener;
import appeng.menu.locator.MenuLocator;
import appeng.menu.me.common.MEStorageMenu;
import com.extendedae_plus.menu.host.CuriosWTMenuHost;
import com.extendedae_plus.menu.host.CuriosWirelessTerminalMenuHost;
import de.mari_023.ae2wtlib.terminal.WTMenuHost;
import de.mari_023.ae2wtlib.wut.WTDefinition;
import de.mari_023.ae2wtlib.wut.WUTHandler;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import org.jetbrains.annotations.Nullable;
import top.theillusivec4.curios.api.CuriosApi;
import top.theillusivec4.curios.api.type.inventory.ICurioStacksHandler;
/**
* 适配 Curios 槽位的自定义 MenuLocator
* 通过 slotId + index 在两端查找 Curios 实际物品引用确保 NBT 变化如耗电能持久化
*/
public record CuriosItemLocator(String slotId, int index) implements MenuLocator {
@Override
@Nullable
public <T> T locate(Player player, Class<T> hostInterface) {
try {
var resolved = CuriosApi.getCuriosInventory(player).resolve();
if (resolved.isPresent()) {
var handler = resolved.get();
ICurioStacksHandler stacksHandler = handler.getCurios().get(slotId);
if (stacksHandler != null) {
ItemStack it = stacksHandler.getStacks().getStackInSlot(index);
if (!it.isEmpty()) {
// 1) ae2wtlib: 优先构造 WTMenuHost 以启用量子卡的跨维/跨距逻辑
String current = WUTHandler.getCurrentTerminal(it);
WTDefinition def = WUTHandler.wirelessTerminals.get(current);
if (def != null) {
WTMenuHost wtHost = new CuriosWTMenuHost(
player,
null,
it,
stacksHandler,
index,
(p, sub) -> MenuOpener.open(MEStorageMenu.WIRELESS_TYPE, p, this)
);
if (hostInterface.isInstance(wtHost)) {
return hostInterface.cast(wtHost);
}
}
// 2) 回退AE2 原生无线终端
if (it.getItem() instanceof WirelessTerminalItem) {
// 首选 CraftAmountMenu 等需要网络/能量上下文的菜单提供 WirelessTerminalMenuHost
WirelessTerminalMenuHost host = new CuriosWirelessTerminalMenuHost(
player,
it,
stacksHandler,
index,
(p, sub) -> MenuOpener.open(MEStorageMenu.WIRELESS_TYPE, p, this)
);
if (hostInterface.isInstance(host)) {
return hostInterface.cast(host);
}
} else if (it.getItem() instanceof IMenuItem guiItem) {
// 回退非无线终端按常规 IMenuItem 处理
ItemMenuHost menuHost = guiItem.getMenuHost(player, -1, it, null);
if (hostInterface.isInstance(menuHost)) {
return hostInterface.cast(menuHost);
}
}
}
}
}
} catch (Throwable ignored) {
}
return null;
}
public void writeToPacket(FriendlyByteBuf buf) {
buf.writeUtf(slotId);
buf.writeVarInt(index);
}
public static CuriosItemLocator readFromPacket(FriendlyByteBuf buf) {
String slotId = buf.readUtf();
int index = buf.readVarInt();
return new CuriosItemLocator(slotId, index);
}
}

View File

@ -0,0 +1,69 @@
package com.extendedae_plus.mixin;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.PickFromWirelessC2SPacket;
import net.minecraft.client.Minecraft;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.GameType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
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;
// no client-side WCT gating; server will check presence (including Curios)
@Mixin(Minecraft.class)
public class PickFromWirelessMixin {
@Shadow public LocalPlayer player;
@Shadow public HitResult hitResult;
@Inject(method = "pickBlock", at = @At("HEAD"), cancellable = true)
private void eap$pickFromAeWireless(CallbackInfo ci) {
if (this.player == null || this.hitResult == null || this.hitResult.getType() != HitResult.Type.BLOCK) {
return;
}
// 仅生存模式
GameType type = Minecraft.getInstance().gameMode != null ? Minecraft.getInstance().gameMode.getPlayerMode() : null;
if (type == null || type.isCreative()) {
return;
}
// 若背包已有该物品让原版逻辑处理将该物品切换到主手
BlockHitResult bhr = (BlockHitResult) this.hitResult;
var level = Minecraft.getInstance().level;
if (level != null) {
try {
BlockState state = level.getBlockState(bhr.getBlockPos());
if (state != null && !state.isAir()) {
ItemStack picked = state.getBlock().getCloneItemStack(state, bhr, level, bhr.getBlockPos(), this.player);
if (picked.isEmpty()) {
picked = state.getBlock().asItem().getDefaultInstance();
}
if (!picked.isEmpty()) {
// 若主手已拿同一物品含标签则仍然走 AE 拉取逻辑进行补充/合并
if (!ItemStack.isSameItemSameTags(picked, this.player.getMainHandItem())) {
int slot = this.player.getInventory().findSlotMatchingItem(picked);
if (slot != -1) {
return; // 交给原版 pickBlock 处理
}
}
}
}
} catch (Throwable t) {
// 若其它模组导致 getCloneItemStack 出异常放弃拦截保持原版行为确保健壮性
return;
}
}
// 不在客户端检查是否持有无线合成终端由服务端权威校验 Curios 支持以避免整合包环境下的软依赖与槽位问题
// 背包没有发送到服务端处理 AE2 网络拉取并拦截原版
Vec3 loc = bhr.getLocation();
ModNetwork.CHANNEL.sendToServer(new PickFromWirelessC2SPacket(bhr.getBlockPos(), bhr.getDirection(), loc));
ci.cancel();
}
}

View File

@ -0,0 +1,14 @@
package com.extendedae_plus.mixin.accessor;
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.gen.Accessor;
@Mixin(AbstractContainerScreen.class)
public interface AbstractContainerScreenAccessor<T extends AbstractContainerMenu> {
@Accessor("leftPos") int eap$getLeftPos();
@Accessor("topPos") int eap$getTopPos();
@Accessor("imageWidth") int eap$getImageWidth();
@Accessor("imageHeight") int eap$getImageHeight();
}

View File

@ -0,0 +1,18 @@
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.screens.Screen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.List;
@Mixin(Screen.class)
public interface ScreenAccessor {
@Accessor("renderables")
List<Renderable> eap$getRenderables();
@Accessor("children")
List<GuiEventListener> eap$getChildren();
}

View File

@ -0,0 +1,22 @@
package com.extendedae_plus.mixin.ae2;
import appeng.crafting.pattern.AEProcessingPattern;
import com.extendedae_plus.api.SmartDoublingAwarePattern;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@Mixin(value = AEProcessingPattern.class, remap = false)
public class AEProcessingPatternMixin implements SmartDoublingAwarePattern {
@Unique
private boolean eap$allowScaling = false; // 默认不允许缩放
@Override
public boolean eap$allowScaling() {
return eap$allowScaling;
}
@Override
public void eap$setAllowScaling(boolean allow) {
this.eap$allowScaling = allow;
}
}

View File

@ -0,0 +1,36 @@
package com.extendedae_plus.mixin.ae2;
import appeng.blockentity.crafting.CraftingBlockEntity;
import appeng.me.cluster.implementations.CraftingCPUCluster;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Constant;
import org.spongepowered.asm.mixin.injection.ModifyConstant;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = CraftingCPUCluster.class, remap = false, priority = 2000)
public abstract class CraftingCPUClusterMixin {
// 1) 提升单方块线程上限的常量避免抛出 IAE IllegalArgumentException
@ModifyConstant(
method = "addBlockEntity(Lappeng/blockentity/crafting/CraftingBlockEntity;)V",
constant = @Constant(intValue = 16)
)
private int extendedae_plus$raisePerUnitLimit(int original) {
// 放宽到极大值完全取消单方块 16 线程的硬限制
return Integer.MAX_VALUE;
}
// 2) 保持统计使用原始线程值若存在多处调用不再返回固定 16
@Redirect(
method = "addBlockEntity(Lappeng/blockentity/crafting/CraftingBlockEntity;)V",
at = @At(
value = "INVOKE",
target = "Lappeng/blockentity/crafting/CraftingBlockEntity;getAcceleratorThreads()I",
ordinal = 1
)
)
private int extendedae_plus$onGetThreadsForLimitCheck(CraftingBlockEntity te) {
// 返回原始线程数确保总并行单元不被错误下限
return te.getAcceleratorThreads();
}
}

View File

@ -0,0 +1,29 @@
package com.extendedae_plus.mixin.ae2;
import appeng.crafting.pattern.EncodedPatternItem;
import com.extendedae_plus.config.ModConfigs;
import net.minecraft.ChatFormatting;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.TooltipFlag;
import net.minecraft.world.level.Level;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
@Mixin(EncodedPatternItem.class)
public class EncodedPatternItemMixin {
// 客户端 HoverText 显示样板的编码玩家
@Inject(method = "appendHoverText", at = @At("TAIL"))
public void epp$appendHoverText(ItemStack stack, Level level, List<Component> lines, TooltipFlag advancedTooltips, CallbackInfo ci){
if (stack.hasTag() && ModConfigs.SHOW_ENCOD_PATTERN_PLAYER.get()) {
CompoundTag tag = stack.getOrCreateTag();
String name = tag.getString("encodePlayer");
lines.add(Component.translatable("extendedae_plus.pattern.hovertext.player", name).withStyle(ChatFormatting.GRAY));
}
}
}

View File

@ -0,0 +1,294 @@
package com.extendedae_plus.mixin.ae2;
import appeng.api.parts.IPartHost;
import appeng.api.parts.SelectedPart;
import appeng.items.tools.quartz.QuartzCuttingKnifeItem;
import com.mojang.blaze3d.platform.Window;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.language.I18n;
import net.minecraft.core.BlockPos;
import net.minecraft.network.chat.Component;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.InteractionResultHolder;
import net.minecraft.world.Nameable;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.context.UseOnContext;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.fml.ModList;
import org.lwjgl.glfw.GLFW;
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.CallbackInfoReturnable;
import java.awt.*;
import java.awt.datatransfer.StringSelection;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
/**
* AE2 石英切割刀添加潜行 + 右键 指向世界中的方块/部件如线缆复制其名称到剪贴板
* <p>
* 设计要点参考原类 QuartzCuttingKnifeItem 的写法
* - 原本右键会打开 QuartzKnifeMenu我们在客户端潜行且命中方块时优先拦截并复制名称
* - 名称优先取方块实体的自定义名若实现 Nameable否则使用方块显示名
* - 仅在客户端执行剪贴板操作避免在服务端加载客户端类使用 level.isClientSide() 的分支内访问 Minecraft
*/
@Mixin(value = QuartzCuttingKnifeItem.class)
public abstract class QuartzCuttingKnifeItemMixin {
/**
* 清理方块名称移除分节符号和其他格式字符
*/
@Unique
private String eap$cleanBlockName(String name) {
if (name == null || name.isBlank()) {
return name;
}
// 移除 Minecraft 分节符号 (§) 及其后面的字符
name = name.replaceAll("§[0-9a-fk-or]", "");
// 移除多余的空白字符
name = name.trim();
// 移除常见的格式字符
name = name.replaceAll("[\\[\\](){}]", "");
return name;
}
@Inject(method = "use", at = @At("HEAD"), cancellable = true)
private void eap$copyNameOnShiftRightClick(Level level, Player player, InteractionHand hand,
CallbackInfoReturnable<InteractionResultHolder<ItemStack>> cir) {
if (!level.isClientSide()) {
return;
}
if (player == null || !player.isCrouching()) {
return;
}
// 仅在客户端分支访问 Minecraft 防止服务端类加载问题
Minecraft mc = Minecraft.getInstance();
HitResult hr = mc.hitResult;
if (!(hr instanceof BlockHitResult bhr)) {
return;
}
var pos = bhr.getBlockPos();
var state = level.getBlockState(pos);
if (state == null || state.isAir()) return;
// 获取方块名称
String name = eap$getBlockName(level, pos, hr.getLocation());
// 清理名称移除分节符号等格式字符
name = eap$cleanBlockName(name);
// 复制到剪贴板并反馈
boolean success = eap$tryCopyToClipboard(Minecraft.getInstance(), name);
player.displayClientMessage(Component.literal(success
? ("已复制方块/部件名: " + name)
: "复制失败:整合包可能限制剪贴板或未聚焦窗口"), true);
// 拦截默认行为不再打开刀具界面
ItemStack held = player.getItemInHand(hand);
cir.setReturnValue(new InteractionResultHolder<>(InteractionResult.SUCCESS, held));
}
@Inject(method = "useOn", at = @At("HEAD"), cancellable = true)
private void eap$copyNameOnShiftRightClickUseOn(UseOnContext context,
CallbackInfoReturnable<InteractionResult> cir) {
Level level = context.getLevel();
Player player = context.getPlayer();
if (!level.isClientSide() || player == null || !player.isCrouching()) {
return;
}
var pos = context.getClickedPos();
var state = level.getBlockState(pos);
if (state.isAir()) return;
// 获取方块名称
String name = eap$getBlockName(level, pos, context.getClickLocation());
// 清理名称移除分节符号等格式字符
name = eap$cleanBlockName(name);
// 复制到剪贴板并反馈
boolean success = eap$tryCopyToClipboard(Minecraft.getInstance(), name);
player.displayClientMessage(Component.literal(success
? ("已复制方块/部件名: " + name)
: "复制失败:整合包可能限制剪贴板或未聚焦窗口"), true);
// 拦截默认行为
cir.setReturnValue(InteractionResult.SUCCESS);
}
/**
* 获取方块或部件的名称优先级自定义名称 > AE2 部件 > GregTech 配方翻译 > 方块名称
*/
@Unique
private String eap$getBlockName(Level level, BlockPos pos, Vec3 clickLocation) {
BlockEntity blockEntity = level.getBlockEntity(pos);
BlockState state = level.getBlockState(pos);
// 1. 自定义名称
if (blockEntity instanceof Nameable nameable && nameable.getCustomName() != null) {
return nameable.getCustomName().getString();
}
// 2. AE2 部件
String ae2Name = eap$handleAE2Block(blockEntity, clickLocation);
if (ae2Name != null && !ae2Name.isBlank()) {
return ae2Name;
}
// 3. GregTech CEu 配方翻译
if (ModList.get().isLoaded("gtceu")) {
String gtceuName = eap$handleGTCEuBlock(blockEntity);
if (gtceuName != null && !gtceuName.isBlank()) {
return gtceuName;
}
}
// 4. 方块名称
return state.getBlock().getName().getString();
}
/**
* 处理 GregTech CEu 方块获取配方翻译名
*/
@Unique
private String eap$handleGTCEuBlock(BlockEntity blockEntity) {
try {
// 动态加载 GTCEu
Class<?> metaMachineBlockEntityClass = Class.forName("com.gregtechceu.gtceu.api.blockentity.MetaMachineBlockEntity");
Class<?> workableElectricMultiblockMachineClass = Class.forName("com.gregtechceu.gtceu.api.machine.multiblock.WorkableElectricMultiblockMachine");
if (metaMachineBlockEntityClass.isInstance(blockEntity)) {
// 获取 metaMachine 字段
Field metaMachineField = metaMachineBlockEntityClass.getDeclaredField("metaMachine");
metaMachineField.setAccessible(true);
Object metaMachine = metaMachineField.get(blockEntity);
if (workableElectricMultiblockMachineClass.isInstance(metaMachine)) {
// 调用 getRecipeType 方法
Method getRecipeTypeMethod = workableElectricMultiblockMachineClass.getMethod("getRecipeType");
getRecipeTypeMethod.setAccessible(true);
Object recipeType = getRecipeTypeMethod.invoke(metaMachine);
if (recipeType != null) {
// 调用 toString 方法获取配方名
String recipeName = recipeType.toString().replace("gtceu:", "");
String translationKey = "gtceu." + recipeName; // e.g., gtceu.cracker
// 客户端使用 I18n
return I18n.get(translationKey, recipeName); // e.g., 裂化机
}
}
}
} catch (ClassNotFoundException e) {
LOGGER.info("GregTech CEu 类未找到,跳过配方翻译处理");
return null; // GTCEu 不可用
} catch (NoSuchFieldException | NoSuchMethodException | IllegalAccessException | java.lang.reflect.InvocationTargetException e) {
LOGGER.error("处理 GTCEu 配方翻译失败: {}", e.getMessage());
return null; // 反射失败
}
return null; // GTCEu 方块实体
}
/**
* 处理 AE2 方块获取部件或外观名称
*/
@Unique
private String eap$handleAE2Block(BlockEntity blockEntity, Vec3 clickLocation) {
if (blockEntity instanceof IPartHost partHost) {
SelectedPart sel = partHost.selectPartWorld(clickLocation);
if (sel.part != null) {
ItemStack stack = new ItemStack(sel.part.getPartItem());
return stack.getHoverName().getString();
} else if (sel.facade != null) {
ItemStack stack = sel.facade.getItemStack();
if (!stack.isEmpty()) {
return stack.getHoverName().getString();
}
}
}
return null;
}
/**
* 多级回退的剪贴板写入
* 1) KeyboardHandler.setClipboard
* 2) GLFW.glfwSetClipboardString(Window handle)
* 3) AWT 系统剪贴板可能在某些整合包/无头环境不可用
*/
@Unique
private static boolean eap$tryCopyToClipboard(Minecraft mc, String text) {
if (text == null || text.isBlank()) return false;
// 确保在游戏主线程执行
if (!mc.isSameThread()) {
AtomicBoolean result = new AtomicBoolean(false);
CountDownLatch latch = new CountDownLatch(1);
mc.execute(() -> {
try {
result.set(eap$doCopy(mc, text));
} finally {
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
LOGGER.error("剪贴板复制线程中断: {}", e.getMessage());
}
return result.get();
} else {
return eap$doCopy(mc, text);
}
}
@Unique
private static boolean eap$doCopy(Minecraft mc, String text) {
try {
mc.keyboardHandler.setClipboard(text);
return true;
} catch (Throwable ignored) {
}
try {
// GLFW 路径 1使用窗口句柄
Window window = mc.getWindow();
long handle = window == null ? 0L : window.getWindow();
if (handle != 0L) {
GLFW.glfwSetClipboardString(handle, text);
return true;
}
} catch (Throwable ignored) {
}
try {
// GLFW 路径 2使用当前上下文部分整合包自定义窗口实现时更稳健
long current = GLFW.glfwGetCurrentContext();
if (current != 0L) {
GLFW.glfwSetClipboardString(current, text);
return true;
}
} catch (Throwable ignored) {
}
try {
// 最后回退到 AWT可能在无头环境不可用
var clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(new StringSelection(text), null);
return true;
} catch (Throwable ignored) {
}
return false;
}
}

View File

@ -0,0 +1,13 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.menu.AEBaseMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(value = AEBaseScreen.class, remap = false)
public interface AEBaseScreenAccessor<T extends AEBaseMenu> {
@Accessor(value = "style", remap = false)
ScreenStyle eap$getStyle();
}

View File

@ -0,0 +1,10 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.client.gui.AEBaseScreen;
import appeng.menu.AEBaseMenu;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(AEBaseScreen.class)
public interface AEBaseScreenInvoker<T extends AEBaseMenu> {
// 空接口避免在 AEBaseScreen 上声明不存在方法的 Invoker 导致编译错误
}

View File

@ -0,0 +1,32 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.storage.MEStorage;
import appeng.api.util.IConfigManager;
import appeng.menu.me.common.MEStorageMenu;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(MEStorageMenu.class)
public interface MEStorageMenuAccessor {
@Accessor("storage")
@Nullable
MEStorage getStorage();
@Accessor("powerSource")
@Nullable
IEnergySource getPowerSource();
@Accessor("hasPower")
boolean getHasPower();
// Access client-side config manager mirror used for syncing settings
@Accessor("clientCM")
IConfigManager getClientCM();
// Access server-side config manager
@Accessor("serverCM")
@Nullable
IConfigManager getServerCM();
}

View File

@ -0,0 +1,16 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.client.gui.me.common.MEStorageScreen;
import appeng.client.gui.widgets.AETextField;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import org.spongepowered.asm.mixin.gen.Invoker;
@Mixin(value = MEStorageScreen.class, remap = false)
public interface MEStorageScreenAccessor {
@Accessor("searchField")
AETextField eap$getSearchField();
@Invoker("setSearchText")
void eap$setSearchText(String text);
}

View File

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

View File

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

View File

@ -0,0 +1,15 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.menu.me.items.PatternEncodingTermMenu;
import appeng.menu.slot.RestrictedInputSlot;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(PatternEncodingTermMenu.class)
public interface PatternEncodingTermMenuAccessor {
@Accessor("encodedPatternSlot")
RestrictedInputSlot eap$getEncodedPatternSlot();
@Accessor("blankPatternSlot")
RestrictedInputSlot eap$getBlankPatternSlot();
}

View File

@ -0,0 +1,16 @@
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;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(PatternProviderLogic.class)
public interface PatternProviderLogicAccessor {
@Accessor("host")
PatternProviderLogicHost eap$host();
@Accessor("mainNode")
IManagedGridNode eap$mainNode();
}

View File

@ -0,0 +1,14 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.api.stacks.AEKey;
import appeng.helpers.patternprovider.PatternProviderLogic;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.Set;
@Mixin(PatternProviderLogic.class)
public interface PatternProviderLogicPatternInputsAccessor {
@Accessor("patternInputs")
Set<AEKey> eap$patternInputs();
}

View File

@ -0,0 +1,14 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.api.crafting.IPatternDetails;
import appeng.helpers.patternprovider.PatternProviderLogic;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
import java.util.List;
@Mixin(value = PatternProviderLogic.class, remap = false)
public interface PatternProviderLogicPatternsAccessor {
@Accessor("patterns")
List<IPatternDetails> eap$patterns();
}

View File

@ -0,0 +1,12 @@
package com.extendedae_plus.mixin.ae2.accessor;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.menu.implementations.PatternProviderMenu;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(PatternProviderMenu.class)
public interface PatternProviderMenuAdvancedAccessor {
@Accessor("logic")
PatternProviderLogic eap$logic();
}

View File

@ -0,0 +1,28 @@
package com.extendedae_plus.mixin.ae2.autopattern;
import appeng.api.crafting.IPatternDetails;
import appeng.me.service.CraftingService;
import com.extendedae_plus.content.ScaledProcessingPattern;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
/**
* CraftingService.getProviders 调用点修改传入的 IPatternDetails 参数回退到网络注册的原始样板
*/
@Mixin(value = CraftingService.class, remap = false)
public class CraftingServiceGetProvidersMixin {
@ModifyArg(method = "getProviders(Lappeng/api/crafting/IPatternDetails;)Ljava/lang/Iterable;",
at = @At(value = "INVOKE", target = "Lappeng/me/service/helpers/NetworkCraftingProviders;getMediums(Lappeng/api/crafting/IPatternDetails;)Ljava/lang/Iterable;"),
index = 0)
private IPatternDetails eap$modifyGetProvidersArg(IPatternDetails original) {
IPatternDetails base = null;
if (original instanceof ScaledProcessingPattern scaledProcessingPattern) {
base = scaledProcessingPattern.getOriginal();
}
return base == null ? original : base;
}
}

View File

@ -0,0 +1,12 @@
package com.extendedae_plus.mixin.ae2.autopattern;
import appeng.api.stacks.AEKey;
import appeng.crafting.CraftingTreeNode;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(CraftingTreeNode.class)
public interface CraftingTreeNodeAccessor {
@Accessor("what")
AEKey eap$getWhat();
}

View File

@ -0,0 +1,30 @@
package com.extendedae_plus.mixin.ae2.autopattern;
import appeng.api.stacks.KeyCounter;
import appeng.crafting.CraftingTreeNode;
import appeng.crafting.inv.CraftingSimulationState;
import com.extendedae_plus.util.RequestedAmountHolder;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
@Mixin(value = CraftingTreeNode.class,remap = false)
public class CraftingTreeNodeMixin {
@Inject(method = "request(Lappeng/crafting/inv/CraftingSimulationState;JLappeng/api/stacks/KeyCounter;)V",
at = @At(value = "INVOKE",
target = "Lappeng/crafting/CraftingTreeNode;addContainerItems(Lappeng/api/stacks/AEKey;JLappeng/api/stacks/KeyCounter;)V"),
locals = LocalCapture.CAPTURE_FAILHARD)
private void captureRequestedAmount(CraftingSimulationState inv, long requestedAmount, KeyCounter containerItems, CallbackInfo ci) {
// push the requestedAmount before addContainerItems is called
RequestedAmountHolder.push(requestedAmount);
}
@Inject(method = "request(Lappeng/crafting/inv/CraftingSimulationState;JLappeng/api/stacks/KeyCounter;)V",
at = @At(value = "RETURN"))
private void clearRequestedAmountOnReturn(CraftingSimulationState inv, long requestedAmount, KeyCounter containerItems, CallbackInfo ci) {
// pop the pushed requested amount on return
RequestedAmountHolder.pop();
}
}

View File

@ -0,0 +1,98 @@
package com.extendedae_plus.mixin.ae2.autopattern;
import appeng.api.crafting.IPatternDetails;
import appeng.api.networking.crafting.ICraftingProvider;
import appeng.api.networking.crafting.ICraftingService;
import appeng.api.stacks.AEKey;
import appeng.crafting.CraftingCalculation;
import appeng.crafting.CraftingTreeNode;
import appeng.crafting.CraftingTreeProcess;
import appeng.crafting.pattern.AEProcessingPattern;
import appeng.me.service.CraftingService;
import com.extendedae_plus.api.SmartDoublingAwarePattern;
import com.extendedae_plus.config.ModConfigs;
import com.extendedae_plus.content.ScaledProcessingPattern;
import com.extendedae_plus.util.PatternScaler;
import com.extendedae_plus.util.RequestedAmountHolder;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyVariable;
import java.util.List;
import java.util.stream.StreamSupport;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
/**
* 注入 CraftingTreeProcess 构造器尾部 AEProcessingPattern 替换为 ScaledProcessingPattern
* 以确保后续执行使用放大后的输入/输出视图
*/
@Mixin(CraftingTreeProcess.class)
public abstract class CraftingTreeProcessMixin {
@ModifyVariable(
method = "<init>(Lappeng/api/networking/crafting/ICraftingService;Lappeng/crafting/CraftingCalculation;Lappeng/api/crafting/IPatternDetails;Lappeng/crafting/CraftingTreeNode;)V",
at = @At("HEAD"),
argsOnly = true
)
private static IPatternDetails eap$replaceDetailsAtHead(IPatternDetails original, ICraftingService cc, CraftingCalculation job, IPatternDetails details, CraftingTreeNode craftingTreeNode) {
try {
// 若传入的 details 已经是缩放样板且原始样板不允许缩放则直接解包为原始样板
if (details instanceof ScaledProcessingPattern sp) {
var proc0 = sp.getOriginal();
if (proc0 instanceof SmartDoublingAwarePattern aware0 && !aware0.eap$allowScaling()) {
return proc0;
}
}
if (!(details instanceof AEProcessingPattern proc)) return original;
// 若样板标记为不允许缩放则直接跳过
if (proc instanceof SmartDoublingAwarePattern aware && !aware.eap$allowScaling()) {
return original;
}
CraftingTreeNodeAccessor parentAcc = (CraftingTreeNodeAccessor) craftingTreeNode;
AEKey parentTarget = parentAcc.eap$getWhat();
long requested = RequestedAmountHolder.get();
// 根据配置决定是否在 provider 间轮询分配请求量默认开启
long perProvider = 1L;
if (!ModConfigs.PROVIDER_ROUND_ROBIN_ENABLE.get()) {
// 关闭轮询直接使用完整请求量不需要查询 provider 列表
perProvider = requested;
if (perProvider <= 0) perProvider = 1L;
} else {
CraftingService craftingService = (CraftingService) cc;
Iterable<ICraftingProvider> providers = craftingService.getProviders(original);
// 计算 provider 数量尝试用反射读取内部 providers 列表以避免消费迭代器
int size;
try {
var cls = providers.getClass();
var f = cls.getDeclaredField("providers"); // private ArrayList<ICraftingProvider>
f.setAccessible(true);
List<?> list = (List<?>) f.get(providers);
size = list == null ? 0 : list.size();
} catch (Exception ex) {
// 反射失败回退为遍历计数会消费迭代器
size = (int) StreamSupport.stream(providers.spliterator(), false).count();
}
// requested providers 间均分向上取整保证每个 provider 分配整数且总量不少于 requested
if (size > 0) {
perProvider = requested / size + ((requested % size) == 0 ? 0 : 1);
if (perProvider <= 0) perProvider = 1L;
}
}
// 使用每-provider 的分配量来缩放样板
var scaled = PatternScaler.scale(proc, parentTarget, perProvider);
return scaled != null ? scaled : original;
} catch (Exception e) {
LOGGER.warn("构建倍增样板出错", e);
e.printStackTrace();
return original;
}
}
}

View File

@ -0,0 +1,38 @@
package com.extendedae_plus.mixin.ae2.autopattern;
import appeng.api.crafting.IPatternDetails;
import appeng.helpers.patternprovider.PatternProviderLogic;
import com.extendedae_plus.content.ScaledProcessingPattern;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.List;
/**适配
* Redirect PatternProviderLogic.pushPattern 中对 List.contains 的调用
* 在遇到缩放样板时回退匹配到原始样板实例
*/
@Mixin(value = PatternProviderLogic.class, remap = false)
public class PatternProviderLogicContainsRedirectMixin {
@Redirect(method = "pushPattern",
at = @At(
value = "INVOKE",
target = "Ljava/util/List;contains(Ljava/lang/Object;)Z")
)
private boolean eap$patternsContains(List<?> list, Object o) {
try {
if (o instanceof ScaledProcessingPattern scaled) {
IPatternDetails base = scaled.getOriginal();
if (base != null && list.indexOf(base) != -1) {
return true;
}
}
// 使用 indexOf 避免再次触发对 List.contains redirect防止递归
return list.indexOf(o) != -1;
} catch (Throwable t) {
return list.indexOf(o) != -1;
}
}
}

View File

@ -0,0 +1,330 @@
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;
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;
import net.minecraft.network.chat.Component;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@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;
}
/**
* 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();
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;
}
/**
* 重写renderSlot方法为所有可见的样板槽位添加数量显示
*/
@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) {
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) {
}
}
@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,189 @@
package com.extendedae_plus.mixin.ae2.client.gui;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.Icon;
import appeng.client.gui.me.items.PatternEncodingTermScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.style.WidgetStyle;
import appeng.client.gui.widgets.IconButton;
import appeng.menu.AEBaseMenu;
import com.extendedae_plus.mixin.accessor.AbstractContainerScreenAccessor;
import com.extendedae_plus.mixin.accessor.ScreenAccessor;
import com.extendedae_plus.mixin.ae2.accessor.AEBaseScreenAccessor;
import com.extendedae_plus.network.ModNetwork;
import net.minecraft.client.gui.GuiGraphics;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.Rect2i;
import net.minecraft.network.chat.Component;
import 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;
/**
* 在图样编码终端界面加入一个上传按钮
* 点击后把当前已编码样板上传到任意可用的样板供应器服务端自动选择
* 通过解析 AE2 样式中 encodePattern 的坐标将按钮放在其左侧紧挨位置
*/
@Mixin(AEBaseScreen.class)
public abstract class PatternEncodingTermScreenMixin<T extends AEBaseMenu> {
@Unique
private IconButton eap$uploadBtn;
@Inject(method = "init", at = @At("TAIL"))
private void eap$addUploadButton(CallbackInfo ci) {
// 仅在图样编码终端界面中添加按钮
if (!(((Object) this) instanceof PatternEncodingTermScreen)) {
return;
}
// 复用已存在的按钮实例避免重复创建
if (eap$uploadBtn == null) {
eap$uploadBtn = new IconButton(btn -> ModNetwork.CHANNEL
.sendToServer(new com.extendedae_plus.network.RequestProvidersListC2SPacket())) {
private final float eap$scale = 0.75f; // 12x12
@Override
protected Icon getIcon() {
return Icon.ARROW_UP;
}
@Override
public void renderWidget(GuiGraphics guiGraphics, int mouseX, int mouseY, float partial) {
// 参照 AE2 IconButton 实现改为自定义缩放
if (this.visible) {
var icon = this.getIcon();
var blitter = icon.getBlitter();
if (!this.active) {
blitter.opacity(0.5f);
}
// 动态更新宽高用于聚焦边框/命中框
this.width = Math.round(16 * eap$scale);
this.height = Math.round(16 * eap$scale);
com.mojang.blaze3d.systems.RenderSystem.disableDepthTest();
com.mojang.blaze3d.systems.RenderSystem.enableBlend();
if (isFocused()) {
guiGraphics.fill(getX() - 1, getY() - 1, getX() + width + 1, getY(), 0xFFFFFFFF);
guiGraphics.fill(getX() - 1, getY(), getX(), getY() + height, 0xFFFFFFFF);
guiGraphics.fill(getX() + width, getY(), getX() + width + 1, getY() + height, 0xFFFFFFFF);
guiGraphics.fill(getX() - 1, getY() + height, getX() + width + 1, getY() + height + 1, 0xFFFFFFFF);
}
var pose = guiGraphics.pose();
pose.pushPose();
pose.translate(getX(), getY(), 0.0F);
pose.scale(eap$scale, eap$scale, 1.f);
if (!this.isDisableBackground()) {
Icon.TOOLBAR_BUTTON_BACKGROUND.getBlitter().dest(0, 0).blit(guiGraphics);
}
blitter.dest(0, 0).blit(guiGraphics);
pose.popPose();
com.mojang.blaze3d.systems.RenderSystem.enableDepthTest();
}
}
@Override
public Rect2i getTooltipArea() {
return new Rect2i(getX(), getY(), Math.round(16 * eap$scale), Math.round(16 * eap$scale));
}
};
eap$uploadBtn.setTooltip(Tooltip.create(Component.translatable("extendedae_plus.button.choose_provider")));
}
// 解析 encodePattern 的样式位置
try {
ScreenStyle style = ((AEBaseScreenAccessor<?>) (Object) this).eap$getStyle();
WidgetStyle ws = style.getWidget("encodePattern");
int leftPos = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getLeftPos();
int topPos = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getTopPos();
int imageWidth = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getImageWidth();
int imageHeight = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getImageHeight();
Rect2i bounds = new Rect2i(leftPos, topPos, imageWidth, imageHeight);
var pos = ws.resolve(bounds);
int baseW = ws.getWidth() > 0 ? ws.getWidth() : 16;
int baseH = ws.getHeight() > 0 ? ws.getHeight() : 16;
int targetW = Math.max(10, Math.round(baseW * 0.75f));
int targetH = Math.max(10, Math.round(baseH * 0.75f));
// 缩小为原尺寸的 0.75稍微变大于 8x8
eap$uploadBtn.setWidth(targetW);
eap$uploadBtn.setHeight(targetH);
// 仍位于其左侧但整体向右微移减小间距 2px
eap$uploadBtn.setX(pos.getX() - targetW); // 原为 -targetW - 2再右移 2px
eap$uploadBtn.setY(pos.getY());
} catch (Throwable t) {
// 回退放在界面右侧大致位置避免不可见
eap$uploadBtn.setWidth(12);
eap$uploadBtn.setHeight(12);
int leftPos = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getLeftPos();
int topPos = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getTopPos();
int imageWidth = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getImageWidth();
eap$uploadBtn.setX(leftPos + imageWidth - 12 - 8 + 2); // 向右微移 2px
eap$uploadBtn.setY(topPos + 88);
}
// 直接向 renderables / children 列表添加避免依赖受保护方法
var accessor = (ScreenAccessor) (Object) this;
var renderables = accessor.eap$getRenderables();
var children = accessor.eap$getChildren();
if (!renderables.contains(eap$uploadBtn)) {
renderables.add(eap$uploadBtn);
}
if (!children.contains(eap$uploadBtn)) {
children.add(eap$uploadBtn);
}
}
@Inject(method = "containerTick", at = @At("TAIL"))
private void eap$ensureUploadButton(CallbackInfo ci) {
if (!(((Object) this) instanceof PatternEncodingTermScreen)) {
return;
}
if (eap$uploadBtn == null) {
return;
}
var renderables2 = ((ScreenAccessor) (Object) this).eap$getRenderables();
if (!renderables2.contains(eap$uploadBtn)) {
// 被其它模组清空/替换后重新计算一次位置并补回
try {
ScreenStyle style = ((AEBaseScreenAccessor<?>) (Object) this).eap$getStyle();
WidgetStyle ws = style.getWidget("encodePattern");
int leftPos = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getLeftPos();
int topPos = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getTopPos();
int imageWidth = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getImageWidth();
int imageHeight = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getImageHeight();
Rect2i bounds = new Rect2i(leftPos, topPos, imageWidth, imageHeight);
var pos = ws.resolve(bounds);
int baseW = ws.getWidth() > 0 ? ws.getWidth() : 16;
int baseH = ws.getHeight() > 0 ? ws.getHeight() : 16;
int targetW = Math.max(10, Math.round(baseW * 0.75f));
int targetH = Math.max(10, Math.round(baseH * 0.75f));
eap$uploadBtn.setWidth(targetW);
eap$uploadBtn.setHeight(targetH);
eap$uploadBtn.setX(pos.getX() - targetW); // 原为 -targetW - 2再右移 2px
eap$uploadBtn.setY(pos.getY());
} catch (Throwable t) {
int leftPos = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getLeftPos();
int topPos = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getTopPos();
int imageWidth = ((AbstractContainerScreenAccessor<?>) (Object) this).eap$getImageWidth();
eap$uploadBtn.setWidth(12);
eap$uploadBtn.setHeight(12);
eap$uploadBtn.setX(leftPos + imageWidth - 12 - 8 + 2);
eap$uploadBtn.setY(topPos + 88);
}
var accessor2 = (ScreenAccessor) (Object) this;
var r = accessor2.eap$getRenderables();
var c = accessor2.eap$getChildren();
if (!r.contains(eap$uploadBtn)) {
r.add(eap$uploadBtn);
}
if (!c.contains(eap$uploadBtn)) {
c.add(eap$uploadBtn);
}
}
}
}

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

@ -0,0 +1,150 @@
package com.extendedae_plus.mixin.ae2.client.gui;
import appeng.api.config.Settings;
import appeng.api.config.YesNo;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.implementations.PatternProviderScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.SettingToggleButton;
import appeng.menu.implementations.PatternProviderMenu;
import com.extendedae_plus.api.ExPatternButtonsAccessor;
import com.extendedae_plus.api.PatternProviderMenuAdvancedSync;
import com.extendedae_plus.api.PatternProviderMenuDoublingSync;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.ToggleAdvancedBlockingC2SPacket;
import com.extendedae_plus.network.ToggleSmartDoublingC2SPacket;
import com.extendedae_plus.util.ExtendedAELogger;
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;
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;
/**
* AE2 原版样板供应器界面添加高级阻挡模式按钮
* - 位于左侧工具栏
* - 点击仅发送 C2S 切换请求状态由 AE2 @GuiSync 回传决定
*/
@Mixin(PatternProviderScreen.class)
public abstract class PatternProviderScreenMixin<C extends PatternProviderMenu> extends AEBaseScreen<C> {
@Unique
private SettingToggleButton<YesNo> eap$AdvancedBlockingToggle;
@Unique
private boolean eap$AdvancedBlockingEnabled = false;
@Unique
private SettingToggleButton<YesNo> eap$SmartDoublingToggle;
@Unique
private boolean eap$SmartDoublingEnabled = false;
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) {
// 使用 @GuiSync 初始化
try {
if (menu instanceof PatternProviderMenuAdvancedSync sync) {
this.eap$AdvancedBlockingEnabled = sync.eap$getAdvancedBlockingSynced();
}
} catch (Throwable t) {
ExtendedAELogger.LOGGER.error("Error initializing advanced sync", t);
}
// 使用 SettingToggleButton<YesNo> 的外观原版图标但自定义悬停描述为智能阻挡
this.eap$AdvancedBlockingToggle = new SettingToggleButton<>(
Settings.BLOCKING_MODE,
this.eap$AdvancedBlockingEnabled ? YesNo.YES : YesNo.NO,
(btn, backwards) -> {
// 不做本地切换点击仅发送自定义C2S显示由@GuiSync回传
ExtendedAELogger.LOGGER.debug("[EAP] Click advanced blocking toggle: send C2S");
ModNetwork.CHANNEL.sendToServer(new ToggleAdvancedBlockingC2SPacket());
}
) {
@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("已启用:对于同一种配方将不再阻挡(需要开启原版的阻挡模式)")
: net.minecraft.network.chat.Component.literal("已禁用:这么好的功能为什么不打开呢");
return java.util.List.of(title, line);
}
};
// 初始化后立刻对齐当前@GuiSync状态避免首帧显示不一致
ExtendedAELogger.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);
// 智能翻倍按钮与高级阻挡同款样式点击仅发送C2S状态由@GuiSync驱动
try {
if (menu instanceof PatternProviderMenuDoublingSync sync2) {
this.eap$SmartDoublingEnabled = sync2.eap$getSmartDoublingSynced();
}
} catch (Throwable t) {
ExtendedAELogger.LOGGER.error("Error initializing smart doubling sync", t);
}
this.eap$SmartDoublingToggle = new SettingToggleButton<>(
Settings.BLOCKING_MODE,
this.eap$SmartDoublingEnabled ? YesNo.YES : YesNo.NO,
(btn, backwards) -> {
ExtendedAELogger.LOGGER.debug("[EAP] Click smart doubling toggle: send C2S");
ModNetwork.CHANNEL.sendToServer(new ToggleSmartDoublingC2SPacket());
}
) {
@Override
public java.util.List<net.minecraft.network.chat.Component> getTooltipMessage() {
boolean enabled = eap$SmartDoublingEnabled;
var title = net.minecraft.network.chat.Component.literal("智能翻倍");
var line = enabled
? net.minecraft.network.chat.Component.literal("已启用:根据请求量对处理样板进行智能缩放")
: net.minecraft.network.chat.Component.literal("已禁用:按原始样板数量进行发配");
return java.util.List.of(title, line);
}
};
this.eap$SmartDoublingToggle.set(this.eap$SmartDoublingEnabled ? YesNo.YES : YesNo.NO);
this.addToLeftToolbar(this.eap$SmartDoublingToggle);
}
// 每帧刷新仅从菜单(@GuiSync)同步布尔值保持按钮状态一致
@Inject(method = "updateBeforeRender", at = @At("HEAD"), remap = false)
private void eap$updateAdvancedBlocking(CallbackInfo ci) {
if (this.eap$AdvancedBlockingToggle != null) {
boolean desired = this.eap$AdvancedBlockingEnabled;
if (this.menu instanceof PatternProviderMenuAdvancedSync sync) {
desired = sync.eap$getAdvancedBlockingSynced();
}
ExtendedAELogger.LOGGER.debug("[EAP] updateBeforeRender tick (adv): desired={}", desired);
this.eap$AdvancedBlockingEnabled = desired;
this.eap$AdvancedBlockingToggle.set(desired ? YesNo.YES : YesNo.NO);
}
if (this.eap$SmartDoublingToggle != null) {
boolean desired2 = this.eap$SmartDoublingEnabled;
if (this.menu instanceof PatternProviderMenuDoublingSync sync2) {
desired2 = sync2.eap$getSmartDoublingSynced();
}
ExtendedAELogger.LOGGER.debug("[EAP] updateBeforeRender tick (dbl): desired={}", desired2);
this.eap$SmartDoublingEnabled = desired2;
this.eap$SmartDoublingToggle.set(desired2 ? YesNo.YES : YesNo.NO);
}
if ((Object) this instanceof GuiExPatternProvider) {
try {
((ExPatternButtonsAccessor) this).eap$updateButtonsLayout();
} catch (Throwable t) {
ExtendedAELogger.LOGGER.debug("[EAP] updateButtonsLayout skipped: {}", t.toString());
}
}
}
}

View File

@ -0,0 +1,82 @@
package com.extendedae_plus.mixin.ae2.client.gui;
import appeng.client.Point;
import appeng.client.gui.layout.SlotGridLayout;
import net.minecraft.client.Minecraft;
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.CallbackInfoReturnable;
import java.lang.reflect.Field;
@Mixin(SlotGridLayout.class)
public abstract class SlotGridLayoutMixin {
@Unique
private static final int SLOTS_PER_PAGE = 36;
@Inject(method = "getRowBreakPosition", at = @At("HEAD"), cancellable = true, remap = false)
private static void onGetRowBreakPosition(int x, int y, int semanticIdx, int cols, CallbackInfoReturnable<Point> cir) {
// 仅在 9 列布局 当前屏幕为 扩展样板供应器 时处理
if (cols != 9) {
return;
}
var screen = Minecraft.getInstance().screen;
if (!(screen instanceof com.glodblock.github.extendedae.client.gui.GuiExPatternProvider)) {
return;
}
// 读取实际当前页码优先从 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

@ -0,0 +1,109 @@
package com.extendedae_plus.mixin.ae2.helpers;
import appeng.api.crafting.IPatternDetails;
import appeng.api.crafting.IPatternDetails.IInput;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.helpers.patternprovider.PatternProviderTarget;
import com.extendedae_plus.api.AdvancedBlockingHolder;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.player.Player;
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;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
@Mixin(value = PatternProviderLogic.class, remap = false)
public class PatternProviderLogicAdvancedMixin implements AdvancedBlockingHolder {
@Unique
private static final String EPP_ADV_BLOCKING_KEY = "epp_advanced_blocking";
@Unique
private boolean eap$advancedBlocking = false;
@Override
public boolean eap$getAdvancedBlocking() {
return eap$advancedBlocking;
}
@Override
public void eap$setAdvancedBlocking(boolean value) {
this.eap$advancedBlocking = value;
}
@Inject(method = "writeToNBT", at = @At("TAIL"))
private void eap$writeAdvancedToNbt(CompoundTag tag, CallbackInfo ci) {
tag.putBoolean(EPP_ADV_BLOCKING_KEY, this.eap$advancedBlocking);
}
@Inject(method = "readFromNBT", at = @At("TAIL"))
private void eap$readAdvancedFromNbt(CompoundTag tag, CallbackInfo ci) {
if (tag.contains(EPP_ADV_BLOCKING_KEY)) {
this.eap$advancedBlocking = tag.getBoolean(EPP_ADV_BLOCKING_KEY);
}
}
// pushPattern 重定向对 adapter.containsPatternInput(...) 的调用
@Redirect(method = "pushPattern", at = @At(value = "INVOKE", target = "Lappeng/helpers/patternprovider/PatternProviderTarget;containsPatternInput(Ljava/util/Set;)Z"))
private boolean eap$redirectBlockingContains(PatternProviderTarget adapter,
java.util.Set<AEKey> patternInputs,
IPatternDetails patternDetails,
appeng.api.stacks.KeyCounter[] inputHolder) {
// 原版是否打开阻挡
boolean vanillaBlocking = ((PatternProviderLogic)(Object)this).isBlocking();
if (!vanillaBlocking) {
return adapter.containsPatternInput(patternInputs);
}
// 仅当高级阻挡启用时启用匹配则不阻挡
if (this.eap$advancedBlocking) {
if (eap$targetFullyMatchesPatternInputs(adapter, patternDetails)) {
// 返回 false 表示不包含阻挡关键物从而不触发 continue允许发配
return false;
}
}
// 否则使用原判定
return adapter.containsPatternInput(patternInputs);
}
@Unique
private boolean eap$targetFullyMatchesPatternInputs(PatternProviderTarget adapter, IPatternDetails patternDetails) {
for (IInput in : patternDetails.getInputs()) {
boolean slotMatched = false;
for (GenericStack candidate : in.getPossibleInputs()) {
AEKey key = candidate.what().dropSecondary();
if (adapter.containsPatternInput(Collections.singleton(key))) {
slotMatched = true;
break;
}
}
if (!slotMatched) {
return false; // 任一输入槽未匹配则失败
}
}
return true; // 每个输入槽都至少匹配了一个候选输入
}
@Shadow public void saveChanges() {}
@Inject(method = "exportSettings(Lnet/minecraft/nbt/CompoundTag;)V", at = @At("TAIL"))
private void onExportSettings(CompoundTag output, CallbackInfo ci) {
System.out.println(this.eap$advancedBlocking);
output.putBoolean("eap_advanced_blocking", this.eap$advancedBlocking);
}
@Inject(method = "importSettings(Lnet/minecraft/nbt/CompoundTag;Lnet/minecraft/world/entity/player/Player;)V", at = @At("TAIL"))
private void onImportSettings(CompoundTag input, Player player, CallbackInfo ci) {
if (input.contains("eap_advanced_blocking")) {
this.eap$advancedBlocking = input.getBoolean("eap_advanced_blocking");
// 持久化到 world
this.saveChanges();
}
}
}

View File

@ -0,0 +1,91 @@
package com.extendedae_plus.mixin.ae2.helpers;
import appeng.api.crafting.IPatternDetails;
import appeng.crafting.pattern.AEProcessingPattern;
import appeng.helpers.patternprovider.PatternProviderLogic;
import com.extendedae_plus.api.SmartDoublingAwarePattern;
import com.extendedae_plus.api.SmartDoublingHolder;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicPatternsAccessor;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.world.entity.player.Player;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = PatternProviderLogic.class, remap = false)
public class PatternProviderLogicDoublingMixin implements SmartDoublingHolder {
@Unique
private static final String EPP_SMART_DOUBLING_KEY = "epp_smart_doubling";
@Unique
private boolean eap$smartDoubling = false;
@Override
public boolean eap$getSmartDoubling() {
return eap$smartDoubling;
}
@Override
public void eap$setSmartDoubling(boolean value) {
this.eap$smartDoubling = value;
// 立即将开关状态应用到当前 Provider 的样板上避免等待下一次 updatePatterns
try {
var list = ((PatternProviderLogicPatternsAccessor) this).eap$patterns();
for (IPatternDetails details : list) {
if (details instanceof AEProcessingPattern proc && proc instanceof SmartDoublingAwarePattern aware) {
aware.eap$setAllowScaling(value);
}
}
// 触发一次刷新让网络及时拿到最新状态也会触发 ICraftingProvider.requestUpdate(mainNode)
((PatternProviderLogic) (Object) this).updatePatterns();
} catch (Throwable ignored) {
}
}
@Inject(method = "writeToNBT", at = @At("TAIL"))
private void eap$writeSmartDoublingToNbt(CompoundTag tag, CallbackInfo ci) {
tag.putBoolean(EPP_SMART_DOUBLING_KEY, this.eap$smartDoubling);
}
@Inject(method = "readFromNBT", at = @At("TAIL"))
private void eap$readSmartDoublingFromNbt(CompoundTag tag, CallbackInfo ci) {
if (tag.contains(EPP_SMART_DOUBLING_KEY)) {
this.eap$smartDoubling = tag.getBoolean(EPP_SMART_DOUBLING_KEY);
}
}
@Inject(method = "updatePatterns", at = @At("TAIL"))
private void eap$applySmartDoublingToPatterns(CallbackInfo ci) {
try {
var list = ((PatternProviderLogicPatternsAccessor) this).eap$patterns();
boolean allow = this.eap$smartDoubling;
for (IPatternDetails details : list) {
if (details instanceof AEProcessingPattern proc && proc instanceof SmartDoublingAwarePattern aware) {
aware.eap$setAllowScaling(allow);
}
}
} catch (Throwable ignored) {
}
}
@Shadow
public void saveChanges() {}
@Inject(method = "exportSettings(Lnet/minecraft/nbt/CompoundTag;)V", at = @At("TAIL"))
private void onExportSettings(CompoundTag output, CallbackInfo ci) {
System.out.println(this.eap$smartDoubling);
output.putBoolean("eap_smart_doubling", this.eap$smartDoubling);
}
@Inject(method = "importSettings(Lnet/minecraft/nbt/CompoundTag;Lnet/minecraft/world/entity/player/Player;)V", at = @At("TAIL"))
private void onImportSettings(CompoundTag input, Player player, CallbackInfo ci) {
if (input.contains("eap_smart_doubling")) {
this.eap$smartDoubling = input.getBoolean("eap_smart_doubling");
// 持久化到 world
this.saveChanges();
}
}
}

View File

@ -0,0 +1,123 @@
package com.extendedae_plus.mixin.ae2.menu;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.menu.me.items.PatternEncodingTermMenu;
import appeng.menu.slot.RestrictedInputSlot;
import appeng.parts.encoding.EncodingMode;
import com.extendedae_plus.util.ExtendedAEPatternUploadUtil;
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.world.item.ItemStack;
import org.jetbrains.annotations.NotNull;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Map;
import java.util.function.Consumer;
/**
* AE2 PatternEncodingTermMenu 增加一个通用动作持有者实现接收 EPP CGenericPacket 动作
* 注册动作 "upload_to_matrix"仅上传合成图样 ExtendedAE 装配矩阵
*/
@Mixin(PatternEncodingTermMenu.class)
public abstract class ContainerPatternEncodingTermMenuMixin implements IActionHolder {
@Unique
private final Map<String, Consumer<Paras>> eap$actions = createHolder();
@Unique
private Player epp$player;
@Shadow(remap = false)
private RestrictedInputSlot encodedPatternSlot;
@Unique
private void eap$scheduleUploadWithRetry(ServerPlayer sp, PatternEncodingTermMenu menu, int attemptsLeft) {
sp.server.execute(() -> {
try {
if (attemptsLeft < 0) {
return;
}
var stack = this.encodedPatternSlot != null ? this.encodedPatternSlot.getItem() : net.minecraft.world.item.ItemStack.EMPTY;
if (stack != null && !stack.isEmpty() && PatternDetailsHelper.isEncodedPattern(stack)) {
ExtendedAEPatternUploadUtil.uploadFromEncodingMenuToMatrix(sp, menu);
} else {
// 槽位可能尚未同步到位继续下一 tick 重试
if (attemptsLeft > 0) {
eap$scheduleUploadWithRetry(sp, menu, attemptsLeft - 1);
}
}
} catch (Throwable ignored) {
}
});
}
// AE2 终端主构造PatternEncodingTermMenu(int id, Inventory ip, IPatternTerminalMenuHost host)
@Inject(method = "<init>(ILnet/minecraft/world/entity/player/Inventory;Lappeng/helpers/IPatternTerminalMenuHost;)V", at = @At("TAIL"), remap = false)
private void eap$ctorA(int id, net.minecraft.world.entity.player.Inventory ip, appeng.helpers.IPatternTerminalMenuHost host, CallbackInfo ci) {
this.epp$player = ip.player;
// 不再注册任何上传相关动作
}
// AE2 另一个构造PatternEncodingTermMenu(MenuType, int, Inventory, IPatternTerminalMenuHost, boolean)
@Inject(method = "<init>(Lnet/minecraft/world/inventory/MenuType;ILnet/minecraft/world/entity/player/Inventory;Lappeng/helpers/IPatternTerminalMenuHost;Z)V", at = @At("TAIL"), remap = false)
private void eap$ctorB(net.minecraft.world.inventory.MenuType<?> menuType, int id, net.minecraft.world.entity.player.Inventory ip, appeng.helpers.IPatternTerminalMenuHost host, boolean bindInventory, CallbackInfo ci) {
this.epp$player = ip.player;
// 不再注册任何上传相关动作
}
@NotNull
@Override
public Map<String, Consumer<Paras>> getActionMap() {
return this.eap$actions;
}
// 服务器端 encode() 执行完毕后如果已编码槽位存在样板且当前为合成模式则上传到装配矩阵
@Inject(method = "encode", at = @At("TAIL"), remap = false)
private void eap$serverUploadAfterEncode(CallbackInfo ci) {
try {
if (!(this.epp$player instanceof ServerPlayer sp)) {
return; // 仅服务器执行
}
var menu = (PatternEncodingTermMenu) (Object) this;
if (menu.getMode() != EncodingMode.CRAFTING
&& menu.getMode() != EncodingMode.SMITHING_TABLE
&& menu.getMode() != EncodingMode.STONECUTTING) {
return; // 只处理合成/锻造台/切石机样板
}
if (this.encodedPatternSlot == null) {
return;
}
var stack = this.encodedPatternSlot.getItem();
if (stack == null || stack.isEmpty()) {
return; // 没有编码样板
}
if (!PatternDetailsHelper.isEncodedPattern(stack)) {
return; // 不是编码样板
}
// 为避免与 AE2 后续同步竞争切到下一 tick 执行
sp.server.execute(() -> {
try {
ExtendedAEPatternUploadUtil.uploadFromEncodingMenuToMatrix(sp, menu);
} catch (Throwable ignored) {
}
});
} catch (Throwable ignored) {
}
}
// 服务器端在构造样板返回前插入编码玩家的名称
@Inject(method = "encodePattern", at = @At("TAIL"), remap = false, cancellable = true)
private void eap$writeEncodePlayerToPattern(CallbackInfoReturnable<ItemStack> cir) {
ItemStack itemStack = cir.getReturnValue();
itemStack.getOrCreateTag().putString("encodePlayer", this.epp$player.getGameProfile().getName());
cir.setReturnValue(itemStack);
}
}

View File

@ -0,0 +1,102 @@
package com.extendedae_plus.mixin.ae2.menu;
import appeng.api.config.Setting;
import appeng.api.util.IConfigManager;
import appeng.menu.me.common.MEStorageMenu;
import com.extendedae_plus.mixin.ae2.accessor.MEStorageMenuAccessor;
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;
/**
* 修复当服务端 ConfigManager 注册了额外设置例如 TERMINAL_SHOW_PATTERN_PROVIDERS
* 而客户端 clientCM 未注册时AE2 在同步环节会对 clientCM 执行 getSetting
* 进而抛出 UnsupportedSettingException
* <p>
* 方案在服务端首次 broadcastChanges 仅为客户端缺失的设置执行注册补齐且占位值与服务端不同
* 以确保 AE2 后续仍会发送 ConfigValuePacket 完成真正的值同步避免影响排序等行为
*/
@Mixin(MEStorageMenu.class)
public abstract class MEStorageMenuMixin {
@Unique
private boolean eap$settingsMirrored = false;
@Inject(method = "broadcastChanges", at = @At("HEAD"))
private void eap$mirrorServerSettingsToClient(CallbackInfo ci) {
var self = (MEStorageMenu) (Object) this;
if (this.eap$settingsMirrored) {
return;
}
try {
var acc = (MEStorageMenuAccessor) (Object) self;
IConfigManager server = acc.getServerCM();
IConfigManager client = acc.getClientCM();
if (server == null || client == null) {
// server==null 通常意味着客户端侧或无服务端配置直接返回
return;
}
for (Setting<?> setting : server.getSettings()) {
boolean clientHasSetting = true;
try {
// 若未注册这里会抛出异常
client.getSetting(setting);
} catch (Throwable unsupported) {
clientHasSetting = false;
}
if (!clientHasSetting) {
try {
Object serverValue = server.getSetting(setting);
Object placeholder = eap$chooseDifferentEnumValue(serverValue);
if (placeholder == null) {
// 若无法选择不同的占位值例如只有一个枚举常量则退回服务端值
placeholder = serverValue;
}
// 使用辅助方法统一进行受检的泛型转换后再注册
eap$registerSettingCompat(client, setting, placeholder);
} catch (Throwable ignore) {
// 防御不让异常影响主流程
}
}
}
this.eap$settingsMirrored = true;
} catch (Throwable t) {
// 防御绝不让同步失败导致崩溃
}
}
@Unique
private Object eap$chooseDifferentEnumValue(Object serverValue) {
if (!(serverValue instanceof Enum<?> sv)) {
return null;
}
Class<? extends Enum<?>> enumClass = sv.getDeclaringClass();
Object[] constants = enumClass.getEnumConstants();
if (constants == null || constants.length == 0) {
return null;
}
for (Object c : constants) {
if (c != sv) {
return c;
}
}
return null;
}
@Unique
@SuppressWarnings({"unchecked", "rawtypes"})
private static <T extends Enum<T>> void eap$registerSettingCompat(
IConfigManager client, Setting<?> setting, Object value) {
// 前置校验仅处理枚举类型的设置值
if (!(value instanceof Enum<?>)) {
// 非枚举则忽略AE2 设置值通常为枚举
return;
}
Setting<T> typedSetting = (Setting<T>) setting;
T typedValue = (T) value;
client.registerSetting(typedSetting, typedValue);
}
}

View File

@ -0,0 +1,155 @@
package com.extendedae_plus.mixin.ae2.menu;
import appeng.api.inventories.InternalInventory;
import appeng.api.networking.energy.IEnergySource;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.AEKey;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import appeng.core.definitions.AEItems;
import appeng.helpers.IPatternTerminalMenuHost;
import appeng.menu.me.common.MEStorageMenu;
import appeng.menu.me.items.PatternEncodingTermMenu;
import appeng.menu.slot.RestrictedInputSlot;
import com.extendedae_plus.mixin.ae2.accessor.MEStorageMenuAccessor;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.MenuType;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(PatternEncodingTermMenu.class)
public abstract class PatternEncodingTermMenuMixin {
// 防止重复执行
@Unique
private boolean eap$blankAutoFilled = false;
@Shadow
private RestrictedInputSlot blankPatternSlot;
@Unique
private void eap$tryFill(IPatternTerminalMenuHost host, Inventory ip) {
try {
var self = (PatternEncodingTermMenu) (Object) this;
var player = ip.player;
// 仅在服务器端执行
if (ip.player.level().isClientSide()) {
return;
}
// 必须可与网络交互
var acc = (MEStorageMenuAccessor) (Object) ((MEStorageMenu) self);
MEStorage storage = acc.getStorage();
IEnergySource power = acc.getPowerSource();
boolean hasPower = acc.getHasPower();
boolean canInteract = storage != null && power != null && hasPower; // 等价于 canInteractWithGrid()
if (!canInteract) {
return;
}
if (storage == null || power == null) {
return;
}
InternalInventory blankInv = host.getLogic().getBlankPatternInv();
var current = blankInv.getStackInSlot(0);
int limit = blankInv.getSlotLimit(0);
int space = Math.max(0, limit - current.getCount());
space = Math.min(space, AEItems.BLANK_PATTERN.asItem().getMaxStackSize());
if (space <= 0) {
return; // 已满无需填充
}
AEKey blankKey = AEItemKey.of(AEItems.BLANK_PATTERN.asItem());
long extracted = StorageHelper.poweredExtraction(power, storage, blankKey, space,
self.getActionSource());
if (extracted <= 0) {
return; // 网络无可用空白样板
}
int toInsert = (int) Math.min(extracted, space);
var stackInSlot = this.blankPatternSlot.getItem();
if (stackInSlot.isEmpty()) {
this.blankPatternSlot.set(AEItems.BLANK_PATTERN.stack(toInsert));
} else {
stackInSlot.grow(toInsert);
this.blankPatternSlot.set(stackInSlot);
}
long leftover = extracted - toInsert;
if (leftover > 0) {
StorageHelper.poweredInsert(power, storage, blankKey, leftover, self.getActionSource());
}
} catch (Throwable t) {
// swallow errors to avoid noisy logs in production
}
}
@Inject(method = "<init>(Lnet/minecraft/world/inventory/MenuType;ILnet/minecraft/world/entity/player/Inventory;Lappeng/helpers/IPatternTerminalMenuHost;Z)V",
at = @At("TAIL"))
private void eap$autoFillBlankPattern(MenuType<?> menuType, int id, Inventory ip,
IPatternTerminalMenuHost host, boolean bindInventory,
CallbackInfo ci) {
eap$tryFill(host, ip);
}
@Inject(method = "<init>(ILnet/minecraft/world/entity/player/Inventory;Lappeng/helpers/IPatternTerminalMenuHost;)V",
at = @At("TAIL"))
private void eap$autoFillCtor3(int id, Inventory ip, IPatternTerminalMenuHost host, CallbackInfo ci) {
eap$tryFill(host, ip);
}
// 在首次 broadcastChanges 后再尝试一次避免构造时网络未激活
@Inject(method = "broadcastChanges", at = @At("TAIL"))
private void eap$retryFillAfterPower(CallbackInfo ci) {
if (this.eap$blankAutoFilled) {
return;
}
// 仅在服务器端执行
var self = (PatternEncodingTermMenu) (Object) this;
var player = self.getPlayerInventory().player;
var acc = (MEStorageMenuAccessor) (Object) ((MEStorageMenu) self);
MEStorage storage = acc.getStorage();
IEnergySource power = acc.getPowerSource();
boolean hasPower = acc.getHasPower();
if (player.level().isClientSide()) {
return;
}
boolean canInteract = storage != null && power != null && hasPower;
if (!canInteract) {
return;
}
// 通过 host 获取 blankPatternInv
var host = ((IPatternTerminalMenuHost) self.getTarget());
InternalInventory blankInv = host.getLogic().getBlankPatternInv();
var current = blankInv.getStackInSlot(0);
int limit = blankInv.getSlotLimit(0);
int space = Math.max(0, limit - current.getCount());
space = Math.min(space, AEItems.BLANK_PATTERN.asItem().getMaxStackSize());
if (space <= 0) {
this.eap$blankAutoFilled = true;
return;
}
AEKey blankKey = AEItemKey.of(AEItems.BLANK_PATTERN.asItem());
long extracted = StorageHelper.poweredExtraction(power, storage, blankKey, space,
self.getActionSource());
if (extracted <= 0) {
return;
}
int toInsert = (int) Math.min(extracted, space);
var stackInSlot = this.blankPatternSlot.getItem();
if (stackInSlot.isEmpty()) {
this.blankPatternSlot.set(AEItems.BLANK_PATTERN.stack(toInsert));
} else {
stackInSlot.grow(toInsert);
this.blankPatternSlot.set(stackInSlot);
}
long leftover = extracted - toInsert;
if (leftover > 0) {
StorageHelper.poweredInsert(power, storage, blankKey, leftover, self.getActionSource());
}
this.eap$blankAutoFilled = true;
}
}

View File

@ -0,0 +1,56 @@
package com.extendedae_plus.mixin.ae2.menu;
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;
import com.extendedae_plus.api.PatternProviderMenuAdvancedSync;
import com.extendedae_plus.util.ExtendedAELogger;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.MenuType;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(PatternProviderMenu.class)
public abstract class PatternProviderMenuAdvancedMixin implements PatternProviderMenuAdvancedSync {
@Shadow
protected PatternProviderLogic logic;
// 选择一个未占用的 GUI 同步 idAE2 已用到 7这里使用 20 以避冲突
@Unique
@GuiSync(20)
public boolean eap$AdvancedBlocking = false;
@Inject(method = "broadcastChanges", at = @At("HEAD"))
private void eap$syncAdvancedBlocking(CallbackInfo ci) {
// 避免@Shadow父类方法改用公共APIAEBaseMenu#isClientSide()
if (!((AEBaseMenu) (Object) this).isClientSide()) {
var l = this.logic;
if (l instanceof AdvancedBlockingHolder holder) {
this.eap$AdvancedBlocking = holder.eap$getAdvancedBlocking();
ExtendedAELogger.LOGGER.debug("[EAP] Menu broadcastChanges HEAD: eap$AdvancedBlocking={}", this.eap$AdvancedBlocking);
}
}
}
@Override
public boolean eap$getAdvancedBlockingSynced() {
return this.eap$AdvancedBlocking;
}
// 调试 Screen 每帧读取这些 getter 时打印验证 Mixin 是否生效
@Inject(method = "getBlockingMode", at = @At("HEAD"), remap = false)
private void eap$debug_getBlockingMode(CallbackInfoReturnable<?> cir) {
}
@Inject(method = "getShowInAccessTerminal", at = @At("HEAD"), remap = false)
private void eap$debug_getShowInAccessTerminal(CallbackInfoReturnable<?> cir) {
}
}

View File

@ -0,0 +1,42 @@
package com.extendedae_plus.mixin.ae2.menu;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.menu.AEBaseMenu;
import appeng.menu.guisync.GuiSync;
import appeng.menu.implementations.PatternProviderMenu;
import com.extendedae_plus.api.PatternProviderMenuDoublingSync;
import com.extendedae_plus.api.SmartDoublingHolder;
import com.extendedae_plus.util.ExtendedAELogger;
import net.minecraft.world.entity.player.Inventory;
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;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(PatternProviderMenu.class)
public abstract class PatternProviderMenuDoublingMixin implements PatternProviderMenuDoublingSync {
@Shadow
protected PatternProviderLogic logic;
@Unique
@GuiSync(21)
public boolean eap$SmartDoubling = false;
@Inject(method = "broadcastChanges", at = @At("HEAD"))
private void eap$syncSmartDoubling(CallbackInfo ci) {
if (!((AEBaseMenu) (Object) this).isClientSide()) {
var l = this.logic;
if (l instanceof SmartDoublingHolder holder) {
this.eap$SmartDoubling = holder.eap$getSmartDoubling();
ExtendedAELogger.LOGGER.debug("[EAP] Menu broadcastChanges HEAD: eap$SmartDoubling={}", this.eap$SmartDoubling);
}
}
}
@Override
public boolean eap$getSmartDoublingSynced() {
return this.eap$SmartDoubling;
}
}

View File

@ -0,0 +1,58 @@
package com.extendedae_plus.mixin.ae2WTlib;
import com.extendedae_plus.util.ExtendedAEPatternUploadUtil;
import com.glodblock.github.extendedae.xmod.wt.ContainerUWirelessExPAT;
import com.glodblock.github.extendedae.xmod.wt.HostUWirelessExPAT;
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 org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
import java.util.function.Consumer;
/**
* 为通用无线样板访问终端AE2WTlib 集成容器注册通用动作CGenericPacket 分发
*/
@Pseudo
@Mixin(ContainerUWirelessExPAT.class)
public abstract class ContainerUWirelessExPatternTerminalMixin implements IActionHolder {
@Unique
private final Map<String, Consumer<Paras>> eap$actions = createHolder();
@Unique
private Player eap$player;
// 明确目标构造签名<init>(int, Inventory, HostUWirelessExPAT)
@Inject(method = "<init>(ILnet/minecraft/world/entity/player/Inventory;Lcom/glodblock/github/extendedae/xmod/wt/HostUWirelessExPAT;)V", at = @At("TAIL"), remap = false)
private void init(int id, net.minecraft.world.entity.player.Inventory playerInventory, HostUWirelessExPAT host, CallbackInfo ci) {
this.eap$player = playerInventory.player;
// 注册上传动作参数顺序必须与客户端 CGenericPacket 保持一致
this.eap$actions.put("upload", p -> {
try {
Object o0 = p.get(0);
Object o1 = p.get(1);
int playerSlotIndex = (o0 instanceof Number) ? ((Number) o0).intValue() : Integer.parseInt(String.valueOf(o0));
long providerId = (o1 instanceof Number) ? ((Number) o1).longValue() : Long.parseLong(String.valueOf(o1));
var sp = (ServerPlayer) this.eap$player;
ExtendedAEPatternUploadUtil.uploadPatternToProvider(sp, playerSlotIndex, providerId);
} catch (Throwable t) {
}
});
}
@NotNull
@Override
public Map<String, Consumer<Paras>> getActionMap() {
return this.eap$actions;
}
}

View File

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

View File

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

View File

@ -0,0 +1,72 @@
package com.extendedae_plus.mixin.extendedae.client;
import com.glodblock.github.extendedae.client.button.HighlightButton;
import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal;
import net.minecraft.client.gui.components.Button;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Objects;
@Mixin(value = HighlightButton.class, priority = 1000)
public abstract class HighlightButtonMixin {
@Shadow(remap = false)
private static void highlight(Button btn) {}
private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAEPlus");
@Inject(method = "highlight", at = @At("TAIL"), remap = false)
private static void onHighlight(Button btn, CallbackInfo ci) {
if (btn instanceof HighlightButton hb) {
var minecraft = net.minecraft.client.Minecraft.getInstance();
if (minecraft.screen instanceof GuiExPatternTerminal<?> terminal) {
try {
var fPos = HighlightButton.class.getDeclaredField("pos");
fPos.setAccessible(true);
Object btnPos = fPos.get(hb);
if (btnPos == null) {
return;
}
var fFace = HighlightButton.class.getDeclaredField("face");
fFace.setAccessible(true);
Object btnFace = fFace.get(hb); // 允许为 null方块形
var infoMapField = GuiExPatternTerminal.class.getDeclaredField("infoMap");
infoMapField.setAccessible(true);
@SuppressWarnings("unchecked")
var infoMap = (java.util.Map<Long, Object>) infoMapField.get(terminal);
for (var entry : infoMap.entrySet()) {
var info = entry.getValue();
var mPos = info.getClass().getMethod("pos");
mPos.setAccessible(true);
Object infoPos = mPos.invoke(info);
var mFace = info.getClass().getMethod("face");
mFace.setAccessible(true);
Object infoFace = mFace.invoke(info); // 允许为 null方块形
// 匹配规则pos 必须相等face 允许为 nullnull 仅与 null 匹配
boolean posEqual = Objects.equals(btnPos, infoPos);
boolean faceEqual = (btnFace == null && infoFace == null) || Objects.equals(btnFace, infoFace);
if (posEqual && faceEqual) {
long serverId = entry.getKey();
var setMethod = terminal.getClass().getMethod("setCurrentlyChoicePatternProvider", long.class);
setMethod.setAccessible(true);
setMethod.invoke(terminal, serverId);
break;
}
}
} catch (Throwable t) {
LOGGER.warn("HighlightButton onHighlight 处理异常", t);
}
}
}
}
}

View File

@ -0,0 +1,475 @@
package com.extendedae_plus.mixin.extendedae.client.gui;
import appeng.client.gui.Icon;
import appeng.client.gui.implementations.PatternProviderScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.menu.SlotSemantics;
import com.extendedae_plus.NewIcon;
import com.extendedae_plus.api.ExPatternButtonsAccessor;
import com.extendedae_plus.config.ModConfigs;
import com.glodblock.github.extendedae.client.button.ActionEPPButton;
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.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
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;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import static com.extendedae_plus.util.ExtendedAELogger.LOGGER;
@Mixin(GuiExPatternProvider.class)
public abstract class GuiExPatternProviderMixin extends PatternProviderScreen<ContainerExPatternProvider> implements ExPatternButtonsAccessor, com.extendedae_plus.api.ExPatternPageAccessor {
@Unique
ScreenStyle eap$screenStyle;
// 跟踪上次屏幕尺寸处理 GUI 缩放/窗口大小变化后按钮丢失问题
@Unique private int eap$lastScreenWidth = -1;
@Unique private int eap$lastScreenHeight = -1;
// 不再使用右侧 VerticalButtonBar直接把按钮注册为独立 AE2 小部件
@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);
}
// 移除手动挪动 Slot 坐标交由 SlotGridLayout + 原生布局控制
@Unique
private int getCurrentPage() {
// 优先使用本地 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 = 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) {}
}
}
public ActionEPPButton nextPage;
public ActionEPPButton prevPage;
public ActionEPPButton x2Button;
public ActionEPPButton divideBy2Button;
public ActionEPPButton x5Button;
public ActionEPPButton divideBy5Button;
public ActionEPPButton x10Button;
public ActionEPPButton divideBy10Button;
// 在构造器返回后初始化按钮与翻页控制
@Inject(method = "<init>", at = @At("RETURN"))
private void injectInit(ContainerExPatternProvider menu, Inventory playerInventory, Component title, ScreenStyle style, CallbackInfo ci) {
this.eap$screenStyle = style;
// 保留不再打印菜单类型
// 计算并下发 maxPage配置优先其次按槽位总数计算
int totalSlots = this.getMenu().getSlots(SlotSemantics.ENCODED_PATTERN).size();
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 = Math.max(this.eap$maxPageLocal, getMaxPage());
int newPage = (currentPage - 1 + maxPage) % maxPage;
try {
ContainerExPatternProvider menu1 = this.getMenu();
// 尝试调用 setPage
try {
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 = Math.max(this.eap$maxPageLocal, getMaxPage());
int newPage = (currentPage + 1) % maxPage;
try {
ContainerExPatternProvider menu1 = this.getMenu();
// 尝试调用 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);
}
// 倍增/除法按钮通过 ExtendedAE 的通用包派发
this.x2Button = new ActionEPPButton((b) -> {
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("multiply2"));
}, NewIcon.MULTIPLY2);
this.x2Button.setVisibility(true);
this.divideBy2Button = new ActionEPPButton((b) -> {
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("divide2"));
}, NewIcon.DIVIDE2);
this.divideBy2Button.setVisibility(true);
this.x10Button = new ActionEPPButton((b) -> {
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("multiply10"));
}, NewIcon.MULTIPLY10);
this.x10Button.setVisibility(true);
this.divideBy10Button = new ActionEPPButton((b) -> {
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("divide10"));
}, NewIcon.DIVIDE10);
this.divideBy10Button.setVisibility(true);
this.divideBy5Button = new ActionEPPButton((b) -> {
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("divide5"));
}, NewIcon.DIVIDE5);
this.divideBy5Button.setVisibility(true);
this.x5Button = new ActionEPPButton((b) -> {
EPPNetworkHandler.INSTANCE.sendToServer(new CGenericPacket("multiply5"));
}, NewIcon.MULTIPLY5);
this.x5Button.setVisibility(true);
// 注册可渲染按钮
this.addRenderableWidget(this.divideBy2Button);
this.addRenderableWidget(this.x2Button);
this.addRenderableWidget(this.divideBy5Button);
this.addRenderableWidget(this.x5Button);
this.addRenderableWidget(this.divideBy10Button);
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);
}
// 若从 JEI 配方界面返回后Screen renderables/children 可能被清空导致按钮丢失
// 这里在每帧保证这些按钮存在于渲染列表中不存在则重新注册
try {
if (this.divideBy2Button != null && !this.renderables.contains(this.divideBy2Button)) {
this.addRenderableWidget(this.divideBy2Button);
}
if (this.x2Button != null && !this.renderables.contains(this.x2Button)) {
this.addRenderableWidget(this.x2Button);
}
if (this.divideBy5Button != null && !this.renderables.contains(this.divideBy5Button)) {
this.addRenderableWidget(this.divideBy5Button);
}
if (this.x5Button != null && !this.renderables.contains(this.x5Button)) {
this.addRenderableWidget(this.x5Button);
}
if (this.divideBy10Button != null && !this.renderables.contains(this.divideBy10Button)) {
this.addRenderableWidget(this.divideBy10Button);
}
if (this.x10Button != null && !this.renderables.contains(this.x10Button)) {
this.addRenderableWidget(this.x10Button);
}
} catch (Throwable ignored) {}
// 如果屏幕尺寸发生变化窗口/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);
}
}
/**
* 在服务器端执行样板缩放操作单机模式
*/
@Unique
private void executePatternScalingOnServer(net.minecraft.server.level.ServerPlayer serverPlayer, String scalingType, double scaleFactor) {
try {
// 将实际逻辑切换到服务端主线程执行避免跨线程访问导致读取到空库存
serverPlayer.getServer().execute(() -> {
try {
// 直接基于容器槽位操作完全绕开 PatternProviderLogic 及其内部字段
if (!(serverPlayer.containerMenu instanceof com.glodblock.github.extendedae.container.ContainerExPatternProvider exMenu)) {
return;
}
int scaled = 0;
int failed = 0;
int total = 0;
final int scale = (int) Math.round(scaleFactor);
final boolean div = !"MULTIPLY".equals(scalingType);
java.util.List<net.minecraft.world.inventory.Slot> slots = exMenu.getSlots(appeng.menu.SlotSemantics.ENCODED_PATTERN);
for (var slot : slots) {
var stack = slot.getItem();
if (stack.getItem() instanceof appeng.crafting.pattern.EncodedPatternItem patternItem) {
total++;
var detail = patternItem.decode(stack, serverPlayer.level(), false);
if (detail instanceof appeng.crafting.pattern.AEProcessingPattern process) {
var input = process.getSparseInputs();
var output = process.getOutputs();
// 检查是否可修改来源ExtendedAE ContainerPatternModifier.checkModify
if (checkModifyLikeExtendedAE(input, scale, div) && checkModifyLikeExtendedAE(output, scale, div)) {
var mulInput = new appeng.api.stacks.GenericStack[input.length];
var mulOutput = new appeng.api.stacks.GenericStack[output.length];
modifyStacksLikeExtendedAE(input, mulInput, scale, div);
modifyStacksLikeExtendedAE(output, mulOutput, scale, div);
var newPattern = appeng.api.crafting.PatternDetailsHelper.encodeProcessingPattern(mulInput, mulOutput);
if (slot instanceof appeng.menu.slot.AppEngSlot as) {
as.set(newPattern);
} else {
slot.set(newPattern);
}
scaled++;
} else {
failed++;
}
} else {
// 非处理样板跳过
failed++;
}
}
}
// 构造结果并回显
String message;
if (scaled == 0) {
message = String.format(
" ExtendedAE Plus: 样板%s完成但未处理任何样板。共发现 %d 个样板,失败 %d 个(可能全为合成样板或数量不满足条件)",
div ? "除法" : "倍增", total, failed);
} else if (failed > 0) {
message = String.format("✅ ExtendedAE Plus: 样板%s完成处理了 %d 个,跳过 %d 个", div ? "除法" : "倍增", scaled, failed);
} else {
message = String.format("✅ ExtendedAE Plus: 样板%s成功处理了 %d 个", div ? "除法" : "倍增", scaled);
}
var minecraft = net.minecraft.client.Minecraft.getInstance();
if (minecraft.player != null) {
minecraft.player.displayClientMessage(net.minecraft.network.chat.Component.literal(message), true);
}
} catch (Exception ignored) {
}
});
} catch (Exception ignored) {
}
}
@Unique
private boolean checkModifyLikeExtendedAE(appeng.api.stacks.GenericStack[] stacks, int scale, boolean div) {
if (div) {
for (var stack : stacks) {
if (stack != null) {
if (stack.amount() % scale != 0) {
return false;
}
}
}
} else {
for (var stack : stacks) {
if (stack != null) {
long upper = 999999L * stack.what().getAmountPerUnit();
if (stack.amount() * scale > upper) {
return false;
}
}
}
}
return true;
}
@Unique
private void modifyStacksLikeExtendedAE(appeng.api.stacks.GenericStack[] stacks,
appeng.api.stacks.GenericStack[] des,
int scale,
boolean div) {
for (int i = 0; i < stacks.length; i++) {
if (stacks[i] != null) {
long amt = div ? stacks[i].amount() / scale : stacks[i].amount() * scale;
des[i] = new appeng.api.stacks.GenericStack(stacks[i].what(), amt);
}
}
}
}

View File

@ -0,0 +1,556 @@
package com.extendedae_plus.mixin.extendedae.client.gui;
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.style.ScreenStyle;
import appeng.client.gui.widgets.AETextField;
import appeng.client.gui.widgets.IconButton;
import appeng.menu.AEBaseMenu;
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;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.renderer.Rect2i;
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.world.entity.player.Inventory;
import net.minecraft.world.inventory.Slot;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.*;
@Pseudo
@Mixin(value = GuiExPatternTerminal.class)
public abstract class GuiExPatternTerminalMixin extends AEBaseScreen<AEBaseMenu> {
@Unique
private static final String UPLOAD_SUCCESS_MESSAGE = "✅ ExtendedAE Plus: 样板快速上传成功!";
@Unique
private static final String UPLOAD_FAILED_MESSAGE = "❌ ExtendedAE Plus: 样板上传失败,请检查供应器状态";
@Unique
private static final String NO_PROVIDER_MESSAGE = "ExtendedAE Plus: 请先选择一个样板供应器点击GroupHeader旁的按钮";
@Unique
private IconButton eap$toggleSlotsButton;
@Unique
private boolean eap$showSlots = false; // 默认由配置初始化
@Unique
private long eap$currentlyChoicePatterProvider = -1; // 当前选择的样板供应器ID
@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;
@Shadow(remap = false) private Set<PatternContainerRecord> matchedProvider;
public GuiExPatternTerminalMixin(AEBaseMenu menu, Inventory playerInventory, Component title, ScreenStyle style) {
super(menu, playerInventory, title, style);
}
/**
* 获取当前选择的样板供应器ID
*/
@Unique
public long getCurrentlyChoicePatternProvider() {
return eap$currentlyChoicePatterProvider;
}
/**
* 设置当前选择的样板供应器ID
*/
@Unique
public void setCurrentlyChoicePatternProvider(long id) {
this.eap$currentlyChoicePatterProvider = id;
}
/**
* 拦截鼠标点击事件实现Shift+左键快速上传样板功能
* 注意某些整合包的 ExtendedAE 版本不在该类中覆写 mouseClicked此处设置 require=0 以防止注入失败导致崩溃
*/
@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()) {
// 获取点击的槽位
Slot hoveredSlot = this.getSlotUnderMouse();
if (hoveredSlot != null && hoveredSlot.container == this.minecraft.player.getInventory()) {
// 点击的是玩家背包槽位
ItemStack clickedItem = hoveredSlot.getItem();
// 检查是否是有效的编码样板
if (!clickedItem.isEmpty() && PatternDetailsHelper.isEncodedPattern(clickedItem)) {
// 检查是否选择了样板供应器
if (eap$currentlyChoicePatterProvider != -1) {
// 执行快速上传
this.eap$quickUploadPattern(hoveredSlot.getSlotIndex());
// 取消默认的点击行为
cir.setReturnValue(true);
} else {
// 显示提示消息请先选择一个样板供应器
if (this.minecraft.player != null) {
this.minecraft.player.displayClientMessage(
Component.literal("ExtendedAE Plus: 请先选择一个样板供应器点击GroupHeader旁的按钮"),
true
);
}
}
}
}
}
}
/**
* 快速上传样板到当前选择的供应器
*/
@Unique
private void eap$quickUploadPattern(int playerSlotIndex) {
if (this.minecraft.player != null) {
// 获取要上传的物品
ItemStack itemToUpload = this.minecraft.player.getInventory().getItem(playerSlotIndex);
if (!itemToUpload.isEmpty() && PatternDetailsHelper.isEncodedPattern(itemToUpload)) {
// 通过反射调用 ExtendedAE 的网络发送软依赖
try {
Class<?> EPPNetworkHandlerClass = Class.forName("com.glodblock.github.extendedae.network.EPPNetworkHandler");
Object handlerInstance = EPPNetworkHandlerClass.getField("INSTANCE").get(null);
Class<?> packetClass = Class.forName("com.glodblock.github.glodium.network.packet.CGenericPacket");
Constructor<?> constructor = packetClass.getConstructor(String.class, Object[].class);
Object packet = constructor.newInstance("upload", new Object[]{playerSlotIndex, eap$currentlyChoicePatterProvider});
Class<?> iMessage = Class.forName("com.glodblock.github.glodium.network.packet.IMessage");
Method sendToServer = EPPNetworkHandlerClass.getMethod("sendToServer", iMessage);
sendToServer.invoke(handlerInstance, packet);
} catch (Throwable t) {
this.minecraft.player.displayClientMessage(
Component.literal("❌ ExtendedAE Plus: 未找到 ExtendedAE 网络支持(可能未安装或版本不兼容)"),
true
);
}
} else {
this.minecraft.player.displayClientMessage(
Component.literal("❌ ExtendedAE Plus: 无效的样板物品"),
true
);
}
}
}
@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避免取到父类导致失败
GuiExPatternTerminalAccessor acc = (GuiExPatternTerminalAccessor) this;
ArrayList<?> rows = acc.getRows();
// 找到该分组对应的第一个 PatternContainerRecord
Class<?> cls = 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)再取第一个元素
Collection<?> containers = (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")
HashMap<Long, Object> infoMap = (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
*/
@Unique
public void resetCurrentlyChoicePatternProvider() {
this.eap$currentlyChoicePatterProvider = -1;
}
@Inject(method = "<init>", at = @At("TAIL"), remap = false)
private void injectConstructor(CallbackInfo ci) {
// 根据配置初始化默认显示/隐藏状态
try {
this.eap$showSlots = ModConfigs.PATTERN_TERMINAL_SHOW_SLOTS_DEFAULT.get();
} catch (Throwable ignored) {
}
// 创建切换槽位显示的按钮
this.eap$toggleSlotsButton = new IconButton((b) -> {
this.eap$showSlots = !this.eap$showSlots; // 开关状态
// 通过反射调用refreshList方法 - 先尝试当前类失败后尝试父类
try {
Method refreshMethod = null;
try {
// 先尝试在当前类中查找
refreshMethod = this.getClass().getDeclaredMethod("refreshList");
} catch (NoSuchMethodException e1) {
// 如果当前类没有尝试在父类中查找
try {
refreshMethod = this.getClass().getSuperclass().getDeclaredMethod("refreshList");
} catch (NoSuchMethodException e2) {
throw e2;
}
}
refreshMethod.setAccessible(true);
refreshMethod.invoke(this);
} catch (Exception ignored) {
}
}) {
@Override
protected Icon getIcon() {
return eap$showSlots ? Icon.PATTERN_ACCESS_HIDE : Icon.PATTERN_ACCESS_SHOW;
}
};
// 设置按钮提示文本
this.eap$toggleSlotsButton.setTooltip(Tooltip.create(Component.translatable("gui.expatternprovider.toggle_slots")));
// 添加到左侧工具栏
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) {
// 更新按钮图标
if (this.eap$toggleSlotsButton != null) {
this.eap$toggleSlotsButton.setTooltip(Tooltip.create(Component.translatable(
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)
private void onRefreshListEnd(CallbackInfo ci) {
// 在refreshList结束后根据showSlots状态过滤SlotsRow
if (!this.eap$showSlots) {
try {
// 通过反射访问rows字段 - 先尝试当前类失败后尝试父类
java.lang.reflect.Field rowsField = null;
try {
// 先尝试在当前类中查找
rowsField = this.getClass().getDeclaredField("rows");
} catch (NoSuchFieldException e1) {
// 如果当前类没有尝试在父类中查找
try {
rowsField = this.getClass().getSuperclass().getDeclaredField("rows");
} catch (NoSuchFieldException e2) {
throw e2;
}
}
rowsField.setAccessible(true);
java.util.ArrayList<?> rows = (java.util.ArrayList<?>) rowsField.get(this);
// 通过反射访问highlightBtns字段
java.lang.reflect.Field highlightBtnsField = null;
try {
// 先尝试在当前类中查找
highlightBtnsField = this.getClass().getDeclaredField("highlightBtns");
} catch (NoSuchFieldException e1) {
// 如果当前类没有尝试在父类中查找
try {
highlightBtnsField = this.getClass().getSuperclass().getDeclaredField("highlightBtns");
} catch (NoSuchFieldException e2) {
throw e2;
}
}
highlightBtnsField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.HashMap<Integer, Object> highlightBtns = (java.util.HashMap<Integer, Object>) highlightBtnsField.get(this);
// 创建新的索引映射
java.util.HashMap<Integer, Object> newHighlightBtns = new java.util.HashMap<>();
int newIndex = 0;
// 移除所有SlotsRow只保留GroupHeaderRow同时重新映射高亮按钮索引
for (int i = 0; i < rows.size(); i++) {
Object row = rows.get(i);
String className = row.getClass().getSimpleName();
if (className.equals("GroupHeaderRow")) {
// 保留GroupHeaderRow并重新映射对应的高亮按钮
@SuppressWarnings("unchecked")
java.util.ArrayList<Object> typedRows = (java.util.ArrayList<Object>) rows;
typedRows.set(newIndex, row);
// 查找原来在这个位置的高亮按钮
// 原始代码中高亮按钮的索引是在添加GroupHeaderRow之后添加第一个SlotsRow之前设置的
// 所以按钮的索引指向的是第一个SlotsRow的位置
// 我们需要查找索引为 i+1 的按钮第一个SlotsRow的位置
if (highlightBtns.containsKey(i + 1)) {
Object button = highlightBtns.get(i + 1);
newHighlightBtns.put(newIndex, button);
}
newIndex++;
} else if (className.equals("SlotsRow")) {
// 不保留SlotsRow也不增加newIndex
}
}
// 移除多余的行
while (rows.size() > newIndex) {
rows.remove(rows.size() - 1);
}
// 更新highlightBtns
highlightBtns.clear();
highlightBtns.putAll(newHighlightBtns);
// 强制刷新滚动条
try {
Method resetScrollbarMethod = null;
try {
// 先尝试在当前类中查找
resetScrollbarMethod = this.getClass().getDeclaredMethod("resetScrollbar");
} catch (NoSuchMethodException e1) {
// 如果当前类没有尝试在父类中查找
try {
resetScrollbarMethod = this.getClass().getSuperclass().getDeclaredMethod("resetScrollbar");
} catch (NoSuchMethodException e2) {
throw e2;
}
}
resetScrollbarMethod.setAccessible(true);
resetScrollbarMethod.invoke(this);
} catch (Exception ignored) {
}
} catch (Exception ignored) {
}
}
}
@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 获取必要的字段避免反射失败
GuiExPatternTerminalAccessor acc = (GuiExPatternTerminalAccessor) this;
java.util.ArrayList<?> rows = acc.getRows();
int currentScroll = acc.getScrollbar().getCurrentScroll();
// 直接引用目标类以获取其静态常量
Class<?> cls = 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 - 11;
int by = this.topPos + GUI_PADDING_Y + GUI_HEADER_HEIGHT + i * ROW_HEIGHT - 2;
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())
|| (this.searchInField != null && !this.searchInField.getValue().isEmpty());
if (!searchActive) {
return;
}
// 使用 GuiUtil 的通用绘制方法绘制槽位高亮包含彩虹流转效果
GuiUtil.drawPatternSlotHighlights(guiGraphics, this.menu.slots, this.matchedStack, this.matchedProvider);
}
@Unique
private void eap$fill(GuiGraphics guiGraphics, Rect2i rect, int argb) {
this.fillRect(guiGraphics, rect, argb);
}
}

View File

@ -0,0 +1,26 @@
package com.extendedae_plus.mixin.extendedae.common;
import com.extendedae_plus.config.ModConfigs;
import com.glodblock.github.extendedae.common.parts.PartExPatternProvider;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(value = PartExPatternProvider.class, priority = 3000, remap = false)
public abstract class PartExPatternProviderMixin {
@ModifyArg(
method = "createLogic",
at = @At(
value = "INVOKE",
target = "Lappeng/helpers/patternprovider/PatternProviderLogic;<init>(Lappeng/api/networking/IManagedGridNode;Lappeng/helpers/patternprovider/PatternProviderLogicHost;I)V"
),
index = 2
)
private int eap$multiplyCapacity(int original) {
int mult = ModConfigs.PAGE_MULTIPLIER.get();
if (mult < 1) mult = 1;
if (mult > 64) mult = 64;
return Math.max(1, original) * mult;
}
}

View File

@ -0,0 +1,26 @@
package com.extendedae_plus.mixin.extendedae.common;
import com.extendedae_plus.config.ModConfigs;
import com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(value = TileExPatternProvider.class, priority = 3000, remap = false)
public abstract class TileExPatternProviderMixin {
@ModifyArg(
method = "createLogic",
at = @At(
value = "INVOKE",
target = "Lappeng/helpers/patternprovider/PatternProviderLogic;<init>(Lappeng/api/networking/IManagedGridNode;Lappeng/helpers/patternprovider/PatternProviderLogicHost;I)V"
),
index = 2
)
private int eap$multiplyCapacity(int original) {
int mult = ModConfigs.PAGE_MULTIPLIER.get();
if (mult < 1) mult = 1;
if (mult > 64) mult = 64;
return Math.max(1, original) * mult;
}
}

View File

@ -0,0 +1,166 @@
package com.extendedae_plus.mixin.extendedae.container;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.stacks.GenericStack;
import appeng.crafting.pattern.AEProcessingPattern;
import appeng.crafting.pattern.EncodedPatternItem;
import appeng.helpers.patternprovider.PatternProviderLogicHost;
import appeng.menu.SlotSemantics;
import appeng.menu.guisync.GuiSync;
import appeng.menu.implementations.PatternProviderMenu;
import appeng.menu.slot.AppEngSlot;
import com.glodblock.github.extendedae.container.ContainerExPatternProvider;
import com.glodblock.github.glodium.network.packet.sync.IActionHolder;
import com.glodblock.github.glodium.network.packet.sync.Paras;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.inventory.MenuType;
import net.minecraft.world.inventory.Slot;
import org.jetbrains.annotations.NotNull;
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;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
@Mixin(value = ContainerExPatternProvider.class, priority = 3000)
public abstract class ContainerExPatternProviderMixin extends PatternProviderMenu implements IActionHolder {
// 使用高位唯一ID避免与其他模组在同一类上的 @GuiSync 冲突
@GuiSync(31415)
@Unique
public int eap$page = 0;
@Unique
public int eap$maxPage = 0;
@Unique
private static final int SLOTS_PER_PAGE = 36; // 每页显示36个槽位
@Unique
private final Map<String, Consumer<Paras>> eap$actions = createHolder();
public ContainerExPatternProviderMixin(MenuType<? extends PatternProviderMenu> menuType, int id, Inventory playerInventory, PatternProviderLogicHost host) {
super(menuType, id, playerInventory, host);
}
@Unique
public void eap$showPage() {
List<Slot> slots = this.getSlots(SlotSemantics.ENCODED_PATTERN);
int totalSlots = slots.size();
// 如果总槽位数不超过36个不需要翻页
if (totalSlots <= SLOTS_PER_PAGE) {
for (Slot s : slots) {
((AppEngSlot) s).setActive(true);
}
return;
}
int slot_id = 0;
for (Slot s : slots) {
int page_id = slot_id / SLOTS_PER_PAGE;
// 当前页的槽位激活
// 其他页的槽位隐藏
((AppEngSlot) s).setActive(page_id == this.eap$page);
++slot_id;
}
}
@Inject(method = "<init>", at = @At("TAIL"))
public void init(int id, Inventory playerInventory, PatternProviderLogicHost host, CallbackInfo ci) {
int maxSlots = this.getSlots(SlotSemantics.ENCODED_PATTERN).size();
this.eap$maxPage = (maxSlots + SLOTS_PER_PAGE - 1) / SLOTS_PER_PAGE;
// 注册通用动作 CGenericPacket 分发
this.eap$actions.put("multiply2", p -> { eap$modifyPatterns(2, false); });
this.eap$actions.put("divide2", p -> { eap$modifyPatterns(2, true); });
this.eap$actions.put("multiply5", p -> { eap$modifyPatterns(5, false); });
this.eap$actions.put("divide5", p -> { eap$modifyPatterns(5, true); });
this.eap$actions.put("multiply10", p -> { eap$modifyPatterns(10, false);});
this.eap$actions.put("divide10", p -> { eap$modifyPatterns(10, true); });
}
@Unique
public int getPage() {
return this.eap$page;
}
@Unique
public void setPage(int page) {
this.eap$page = page;
}
@Unique
private void eap$modifyPatterns(int scale, boolean div) {
if (scale <= 0) return;
for (var slot : this.getSlots(SlotSemantics.ENCODED_PATTERN)) {
var stack = slot.getItem();
if (stack.getItem() instanceof EncodedPatternItem pattern) {
var detail = pattern.decode(stack, this.getPlayer().level(), false);
if (detail instanceof AEProcessingPattern process) {
var input = process.getSparseInputs();
var output = process.getOutputs();
if (eap$checkModify(input, scale, div) && eap$checkModify(output, scale, div)) {
var mulInput = new GenericStack[input.length];
var mulOutput = new GenericStack[output.length];
eap$modifyStacks(input, mulInput, scale, div);
eap$modifyStacks(output, mulOutput, scale, div);
var newPattern = PatternDetailsHelper.encodeProcessingPattern(mulInput, mulOutput);
slot.set(newPattern);
}
}
}
}
}
@Unique
private boolean eap$checkModify(GenericStack[] stacks, int scale, boolean div) {
if (stacks == null) return false;
if (div) {
for (var stack : stacks) {
if (stack != null) {
if (stack.amount() % scale != 0) {
return false;
}
}
}
return true;
} else {
for (var stack : stacks) {
if (stack != null) {
long upper = 999999L * stack.what().getAmountPerUnit();
if (stack.amount() * scale > upper) {
return false;
}
}
}
return true;
}
}
@Unique
private void eap$modifyStacks(GenericStack[] src, GenericStack[] dst, int scale, boolean div) {
for (int i = 0; i < src.length; i++) {
var stack = src[i];
if (stack != null) {
long amt = stack.amount();
long newAmt = div ? (amt / scale) : (amt * scale);
dst[i] = new GenericStack(stack.what(), newAmt);
} else {
dst[i] = null;
}
}
}
@NotNull
@Override
public Map<String, Consumer<Paras>> getActionMap() {
return this.eap$actions;
}
}

View File

@ -0,0 +1,197 @@
package com.extendedae_plus.mixin.extendedae.container;
import appeng.api.util.IConfigurableObject;
import appeng.menu.guisync.GuiSync;
import com.extendedae_plus.util.ExtendedAEPatternUploadUtil;
import com.glodblock.github.extendedae.container.ContainerExPatternTerminal;
import com.glodblock.github.glodium.network.packet.sync.IActionHolder;
import com.glodblock.github.glodium.network.packet.sync.Paras;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.registries.Registries;
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.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.MenuProvider;
import net.minecraft.world.entity.player.Player;
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.minecraftforge.network.NetworkHooks;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
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;
import java.util.Map;
import java.util.function.Consumer;
@Mixin(ContainerExPatternTerminal.class)
public abstract class ContainerExPatternTerminalMixin implements IActionHolder {
@GuiSync(25564)
@Unique
public boolean eap$hidePatternSlots = false;
@Unique
public boolean isHidePatternSlots() {
return this.eap$hidePatternSlots;
}
@Unique
public void setHidePatternSlots(boolean hide) {
this.eap$hidePatternSlots = hide;
}
@Unique
public void toggleHidePatternSlots() {
this.eap$hidePatternSlots = !this.eap$hidePatternSlots;
}
@Unique
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 -> {
try {
Object o0 = p.get(0);
Object o1 = p.get(1);
int playerSlotIndex = (o0 instanceof Number) ? ((Number) o0).intValue() : Integer.parseInt(String.valueOf(o0));
long providerId = (o1 instanceof Number) ? ((Number) o1).longValue() : Long.parseLong(String.valueOf(o1));
var sp = (ServerPlayer) this.epp$player;
ExtendedAEPatternUploadUtil.uploadPatternToProvider(sp, playerSlotIndex, providerId);
} 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
@Override
public Map<String, Consumer<Paras>> getActionMap() {
return this.eap$actions;
}
}

View File

@ -0,0 +1,57 @@
package com.extendedae_plus.mixin.extendedae.container;
import com.extendedae_plus.util.ExtendedAEPatternUploadUtil;
import com.glodblock.github.extendedae.common.me.itemhost.HostWirelessExPAT;
import com.glodblock.github.extendedae.container.ContainerWirelessExPAT;
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 org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Pseudo;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
import java.util.function.Consumer;
/**
* 为无线样板访问终端容器注册通用动作CGenericPacket 分发
*/
@Pseudo
@Mixin(ContainerWirelessExPAT.class)
public abstract class ContainerWirelessExPatternTerminalMixin implements IActionHolder {
@Unique
private final Map<String, Consumer<Paras>> eap$actions = createHolder();
@Unique
private Player epp$player;
// 明确目标构造签名<init>(int, Inventory, HostWirelessExPAT)
@Inject(method = "<init>(ILnet/minecraft/world/entity/player/Inventory;Lcom/glodblock/github/extendedae/common/me/itemhost/HostWirelessExPAT;)V", at = @At("TAIL"), require = 0)
private void init(int id, net.minecraft.world.entity.player.Inventory playerInventory, HostWirelessExPAT host, CallbackInfo ci) {
this.epp$player = playerInventory.player;
// 注册上传动作参数顺序必须与客户端 CGenericPacket 保持一致
this.eap$actions.put("upload", p -> {
try {
Object o0 = p.get(0);
Object o1 = p.get(1);
int playerSlotIndex = (o0 instanceof Number) ? ((Number) o0).intValue() : Integer.parseInt(String.valueOf(o0));
long providerId = (o1 instanceof Number) ? ((Number) o1).longValue() : Long.parseLong(String.valueOf(o1));
var sp = (ServerPlayer) this.epp$player;
ExtendedAEPatternUploadUtil.uploadPatternToProvider(sp, playerSlotIndex, providerId);
} catch (Throwable ignored) {
}
});
}
@NotNull
@Override
public Map<String, Consumer<Paras>> getActionMap() {
return this.eap$actions;
}
}

View File

@ -0,0 +1,33 @@
package com.extendedae_plus.mixin.hooks;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import com.extendedae_plus.hooks.BuiltInModelHooks;
/**
* 复制 MAE2/AE2 的做法在模型加载时优先查询我们的内置模型表
* 若命中则缓存并阻止继续查找 JSON 模型
*/
@Mixin(ModelBakery.class)
public class ModelBakeryMixin {
@Inject(method = "loadModel", at = @At("HEAD"), cancellable = true)
private void extendedae_plus$loadModelHook(ResourceLocation id, CallbackInfo ci) {
var model = BuiltInModelHooks.getBuiltInModel(id);
if (model != null) {
cacheAndQueueDependencies(id, model);
ci.cancel();
}
}
@Shadow
protected void cacheAndQueueDependencies(ResourceLocation id, UnbakedModel unbakedModel) {
}
}

View File

@ -0,0 +1,58 @@
package com.extendedae_plus.mixin.jei;
import appeng.integration.modules.jei.transfer.EncodePatternTransferHandler;
import appeng.integration.modules.jeirei.EncodingHelper;
import appeng.menu.me.items.PatternEncodingTermMenu;
import com.extendedae_plus.util.ExtendedAEPatternUploadUtil;
import mezz.jei.api.gui.ingredient.IRecipeSlotsView;
import mezz.jei.api.recipe.transfer.IRecipeTransferError;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.crafting.Recipe;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
/**
* 捕获通过 JEI 点击 + 填充到样板编码终端的处理配方并记录其工艺名称烧炼
*/
@Mixin(value = EncodePatternTransferHandler.class, remap = false)
public abstract class EncodePatternTransferHandlerMixin {
@Inject(method = "transferRecipe", at = @At("HEAD"), require = 0)
private void extendedae_plus$captureProcessingName(PatternEncodingTermMenu menu,
Object recipeBase,
IRecipeSlotsView slotsView,
Player player,
boolean maxTransfer,
boolean doTransfer,
CallbackInfoReturnable<IRecipeTransferError> cir) {
if (!doTransfer) return;
String name = null;
if (recipeBase instanceof Recipe<?> recipe) {
// 仅记录处理配方 3x3 合成
if (EncodingHelper.isSupportedCraftingRecipe(recipe)) return;
name = ExtendedAEPatternUploadUtil.mapRecipeTypeToSearchKey(recipe);
} else if (recipeBase != null &&
"com.gregtechceu.gtceu.api.recipe.GTRecipe".equals(recipeBase.getClass().getName())) {
// 反射路径GTCEu 专用 GTRecipeType 提取注册ID并映射为中文或path
name = ExtendedAEPatternUploadUtil.mapGTCEuRecipeToSearchKey(recipeBase);
} else if ("com.gregtechceu.gtceu.integration.jei.recipe.GTRecipeWrapper".equals(recipeBase.getClass().getName())) {
// 通过反射处理 GTCEu JEI 包装类避免硬依赖
try {
var field = recipeBase.getClass().getField("recipe"); // public final GTRecipe recipe;
Object inner = field.get(recipeBase);
// 反射路径将内部 GTRecipe Object 传入
name = ExtendedAEPatternUploadUtil.mapGTCEuRecipeToSearchKey(inner);
} catch (Throwable ignored) {
// 反射失败则继续走通用回退
}
} else {
// 非原版 Recipe<?> JEI 条目尝试从类名/包名推导关键词
name = ExtendedAEPatternUploadUtil.deriveSearchKeyFromUnknownRecipe(recipeBase);
}
if (name != null && !name.isBlank()) {
ExtendedAEPatternUploadUtil.setLastProcessingName(name);
}
}
}

View File

@ -0,0 +1,37 @@
package com.extendedae_plus.mixin.jei;
import appeng.api.stacks.AEFluidKey;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.AEKey;
import appeng.integration.modules.jeirei.EncodingHelper;
import appeng.menu.me.common.GridInventoryEntry;
import appeng.menu.me.common.MEStorageMenu;
import com.extendedae_plus.integration.jei.JeiRuntimeProxy;
import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.forge.ForgeTypes;
import mezz.jei.api.ingredients.ITypedIngredient;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@Mixin(EncodingHelper.class)
public class EncodingHelperMixin {
// 客户端注入优先使用JEI书签的物品流体
@Inject(method = "getIngredientPriorities", at = @At("TAIL"), cancellable = true, remap = false)
private static void epp$addJeiIngredientPriorities(MEStorageMenu menu, Comparator<GridInventoryEntry> comparator, CallbackInfoReturnable<Map<AEKey, Integer>> cir){
Map<AEKey, Integer> result = cir.getReturnValue();
AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE);
List<? extends ITypedIngredient<?>> list = JeiRuntimeProxy.getBookmarkList();
for (ITypedIngredient<?> ingredient : list) {
ingredient.getIngredient(VanillaTypes.ITEM_STACK).ifPresent(itemStack -> result.put(AEItemKey.of(itemStack), index.getAndDecrement()));
ingredient.getIngredient(ForgeTypes.FLUID_STACK).ifPresent(fluidStack -> result.put(AEFluidKey.of(fluidStack), index.getAndDecrement()));
}
cir.setReturnValue(result);
}
}

View File

@ -0,0 +1,12 @@
package com.extendedae_plus.mixin.jei.accessor;
import mezz.jei.gui.bookmarks.BookmarkList;
import mezz.jei.gui.overlay.bookmarks.BookmarkOverlay;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(BookmarkOverlay.class)
public interface BookmarkOverlayAccessor {
@Accessor("bookmarkList")
BookmarkList eap$getBookmarkList();
}

View File

@ -0,0 +1,41 @@
package com.extendedae_plus.network;
import com.extendedae_plus.client.ClientAdvancedBlockingState;
import net.minecraft.network.FriendlyByteBuf;
import net.neoforged.neoforge.network.NetworkEvent;
import java.util.function.Supplier;
public class AdvancedBlockingSyncS2CPacket {
private final String dimensionId;
private final long blockPosLong;
private final boolean enabled;
public AdvancedBlockingSyncS2CPacket(String dimensionId, long blockPosLong, boolean enabled) {
this.dimensionId = dimensionId;
this.blockPosLong = blockPosLong;
this.enabled = enabled;
}
public static void encode(AdvancedBlockingSyncS2CPacket msg, FriendlyByteBuf buf) {
buf.writeUtf(msg.dimensionId);
buf.writeLong(msg.blockPosLong);
buf.writeBoolean(msg.enabled);
}
public static AdvancedBlockingSyncS2CPacket decode(FriendlyByteBuf buf) {
String dim = buf.readUtf();
long pos = buf.readLong();
boolean en = buf.readBoolean();
return new AdvancedBlockingSyncS2CPacket(dim, pos, en);
}
public static void handle(AdvancedBlockingSyncS2CPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
ctx.enqueueWork(() -> {
String key = ClientAdvancedBlockingState.key(msg.dimensionId, msg.blockPosLong);
ClientAdvancedBlockingState.set(key, msg.enabled);
});
ctx.setPacketHandled(true);
}
}

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,167 @@
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.locator.MenuLocators;
import appeng.menu.me.crafting.CraftingCPUMenu;
import appeng.parts.AEBasePart;
import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor;
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 的样板供应器
* 打开该供应器自身的 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;
// 必须在 CraftingCPU 界面内
if (!(player.containerMenu instanceof CraftingCPUMenu menu)) {
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) {
return;
}
var cs = grid.getCraftingService();
if (!(cs instanceof CraftingService craftingService)) {
return;
}
// 1) 根据 AEKey 找到可能的样板pattern
Collection<IPatternDetails> patterns = craftingService.getCraftingFor(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;
// 跳过未连接到网格或不活跃的 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);
}
// 高亮打开的供应器位置并发送聊天提示
// 先在该 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 (Exception ignored) {
}
}
}
}
});
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

@ -0,0 +1,208 @@
package com.extendedae_plus.network;
import appeng.api.config.Settings;
import appeng.api.config.YesNo;
import appeng.api.networking.IGrid;
import appeng.blockentity.crafting.PatternProviderBlockEntity;
import appeng.helpers.patternprovider.PatternProviderLogic;
import appeng.helpers.patternprovider.PatternProviderLogicHost;
import appeng.parts.crafting.PatternProviderPart;
import com.extendedae_plus.api.AdvancedBlockingHolder;
import com.extendedae_plus.api.SmartDoublingHolder;
import com.extendedae_plus.content.controller.NetworkPatternControllerBlockEntity;
import net.minecraft.core.BlockPos;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
import java.util.Set;
import java.util.HashSet;
import java.util.function.Supplier;
/**
* C2S全网批量切换样板供应器的三种模式
* - 阻挡模式AE2 内置 BLOCKING_MODE 设置
* - 高级阻挡模式AdvancedBlockingHolder mixin
* - 智能翻倍模式SmartDoublingHolder mixin
*
* 负载为三个操作码各1字节分别对应blockingadvancedBlockingsmartDoubling
*/
public class GlobalToggleProviderModesC2SPacket {
public enum Op {
NOOP((byte) 0),
SET_TRUE((byte) 1),
SET_FALSE((byte) 2),
TOGGLE((byte) 3);
public final byte id;
Op(byte id) { this.id = id; }
public static Op byId(byte id) {
return switch (id) {
case 1 -> SET_TRUE;
case 2 -> SET_FALSE;
case 3 -> TOGGLE;
default -> NOOP;
};
}
}
private final Op opBlocking;
private final Op opAdvancedBlocking;
private final Op opSmartDoubling;
private final BlockPos controllerPos;
public GlobalToggleProviderModesC2SPacket(Op opBlocking, Op opAdvancedBlocking, Op opSmartDoubling, BlockPos controllerPos) {
this.opBlocking = opBlocking;
this.opAdvancedBlocking = opAdvancedBlocking;
this.opSmartDoubling = opSmartDoubling;
this.controllerPos = controllerPos;
}
public static void encode(GlobalToggleProviderModesC2SPacket msg, FriendlyByteBuf buf) {
buf.writeByte(msg.opBlocking.id);
buf.writeByte(msg.opAdvancedBlocking.id);
buf.writeByte(msg.opSmartDoubling.id);
buf.writeBlockPos(msg.controllerPos);
}
public static GlobalToggleProviderModesC2SPacket decode(FriendlyByteBuf buf) {
Op b = Op.byId(buf.readByte());
Op ab = Op.byId(buf.readByte());
Op sd = Op.byId(buf.readByte());
BlockPos pos = buf.readBlockPos();
return new GlobalToggleProviderModesC2SPacket(b, ab, sd, pos);
}
public static void handle(GlobalToggleProviderModesC2SPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
ctx.enqueueWork(() -> {
ServerPlayer player = ctx.getSender();
if (player == null) return;
// 从控制方块实体的 AE2 节点确定 AE 网络上下文
var level = player.serverLevel();
var be = level.getBlockEntity(msg.controllerPos);
if (!(be instanceof NetworkPatternControllerBlockEntity controller)) return;
var node = controller.getGridNode(null);
if (node == null) return;
IGrid grid = node.getGrid();
if (grid == null) return;
int affected = applyToAllProviders(grid, msg);
// 向发起玩家反馈影响数量便于判断按钮是否生效
player.displayClientMessage(Component.literal("E+ 全局切换已应用到 " + affected + " 个样板供应器"), true);
});
ctx.setPacketHandled(true);
}
private static int applyToAllProviders(IGrid grid, GlobalToggleProviderModesC2SPacket msg) {
int affected = 0;
// 去重集合避免同一逻辑重复计数
Set<PatternProviderLogic> all = new HashSet<>();
// 方块形式的样板供应器全部/在线
try {
Set<PatternProviderBlockEntity> blocksAll = grid.getMachines(PatternProviderBlockEntity.class);
Set<PatternProviderBlockEntity> blocksActive = grid.getActiveMachines(PatternProviderBlockEntity.class);
for (PatternProviderBlockEntity be : blocksAll) if (be != null && be.getLogic() != null) all.add(be.getLogic());
for (PatternProviderBlockEntity be : blocksActive) if (be != null && be.getLogic() != null) all.add(be.getLogic());
} catch (Throwable ignored) {}
// Part 形式的样板供应器全部/在线
try {
Set<PatternProviderPart> partsAll = grid.getMachines(PatternProviderPart.class);
Set<PatternProviderPart> partsActive = grid.getActiveMachines(PatternProviderPart.class);
for (PatternProviderPart part : partsAll) if (part != null && part.getLogic() != null) all.add(part.getLogic());
for (PatternProviderPart part : partsActive) if (part != null && part.getLogic() != null) all.add(part.getLogic());
} catch (Throwable ignored) {}
// 兼容任意实现了 PatternProviderLogicHost 的机器例如 ExtendedAE PartExPatternProvider
try {
Set<PatternProviderLogicHost> hostsAll = grid.getMachines(PatternProviderLogicHost.class);
Set<PatternProviderLogicHost> hostsActive = grid.getActiveMachines(PatternProviderLogicHost.class);
for (PatternProviderLogicHost host : hostsAll) if (host != null && host.getLogic() != null) all.add(host.getLogic());
for (PatternProviderLogicHost host : hostsActive) if (host != null && host.getLogic() != null) all.add(host.getLogic());
} catch (Throwable ignored) {}
// 兼容显式匹配第三方具体类通过反射避免 AE2 仅按精确类型匹配导致 interface 不返回的问题
collectByClassName(grid, all, "com.glodblock.github.extendedae.common.parts.PartExPatternProvider");
collectByClassName(grid, all, "com.glodblock.github.extendedae.common.tileentities.TileExPatternProvider");
for (PatternProviderLogic logic : all) {
if (applyToLogic(logic, msg)) affected++;
}
return affected;
}
private static void collectByClassName(IGrid grid, Set<PatternProviderLogic> out, String className) {
try {
Class<?> cls = Class.forName(className);
// 收集全部与在线两类机器
Set<?> all = grid.getMachines((Class) cls);
Set<?> active = grid.getActiveMachines((Class) cls);
for (Object o : all) addLogicIfPresent(out, o);
for (Object o : active) addLogicIfPresent(out, o);
} catch (Throwable ignored) {}
}
private static void addLogicIfPresent(Set<PatternProviderLogic> out, Object o) {
try {
if (o instanceof PatternProviderLogicHost host) {
var logic = host.getLogic();
if (logic != null) out.add(logic);
return;
}
// 兜底若对象有 getLogic 方法且返回 PatternProviderLogic
var m = o.getClass().getMethod("getLogic");
Object ret = m.invoke(o);
if (ret instanceof PatternProviderLogic logic) out.add(logic);
} catch (Throwable ignored) {}
}
private static boolean applyToLogic(PatternProviderLogic logic, GlobalToggleProviderModesC2SPacket msg) {
if (logic == null) return false;
boolean changed = false;
// 1) 阻挡模式AE2 内置设置
if (msg.opBlocking != Op.NOOP) {
boolean current = safeIsBlocking(logic);
boolean target = computeTarget(current, msg.opBlocking);
var cm = logic.getConfigManager();
if (cm != null) {
cm.putSetting(Settings.BLOCKING_MODE, target ? YesNo.YES : YesNo.NO);
changed = changed || (current != target);
}
}
// 2) 高级阻挡mixin 接口
if (msg.opAdvancedBlocking != Op.NOOP && logic instanceof AdvancedBlockingHolder adv) {
boolean current = adv.eap$getAdvancedBlocking();
boolean target = computeTarget(current, msg.opAdvancedBlocking);
adv.eap$setAdvancedBlocking(target);
changed = changed || (current != target);
}
// 3) 智能翻倍mixin 接口
if (msg.opSmartDoubling != Op.NOOP && logic instanceof SmartDoublingHolder sd) {
boolean current = sd.eap$getSmartDoubling();
boolean target = computeTarget(current, msg.opSmartDoubling);
sd.eap$setSmartDoubling(target);
changed = changed || (current != target);
}
// 保存更改并让 AE2 同步
if (changed) {
try { logic.saveChanges(); } catch (Throwable ignored) {}
}
return changed;
}
private static boolean computeTarget(boolean current, Op op) {
return switch (op) {
case SET_TRUE -> true;
case SET_FALSE -> false;
case TOGGLE -> !current;
default -> current;
};
}
private static boolean safeIsBlocking(PatternProviderLogic logic) {
try { return logic.isBlocking(); } catch (Throwable t) { return false; }
}
}

View File

@ -0,0 +1,19 @@
package com.extendedae_plus.network;
/**
* 临时的网络通道占位实现仅用于让 GUI 方案A 最小子集通过编译
* 后续将替换为 NeoForge SimpleChannel 正式实现
*/
public class ModNetwork {
public static final DummyChannel CHANNEL = new DummyChannel();
public static void register() {
// TODO: 后续接入 NeoForge SimpleChannel 正式注册
}
public static class DummyChannel {
public void sendToServer(Object any) {
// no-op 占位
}
}
}

View File

@ -0,0 +1,87 @@
package com.extendedae_plus.network;
import appeng.api.networking.IGrid;
import appeng.api.stacks.AEKey;
import appeng.api.stacks.GenericStack;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.menu.locator.MenuLocators;
import appeng.menu.me.crafting.CraftAmountMenu;
import com.extendedae_plus.menu.locator.CuriosItemLocator;
import com.extendedae_plus.util.WirelessTerminalLocator;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
/**
* C2S JEI 中键点击请求打开 AE 的下单界面
* 负载为一个 GenericStack物品或流体
*/
public class OpenCraftFromJeiC2SPacket {
private final GenericStack stack;
public OpenCraftFromJeiC2SPacket(GenericStack stack) {
this.stack = stack;
}
public static void encode(OpenCraftFromJeiC2SPacket msg, FriendlyByteBuf buf) {
GenericStack.writeBuffer(msg.stack, buf);
}
public static OpenCraftFromJeiC2SPacket decode(FriendlyByteBuf buf) {
var gs = GenericStack.readBuffer(buf);
return new OpenCraftFromJeiC2SPacket(gs);
}
public static void handle(OpenCraftFromJeiC2SPacket msg, Supplier<NetworkEvent.Context> ctx) {
var context = ctx.get();
context.enqueueWork(() -> {
ServerPlayer player = context.getSender();
if (player == null || msg.stack == null) return;
// 仅支持 AEKey 为可合成的种类
AEKey what = msg.stack.what();
// 定位无线终端
var located = WirelessTerminalLocator.find(player);
if (located.isEmpty()) return;
// 若为 Curios 槽位跳过 AE2 基类的距离/电量前置校验直接打开数量界面
// 让菜单与宿主WirelessTerminalMenuHost以及 ae2wtlib 自身处理量子卡跨维/跨距逻辑
String curiosSlotId = located.getCuriosSlotId();
int curiosIndex = located.getCuriosIndex();
if (curiosSlotId != null && curiosIndex >= 0) {
int initial = 1;
CraftAmountMenu.open(player, new CuriosItemLocator(curiosSlotId, curiosIndex), what, initial);
return;
}
// Curios主手/副手/背包仍按原先流程做前置校验保持行为一致
if (!(located.stack.getItem() instanceof WirelessTerminalItem wt)) return;
// 基本前置校验联网电量
IGrid grid = wt.getLinkedGrid(located.stack, player.level(), player);
if (grid == null) return;
if (!wt.hasPower(player, 0.5, located.stack)) return;
// Key 是否可被网络自动合成
var craftingService = grid.getCraftingService();
if (!craftingService.isCraftable(what)) return;
var hand = located.getHand();
int slot = located.getSlotIndex();
if (hand != null) {
int initial = 1;
CraftAmountMenu.open(player, MenuLocators.forHand(player, hand), what, initial);
} else if (slot >= 0) {
// 直接基于物品槽位作为菜单宿主打开数量输入界面
int initial = 1; // 初始数量避免依赖具体 Key 的单位定义
CraftAmountMenu.open(player, MenuLocators.forInventorySlot(slot), what, initial);
} else {
// 未知宿主回退忽略
}
});
context.setPacketHandled(true);
}
}

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

@ -0,0 +1,209 @@
package com.extendedae_plus.network;
import appeng.api.networking.IGrid;
import appeng.api.networking.energy.IEnergyService;
import appeng.api.stacks.AEItemKey;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import appeng.items.tools.powered.WirelessCraftingTerminalItem;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.me.helpers.PlayerSource;
import com.extendedae_plus.util.WirelessTerminalLocator;
import com.extendedae_plus.util.WirelessTerminalLocator.LocatedTerminal;
import de.mari_023.ae2wtlib.terminal.WTMenuHost;
import de.mari_023.ae2wtlib.wut.WTDefinition;
import de.mari_023.ae2wtlib.wut.WUTHandler;
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.item.ItemStack;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
public class PickFromWirelessC2SPacket {
private final BlockPos pos;
private final Direction face;
private final Vec3 hitLoc;
public PickFromWirelessC2SPacket(BlockPos pos, Direction face, Vec3 hitLoc) {
this.pos = pos;
this.face = face;
this.hitLoc = hitLoc;
}
public static void encode(PickFromWirelessC2SPacket msg, FriendlyByteBuf buf) {
buf.writeBlockPos(msg.pos);
buf.writeEnum(msg.face);
buf.writeDouble(msg.hitLoc.x);
buf.writeDouble(msg.hitLoc.y);
buf.writeDouble(msg.hitLoc.z);
}
public static PickFromWirelessC2SPacket decode(FriendlyByteBuf buf) {
BlockPos pos = buf.readBlockPos();
Direction face = buf.readEnum(Direction.class);
double x = buf.readDouble();
double y = buf.readDouble();
double z = buf.readDouble();
return new PickFromWirelessC2SPacket(pos, face, new Vec3(x, y, z));
}
public static void handle(PickFromWirelessC2SPacket msg, Supplier<NetworkEvent.Context> ctx) {
NetworkEvent.Context context = ctx.get();
context.enqueueWork(() -> {
ServerPlayer player = context.getSender();
if (player == null || player.isCreative()) {
return;
}
ServerLevel level = player.serverLevel();
BlockState state = level.getBlockState(msg.pos);
if (state == null || state.isAir()) {
return;
}
// 服务端权威定位玩家任意槽位的无线终端 Curios
LocatedTerminal located = WirelessTerminalLocator.find(player);
ItemStack terminal = located.stack;
if (terminal.isEmpty()) {
return;
}
IGrid grid;
boolean usedWtHost = false;
// 若来自 Curios优先通过 ae2wtlib WTMenuHost 获取量子桥网络绕过距离限制
String curiosSlotId = located.getCuriosSlotId();
int curiosIndex = located.getCuriosIndex();
WTMenuHost wtHost = null;
if (curiosSlotId != null && curiosIndex >= 0) {
String current = WUTHandler.getCurrentTerminal(terminal);
WTDefinition def = WUTHandler.wirelessTerminals.get(current);
if (def != null) {
wtHost = def.wTMenuHostFactory().create(player, null, terminal, (p, sub) -> {});
if (wtHost != null) {
var node = wtHost.getActionableNode();
if (node != null) {
grid = node.getGrid();
if (grid == null) {
return;
}
// 通过 WTMenuHost 的电力处理以兼容量子卡补能
if (!wtHost.drainPower()) {
return;
}
usedWtHost = true;
} else {
return;
}
} else {
return;
}
} else {
return;
}
} else {
// Curios AE2 原生路径处理
WirelessCraftingTerminalItem wct = terminal.getItem() instanceof WirelessCraftingTerminalItem c ? c : null;
WirelessTerminalItem wt = wct != null ? wct : (terminal.getItem() instanceof WirelessTerminalItem t ? t : null);
if (wt == null) {
return;
}
grid = wt.getLinkedGrid(terminal, level, player);
if (grid == null) {
return;
}
if (!wt.hasPower(player, 0.5, terminal)) {
return;
}
}
// 计算 pick 对应的物品使用客户端实际命中位置保证多部件方块AE2 CableBus/部件能返回正确克隆物品
BlockHitResult bhr = new BlockHitResult(msg.hitLoc, msg.face, msg.pos, true);
ItemStack picked = state.getBlock().getCloneItemStack(state, bhr, level, msg.pos, player);
if (picked.isEmpty()) {
// 兜底用方块本身
picked = state.getBlock().asItem().getDefaultInstance();
}
if (picked.isEmpty()) {
return;
}
int targetMax = picked.getMaxStackSize();
AEItemKey targetKey = AEItemKey.of(picked);
IEnergyService energy = grid.getEnergyService();
MEStorage storage = grid.getStorageService().getInventory();
ItemStack inHand = player.getMainHandItem();
var inv = player.getInventory();
// 决定放置目标
// 1) 若主手为空 -> 放主手空间为整组
// 2) 若主手为同一物品且未满 -> 合并到主手空间为主手剩余空间
// 3) 其他情况主手不为空且不是同物品-> 放入背包空槽空间为整组
boolean handIsSameItem = !inHand.isEmpty() && AEItemKey.of(inHand).equals(targetKey);
boolean placeToMainHand = inHand.isEmpty() || (handIsSameItem && inHand.getCount() < inHand.getMaxStackSize());
int space;
if (placeToMainHand) {
space = inHand.isEmpty() ? targetMax : Math.min(targetMax, inHand.getMaxStackSize() - inHand.getCount());
} else {
int free = inv.getFreeSlot();
if (free == -1) {
return; // 背包已满不进行拉取
}
space = targetMax;
}
if (space <= 0) {
return;
}
long extracted = StorageHelper.poweredExtraction(energy, storage, targetKey, space, new PlayerSource(player));
if (extracted <= 0) {
return;
}
if (placeToMainHand) {
if (inHand.isEmpty()) {
inv.setItem(inv.selected, targetKey.toStack((int) extracted));
} else {
// 合并到主手
int add = (int) Math.min(extracted, inHand.getMaxStackSize() - inHand.getCount());
if (add > 0) {
inHand.grow(add);
inv.setItem(inv.selected, inHand); // 写回以确保同步
}
}
} else {
int free = inv.getFreeSlot();
if (free == -1) {
// 理论上不会发生上面已判断为安全起见将提取物退回网络
StorageHelper.poweredInsert(energy, storage, targetKey, extracted, new PlayerSource(player));
return;
}
inv.setItem(free, targetKey.toStack((int) extracted));
}
if (usedWtHost) {
// WTMenuHost 已在 drainPower 中处理能量消耗/回充此处不重复扣除
} else {
// 原生 AE2 扣能
WirelessCraftingTerminalItem wct2 = terminal.getItem() instanceof WirelessCraftingTerminalItem c2 ? c2 : null;
WirelessTerminalItem wt2 = wct2 != null ? wct2 : (terminal.getItem() instanceof WirelessTerminalItem t2 ? t2 : null);
if (wt2 != null) {
wt2.usePower(player, Math.max(0.5, extracted * 0.05), terminal);
}
}
// 确保写回若位于 Curios 等需要显式写回的容器
located.commit();
player.containerMenu.broadcastChanges();
});
context.setPacketHandled(true);
}
}

Some files were not shown because too many files have changed in this diff Show More