初步更改
This commit is contained in:
parent
31e04d9415
commit
701affe217
42
build.gradle
42
build.gradle
|
|
@ -125,6 +125,47 @@ neoForge {
|
|||
// Include resources generated by data generators.
|
||||
sourceSets.main.resources { srcDir 'src/generated/resources' }
|
||||
|
||||
// 暂时排除缺少依赖的可选联动源码,待补齐依赖后再启用
|
||||
sourceSets.main.java {
|
||||
// 广泛屏蔽迁移中的旧源码,仅保留模板核心类(ExtendedAEPlus、ExtendedAEPlusClient、Config)
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
28
src/main/java/com/extendedae_plus/NewIcon.java
Normal file
28
src/main/java/com/extendedae_plus/NewIcon.java
Normal 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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.extendedae_plus.api;
|
||||
|
||||
public interface AdvancedBlockingHolder {
|
||||
boolean eap$getAdvancedBlocking();
|
||||
void eap$setAdvancedBlocking(boolean value);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.extendedae_plus.api;
|
||||
|
||||
/**
|
||||
* 由 {@code GuiExPatternProviderMixin} 实现,用于从通用的 Screen Mixin 中更新按钮布局。
|
||||
*/
|
||||
public interface ExPatternButtonsAccessor {
|
||||
/**
|
||||
* 在每帧调用以维护扩展样板供应器右侧按钮的可见性、重注册(窗口尺寸变化)与定位。
|
||||
*/
|
||||
void eap$updateButtonsLayout();
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.extendedae_plus.api;
|
||||
|
||||
/**
|
||||
* 由 GuiExPatternProviderMixin 实现,用于在客户端侧提供当前页号,避免反射读取 AE2 内部字段失败。
|
||||
*/
|
||||
public interface ExPatternPageAccessor {
|
||||
int eap$getCurrentPage();
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.extendedae_plus.api;
|
||||
|
||||
public interface PatternProviderMenuAdvancedSync {
|
||||
boolean eap$getAdvancedBlockingSynced();
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.extendedae_plus.api;
|
||||
|
||||
public interface PatternProviderMenuDoublingSync {
|
||||
boolean eap$getSmartDoublingSynced();
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.extendedae_plus.api;
|
||||
|
||||
public interface SmartDoublingAwarePattern {
|
||||
boolean eap$allowScaling();
|
||||
void eap$setAllowScaling(boolean allow);
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.extendedae_plus.api;
|
||||
|
||||
public interface SmartDoublingHolder {
|
||||
boolean eap$getSmartDoubling();
|
||||
void eap$setSmartDoubling(boolean value);
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
57
src/main/java/com/extendedae_plus/client/ClientProxy.java
Normal file
57
src/main/java/com/extendedae_plus/client/ClientProxy.java
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
126
src/main/java/com/extendedae_plus/client/InputEvents.java
Normal file
126
src/main/java/com/extendedae_plus/client/InputEvents.java
Normal 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
161
src/main/java/com/extendedae_plus/client/ModConfigScreen.java
Normal file
161
src/main/java/com/extendedae_plus/client/ModConfigScreen.java
Normal 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)); }
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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() 中手动绘制
|
||||
}
|
||||
}
|
||||
|
|
@ -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 + "' 的映射"));
|
||||
}
|
||||
}
|
||||
}
|
||||
77
src/main/java/com/extendedae_plus/config/ModConfigs.java
Normal file
77
src/main/java/com/extendedae_plus/config/ModConfigs.java
Normal 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() {}
|
||||
}
|
||||
|
|
@ -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(); }
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
104
src/main/java/com/extendedae_plus/hooks/WrenchHook.java
Normal file
104
src/main/java/com/extendedae_plus/hooks/WrenchHook.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
src/main/java/com/extendedae_plus/init/ModBlockEntities.java
Normal file
26
src/main/java/com/extendedae_plus/init/ModBlockEntities.java
Normal 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));
|
||||
}
|
||||
86
src/main/java/com/extendedae_plus/init/ModBlocks.java
Normal file
86
src/main/java/com/extendedae_plus/init/ModBlocks.java
Normal 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;
|
||||
}
|
||||
);
|
||||
}
|
||||
31
src/main/java/com/extendedae_plus/init/ModCreativeTabs.java
Normal file
31
src/main/java/com/extendedae_plus/init/ModCreativeTabs.java
Normal 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());
|
||||
}
|
||||
49
src/main/java/com/extendedae_plus/init/ModItems.java
Normal file
49
src/main/java/com/extendedae_plus/init/ModItems.java
Normal 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())
|
||||
);
|
||||
}
|
||||
19
src/main/java/com/extendedae_plus/init/ModMenuTypes.java
Normal file
19
src/main/java/com/extendedae_plus/init/ModMenuTypes.java
Normal 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));
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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 导致编译错误
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 同步 id(AE2 已用到 7),这里使用 20 以避冲突
|
||||
@Unique
|
||||
@GuiSync(20)
|
||||
public boolean eap$AdvancedBlocking = false;
|
||||
|
||||
@Inject(method = "broadcastChanges", at = @At("HEAD"))
|
||||
private void eap$syncAdvancedBlocking(CallbackInfo ci) {
|
||||
// 避免@Shadow父类方法,改用公共API:AEBaseMenu#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) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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 允许为 null,null 仅与 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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字节),分别对应:blocking、advancedBlocking、smartDoubling。
|
||||
*/
|
||||
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; }
|
||||
}
|
||||
}
|
||||
19
src/main/java/com/extendedae_plus/network/ModNetwork.java
Normal file
19
src/main/java/com/extendedae_plus/network/ModNetwork.java
Normal 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 占位
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in New Issue
Block a user