diff --git a/build.gradle b/build.gradle index d31f0f1..007bfa3 100644 --- a/build.gradle +++ b/build.gradle @@ -102,8 +102,9 @@ dependencies { //jec modCompileOnly "curse.maven:just-enough-characters-250702:6680042" + //mae2 - modRuntimeOnly "curse.maven:modern-ae2-additions-1028068:6342203" +// modRuntimeOnly "curse.maven:modern-ae2-additions-1028068:6342203" modCompileOnly "curse.maven:modern-ae2-additions-1028068:6342203" } diff --git a/gradle.properties b/gradle.properties index d28f3d4..f099302 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,7 +3,7 @@ org.gradle.jvmargs=-Xmx1G loom.platform = forge # Mod properties -mod_version = 1.3.3 +mod_version = 1.4.0 maven_group = com.extendedae_plus archives_name = extendedae_plus @@ -16,7 +16,7 @@ glodium_version=5006780 ae2_version=15.4.2 guideme_version=20.1.7 wireless_terminals_version=5162352 -jei_version=15.0.0.12 +jei_version=15.19.5.99 applied_flux_version=5329825 mega_cells_version=5320730 jade_version=4768593 diff --git a/src/main/java/com/extendedae_plus/ExtendedAEPlus.java b/src/main/java/com/extendedae_plus/ExtendedAEPlus.java index 42d69e6..bdb0611 100644 --- a/src/main/java/com/extendedae_plus/ExtendedAEPlus.java +++ b/src/main/java/com/extendedae_plus/ExtendedAEPlus.java @@ -6,8 +6,10 @@ import com.extendedae_plus.init.ModBlockEntities; import com.extendedae_plus.init.ModBlocks; import com.extendedae_plus.init.ModCreativeTabs; import com.extendedae_plus.init.ModItems; +import com.extendedae_plus.init.ModMenuTypes; import com.extendedae_plus.menu.locator.CuriosItemLocator; import com.extendedae_plus.network.ModNetwork; +import net.minecraftforge.client.ConfigScreenHandler; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.ModLoadingContext; @@ -20,6 +22,7 @@ import net.minecraft.resources.ResourceLocation; import com.extendedae_plus.client.ClientProxy; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.fml.DistExecutor; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; /** * ExtendedAE Plus 主mod类 @@ -29,17 +32,14 @@ public class ExtendedAEPlus { public static final String MODID = "extendedae_plus"; - // 在类加载时(尽可能早)在客户端注册内置模型,避免首次资源加载时错过。 - static { - DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> { - System.out.println("[ExtendedAE_Plus] Static init: register built-in models"); - ClientProxy.init(); - }); - } + // 注意:避免在静态初始化阶段访问注册对象,相关客户端注册改在 FMLClientSetupEvent 中执行。 public ExtendedAEPlus() { IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); + // 在客户端尽早注册内置模型,保证首次资源加载前映射已建立(仿照 AE2 的 AppEngClient 构造期注册) + DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> ClientProxy::init); + // 注册mod初始化事件 modEventBus.addListener(this::commonSetup); @@ -48,6 +48,7 @@ public class ExtendedAEPlus { ModBlockEntities.BLOCK_ENTITY_TYPES.register(modEventBus); ModItems.ITEMS.register(modEventBus); ModCreativeTabs.TABS.register(modEventBus); + ModMenuTypes.MENUS.register(modEventBus); // 注册到Forge事件总线 MinecraftForge.EVENT_BUS.register(this); @@ -55,8 +56,8 @@ public class ExtendedAEPlus { // 注册通用配置 ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, ModConfigs.COMMON_SPEC); - // 构造期在客户端再确保一次注册(幂等) - DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> ClientProxy::init); + // 客户端侧延迟注册:在 FMLClientSetupEvent 阶段执行(包含 MenuScreens 绑定等) + modEventBus.addListener((FMLClientSetupEvent e) -> ClientProxy.onClientSetup(e)); } /** diff --git a/src/main/java/com/extendedae_plus/api/PatternProviderMenuDoublingSync.java b/src/main/java/com/extendedae_plus/api/PatternProviderMenuDoublingSync.java new file mode 100644 index 0000000..a9d1492 --- /dev/null +++ b/src/main/java/com/extendedae_plus/api/PatternProviderMenuDoublingSync.java @@ -0,0 +1,5 @@ +package com.extendedae_plus.api; + +public interface PatternProviderMenuDoublingSync { + boolean eap$getSmartDoublingSynced(); +} diff --git a/src/main/java/com/extendedae_plus/api/SmartDoublingAwarePattern.java b/src/main/java/com/extendedae_plus/api/SmartDoublingAwarePattern.java new file mode 100644 index 0000000..5c18658 --- /dev/null +++ b/src/main/java/com/extendedae_plus/api/SmartDoublingAwarePattern.java @@ -0,0 +1,6 @@ +package com.extendedae_plus.api; + +public interface SmartDoublingAwarePattern { + boolean eap$allowScaling(); + void eap$setAllowScaling(boolean allow); +} diff --git a/src/main/java/com/extendedae_plus/api/SmartDoublingHolder.java b/src/main/java/com/extendedae_plus/api/SmartDoublingHolder.java new file mode 100644 index 0000000..b5af4ef --- /dev/null +++ b/src/main/java/com/extendedae_plus/api/SmartDoublingHolder.java @@ -0,0 +1,6 @@ +package com.extendedae_plus.api; + +public interface SmartDoublingHolder { + boolean eap$getSmartDoubling(); + void eap$setSmartDoubling(boolean value); +} diff --git a/src/main/java/com/extendedae_plus/client/ClientModelEvents.java b/src/main/java/com/extendedae_plus/client/ClientModelEvents.java index ad33bef..c867034 100644 --- a/src/main/java/com/extendedae_plus/client/ClientModelEvents.java +++ b/src/main/java/com/extendedae_plus/client/ClientModelEvents.java @@ -1,5 +1,6 @@ package com.extendedae_plus.client; +import com.extendedae_plus.ExtendedAEPlus; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.client.event.ModelEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; @@ -8,13 +9,19 @@ import net.minecraftforge.fml.common.Mod; /** * 确保在模型烘焙/资源重载期间也会注册内置模型,避免在刷新资源后丢失内置模型映射。 */ -@Mod.EventBusSubscriber(value = Dist.CLIENT, bus = Mod.EventBusSubscriber.Bus.MOD) +@Mod.EventBusSubscriber(modid = ExtendedAEPlus.MODID, value = Dist.CLIENT, bus = Mod.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(); } } diff --git a/src/main/java/com/extendedae_plus/client/ClientProxy.java b/src/main/java/com/extendedae_plus/client/ClientProxy.java index f6922e3..55f9e5f 100644 --- a/src/main/java/com/extendedae_plus/client/ClientProxy.java +++ b/src/main/java/com/extendedae_plus/client/ClientProxy.java @@ -3,8 +3,14 @@ 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.minecraftforge.client.ConfigScreenHandler; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; +import net.minecraft.client.gui.screens.MenuScreens; /** * 客户端模型注册,将 formed 模型注册为内置模型。 @@ -38,4 +44,31 @@ public final class ClientProxy { 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(); + // 仅在客户端设置阶段执行与 UI 相关的一次性绑定 + registerConfigScreen(); + // 菜单 -> 屏幕 绑定 + MenuScreens.register(ModMenuTypes.NETWORK_PATTERN_CONTROLLER.get(), GlobalProviderModesScreen::new); + }); + } + + /** + * 仅客户端:在 Mods 菜单注册配置界面入口。 + * 将对 Screen 的引用限制在客户端侧,避免服务端类加载。 + */ + public static void registerConfigScreen() { + // 将 ModConfigScreen 的引用放在此处,确保仅在 Dist.CLIENT 下解析该类 + ModLoadingContext.get().registerExtensionPoint( + ConfigScreenHandler.ConfigScreenFactory.class, + () -> new ConfigScreenHandler.ConfigScreenFactory( + (mc, parent) -> new com.extendedae_plus.client.ModConfigScreen(parent)) + ); + } } diff --git a/src/main/java/com/extendedae_plus/client/ModConfigScreen.java b/src/main/java/com/extendedae_plus/client/ModConfigScreen.java new file mode 100644 index 0000000..62ffe64 --- /dev/null +++ b/src/main/java/com/extendedae_plus/client/ModConfigScreen.java @@ -0,0 +1,134 @@ +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 crossDimToggle; + private CycleButton providerRoundRobinToggle; + + 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++; + + // 按钮:保存、返回 + 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(); + + // 应用到 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); + + // Forge 会在合适的时机写回到配置文件;部分改动可能需要重启游戏或世界才完全生效 + onClose(); + } + + // Helper to create a boolean on/off CycleButton which shows localized on/off text + private CycleButton createToggle(int x, int y, int width, int height, boolean initial) { + CycleButton 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); + } + + 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)); } +} diff --git a/src/main/java/com/extendedae_plus/client/render/crafting/EPlusCraftingCubeModelProvider.java b/src/main/java/com/extendedae_plus/client/render/crafting/EPlusCraftingCubeModelProvider.java index 3e98d51..850836a 100644 --- a/src/main/java/com/extendedae_plus/client/render/crafting/EPlusCraftingCubeModelProvider.java +++ b/src/main/java/com/extendedae_plus/client/render/crafting/EPlusCraftingCubeModelProvider.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.function.Function; /** - * 参照 MAE2 的 DynamicCraftingCubeModelProvider,实现形成态光照模型。 + * 形成态光照模型。 */ public class EPlusCraftingCubeModelProvider extends AbstractCraftingUnitModelProvider { @@ -29,13 +29,13 @@ public class EPlusCraftingCubeModelProvider public static final ChunkRenderTypeSet CUTOUT = ChunkRenderTypeSet.of(RenderType.cutout()); private static final List MATERIALS = new ArrayList<>(); - // 与 MAE2 一致:将环形边框与基础发光底图放在本模组命名空间 + //将环形边框与基础发光底图放在本模组命名空间 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 时使用) + // 亮面贴图(formed 时使用) protected static final Material ACCELERATOR_4X_LIGHT = texture(ExtendedAEPlus.MODID, "4x_accelerator_light"); protected static final Material ACCELERATOR_16X_LIGHT = texture(ExtendedAEPlus.MODID, diff --git a/src/main/java/com/extendedae_plus/client/screen/GlobalProviderModesScreen.java b/src/main/java/com/extendedae_plus/client/screen/GlobalProviderModesScreen.java new file mode 100644 index 0000000..c70a53f --- /dev/null +++ b/src/main/java/com/extendedae_plus/client/screen/GlobalProviderModesScreen.java @@ -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 { + 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() 中手动绘制 + } +} diff --git a/src/main/java/com/extendedae_plus/config/ModConfigs.java b/src/main/java/com/extendedae_plus/config/ModConfigs.java index a0e9f19..b1689b1 100644 --- a/src/main/java/com/extendedae_plus/config/ModConfigs.java +++ b/src/main/java/com/extendedae_plus/config/ModConfigs.java @@ -8,6 +8,7 @@ public final class ModConfigs { public static final ForgeConfigSpec.DoubleValue WIRELESS_MAX_RANGE; public static final ForgeConfigSpec.BooleanValue WIRELESS_CROSS_DIM_ENABLE; public static final ForgeConfigSpec.BooleanValue SHOW_ENCOD_PATTERN_PLAYER; + public static final ForgeConfigSpec.BooleanValue PROVIDER_ROUND_ROBIN_ENABLE; static { ForgeConfigSpec.Builder builder = new ForgeConfigSpec.Builder(); @@ -41,6 +42,14 @@ public final class ModConfigs { ) .define("showEncoderPatternPlayer", true); + // 智能倍增后,是否在样板供应器间轮询分配请求量(开启:按 provider 均分;关闭:不拆分) + PROVIDER_ROUND_ROBIN_ENABLE = builder + .comment( + "智能倍增时是否对样板供应器轮询分配", + "仅多个供应器有相同样板时生效,开启后请求会均分到所有可用供应器,关闭则全部分配给单一供应器", + "注意:所有相关供应器需开启智能倍增,否则可能失效", + "默认: true") + .define("providerRoundRobinEnable", true); builder.pop(); COMMON_SPEC = builder.build(); } diff --git a/src/main/java/com/extendedae_plus/content/ScaledProcessingPattern.java b/src/main/java/com/extendedae_plus/content/ScaledProcessingPattern.java new file mode 100644 index 0000000..27a7c5a --- /dev/null +++ b/src/main/java/com/extendedae_plus/content/ScaledProcessingPattern.java @@ -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; + } + } +} diff --git a/src/main/java/com/extendedae_plus/content/controller/NetworkPatternControllerBlock.java b/src/main/java/com/extendedae_plus/content/controller/NetworkPatternControllerBlock.java new file mode 100644 index 0000000..a8e1dde --- /dev/null +++ b/src/main/java/com/extendedae_plus/content/controller/NetworkPatternControllerBlock.java @@ -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; +import net.minecraftforge.network.NetworkHooks; + +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) { + NetworkHooks.openScreen(sp, provider, pos); + } + } + return InteractionResult.sidedSuccess(level.isClientSide); + } +} diff --git a/src/main/java/com/extendedae_plus/content/controller/NetworkPatternControllerBlockEntity.java b/src/main/java/com/extendedae_plus/content/controller/NetworkPatternControllerBlockEntity.java new file mode 100644 index 0000000..0dff6a5 --- /dev/null +++ b/src/main/java/com/extendedae_plus/content/controller/NetworkPatternControllerBlockEntity.java @@ -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 { + INSTANCE; + @Override + public void onSaveChanges(NetworkPatternControllerBlockEntity host, IGridNode node) { + host.setChanged(); + } + } +} diff --git a/src/main/java/com/extendedae_plus/init/ModBlockEntities.java b/src/main/java/com/extendedae_plus/init/ModBlockEntities.java index a82eb16..7db517e 100644 --- a/src/main/java/com/extendedae_plus/init/ModBlockEntities.java +++ b/src/main/java/com/extendedae_plus/init/ModBlockEntities.java @@ -2,6 +2,7 @@ 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.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.ForgeRegistries; @@ -17,4 +18,9 @@ public final class ModBlockEntities { BLOCK_ENTITY_TYPES.register("wireless_transceiver", () -> BlockEntityType.Builder.of(WirelessTransceiverBlockEntity::new, ModBlocks.WIRELESS_TRANSCEIVER.get()).build(null)); + + public static final RegistryObject> NETWORK_PATTERN_CONTROLLER_BE = + BLOCK_ENTITY_TYPES.register("network_pattern_controller", + () -> BlockEntityType.Builder.of(NetworkPatternControllerBlockEntity::new, + ModBlocks.NETWORK_PATTERN_CONTROLLER.get()).build(null)); } diff --git a/src/main/java/com/extendedae_plus/init/ModBlocks.java b/src/main/java/com/extendedae_plus/init/ModBlocks.java index db87bbb..4316e2a 100644 --- a/src/main/java/com/extendedae_plus/init/ModBlocks.java +++ b/src/main/java/com/extendedae_plus/init/ModBlocks.java @@ -28,6 +28,17 @@ public final class ModBlocks { ) ); + // AE2 网络模式控制器方块 + public static final RegistryObject 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 RegistryObject ACCELERATOR_4x = BLOCKS.register( "4x_crafting_accelerator", diff --git a/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java b/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java index 6218e93..e48a376 100644 --- a/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java +++ b/src/main/java/com/extendedae_plus/init/ModCreativeTabs.java @@ -20,6 +20,7 @@ public final class ModCreativeTabs { .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()); diff --git a/src/main/java/com/extendedae_plus/init/ModItems.java b/src/main/java/com/extendedae_plus/init/ModItems.java index 150c5e1..d2d290f 100644 --- a/src/main/java/com/extendedae_plus/init/ModItems.java +++ b/src/main/java/com/extendedae_plus/init/ModItems.java @@ -17,6 +17,11 @@ public final class ModItems { () -> new BlockItem(ModBlocks.WIRELESS_TRANSCEIVER.get(), new Item.Properties()) ); + public static final RegistryObject NETWORK_PATTERN_CONTROLLER = ITEMS.register( + "network_pattern_controller", + () -> new BlockItem(ModBlocks.NETWORK_PATTERN_CONTROLLER.get(), new Item.Properties()) + ); + // Crafting Accelerators public static final RegistryObject ACCELERATOR_4x = ITEMS.register( "4x_crafting_accelerator", diff --git a/src/main/java/com/extendedae_plus/init/ModMenuTypes.java b/src/main/java/com/extendedae_plus/init/ModMenuTypes.java new file mode 100644 index 0000000..47a0936 --- /dev/null +++ b/src/main/java/com/extendedae_plus/init/ModMenuTypes.java @@ -0,0 +1,20 @@ +package com.extendedae_plus.init; + +import com.extendedae_plus.ExtendedAEPlus; +import com.extendedae_plus.menu.NetworkPatternControllerMenu; +import net.minecraft.world.inventory.MenuType; +import net.minecraftforge.common.extensions.IForgeMenuType; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistries; +import net.minecraftforge.registries.RegistryObject; + +public final class ModMenuTypes { + private ModMenuTypes() {} + + public static final DeferredRegister> MENUS = + DeferredRegister.create(ForgeRegistries.MENU_TYPES, ExtendedAEPlus.MODID); + + public static final RegistryObject> NETWORK_PATTERN_CONTROLLER = + MENUS.register("network_pattern_controller", + () -> IForgeMenuType.create(NetworkPatternControllerMenu::new)); +} diff --git a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePlugin.java b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePlugin.java new file mode 100644 index 0000000..34159cd --- /dev/null +++ b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePlugin.java @@ -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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePluginComponents.java b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePluginComponents.java new file mode 100644 index 0000000..4cd98a9 --- /dev/null +++ b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverJadePluginComponents.java @@ -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); +} + + diff --git a/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverProvider.java b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverProvider.java new file mode 100644 index 0000000..05e34a8 --- /dev/null +++ b/src/main/java/com/extendedae_plus/integration/jade/WirelessTransceiverProvider.java @@ -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 { + 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()); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java b/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java index 9bac388..ce96613 100644 --- a/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java +++ b/src/main/java/com/extendedae_plus/integration/jei/JeiRuntimeProxy.java @@ -1,12 +1,17 @@ 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; /** @@ -113,4 +118,18 @@ public final class JeiRuntimeProxy { } return ""; } + + /** + * 获取JEI书签列表 + */ + public static List> 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(); + } } diff --git a/src/main/java/com/extendedae_plus/menu/NetworkPatternControllerMenu.java b/src/main/java/com/extendedae_plus/menu/NetworkPatternControllerMenu.java new file mode 100644 index 0000000..3c954c5 --- /dev/null +++ b/src/main/java/com/extendedae_plus/menu/NetworkPatternControllerMenu.java @@ -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; + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/accessor/ScreenInvoker.java b/src/main/java/com/extendedae_plus/mixin/accessor/ScreenInvoker.java deleted file mode 100644 index c6edc7a..0000000 --- a/src/main/java/com/extendedae_plus/mixin/accessor/ScreenInvoker.java +++ /dev/null @@ -1,14 +0,0 @@ -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.narration.NarratableEntry; -import net.minecraft.client.gui.screens.Screen; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Invoker; - -@Mixin(Screen.class) -public interface ScreenInvoker { - @Invoker("addRenderableWidget") - W eap$invokeAddRenderableWidget(W widget); -} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/AEBaseScreenMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/AEBaseScreenMixin.java index 17d1ce8..4194d24 100644 --- a/src/main/java/com/extendedae_plus/mixin/ae2/AEBaseScreenMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/ae2/AEBaseScreenMixin.java @@ -2,21 +2,29 @@ package com.extendedae_plus.mixin.ae2; import appeng.client.Point; import appeng.client.gui.AEBaseScreen; +import appeng.client.gui.StackWithBounds; +import appeng.client.gui.me.crafting.CraftingCPUScreen; import appeng.client.gui.TextOverride; 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.api.stacks.AEKey; import appeng.menu.slot.AppEngSlot; import com.extendedae_plus.api.ExPatternPageAccessor; +import com.extendedae_plus.network.CraftingMonitorJumpC2SPacket; +import com.extendedae_plus.network.ModNetwork; +import com.extendedae_plus.network.CraftingMonitorOpenProviderC2SPacket; import com.extendedae_plus.util.GuiUtil; import com.glodblock.github.extendedae.client.gui.GuiExPatternProvider; +import com.mojang.logging.LogUtils; 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.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; @@ -38,6 +46,74 @@ public abstract class AEBaseScreenMixin { 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 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 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(); diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/AEProcessingPatternMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/AEProcessingPatternMixin.java new file mode 100644 index 0000000..4381601 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/AEProcessingPatternMixin.java @@ -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 = true; // 默认允许缩放 + + @Override + public boolean eap$allowScaling() { + return eap$allowScaling; + } + + @Override + public void eap$setAllowScaling(boolean allow) { + this.eap$allowScaling = allow; + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/jei/EncodePatternTransferHandlerMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/EncodePatternTransferHandlerMixin.java similarity index 98% rename from src/main/java/com/extendedae_plus/mixin/jei/EncodePatternTransferHandlerMixin.java rename to src/main/java/com/extendedae_plus/mixin/ae2/EncodePatternTransferHandlerMixin.java index 820b3ce..c71d818 100644 --- a/src/main/java/com/extendedae_plus/mixin/jei/EncodePatternTransferHandlerMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/ae2/EncodePatternTransferHandlerMixin.java @@ -1,4 +1,4 @@ -package com.extendedae_plus.mixin.jei; +package com.extendedae_plus.mixin.ae2; import appeng.integration.modules.jei.transfer.EncodePatternTransferHandler; import appeng.integration.modules.jeirei.EncodingHelper; diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/EncodingHelperMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/EncodingHelperMixin.java new file mode 100644 index 0000000..4990054 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/EncodingHelperMixin.java @@ -0,0 +1,37 @@ +package com.extendedae_plus.mixin.ae2; + +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 comparator, CallbackInfoReturnable> cir){ + Map result = cir.getReturnValue(); + AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE); + List> 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); + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderLogicDoublingMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderLogicDoublingMixin.java new file mode 100644 index 0000000..eaf1aac --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderLogicDoublingMixin.java @@ -0,0 +1,71 @@ +package com.extendedae_plus.mixin.ae2; + +import appeng.helpers.patternprovider.PatternProviderLogic; +import com.extendedae_plus.api.SmartDoublingHolder; +import com.extendedae_plus.api.SmartDoublingAwarePattern; +import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicPatternsAccessor; +import appeng.api.crafting.IPatternDetails; +import appeng.crafting.pattern.AEProcessingPattern; +import net.minecraft.nbt.CompoundTag; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(PatternProviderLogic.class) +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"), remap = false) + private void eap$writeSmartDoublingToNbt(CompoundTag tag, CallbackInfo ci) { + tag.putBoolean(EPP_SMART_DOUBLING_KEY, this.eap$smartDoubling); + } + + @Inject(method = "readFromNBT", at = @At("TAIL"), remap = false) + 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"), remap = false) + 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) { + } + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderMenuDoublingMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderMenuDoublingMixin.java new file mode 100644 index 0000000..ee21660 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderMenuDoublingMixin.java @@ -0,0 +1,69 @@ +package com.extendedae_plus.mixin.ae2; + +import appeng.helpers.patternprovider.PatternProviderLogic; +import appeng.helpers.patternprovider.PatternProviderLogicHost; +import appeng.menu.AEBaseMenu; +import appeng.menu.guisync.GuiSync; +import appeng.menu.implementations.PatternProviderMenu; +import com.extendedae_plus.api.PatternProviderMenuDoublingSync; +import com.extendedae_plus.api.SmartDoublingHolder; +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 static com.extendedae_plus.util.ExtendedAELogger.LOGGER; + +@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(); + LOGGER.debug("[EAP] Menu broadcastChanges HEAD: eap$SmartDoubling={}", this.eap$SmartDoubling); + } + } + } + + @Inject(method = "(ILnet/minecraft/world/entity/player/Inventory;Lappeng/helpers/patternprovider/PatternProviderLogicHost;)V", at = @At("TAIL")) + private void eap$initSmartSync_Public(int id, Inventory playerInventory, PatternProviderLogicHost host, CallbackInfo ci) { + try { + var l = this.logic; + if (l instanceof SmartDoublingHolder holder) { + this.eap$SmartDoubling = holder.eap$getSmartDoubling(); + } + } catch (Throwable t) { + LOGGER.error("Error initializing smart doubling sync", t); + } + } + + @Inject(method = "(Lnet/minecraft/world/inventory/MenuType;ILnet/minecraft/world/entity/player/Inventory;Lappeng/helpers/patternprovider/PatternProviderLogicHost;)V", at = @At("TAIL")) + private void eap$initSmartSync_Protected(MenuType menuType, int id, Inventory playerInventory, PatternProviderLogicHost host, CallbackInfo ci) { + try { + var l = this.logic; + if (l instanceof SmartDoublingHolder holder) { + this.eap$SmartDoubling = holder.eap$getSmartDoubling(); + } + } catch (Throwable t) { + LOGGER.error("Error initializing smart doubling sync", t); + } + } + + @Override + public boolean eap$getSmartDoublingSynced() { + return this.eap$SmartDoubling; + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderScreenMixin.java b/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderScreenMixin.java index fe2a81e..b0d383d 100644 --- a/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderScreenMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/ae2/PatternProviderScreenMixin.java @@ -9,8 +9,10 @@ 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.glodblock.github.extendedae.client.gui.GuiExPatternProvider; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; @@ -36,6 +38,12 @@ public abstract class PatternProviderScreenMixin @Unique private boolean eap$AdvancedBlockingEnabled = false; + @Unique + private SettingToggleButton eap$SmartDoublingToggle; + + @Unique + private boolean eap$SmartDoublingEnabled = false; + public PatternProviderScreenMixin(C menu, Inventory playerInventory, Component title, ScreenStyle style) { super(menu, playerInventory, title, style); } @@ -76,27 +84,62 @@ public abstract class PatternProviderScreenMixin 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) { + 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) -> { + LOGGER.debug("[EAP] Click smart doubling toggle: send C2S"); + ModNetwork.CHANNEL.sendToServer(new ToggleSmartDoublingC2SPacket()); + } + ) { + @Override + public java.util.List 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) return; - - boolean desired = this.eap$AdvancedBlockingEnabled; - if (this.menu instanceof PatternProviderMenuAdvancedSync sync) { - desired = sync.eap$getAdvancedBlockingSynced(); + if (this.eap$AdvancedBlockingToggle != null) { + boolean desired = this.eap$AdvancedBlockingEnabled; + if (this.menu instanceof PatternProviderMenuAdvancedSync sync) { + desired = sync.eap$getAdvancedBlockingSynced(); + } + LOGGER.debug("[EAP] updateBeforeRender tick (adv): desired={}", desired); + this.eap$AdvancedBlockingEnabled = desired; + this.eap$AdvancedBlockingToggle.set(desired ? YesNo.YES : YesNo.NO); } - // 与AE2一致:每帧无条件对齐按钮状态至@GuiSync(使用YesNo以获得原版图标与提示) - LOGGER.debug("[EAP] updateBeforeRender tick: desired={}", desired); - if (this.eap$AdvancedBlockingEnabled != desired) { - LOGGER.debug("[EAP] updateBeforeRender: desired changed {} -> {}", this.eap$AdvancedBlockingEnabled, desired); + if (this.eap$SmartDoublingToggle != null) { + boolean desired2 = this.eap$SmartDoublingEnabled; + if (this.menu instanceof PatternProviderMenuDoublingSync sync2) { + desired2 = sync2.eap$getSmartDoublingSynced(); + } + LOGGER.debug("[EAP] updateBeforeRender tick (dbl): desired={}", desired2); + this.eap$SmartDoublingEnabled = desired2; + this.eap$SmartDoublingToggle.set(desired2 ? YesNo.YES : YesNo.NO); } - this.eap$AdvancedBlockingEnabled = desired; - this.eap$AdvancedBlockingToggle.set(desired ? YesNo.YES : YesNo.NO); - // 如果当前屏幕是 ExtendedAE 的 GuiExPatternProvider,则委托布局更新到 accessor if ((Object) this instanceof GuiExPatternProvider) { try { ((ExPatternButtonsAccessor) this).eap$updateButtonsLayout(); diff --git a/src/main/java/com/extendedae_plus/mixin/ae2/accessor/PatternProviderLogicPatternsAccessor.java b/src/main/java/com/extendedae_plus/mixin/ae2/accessor/PatternProviderLogicPatternsAccessor.java new file mode 100644 index 0000000..c1471e3 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/ae2/accessor/PatternProviderLogicPatternsAccessor.java @@ -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 eap$patterns(); +} diff --git a/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingServiceGetProvidersMixin.java b/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingServiceGetProvidersMixin.java new file mode 100644 index 0000000..5ae4d03 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingServiceGetProvidersMixin.java @@ -0,0 +1,28 @@ +package com.extendedae_plus.mixin.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; + } +} + + diff --git a/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeNodeAccessor.java b/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeNodeAccessor.java new file mode 100644 index 0000000..b6a274f --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeNodeAccessor.java @@ -0,0 +1,12 @@ +package com.extendedae_plus.mixin.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(); +} diff --git a/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeNodeMixin.java b/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeNodeMixin.java new file mode 100644 index 0000000..d792687 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeNodeMixin.java @@ -0,0 +1,30 @@ +package com.extendedae_plus.mixin.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(); + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeProcessMixin.java b/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeProcessMixin.java new file mode 100644 index 0000000..9311e06 --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/autopattern/CraftingTreeProcessMixin.java @@ -0,0 +1,98 @@ +package com.extendedae_plus.mixin.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.content.ScaledProcessingPattern; +import com.extendedae_plus.util.PatternScaler; +import com.extendedae_plus.config.ModConfigs; +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 = "(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 providers = craftingService.getProviders(original); + + // 计算 provider 数量;尝试用反射读取内部 providers 列表以避免消费迭代器 + int size; + try { + var cls = providers.getClass(); + var f = cls.getDeclaredField("providers"); // private ArrayList + 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; + } + } +} diff --git a/src/main/java/com/extendedae_plus/mixin/autopattern/PatternProviderLogicContainsRedirectMixin.java b/src/main/java/com/extendedae_plus/mixin/autopattern/PatternProviderLogicContainsRedirectMixin.java new file mode 100644 index 0000000..bd1500f --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/autopattern/PatternProviderLogicContainsRedirectMixin.java @@ -0,0 +1,38 @@ +package com.extendedae_plus.mixin.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; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/extendedae_plus/mixin/extendedae/ContainerExPatternTerminalMixin.java b/src/main/java/com/extendedae_plus/mixin/extendedae/ContainerExPatternTerminalMixin.java index b408a42..fde3756 100644 --- a/src/main/java/com/extendedae_plus/mixin/extendedae/ContainerExPatternTerminalMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/extendedae/ContainerExPatternTerminalMixin.java @@ -8,6 +8,21 @@ import com.glodblock.github.glodium.network.packet.sync.IActionHolder; import com.glodblock.github.glodium.network.packet.sync.Paras; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.entity.player.Player; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.InteractionHand; +import net.minecraft.world.MenuProvider; +import net.minecraft.world.InteractionResult; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.phys.BlockHitResult; +import net.minecraft.world.phys.Vec3; +import net.minecraft.core.registries.Registries; +import net.minecraftforge.network.NetworkHooks; import org.jetbrains.annotations.NotNull; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; @@ -17,11 +32,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import java.util.Map; import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; @Mixin(ContainerExPatternTerminal.class) public abstract class ContainerExPatternTerminalMixin implements IActionHolder { - @GuiSync(11452) + @GuiSync(25564) @Unique public boolean eap$hidePatternSlots = false; @@ -41,13 +58,19 @@ public abstract class ContainerExPatternTerminalMixin implements IActionHolder { } @Unique - private final Map> eap$actions = createHolder(); + private Map> eap$actions; @Unique private Player epp$player; + @Unique + private static final Logger EAP_LOGGER = LogManager.getLogger("ExtendedAE_Plus"); + @Inject(method = "*", at = @At("TAIL")) private void init(int id, net.minecraft.world.entity.player.Inventory playerInventory, IConfigurableObject host, CallbackInfo ci) { + if (this.eap$actions == null) { + this.eap$actions = createHolder(); + } this.epp$player = playerInventory.player; // 注册上传动作:参数顺序必须与客户端 CGenericPacket 保持一致 this.eap$actions.put("upload", p -> { @@ -61,6 +84,110 @@ public abstract class ContainerExPatternTerminalMixin implements IActionHolder { } catch (Throwable ignored) { } }); + + // 注册打开UI动作:open_ui(posLong, dimensionId, faceOrdinal?) + this.eap$actions.put("open_ui", p -> { + try { + // 参数解析 + Object po = p.get(0); // BlockPos as long (BlockPos#asLong) + Object do0 = p.get(1); // Dimension id string (e.g., minecraft:overworld) + Object fo; + try { + fo = p.get(2); // Optional face ordinal + } catch (Throwable __ignored) { + fo = null; + } + + long posLong = (po instanceof Number) ? ((Number) po).longValue() : Long.parseLong(String.valueOf(po)); + String dimStr = String.valueOf(do0); + int faceOrd = -1; + if (fo != null) { + faceOrd = (fo instanceof Number) ? ((Number) fo).intValue() : Integer.parseInt(String.valueOf(fo)); + } + + BlockPos pos = BlockPos.of(posLong); + ResourceLocation dimId = ResourceLocation.tryParse(dimStr); + if (dimId == null) { + EAP_LOGGER.warn("[EPlus] open_ui: invalid dim '{}'", dimStr); + return; + } + ResourceKey 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 diff --git a/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java b/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java index e529c5c..94655ab 100644 --- a/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java +++ b/src/main/java/com/extendedae_plus/mixin/extendedae/GuiExPatternTerminalMixin.java @@ -10,13 +10,22 @@ import appeng.client.gui.widgets.AETextField; import appeng.client.gui.widgets.IconButton; import appeng.menu.AEBaseMenu; import com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal; +import com.extendedae_plus.network.ModNetwork; +import com.extendedae_plus.network.OpenProviderUiC2SPacket; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.components.Button; import net.minecraft.client.gui.components.Tooltip; +import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.Rect2i; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.inventory.Slot; import net.minecraft.world.item.ItemStack; +import net.minecraft.resources.ResourceKey; +import net.minecraft.world.level.Level; +import net.minecraft.resources.ResourceLocation; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Pseudo; import org.spongepowered.asm.mixin.Shadow; @@ -29,9 +38,13 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.Set; +import java.util.HashMap; +import java.util.Map; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; @Pseudo -@Mixin(GuiExPatternTerminal.class) +@Mixin(value = GuiExPatternTerminal.class) public abstract class GuiExPatternTerminalMixin extends AEBaseScreen { @Unique @@ -46,6 +59,14 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen private boolean eap$showSlots = false; // 默认显示槽位 @Unique private long eap$currentlyChoicePatterProvider = -1; // 当前选择的样板供应器ID + @Unique + private final Map 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 matchedStack; @@ -109,8 +130,9 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen /** * 拦截鼠标点击事件,实现Shift+左键快速上传样板功能 + * 注意:某些整合包的 ExtendedAE 版本不在该类中覆写 mouseClicked,此处设置 require=0 以防止注入失败导致崩溃。 */ - @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true) + @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true, require = 0) private void onMouseClicked(double mouseX, double mouseY, int button, CallbackInfoReturnable cir) { // 检查是否是左键点击 + Shift键 if (button == 0 && hasShiftDown()) { @@ -181,6 +203,86 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen } } + @Unique + private int getIntConst(Class cls, String name, int defVal) { + try { + var f = cls.getDeclaredField(name); + f.setAccessible(true); + return (int) f.get(null); + } catch (Throwable t) { + return defVal; + } + } + + @Unique + private void eap$tryOpenProviderUI(int rowIndex) { + try { + // 使用 Accessor 获取 rows,避免取到父类导致失败 + com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor acc = + (com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor) (Object) this; + java.util.ArrayList rows = acc.getRows(); + + // 找到该分组对应的第一个 PatternContainerRecord + Class cls = com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal.class; + var byGroupField = cls.getDeclaredField("byGroup"); + byGroupField.setAccessible(true); + Object byGroup = byGroupField.get(this); // HashMultimap + + Object headerRow = rows.get(rowIndex); + var groupField = headerRow.getClass().getDeclaredField("group"); + groupField.setAccessible(true); + Object group = groupField.get(headerRow); + + // 调用 byGroup.get(group),再取第一个元素 + java.util.Collection containers = (java.util.Collection) byGroup.getClass().getMethod("get", Object.class).invoke(byGroup, group); + if (containers == null || containers.isEmpty()) { + return; + } + Object firstRecord = containers.iterator().next(); // PatternContainerRecord + long serverId = (long) firstRecord.getClass().getMethod("getServerId").invoke(firstRecord); + + // 通过 infoMap 获取位置信息 + var infoMapField = cls.getDeclaredField("infoMap"); + infoMapField.setAccessible(true); + @SuppressWarnings("unchecked") + java.util.HashMap infoMap = (java.util.HashMap) 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) 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 */ @@ -228,6 +330,70 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen 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) { // 更新按钮图标 @@ -236,6 +402,9 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen this.eap$showSlots ? "gui.expatternprovider.hide_slots" : "gui.expatternprovider.show_slots" ))); } + // 清理旧的打开UI按钮 + this.eap$openUIButtons.values().forEach(this::removeWidget); + this.eap$openUIButtons.clear(); } @Inject(method = "refreshList", at = @At("TAIL"), remap = false) @@ -342,6 +511,63 @@ public abstract class GuiExPatternTerminalMixin extends AEBaseScreen @Inject(method = "drawFG", at = @At("TAIL"), remap = false) private void eap$afterDrawFG(GuiGraphics guiGraphics, int offsetX, int offsetY, int mouseX, int mouseY, CallbackInfo ci) { + // 动态放置/创建每个组标题后的“打开UI”按钮 + try { + // 使用 Accessor 获取必要的字段,避免反射失败 + com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor acc = + (com.extendedae_plus.mixin.extendedae.accessor.GuiExPatternTerminalAccessor) (Object) this; + java.util.ArrayList rows = acc.getRows(); + int currentScroll = acc.getScrollbar().getCurrentScroll(); + + // 直接引用目标类以获取其静态常量 + Class cls = com.glodblock.github.extendedae.client.gui.GuiExPatternTerminal.class; + int GUI_PADDING_X = getIntConst(cls, "GUI_PADDING_X", 22); + int GUI_PADDING_Y = getIntConst(cls, "GUI_PADDING_Y", 6); + int GUI_HEADER_HEIGHT = getIntConst(cls, "GUI_HEADER_HEIGHT", 51); + int ROW_HEIGHT = getIntConst(cls, "ROW_HEIGHT", 18); + int TEXT_MAX_WIDTH = getIntConst(cls, "TEXT_MAX_WIDTH", 155); + + int visibleRows = acc.getVisibleRows(); + + // 生产环境移除调试日志 + + // 先隐藏旧按钮,避免残留 + for (Button b : this.eap$openUIButtons.values()) { + b.visible = false; + } + + int shownCount = 0; + for (int i = 0; i < visibleRows; i++) { + int rowIndex = currentScroll + i; + if (rowIndex < 0 || rowIndex >= rows.size()) { + continue; + } + Object row = rows.get(rowIndex); + if (!row.getClass().getSimpleName().equals("GroupHeaderRow")) { + continue; + } + + // 放置按钮:位于名称文本右侧,与原类 choiceButton 锚点相邻,向右偏移 20px + int bx = this.leftPos + GUI_PADDING_X + TEXT_MAX_WIDTH - 40; + int by = this.topPos + GUI_PADDING_Y + GUI_HEADER_HEIGHT + i * ROW_HEIGHT - 3; + + 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()) diff --git a/src/main/java/com/extendedae_plus/mixin/jei/accessor/BookmarkOverlayAccessor.java b/src/main/java/com/extendedae_plus/mixin/jei/accessor/BookmarkOverlayAccessor.java new file mode 100644 index 0000000..508845d --- /dev/null +++ b/src/main/java/com/extendedae_plus/mixin/jei/accessor/BookmarkOverlayAccessor.java @@ -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(); +} diff --git a/src/main/java/com/extendedae_plus/network/CraftingMonitorJumpC2SPacket.java b/src/main/java/com/extendedae_plus/network/CraftingMonitorJumpC2SPacket.java new file mode 100644 index 0000000..eacd863 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/CraftingMonitorJumpC2SPacket.java @@ -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 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 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); + } +} diff --git a/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java b/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java new file mode 100644 index 0000000..216546c --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/CraftingMonitorOpenProviderC2SPacket.java @@ -0,0 +1,115 @@ +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.me.crafting.CraftingCPUMenu; +import appeng.menu.locator.MenuLocators; +import com.extendedae_plus.mixin.ae2.accessor.PatternProviderLogicAccessor; +import com.mojang.logging.LogUtils; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; + +import java.util.Collection; +import java.util.function.Supplier; + +/** + * 客户端从 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 ctx) { + NetworkEvent.Context context = ctx.get(); + context.enqueueWork(() -> { + ServerPlayer player = context.getSender(); + if (player == null) return; + + LogUtils.getLogger().info("EAP[S]: recv CraftingMonitorOpenProviderC2SPacket key={} from {}", msg.what, player.getGameProfile().getName()); + + // 必须在 CraftingCPU 界面内 + if (!(player.containerMenu instanceof CraftingCPUMenu menu)) { + LogUtils.getLogger().info("EAP[S]: not in CraftingCPUMenu, abort"); + return; + } + + // 通过菜单的 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) { + 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 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); + 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; + // 在服务端上下文中执行,pbe 仅用于构造菜单定位器 + + // 直接打开供应器自身的 UI(调用 Host 默认方法) + try { + // 部件与方块实体分别选择定位器 + if (host instanceof appeng.parts.AEBasePart part) { + host.openMenu(player, MenuLocators.forPart(part)); + } else { + host.openMenu(player, MenuLocators.forBlockEntity(pbe)); + } + context.setPacketHandled(true); + return; + } catch (Throwable t) { + LogUtils.getLogger().error("EAP[S]: open provider UI failed at {}", pbe.getBlockPos(), t); + } + } + } + } + + LogUtils.getLogger().info("EAP[S]: no provider UI opened for key={}", msg.what); + }); + context.setPacketHandled(true); + } +} diff --git a/src/main/java/com/extendedae_plus/network/GlobalToggleProviderModesC2SPacket.java b/src/main/java/com/extendedae_plus/network/GlobalToggleProviderModesC2SPacket.java new file mode 100644 index 0000000..c37d0bc --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/GlobalToggleProviderModesC2SPacket.java @@ -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 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 all = new HashSet<>(); + + // 方块形式的样板供应器(全部/在线) + try { + Set blocksAll = grid.getMachines(PatternProviderBlockEntity.class); + Set 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 partsAll = grid.getMachines(PatternProviderPart.class); + Set 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 hostsAll = grid.getMachines(PatternProviderLogicHost.class); + Set 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 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 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; } + } +} diff --git a/src/main/java/com/extendedae_plus/network/ModNetwork.java b/src/main/java/com/extendedae_plus/network/ModNetwork.java index 3aad8ea..a869cfe 100644 --- a/src/main/java/com/extendedae_plus/network/ModNetwork.java +++ b/src/main/java/com/extendedae_plus/network/ModNetwork.java @@ -18,6 +18,12 @@ public class ModNetwork { private static int id = 0; public static void register() { + CHANNEL.messageBuilder(OpenProviderUiC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(OpenProviderUiC2SPacket::encode) + .decoder(OpenProviderUiC2SPacket::decode) + .consumerNetworkThread(OpenProviderUiC2SPacket::handle) + .add(); + CHANNEL.messageBuilder(PickFromWirelessC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) .encoder(PickFromWirelessC2SPacket::encode) .decoder(PickFromWirelessC2SPacket::decode) @@ -60,11 +66,35 @@ public class ModNetwork { .consumerNetworkThread(ToggleAdvancedBlockingC2SPacket::handle) .add(); + CHANNEL.messageBuilder(ToggleSmartDoublingC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(ToggleSmartDoublingC2SPacket::encode) + .decoder(ToggleSmartDoublingC2SPacket::decode) + .consumerNetworkThread(ToggleSmartDoublingC2SPacket::handle) + .add(); + + CHANNEL.messageBuilder(GlobalToggleProviderModesC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(GlobalToggleProviderModesC2SPacket::encode) + .decoder(GlobalToggleProviderModesC2SPacket::decode) + .consumerNetworkThread(GlobalToggleProviderModesC2SPacket::handle) + .add(); + CHANNEL.messageBuilder(AdvancedBlockingSyncS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT) .encoder(AdvancedBlockingSyncS2CPacket::encode) .decoder(AdvancedBlockingSyncS2CPacket::decode) .consumerNetworkThread(AdvancedBlockingSyncS2CPacket::handle) .add(); + + CHANNEL.messageBuilder(CraftingMonitorJumpC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(CraftingMonitorJumpC2SPacket::encode) + .decoder(CraftingMonitorJumpC2SPacket::decode) + .consumerNetworkThread(CraftingMonitorJumpC2SPacket::handle) + .add(); + + CHANNEL.messageBuilder(CraftingMonitorOpenProviderC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER) + .encoder(CraftingMonitorOpenProviderC2SPacket::encode) + .decoder(CraftingMonitorOpenProviderC2SPacket::decode) + .consumerNetworkThread(CraftingMonitorOpenProviderC2SPacket::handle) + .add(); } private static int nextId() { return id++; } diff --git a/src/main/java/com/extendedae_plus/network/OpenProviderUiC2SPacket.java b/src/main/java/com/extendedae_plus/network/OpenProviderUiC2SPacket.java new file mode 100644 index 0000000..8a02170 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/OpenProviderUiC2SPacket.java @@ -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 ctx) { + NetworkEvent.Context context = ctx.get(); + context.enqueueWork(() -> { + ServerPlayer player = context.getSender(); + if (player == null) return; + + + // 校验维度与方块 + ResourceKey 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); + }); + } +} diff --git a/src/main/java/com/extendedae_plus/network/ToggleSmartDoublingC2SPacket.java b/src/main/java/com/extendedae_plus/network/ToggleSmartDoublingC2SPacket.java new file mode 100644 index 0000000..3ad6405 --- /dev/null +++ b/src/main/java/com/extendedae_plus/network/ToggleSmartDoublingC2SPacket.java @@ -0,0 +1,43 @@ +package com.extendedae_plus.network; + +import appeng.menu.implementations.PatternProviderMenu; +import com.extendedae_plus.api.SmartDoublingHolder; +import com.extendedae_plus.mixin.ae2.accessor.PatternProviderMenuAdvancedAccessor; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.server.level.ServerPlayer; +import net.minecraftforge.network.NetworkEvent; + +import java.util.function.Supplier; + +/** + * C2S:切换智能翻倍启用状态。 + * 不含额外负载,基于玩家当前打开的 PatternProviderMenu 进行切换。 + */ +public class ToggleSmartDoublingC2SPacket { + public ToggleSmartDoublingC2SPacket() {} + + public static void encode(ToggleSmartDoublingC2SPacket msg, FriendlyByteBuf buf) {} + + public static ToggleSmartDoublingC2SPacket decode(FriendlyByteBuf buf) { + return new ToggleSmartDoublingC2SPacket(); + } + + public static void handle(ToggleSmartDoublingC2SPacket msg, Supplier ctxSupplier) { + var ctx = ctxSupplier.get(); + ctx.enqueueWork(() -> { + ServerPlayer player = ctx.getSender(); + if (player == null) return; + if (!(player.containerMenu instanceof PatternProviderMenu menu)) return; + + var accessor = (PatternProviderMenuAdvancedAccessor) menu; + var logic = accessor.eap$logic(); + if (logic instanceof SmartDoublingHolder holder) { + boolean current = holder.eap$getSmartDoubling(); + boolean next = !current; + holder.eap$setSmartDoubling(next); + logic.saveChanges(); + } + }); + ctx.setPacketHandled(true); + } +} diff --git a/src/main/java/com/extendedae_plus/util/PatternScaler.java b/src/main/java/com/extendedae_plus/util/PatternScaler.java new file mode 100644 index 0000000..7340f73 --- /dev/null +++ b/src/main/java/com/extendedae_plus/util/PatternScaler.java @@ -0,0 +1,108 @@ +package com.extendedae_plus.util; + +import appeng.api.crafting.IPatternDetails.IInput; +import appeng.api.stacks.AEKey; +import appeng.api.stacks.GenericStack; +import appeng.crafting.pattern.AEProcessingPattern; +import com.extendedae_plus.content.ScaledProcessingPattern; +import com.extendedae_plus.api.SmartDoublingAwarePattern; + + +import static com.extendedae_plus.util.ExtendedAELogger.LOGGER; + +public final class PatternScaler { + private PatternScaler() { + } + + public static ScaledProcessingPattern scale(AEProcessingPattern base, AEKey target, long requestedAmount) { + if (base == null) throw new IllegalArgumentException("base"); + if (target == null) throw new IllegalArgumentException("target"); + + // 双保险:若样板标记为不允许缩放,直接放弃缩放(返回 null 表示调用方应保持原样板) + if (base instanceof SmartDoublingAwarePattern aware && !aware.eap$allowScaling()) { + return null; + } + + GenericStack[] baseSparseInputs = base.getSparseInputs(); + GenericStack[] baseSparseOutputs = base.getSparseOutputs(); + IInput[] baseInputs = base.getInputs(); + GenericStack[] baseOutputs = base.getOutputs(); + + // 新逻辑:不再对样板进行单位化处理 + // 找到目标输出在 outputs 中的索引(尝试匹配 target,否则取第一个非空输出) + int targetOutIndex = -1; + for (int i = 0; i < baseOutputs.length; i++) { + var out = baseOutputs[i]; + if (out != null && target != null && out.what() != null && out.what().equals(target)) { + targetOutIndex = i; + break; + } + } + if (targetOutIndex == -1) { + for (int i = 0; i < baseOutputs.length; i++) { + if (baseOutputs[i] != null) { + targetOutIndex = i; + break; + } + } + } + if (targetOutIndex == -1 && baseOutputs.length > 0) targetOutIndex = 0; + + long perOperationTarget = 1L; + if (targetOutIndex >= 0 && baseOutputs[targetOutIndex] != null) { + long amt = baseOutputs[targetOutIndex].amount(); + if (amt > 0) perOperationTarget = amt; + } + + // 使用最小整数倍(ceil)策略:直接选择满足请求的最小倍数 + long multiplier = 1L; + if (requestedAmount > 0) { + long needed = requestedAmount / perOperationTarget + ((requestedAmount % perOperationTarget) == 0 ? 0 : 1); + multiplier = needed <= 1L ? 1L : needed; + } + + // 构建压缩输入(将每个输入的 multiplier 翻倍,保留每个模板的原始数量) + IInput[] scaledInputs = new IInput[baseInputs.length]; + for (int i = 0; i < baseInputs.length; i++) { + var in = baseInputs[i]; + var template = in.getPossibleInputs(); + GenericStack[] scaledTemplates = new GenericStack[template.length]; + for (int j = 0; j < template.length; j++) { + scaledTemplates[j] = new GenericStack(template[j].what(), template[j].amount()); + } + scaledInputs[i] = new ScaledProcessingPattern.Input(scaledTemplates, in.getMultiplier() * multiplier); + } + + /* 4. 构建压缩输出 */ + GenericStack[] scaledCondensedOutputs = new GenericStack[baseOutputs.length]; + for (int i = 0; i < baseOutputs.length; i++) { + GenericStack out = baseOutputs[i]; + if (out != null) { + scaledCondensedOutputs[i] = new GenericStack(out.what(), out.amount() * multiplier); + } + } + + // 构建并打印稀疏表示(直接按 multiplier 放大) + GenericStack[] scaledSparseInputs = new GenericStack[baseSparseInputs.length]; + for (int i = 0; i < baseSparseInputs.length; i++) { + var in = baseSparseInputs[i]; + if (in != null) { + scaledSparseInputs[i] = new GenericStack(in.what(), in.amount() * multiplier); + } + } + GenericStack[] scaledSparseOutputs = new GenericStack[baseSparseOutputs.length]; + for (int i = 0; i < baseSparseOutputs.length; i++) { + var out = baseSparseOutputs[i]; + if (out != null) { + scaledSparseOutputs[i] = new GenericStack(out.what(), out.amount() * multiplier); + } + } + + return new ScaledProcessingPattern(base, + base.getDefinition(), + scaledSparseInputs, + scaledSparseOutputs, + scaledInputs, + scaledCondensedOutputs); + } +} diff --git a/src/main/java/com/extendedae_plus/util/RequestedAmountHolder.java b/src/main/java/com/extendedae_plus/util/RequestedAmountHolder.java new file mode 100644 index 0000000..e70187a --- /dev/null +++ b/src/main/java/com/extendedae_plus/util/RequestedAmountHolder.java @@ -0,0 +1,44 @@ +package com.extendedae_plus.util; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Thread-local stack holder for requested amounts to support nested requests. + */ +public final class RequestedAmountHolder { + private static final ThreadLocal> HOLDER = ThreadLocal.withInitial(ArrayDeque::new); + + private RequestedAmountHolder() { + } + + /** + * Push a requested amount onto the thread-local stack. + */ + public static void push(long v) { + Deque dq = HOLDER.get(); + dq.push(v); + } + + /** + * Pop the top value from the thread-local stack. Safe if empty. + */ + public static void pop() { + Deque dq = HOLDER.get(); + if (dq.isEmpty()) { + return; + } + dq.pop(); + } + + /** + * Peek the current requested amount or return 0 if none. + */ + public static long get() { + Deque dq = HOLDER.get(); + Long v = dq.peek(); + return v == null ? 0L : v; + } +} + + diff --git a/src/main/resources/assets/extendedae_plus/blockstates/network_pattern_controller.json b/src/main/resources/assets/extendedae_plus/blockstates/network_pattern_controller.json new file mode 100644 index 0000000..6b2af88 --- /dev/null +++ b/src/main/resources/assets/extendedae_plus/blockstates/network_pattern_controller.json @@ -0,0 +1,5 @@ +{ + "variants": { + "": { "model": "extendedae_plus:block/network_pattern_controller" } + } +} diff --git a/src/main/resources/assets/extendedae_plus/lang/en_us.json b/src/main/resources/assets/extendedae_plus/lang/en_us.json index 80f5806..3a7c889 100644 --- a/src/main/resources/assets/extendedae_plus/lang/en_us.json +++ b/src/main/resources/assets/extendedae_plus/lang/en_us.json @@ -14,5 +14,25 @@ "gui.extendedae_plus.group_header.choice": "Toggle Group Choice", "gui.extendedae_plus.group_header.choiceable": "Group is choiceable", "gui.extendedae_plus.group_header.not_choiceable": "Group is not choiceable", - "itemGroup.extendedae_plus.main": "ExtendedAE Plus" + "itemGroup.extendedae_plus.main": "ExtendedAE Plus", + + "config.jade.plugin_extendedae_plus.wireless_transceiver_info": "Wireless Transceiver Info", + "config.jade.plugin_extendedae_plus.wt_frequency": "Show Frequency", + "config.jade.plugin_extendedae_plus.wt_master_mode": "Show Master/Slave Mode", + "config.jade.plugin_extendedae_plus.wt_master_location": "Show Master Position", + "config.jade.plugin_extendedae_plus.wt_locked": "Show Locked State", + "config.jade.plugin_extendedae_plus.wt_network_usable": "Show Network Online State", + "extendedae_plus.tooltip.frequency": "Frequency: %d", + "extendedae_plus.tooltip.master_mode": "Mode: %s", + "extendedae_plus.tooltip.locked": "Locked: %s" + , + "screen.extendedae_plus.title": "ExtendedAE Plus Config", + "config.extendedae_plus.pageMultiplier": "Pattern Provider Page Multiplier", + "config.extendedae_plus.pageMultiplier_with_range": "Pattern Provider Page Multiplier (1-64)", + "config.extendedae_plus.wirelessMaxRange": "Wireless Max Range", + "config.extendedae_plus.wirelessMaxRange_with_range": "Wireless Max Range (1-4096)", + "config.extendedae_plus.wirelessCrossDimEnable": "Allow Wireless Cross-Dimension", + "config.extendedae_plus.providerRoundRobinEnable": "Enable Provider Round-Robin (Smart Doubling)", + "config.extendedae_plus.state_on": "On", + "config.extendedae_plus.state_off": "Off" } \ No newline at end of file diff --git a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json index 82e3da9..30bc69e 100644 --- a/src/main/resources/assets/extendedae_plus/lang/zh_cn.json +++ b/src/main/resources/assets/extendedae_plus/lang/zh_cn.json @@ -27,5 +27,32 @@ "item.extendedae_plus.16x_crafting_accelerator": "16x并行处理单元", "item.extendedae_plus.64x_crafting_accelerator": "64x并行处理单元", "item.extendedae_plus.256x_crafting_accelerator": "256x并行处理单元", - "item.extendedae_plus.1024x_crafting_accelerator": "1024x并行处理单元" + "item.extendedae_plus.1024x_crafting_accelerator": "1024x并行处理单元", + + "config.jade.plugin_extendedae_plus.wireless_transceiver_info": "无线收发器信息", + "config.jade.plugin_extendedae_plus.wt_frequency": "显示频率", + "config.jade.plugin_extendedae_plus.wt_master_mode": "显示主/从模式", + "config.jade.plugin_extendedae_plus.wt_master_location": "显示主节点位置", + "config.jade.plugin_extendedae_plus.wt_locked": "显示锁定状态", + "config.jade.plugin_extendedae_plus.wt_network_usable": "显示网络在线状态", + "extendedae_plus.tooltip.frequency": "频率: %d", + "extendedae_plus.tooltip.master_mode": "模式: %s", + "extendedae_plus.tooltip.locked": "锁定状态: %s", + + "screen.extendedae_plus.title": "ExtendedAE Plus 配置", + "config.extendedae_plus.pageMultiplier": "扩展样板供应器槽位倍率", + "config.extendedae_plus.pageMultiplier_with_range": "扩展样板供应器槽位倍率 (1-64)", + "config.extendedae_plus.wirelessMaxRange": "无线最大距离", + "config.extendedae_plus.wirelessMaxRange_with_range": "无线最大距离 (1-4096)", + "config.extendedae_plus.wirelessCrossDimEnable": "无线收发器允许跨维度连接", + "config.extendedae_plus.providerRoundRobinEnable": "启用样板供应器轮询分配(智能翻倍)", + "config.extendedae_plus.state_on": "开", + "config.extendedae_plus.state_off": "关", + "block.extendedae_plus.network_pattern_controller": "样板供应器状态控制器", + "item.extendedae_plus.network_pattern_controller": "样板供应器状态控制器", + "gui.extendedae_plus.global.toggle_blocking": "切换阻挡模式", + "gui.extendedae_plus.global.toggle_adv_blocking": "切换高级阻挡", + "gui.extendedae_plus.global.toggle_smart_doubling": "切换智能翻倍", + "gui.extendedae_plus.global.all_on": "全部开启", + "gui.extendedae_plus.global.all_off": "全部关闭" } \ No newline at end of file diff --git a/src/main/resources/assets/extendedae_plus/models/block/network_pattern_controller.json b/src/main/resources/assets/extendedae_plus/models/block/network_pattern_controller.json new file mode 100644 index 0000000..3a3c837 --- /dev/null +++ b/src/main/resources/assets/extendedae_plus/models/block/network_pattern_controller.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:block/cube_all", + "textures": { + "all": "extendedae_plus:block/network_pattern_controller" + } +} diff --git a/src/main/resources/assets/extendedae_plus/models/item/network_pattern_controller.json b/src/main/resources/assets/extendedae_plus/models/item/network_pattern_controller.json new file mode 100644 index 0000000..a439891 --- /dev/null +++ b/src/main/resources/assets/extendedae_plus/models/item/network_pattern_controller.json @@ -0,0 +1,3 @@ +{ + "parent": "extendedae_plus:block/network_pattern_controller" +} diff --git a/src/main/resources/assets/extendedae_plus/textures/block/network_pattern_controller.png b/src/main/resources/assets/extendedae_plus/textures/block/network_pattern_controller.png new file mode 100644 index 0000000..56c43c2 Binary files /dev/null and b/src/main/resources/assets/extendedae_plus/textures/block/network_pattern_controller.png differ diff --git a/src/main/resources/data/extendedae_plus/loot_tables/blocks/network_pattern_controller.json b/src/main/resources/data/extendedae_plus/loot_tables/blocks/network_pattern_controller.json new file mode 100644 index 0000000..f167e4a --- /dev/null +++ b/src/main/resources/data/extendedae_plus/loot_tables/blocks/network_pattern_controller.json @@ -0,0 +1,14 @@ +{ + "type": "minecraft:block", + "pools": [ + { + "rolls": 1, + "entries": [ + { "type": "minecraft:item", "name": "extendedae_plus:network_pattern_controller" } + ], + "conditions": [ + { "condition": "minecraft:survives_explosion" } + ] + } + ] +} diff --git a/src/main/resources/data/extendedae_plus/recipes/network_pattern_controller.json b/src/main/resources/data/extendedae_plus/recipes/network_pattern_controller.json new file mode 100644 index 0000000..38721c7 --- /dev/null +++ b/src/main/resources/data/extendedae_plus/recipes/network_pattern_controller.json @@ -0,0 +1,12 @@ +{ + "type": "minecraft:crafting_shapeless", + "ingredients": [ + { "item": "ae2:semi_dark_monitor" }, + { "item": "ae2:pattern_provider" }, + { "item": "ae2:network_tool" } + ], + "result": { + "item": "extendedae_plus:network_pattern_controller", + "count": 1 + } +} diff --git a/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json b/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json index a01207f..388e10b 100644 --- a/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json +++ b/src/main/resources/data/minecraft/tags/blocks/mineable/pickaxe.json @@ -1,6 +1,7 @@ { "replace": false, "values": [ + "extendedae_plus:network_pattern_controller", "extendedae_plus:wireless_transceiver", "extendedae_plus:4x_crafting_accelerator", "extendedae_plus:16x_crafting_accelerator", diff --git a/src/main/resources/extendedae_plus.mixins.json b/src/main/resources/extendedae_plus.mixins.json index ebebd0c..cf41575 100644 --- a/src/main/resources/extendedae_plus.mixins.json +++ b/src/main/resources/extendedae_plus.mixins.json @@ -7,7 +7,6 @@ "PickFromWirelessMixin", "accessor.AbstractContainerScreenAccessor", "accessor.ScreenAccessor", - "accessor.ScreenInvoker", "ae2.AEBaseScreenMixin", "ae2.EncodedPatternItemMixin", "ae2.PatternEncodingTermScreenMixin", @@ -24,25 +23,38 @@ "extendedae.HighlightButtonMixin", "extendedae.accessor.GuiExPatternTerminalAccessor", "extendedae.accessor.GuiExPatternTerminalSlotsRowAccessor", - "jei.EncodePatternTransferHandlerMixin", - "hooks.ModelBakeryMixin" + "ae2.EncodePatternTransferHandlerMixin", + "ae2.EncodingHelperMixin", + "jei.accessor.BookmarkOverlayAccessor", + "hooks.ModelBakeryMixin", + "jei.EncodePatternTransferHandlerMixin" ], "mixins": [ + "ae2.AEProcessingPatternMixin", "ae2.ContainerPatternEncodingTermMenuMixin", + "ae2.CraftingCPUClusterMixin", "ae2.MEStorageMenuMixin", "ae2.PatternEncodingTermMenuMixin", "ae2.PatternProviderLogicAdvancedMixin", + "ae2.PatternProviderLogicDoublingMixin", "ae2.PatternProviderMenuAdvancedMixin", + "ae2.PatternProviderMenuDoublingMixin", "ae2.accessor.MEStorageMenuAccessor", "ae2.accessor.PatternEncodingTermMenuAccessor", "ae2.accessor.PatternProviderLogicAccessor", "ae2.accessor.PatternProviderLogicPatternInputsAccessor", + "ae2.accessor.PatternProviderLogicPatternsAccessor", "ae2.accessor.PatternProviderMenuAdvancedAccessor", "ae2WTlib.ContainerUWirelessExPatternTerminalMixin", + "autopattern.CraftingProviderListAccessor", + "autopattern.CraftingServiceGetProvidersMixin", + "autopattern.CraftingTreeNodeAccessor", + "autopattern.CraftingTreeNodeMixin", + "autopattern.CraftingTreeProcessMixin", + "autopattern.PatternProviderLogicContainsRedirectMixin", "extendedae.ContainerExPatternProviderMixin", "extendedae.ContainerExPatternTerminalMixin", "extendedae.ContainerWirelessExPatternTerminalMixin", - "ae2.CraftingCPUClusterMixin", "extendedae.PartExPatternProviderMixin", "extendedae.TileExPatternProviderMixin" ],