基础移植

This commit is contained in:
GaLi 2026-03-01 15:58:50 +08:00
parent 675123e8e9
commit 236a1fbe2d
15 changed files with 1541 additions and 3 deletions

View File

@ -0,0 +1,39 @@
package com.extendedae_plus.client;
import com.extendedae_plus.ExtendedAEPlus;
import com.mojang.blaze3d.platform.InputConstants;
import net.minecraft.client.KeyMapping;
import net.neoforged.api.distmarker.Dist;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.client.event.RegisterKeyMappingsEvent;
import net.neoforged.neoforge.client.settings.KeyConflictContext;
import net.neoforged.neoforge.client.settings.KeyModifier;
import org.lwjgl.glfw.GLFW;
/**
* ExtendedAE Plus 快捷键定义
*/
@EventBusSubscriber(modid = ExtendedAEPlus.MODID, bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public final class ModKeybindings {
private ModKeybindings() {
}
/**
* Ctrl+Q 快速创建样板快捷键
*/
public static final KeyMapping CREATE_PATTERN_KEY = new KeyMapping(
"key.extendedae_plus.create_pattern",
KeyConflictContext.GUI,
KeyModifier.CONTROL,
InputConstants.Type.KEYSYM,
GLFW.GLFW_KEY_Q,
"key.categories.extendedae_plus"
);
@SubscribeEvent
public static void register(RegisterKeyMappingsEvent event) {
event.register(CREATE_PATTERN_KEY);
}
}

View File

@ -0,0 +1,319 @@
package com.extendedae_plus.client.event;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.GenericStack;
import com.extendedae_plus.client.ModKeybindings;
import com.extendedae_plus.integration.jei.JeiRuntimeProxy;
import com.extendedae_plus.network.CreateAndUploadPatternC2SPacket;
import com.extendedae_plus.network.CreateCtrlQPatternC2SPacket;
import com.extendedae_plus.network.RequestProvidersListC2SPacket;
import com.extendedae_plus.util.RecipeFinderUtil;
import com.extendedae_plus.util.RecipeInfo;
import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil;
import mezz.jei.api.constants.RecipeTypes;
import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.ingredients.ITypedIngredient;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Recipe;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.neoforge.client.event.ScreenEvent;
import net.neoforged.neoforge.network.PacketDistributor;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Ctrl+Q 快速创建样板事件监听器
*/
public final class CtrlQPatternKeyHandler {
private CtrlQPatternKeyHandler() {
}
@SubscribeEvent
public static void onScreenKeyPressed(ScreenEvent.KeyPressed.Pre event) {
Screen screen = event.getScreen();
if (screen == null) {
return;
}
int keyCode = event.getKeyCode();
int scanCode = event.getScanCode();
if (!ModKeybindings.CREATE_PATTERN_KEY.matches(keyCode, scanCode)) {
return;
}
if (JeiRuntimeProxy.get() == null) {
return;
}
Optional<?> recipeBookmark = JeiRuntimeProxy.getRecipeBookmarkUnderMouse();
if (recipeBookmark.isPresent()) {
handleRecipeBookmark(recipeBookmark.get());
event.setCanceled(true);
return;
}
Optional<ITypedIngredient<?>> ingredient = castTypedIngredient(JeiRuntimeProxy.getIngredientUnderMouse());
if (ingredient.isEmpty()) {
Minecraft mc = Minecraft.getInstance();
if (mc.player != null) {
mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.hover_item_first"), true);
}
return;
}
List<RecipeInfo> recipes = RecipeFinderUtil.findRecipesByIngredient(ingredient.get());
if (recipes.isEmpty()) {
Minecraft mc = Minecraft.getInstance();
if (mc.player != null) {
mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.no_recipes_found"), true);
}
return;
}
RecipeInfo selected = RecipeFinderUtil.selectBestRecipe(recipes);
if (selected == null || selected.getRecipeId() == null) {
return;
}
List<ItemStack> selectedIngredients = selectIngredientsWithJeiPriority(selected);
List<ItemStack> selectedOutputs = convertOutputsToItemStacks(selected);
PacketDistributor.sendToServer(new CreateCtrlQPatternC2SPacket(
selected.getRecipeId(),
selected.isCraftingRecipe(),
selectedIngredients,
selectedOutputs
));
event.setCanceled(true);
}
private static void handleRecipeBookmark(Object recipeBookmark) {
if (isCraftingRecipe(recipeBookmark)) {
handleCraftingRecipeBookmark(recipeBookmark);
} else {
handleProcessingRecipeBookmark(recipeBookmark);
}
}
private static boolean isCraftingRecipe(Object recipeBookmark) {
try {
var getRecipeCategoryMethod = recipeBookmark.getClass().getMethod("getRecipeCategory");
Object recipeCategory = getRecipeCategoryMethod.invoke(recipeBookmark);
var getRecipeTypeMethod = recipeCategory.getClass().getMethod("getRecipeType");
Object recipeType = getRecipeTypeMethod.invoke(recipeCategory);
return RecipeTypes.CRAFTING.equals(recipeType)
|| RecipeTypes.STONECUTTING.equals(recipeType)
|| RecipeTypes.SMITHING.equals(recipeType);
} catch (Throwable ignored) {
return false;
}
}
private static void handleCraftingRecipeBookmark(Object recipeBookmark) {
try {
ResourceLocation recipeId = getRecipeId(recipeBookmark);
if (recipeId == null) {
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) {
return;
}
var recipeOpt = mc.level.getRecipeManager().byKey(recipeId);
if (recipeOpt.isEmpty()) {
if (mc.player != null) {
mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), true);
}
return;
}
List<RecipeInfo> recipeInfos = findRecipeInfosForBookmark(recipeBookmark);
if (recipeInfos.isEmpty()) {
if (mc.player != null) {
mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.no_recipes_found"), true);
}
return;
}
RecipeInfo matching = matchById(recipeInfos, recipeId);
List<ItemStack> selectedIngredients = selectIngredientsWithJeiPriority(matching);
List<ItemStack> selectedOutputs = convertOutputsToItemStacks(matching);
PacketDistributor.sendToServer(new CreateAndUploadPatternC2SPacket(
recipeId,
matching.isCraftingRecipe(),
selectedIngredients,
selectedOutputs
));
} catch (Throwable ignored) {
}
}
private static void handleProcessingRecipeBookmark(Object recipeBookmark) {
try {
ResourceLocation recipeId = getRecipeId(recipeBookmark);
if (recipeId == null) {
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) {
return;
}
var recipeOpt = mc.level.getRecipeManager().byKey(recipeId);
if (recipeOpt.isEmpty()) {
if (mc.player != null) {
mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), true);
}
return;
}
Object recipeBase = null;
try {
var getRecipeMethod = recipeBookmark.getClass().getMethod("getRecipe");
recipeBase = getRecipeMethod.invoke(recipeBookmark);
} catch (Throwable ignored) {
}
setLastProcessingNameFromRecipe(recipeBase != null ? recipeBase : recipeOpt.get());
List<RecipeInfo> recipeInfos = findRecipeInfosForBookmark(recipeBookmark);
if (recipeInfos.isEmpty()) {
if (mc.player != null) {
mc.player.displayClientMessage(Component.translatable("message.extendedae_plus.no_recipes_found"), true);
}
return;
}
RecipeInfo matching = matchById(recipeInfos, recipeId);
List<ItemStack> selectedIngredients = selectIngredientsWithJeiPriority(matching);
List<ItemStack> selectedOutputs = convertOutputsToItemStacks(matching);
PacketDistributor.sendToServer(new CreateCtrlQPatternC2SPacket(
recipeId,
matching.isCraftingRecipe(),
selectedIngredients,
selectedOutputs,
true
));
PacketDistributor.sendToServer(RequestProvidersListC2SPacket.INSTANCE);
} catch (Throwable ignored) {
}
}
private static List<RecipeInfo> findRecipeInfosForBookmark(Object recipeBookmark) {
Optional<ITypedIngredient<?>> hovered = castTypedIngredient(JeiRuntimeProxy.getIngredientUnderMouse());
if (hovered.isPresent()) {
List<RecipeInfo> infos = RecipeFinderUtil.findRecipesByIngredient(hovered.get());
if (!infos.isEmpty()) {
return infos;
}
}
try {
var getRecipeOutputMethod = recipeBookmark.getClass().getMethod("getRecipeOutput");
Object recipeOutput = getRecipeOutputMethod.invoke(recipeBookmark);
if (recipeOutput instanceof ITypedIngredient<?> typed) {
return RecipeFinderUtil.findRecipesByIngredient(typed);
}
} catch (Throwable ignored) {
}
return List.of();
}
private static RecipeInfo matchById(List<RecipeInfo> recipeInfos, ResourceLocation recipeId) {
for (RecipeInfo info : recipeInfos) {
if (recipeId.equals(info.getRecipeId())) {
return info;
}
}
return recipeInfos.get(0);
}
private static ResourceLocation getRecipeId(Object recipeBookmark) {
try {
var getRecipeUidMethod = recipeBookmark.getClass().getMethod("getRecipeUid");
Object recipeId = getRecipeUidMethod.invoke(recipeBookmark);
if (recipeId instanceof ResourceLocation rl) {
return rl;
}
} catch (Throwable ignored) {
}
return null;
}
@SuppressWarnings("unchecked")
private static Optional<ITypedIngredient<?>> castTypedIngredient(Optional<?> opt) {
if (opt == null || opt.isEmpty()) {
return Optional.empty();
}
Object value = opt.get();
if (value instanceof ITypedIngredient<?>) {
return (Optional<ITypedIngredient<?>>) (Optional<?>) Optional.of(value);
}
return Optional.empty();
}
private static void setLastProcessingNameFromRecipe(Object recipeBase) {
String name = null;
if (recipeBase instanceof Recipe<?> recipe) {
name = ExtendedAEPatternUploadUtil.mapRecipeTypeToSearchKey(recipe);
} else if (recipeBase != null
&& "com.gregtechceu.gtceu.api.recipe.GTRecipe".equals(recipeBase.getClass().getName())) {
name = ExtendedAEPatternUploadUtil.mapGTCEuRecipeToSearchKey(recipeBase);
} else if (recipeBase != null
&& "com.gregtechceu.gtceu.integration.jei.recipe.GTRecipeWrapper".equals(recipeBase.getClass().getName())) {
try {
var field = recipeBase.getClass().getField("recipe");
Object inner = field.get(recipeBase);
name = ExtendedAEPatternUploadUtil.mapGTCEuRecipeToSearchKey(inner);
} catch (Throwable ignored) {
}
}
if (name == null || name.isBlank()) {
name = ExtendedAEPatternUploadUtil.deriveSearchKeyFromUnknownRecipe(recipeBase);
}
if (name != null && !name.isBlank()) {
ExtendedAEPatternUploadUtil.setLastProcessingName(name);
}
}
private static List<ItemStack> selectIngredientsWithJeiPriority(RecipeInfo recipeInfo) {
List<?> bookmarks = JeiRuntimeProxy.getBookmarkList();
Map<Item, Integer> priorities = new HashMap<>();
AtomicInteger index = new AtomicInteger(Integer.MAX_VALUE);
for (Object obj : bookmarks) {
if (obj instanceof ITypedIngredient<?> ingredient) {
ingredient.getIngredient(VanillaTypes.ITEM_STACK).ifPresent(itemStack ->
priorities.put(itemStack.getItem(), index.getAndDecrement())
);
}
}
return recipeInfo.selectBestInputs(priorities);
}
private static List<ItemStack> convertOutputsToItemStacks(RecipeInfo recipeInfo) {
return recipeInfo.getOutputs().stream()
.map(genericStack -> {
if (genericStack.what() instanceof AEItemKey itemKey) {
return itemKey.toStack((int) genericStack.amount());
}
return GenericStack.wrapInItemStack(genericStack);
})
.toList();
}
}

View File

@ -22,6 +22,8 @@ public class ModNetwork {
registrar.playToServer(OpenProviderUiC2SPacket.TYPE, OpenProviderUiC2SPacket.STREAM_CODEC, OpenProviderUiC2SPacket::handle); registrar.playToServer(OpenProviderUiC2SPacket.TYPE, OpenProviderUiC2SPacket.STREAM_CODEC, OpenProviderUiC2SPacket::handle);
registrar.playToServer(UploadEncodedPatternToProviderC2SPacket.TYPE, UploadEncodedPatternToProviderC2SPacket.STREAM_CODEC, UploadEncodedPatternToProviderC2SPacket::handle); registrar.playToServer(UploadEncodedPatternToProviderC2SPacket.TYPE, UploadEncodedPatternToProviderC2SPacket.STREAM_CODEC, UploadEncodedPatternToProviderC2SPacket::handle);
registrar.playToServer(UploadInventoryPatternToProviderC2SPacket.TYPE, UploadInventoryPatternToProviderC2SPacket.STREAM_CODEC, UploadInventoryPatternToProviderC2SPacket::handle); registrar.playToServer(UploadInventoryPatternToProviderC2SPacket.TYPE, UploadInventoryPatternToProviderC2SPacket.STREAM_CODEC, UploadInventoryPatternToProviderC2SPacket::handle);
registrar.playToServer(CreateCtrlQPatternC2SPacket.TYPE, CreateCtrlQPatternC2SPacket.STREAM_CODEC, CreateCtrlQPatternC2SPacket::handle);
registrar.playToServer(CreateAndUploadPatternC2SPacket.TYPE, CreateAndUploadPatternC2SPacket.STREAM_CODEC, CreateAndUploadPatternC2SPacket::handle);
registrar.playToServer(EncodeWithShiftFlagC2SPacket.TYPE, EncodeWithShiftFlagC2SPacket.STREAM_CODEC, EncodeWithShiftFlagC2SPacket::handle); registrar.playToServer(EncodeWithShiftFlagC2SPacket.TYPE, EncodeWithShiftFlagC2SPacket.STREAM_CODEC, EncodeWithShiftFlagC2SPacket::handle);
// 新增JEI 中键打开合成界面 & 无线终端拾取方块物品 // 新增JEI 中键打开合成界面 & 无线终端拾取方块物品
registrar.playToServer(com.extendedae_plus.network.OpenCraftFromJeiC2SPacket.TYPE, registrar.playToServer(com.extendedae_plus.network.OpenCraftFromJeiC2SPacket.TYPE,

View File

@ -6,5 +6,6 @@ public final class JeiClientBootstrap {
public static void register() { public static void register() {
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.InputEvents::onMouseButtonPre); net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.InputEvents::onMouseButtonPre);
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.InputEvents::onKeyPressedPre); net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.InputEvents::onKeyPressedPre);
net.neoforged.neoforge.common.NeoForge.EVENT_BUS.addListener(com.extendedae_plus.client.event.CtrlQPatternKeyHandler::onScreenKeyPressed);
} }
} }

View File

@ -141,6 +141,54 @@ public final class JeiRuntimeProxy {
return Collections.emptyList(); return Collections.emptyList();
} }
/**
* 获取鼠标下的配方书签RecipeBookmark用于 Ctrl+Q 直接识别配方书签而非普通配料
*/
public static Optional<?> getRecipeBookmarkUnderMouse() {
Object rt = RUNTIME;
if (rt == null) return Optional.empty();
try {
Object overlay = rt.getClass().getMethod("getBookmarkOverlay").invoke(rt);
if (overlay == null) return Optional.empty();
Object ingredientOpt = overlay.getClass().getMethod("getIngredientUnderMouse").invoke(overlay);
if (!(ingredientOpt instanceof Optional<?> opt) || opt.isEmpty()) return Optional.empty();
Object hoveredIngredient = opt.get();
Field f = overlay.getClass().getDeclaredField("bookmarkList");
f.setAccessible(true);
Object bookmarkList = f.get(overlay);
if (bookmarkList == null) return Optional.empty();
Object elementsObj = bookmarkList.getClass().getMethod("getElements").invoke(bookmarkList);
if (!(elementsObj instanceof List<?> elements)) return Optional.empty();
for (Object element : elements) {
if (element == null) continue;
Object typedIngredient = null;
try {
typedIngredient = element.getClass().getMethod("getTypedIngredient").invoke(element);
} catch (Throwable ignored) {
}
if (typedIngredient == null || !typedIngredient.equals(hoveredIngredient)) continue;
Object bookmarkOpt = null;
try {
bookmarkOpt = element.getClass().getMethod("getBookmark").invoke(element);
} catch (Throwable ignored) {
}
if (bookmarkOpt instanceof Optional<?> b && b.isPresent()) {
Object bookmark = b.get();
if (bookmark != null && "RecipeBookmark".equals(bookmark.getClass().getSimpleName())) {
return Optional.of(bookmark);
}
}
}
} catch (Throwable ignored) {
}
return Optional.empty();
}
public static void addBookmark(ItemStack stack) { public static void addBookmark(ItemStack stack) {
Object rt = RUNTIME; Object rt = RUNTIME;
if (rt == null || stack == null || stack.isEmpty()) return; if (rt == null || stack == null || stack.isEmpty()) return;

View File

@ -0,0 +1,216 @@
package com.extendedae_plus.network;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.networking.IGrid;
import appeng.api.networking.energy.IEnergyService;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.GenericStack;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import appeng.core.definitions.AEItems;
import appeng.me.helpers.PlayerSource;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.util.uploadPattern.CtrlQPendingUploadUtil;
import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import java.util.ArrayList;
import java.util.List;
/**
* C2S: 创建样板并上传到装配矩阵合成书签分支
*/
public class CreateAndUploadPatternC2SPacket implements CustomPacketPayload {
public static final Type<CreateAndUploadPatternC2SPacket> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(ExtendedAEPlus.MODID, "create_and_upload_pattern"));
public static final StreamCodec<RegistryFriendlyByteBuf, CreateAndUploadPatternC2SPacket> STREAM_CODEC = StreamCodec.of(
(buf, pkt) -> {
buf.writeResourceLocation(pkt.recipeId);
buf.writeBoolean(pkt.isCraftingPattern);
ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(buf, pkt.selectedIngredients);
ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(buf, pkt.outputs);
},
buf -> {
ResourceLocation recipeId = buf.readResourceLocation();
boolean isCraftingPattern = buf.readBoolean();
List<ItemStack> ingredients = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buf);
List<ItemStack> outputs = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buf);
return new CreateAndUploadPatternC2SPacket(recipeId, isCraftingPattern, ingredients, outputs);
}
);
private final ResourceLocation recipeId;
private final boolean isCraftingPattern;
private final List<ItemStack> selectedIngredients;
private final List<ItemStack> outputs;
public CreateAndUploadPatternC2SPacket(
ResourceLocation recipeId,
boolean isCraftingPattern,
List<ItemStack> selectedIngredients,
List<ItemStack> outputs
) {
this.recipeId = recipeId;
this.isCraftingPattern = isCraftingPattern;
this.selectedIngredients = selectedIngredients;
this.outputs = outputs;
}
public static void handle(final CreateAndUploadPatternC2SPacket msg, final IPayloadContext ctx) {
ctx.enqueueWork(() -> {
if (!(ctx.player() instanceof ServerPlayer player)) {
return;
}
var recipeOpt = player.level().getRecipeManager().byKey(msg.recipeId);
if (recipeOpt.isEmpty()) {
player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), false);
return;
}
RecipeHolder<?> recipeHolder = recipeOpt.get();
IGrid grid = CtrlQPendingUploadUtil.findPlayerGrid(player);
if (grid == null) {
player.displayClientMessage(Component.translatable("message.extendedae_plus.no_network"), false);
return;
}
if (!consumeBlankPattern(player, grid)) {
player.displayClientMessage(Component.translatable("message.extendedae_plus.no_blank_pattern"), false);
return;
}
ItemStack pattern = createPattern(recipeHolder, msg.isCraftingPattern, msg.selectedIngredients, msg.outputs, player);
if (pattern.isEmpty()) {
refundBlankPattern(player, grid);
player.displayClientMessage(Component.translatable("message.extendedae_plus.pattern_creation_failed"), false);
return;
}
boolean uploaded = ExtendedAEPatternUploadUtil.uploadPatternToMatrix(player, pattern, grid);
if (!uploaded) {
refundBlankPattern(player, grid);
}
});
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
private static boolean consumeBlankPattern(ServerPlayer player, IGrid grid) {
AEItemKey blankPatternKey = AEItemKey.of(AEItems.BLANK_PATTERN.stack());
IEnergyService energy = grid.getEnergyService();
MEStorage storage = grid.getStorageService().getInventory();
long extracted = StorageHelper.poweredExtraction(
energy,
storage,
blankPatternKey,
1,
new PlayerSource(player)
);
return extracted > 0;
}
private static void refundBlankPattern(ServerPlayer player, IGrid grid) {
AEItemKey blankPatternKey = AEItemKey.of(AEItems.BLANK_PATTERN.stack());
IEnergyService energy = grid.getEnergyService();
MEStorage storage = grid.getStorageService().getInventory();
StorageHelper.poweredInsert(
energy,
storage,
blankPatternKey,
1,
new PlayerSource(player)
);
}
private static ItemStack createPattern(
RecipeHolder<?> recipeHolder,
boolean isCrafting,
List<ItemStack> selectedIngredients,
List<ItemStack> selectedOutputs,
ServerPlayer player
) {
try {
if (isCrafting && recipeHolder.value() instanceof CraftingRecipe) {
@SuppressWarnings("unchecked")
RecipeHolder<CraftingRecipe> craftingHolder = (RecipeHolder<CraftingRecipe>) (RecipeHolder<?>) recipeHolder;
ItemStack[] inputs = new ItemStack[9];
for (int i = 0; i < 9; i++) {
if (i < selectedIngredients.size()) {
inputs[i] = selectedIngredients.get(i).copy();
} else {
inputs[i] = ItemStack.EMPTY;
}
}
ItemStack output = ItemStack.EMPTY;
if (!selectedOutputs.isEmpty()) {
output = selectedOutputs.get(0).copy();
}
if (output.isEmpty()) {
output = recipeHolder.value().getResultItem(player.level().registryAccess()).copy();
}
ItemStack encodedPattern = PatternDetailsHelper.encodeCraftingPattern(
craftingHolder,
inputs,
output,
true,
false
);
CustomData.update(DataComponents.CUSTOM_DATA, encodedPattern, tag -> tag.putString("encodePlayer", player.getGameProfile().getName()));
return encodedPattern;
}
List<GenericStack> inputs = new ArrayList<>();
List<GenericStack> outputs = new ArrayList<>();
for (ItemStack item : selectedIngredients) {
if (!item.isEmpty()) {
GenericStack stack = GenericStack.unwrapItemStack(item);
if (stack == null) {
stack = GenericStack.fromItemStack(item);
}
if (stack != null) {
inputs.add(stack);
}
}
}
for (ItemStack item : selectedOutputs) {
if (!item.isEmpty()) {
GenericStack stack = GenericStack.unwrapItemStack(item);
if (stack == null) {
stack = GenericStack.fromItemStack(item);
}
if (stack != null) {
outputs.add(stack);
}
}
}
ItemStack encodedPattern = PatternDetailsHelper.encodeProcessingPattern(inputs, outputs);
CustomData.update(DataComponents.CUSTOM_DATA, encodedPattern, tag -> tag.putString("encodePlayer", player.getGameProfile().getName()));
return encodedPattern;
} catch (Throwable ignored) {
return ItemStack.EMPTY;
}
}
}

View File

@ -0,0 +1,260 @@
package com.extendedae_plus.network;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.networking.IGrid;
import appeng.api.networking.energy.IEnergyService;
import appeng.api.stacks.GenericStack;
import appeng.api.storage.MEStorage;
import appeng.api.storage.StorageHelper;
import appeng.core.definitions.AEItems;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.me.helpers.PlayerSource;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.util.uploadPattern.CtrlQPendingUploadUtil;
import com.extendedae_plus.util.wireless.WirelessTerminalLocator;
import net.minecraft.core.component.DataComponents;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Inventory;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.neoforged.neoforge.network.handling.IPayloadContext;
import java.util.ArrayList;
import java.util.List;
/**
* C2S: Ctrl+Q 快速创建样板请求
*/
public class CreateCtrlQPatternC2SPacket implements CustomPacketPayload {
public static final Type<CreateCtrlQPatternC2SPacket> TYPE = new Type<>(
ResourceLocation.fromNamespaceAndPath(ExtendedAEPlus.MODID, "create_ctrlq_pattern"));
public static final StreamCodec<RegistryFriendlyByteBuf, CreateCtrlQPatternC2SPacket> STREAM_CODEC = StreamCodec.of(
(buf, pkt) -> {
buf.writeResourceLocation(pkt.recipeId);
buf.writeBoolean(pkt.isCraftingPattern);
ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(buf, pkt.selectedIngredients);
ItemStack.OPTIONAL_LIST_STREAM_CODEC.encode(buf, pkt.outputs);
buf.writeBoolean(pkt.openProviderSelector);
},
buf -> {
ResourceLocation recipeId = buf.readResourceLocation();
boolean isCraftingPattern = buf.readBoolean();
List<ItemStack> ingredients = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buf);
List<ItemStack> outputs = ItemStack.OPTIONAL_LIST_STREAM_CODEC.decode(buf);
boolean openProviderSelector = buf.readableBytes() > 0 && buf.readBoolean();
return new CreateCtrlQPatternC2SPacket(recipeId, isCraftingPattern, ingredients, outputs, openProviderSelector);
}
);
private final ResourceLocation recipeId;
private final boolean isCraftingPattern;
private final List<ItemStack> selectedIngredients;
private final List<ItemStack> outputs;
private final boolean openProviderSelector;
public CreateCtrlQPatternC2SPacket(
ResourceLocation recipeId,
boolean isCraftingPattern,
List<ItemStack> selectedIngredients,
List<ItemStack> outputs
) {
this(recipeId, isCraftingPattern, selectedIngredients, outputs, false);
}
public CreateCtrlQPatternC2SPacket(
ResourceLocation recipeId,
boolean isCraftingPattern,
List<ItemStack> selectedIngredients,
List<ItemStack> outputs,
boolean openProviderSelector
) {
this.recipeId = recipeId;
this.isCraftingPattern = isCraftingPattern;
this.selectedIngredients = selectedIngredients;
this.outputs = outputs;
this.openProviderSelector = openProviderSelector;
}
public static void handle(final CreateCtrlQPatternC2SPacket msg, final IPayloadContext ctx) {
ctx.enqueueWork(() -> {
if (!(ctx.player() instanceof ServerPlayer player)) {
return;
}
var recipeOpt = player.level().getRecipeManager().byKey(msg.recipeId);
if (recipeOpt.isEmpty()) {
player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), false);
return;
}
RecipeHolder<?> recipeHolder = recipeOpt.get();
if (!consumeBlankPattern(player)) {
player.displayClientMessage(Component.translatable("message.extendedae_plus.no_blank_pattern"), false);
return;
}
ItemStack pattern = createPattern(recipeHolder, msg.isCraftingPattern, msg.selectedIngredients, msg.outputs, player);
if (pattern.isEmpty()) {
player.getInventory().add(AEItems.BLANK_PATTERN.stack());
player.displayClientMessage(Component.translatable("message.extendedae_plus.pattern_creation_failed"), false);
return;
}
if (msg.openProviderSelector) {
String pendingId = CtrlQPendingUploadUtil.beginPendingCtrlQUpload(player, pattern);
if (pendingId == null) {
if (!player.getInventory().add(pattern)) {
player.drop(pattern, false);
}
}
return;
}
if (!player.getInventory().add(pattern)) {
player.drop(pattern, false);
}
});
}
@Override
public Type<? extends CustomPacketPayload> type() {
return TYPE;
}
private static boolean consumeBlankPattern(ServerPlayer player) {
if (tryExtractFromNetwork(player)) {
return true;
}
Inventory inventory = player.getInventory();
for (int i = 0; i < inventory.getContainerSize(); i++) {
ItemStack stack = inventory.getItem(i);
if (AEItems.BLANK_PATTERN.is(stack)) {
stack.shrink(1);
return true;
}
}
return false;
}
private static boolean tryExtractFromNetwork(ServerPlayer player) {
WirelessTerminalLocator.LocatedTerminal located = WirelessTerminalLocator.find(player);
ItemStack terminal = located.stack;
if (terminal.isEmpty()) {
return false;
}
WirelessTerminalItem wt = terminal.getItem() instanceof WirelessTerminalItem t ? t : null;
if (wt == null) {
return false;
}
IGrid grid = wt.getLinkedGrid(terminal, player.serverLevel(), null);
if (grid == null) {
return false;
}
if (!wt.hasPower(player, 0.5, terminal)) {
return false;
}
IEnergyService energy = grid.getEnergyService();
MEStorage storage = grid.getStorageService().getInventory();
long extracted = StorageHelper.poweredExtraction(
energy,
storage,
appeng.api.stacks.AEItemKey.of(AEItems.BLANK_PATTERN.stack()),
1,
new PlayerSource(player)
);
if (extracted > 0) {
wt.usePower(player, 0.5, terminal);
located.commit();
return true;
}
return false;
}
private static ItemStack createPattern(
RecipeHolder<?> recipeHolder,
boolean isCrafting,
List<ItemStack> selectedIngredients,
List<ItemStack> selectedOutputs,
ServerPlayer player
) {
try {
if (isCrafting && recipeHolder.value() instanceof CraftingRecipe) {
@SuppressWarnings("unchecked")
RecipeHolder<CraftingRecipe> craftingHolder = (RecipeHolder<CraftingRecipe>) (RecipeHolder<?>) recipeHolder;
ItemStack[] inputs = new ItemStack[9];
for (int i = 0; i < 9; i++) {
if (i < selectedIngredients.size()) {
inputs[i] = selectedIngredients.get(i).copy();
} else {
inputs[i] = ItemStack.EMPTY;
}
}
ItemStack output = ItemStack.EMPTY;
if (!selectedOutputs.isEmpty()) {
output = selectedOutputs.get(0).copy();
}
if (output.isEmpty()) {
output = recipeHolder.value().getResultItem(player.level().registryAccess()).copy();
}
ItemStack encodedPattern = PatternDetailsHelper.encodeCraftingPattern(
craftingHolder,
inputs,
output,
true,
false
);
CustomData.update(DataComponents.CUSTOM_DATA, encodedPattern, tag -> tag.putString("encodePlayer", player.getGameProfile().getName()));
return encodedPattern;
}
List<GenericStack> inputs = new ArrayList<>();
List<GenericStack> outputs = new ArrayList<>();
for (ItemStack item : selectedIngredients) {
if (!item.isEmpty()) {
GenericStack stack = GenericStack.unwrapItemStack(item);
if (stack == null) {
stack = GenericStack.fromItemStack(item);
}
if (stack != null) {
inputs.add(stack);
}
}
}
for (ItemStack item : selectedOutputs) {
if (!item.isEmpty()) {
GenericStack stack = GenericStack.unwrapItemStack(item);
if (stack == null) {
stack = GenericStack.fromItemStack(item);
}
if (stack != null) {
outputs.add(stack);
}
}
}
ItemStack encodedPattern = PatternDetailsHelper.encodeProcessingPattern(inputs, outputs);
CustomData.update(DataComponents.CUSTOM_DATA, encodedPattern, tag -> tag.putString("encodePlayer", player.getGameProfile().getName()));
return encodedPattern;
} catch (Throwable ignored) {
return ItemStack.EMPTY;
}
}
}

View File

@ -4,6 +4,7 @@ import appeng.helpers.patternprovider.PatternContainer;
import appeng.menu.implementations.PatternAccessTermMenu; import appeng.menu.implementations.PatternAccessTermMenu;
import appeng.menu.me.items.PatternEncodingTermMenu; import appeng.menu.me.items.PatternEncodingTermMenu;
import com.extendedae_plus.ExtendedAEPlus; import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.util.uploadPattern.CtrlQPendingUploadUtil;
import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil; import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
@ -32,6 +33,27 @@ public class RequestProvidersListC2SPacket implements CustomPacketPayload {
public static void handle(final RequestProvidersListC2SPacket msg, final IPayloadContext ctx) { public static void handle(final RequestProvidersListC2SPacket msg, final IPayloadContext ctx) {
ctx.enqueueWork(() -> { ctx.enqueueWork(() -> {
if (!(ctx.player() instanceof ServerPlayer player)) return; if (!(ctx.player() instanceof ServerPlayer player)) return;
// Ctrl+Q pending 模式不依赖编码终端直接基于玩家网络给出列表负数索引 ID
if (CtrlQPendingUploadUtil.hasPendingCtrlQPattern(player)) {
List<PatternContainer> containers = CtrlQPendingUploadUtil.listAvailableProvidersFromPlayerNetwork(player);
List<Long> idxIds = new ArrayList<>();
List<String> names = new ArrayList<>();
List<Integer> slots = new ArrayList<>();
for (int i = 0; i < containers.size(); i++) {
var c = containers.get(i);
if (c == null) continue;
int empty = ExtendedAEPatternUploadUtil.getAvailableSlots(c);
if (empty <= 0) continue;
long encodedId = -1L - i;
idxIds.add(encodedId);
names.add(ExtendedAEPatternUploadUtil.getProviderDisplayName(c));
slots.add(empty);
}
player.connection.send(new ProvidersListS2CPacket(idxIds, names, slots));
return;
}
if (!(player.containerMenu instanceof PatternEncodingTermMenu encMenu)) return; if (!(player.containerMenu instanceof PatternEncodingTermMenu encMenu)) return;
// 优先若玩家也打开了样板访问终端则用 byId 方式精确服务器ID // 优先若玩家也打开了样板访问终端则用 byId 方式精确服务器ID

View File

@ -1,6 +1,7 @@
package com.extendedae_plus.network; package com.extendedae_plus.network;
import appeng.menu.me.items.PatternEncodingTermMenu; import appeng.menu.me.items.PatternEncodingTermMenu;
import com.extendedae_plus.util.uploadPattern.CtrlQPendingUploadUtil;
import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil; import com.extendedae_plus.util.uploadPattern.ExtendedAEPatternUploadUtil;
import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.codec.StreamCodec;
@ -29,6 +30,14 @@ public class UploadEncodedPatternToProviderC2SPacket implements CustomPacketPayl
public static void handle(final UploadEncodedPatternToProviderC2SPacket msg, final IPayloadContext ctx) { public static void handle(final UploadEncodedPatternToProviderC2SPacket msg, final IPayloadContext ctx) {
ctx.enqueueWork(() -> { ctx.enqueueWork(() -> {
if (!(ctx.player() instanceof ServerPlayer player)) return; if (!(ctx.player() instanceof ServerPlayer player)) return;
// 优先处理 Ctrl+Q pending 上传
if (CtrlQPendingUploadUtil.hasPendingCtrlQPattern(player)) {
if (CtrlQPendingUploadUtil.uploadPendingCtrlQPattern(player, msg.providerId)) {
return;
}
}
if (!(player.containerMenu instanceof PatternEncodingTermMenu menu)) return; if (!(player.containerMenu instanceof PatternEncodingTermMenu menu)) return;
// 支持两种模式 // 支持两种模式
// 1) providerId >= 0: 访问终端 byId 模式 // 1) providerId >= 0: 访问终端 byId 模式

View File

@ -0,0 +1,227 @@
package com.extendedae_plus.util;
import appeng.api.stacks.AEFluidKey;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.GenericStack;
import com.extendedae_plus.integration.jei.JeiRuntimeProxy;
import mezz.jei.api.constants.RecipeTypes;
import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.gui.IRecipeLayoutDrawable;
import mezz.jei.api.gui.ingredient.IRecipeSlotView;
import mezz.jei.api.gui.ingredient.IRecipeSlotsView;
import mezz.jei.api.helpers.IJeiHelpers;
import mezz.jei.api.ingredients.ITypedIngredient;
import mezz.jei.api.neoforge.NeoForgeTypes;
import mezz.jei.api.recipe.IFocus;
import mezz.jei.api.recipe.IFocusFactory;
import mezz.jei.api.recipe.IRecipeLookup;
import mezz.jei.api.recipe.IRecipeManager;
import mezz.jei.api.recipe.RecipeIngredientRole;
import mezz.jei.api.recipe.RecipeType;
import mezz.jei.api.recipe.category.IRecipeCategory;
import mezz.jei.api.runtime.IJeiRuntime;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.CraftingRecipe;
import net.minecraft.world.item.crafting.RecipeHolder;
import net.neoforged.neoforge.fluids.FluidStack;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* 基于 JEI 运行时查找配方提取输入输出槽位与数量信息
*/
public final class RecipeFinderUtil {
private static final Logger LOGGER = LoggerFactory.getLogger("ExtendedAE Plus - RecipeFinder");
private RecipeFinderUtil() {
}
public static List<RecipeInfo> findRecipesByIngredient(ITypedIngredient<?> ingredient) {
if (ingredient == null) {
return List.of();
}
Object runtimeObj = JeiRuntimeProxy.get();
if (!(runtimeObj instanceof IJeiRuntime jeiRuntime)) {
LOGGER.warn("[RecipeFinder] JEI runtime not available");
return List.of();
}
IJeiHelpers jeiHelpers = jeiRuntime.getJeiHelpers();
IRecipeManager recipeManager = jeiRuntime.getRecipeManager();
IFocusFactory focusFactory = jeiHelpers.getFocusFactory();
IFocus<?> outputFocus = focusFactory.createFocus(RecipeIngredientRole.OUTPUT, ingredient);
List<RecipeInfo> results = new ArrayList<>();
// 1) Crafting recipes
try {
IRecipeCategory<RecipeHolder<CraftingRecipe>> category = recipeManager.getRecipeCategory(RecipeTypes.CRAFTING);
recipeManager.createRecipeLookup(RecipeTypes.CRAFTING)
.limitFocus(List.of(outputFocus))
.get()
.forEach(recipeHolder -> {
Optional<IRecipeLayoutDrawable<Object>> layoutOpt = createLayout(recipeManager, category, recipeHolder, focusFactory);
layoutOpt.ifPresent(layout -> {
RecipeInfo info = extractRecipeInfo(recipeHolder, layout, true);
if (info != null) {
results.add(info);
}
});
});
} catch (Throwable t) {
LOGGER.warn("[RecipeFinder] Error searching crafting recipes: {}", t.toString());
}
// 2) Other recipe types
try {
jeiHelpers.getAllRecipeTypes().forEach(recipeType -> {
if (recipeType.equals(RecipeTypes.CRAFTING)) {
return;
}
try {
IRecipeCategory<?> category = recipeManager.getRecipeCategory(recipeType);
@SuppressWarnings({ "rawtypes", "unchecked" })
IRecipeLookup<Object> lookup = (IRecipeLookup) recipeManager.createRecipeLookup((RecipeType) recipeType);
lookup.limitFocus(List.of(outputFocus))
.get()
.forEach(recipeObj -> {
Optional<IRecipeLayoutDrawable<Object>> layoutOpt = createLayout(recipeManager, category, recipeObj, focusFactory);
layoutOpt.ifPresent(layout -> {
RecipeInfo info = extractRecipeInfo(recipeObj, layout, false);
if (info != null) {
results.add(info);
}
});
});
} catch (Throwable ignored) {
}
});
} catch (Throwable t) {
LOGGER.warn("[RecipeFinder] Error searching other recipes: {}", t.toString());
}
return results;
}
public static RecipeInfo selectBestRecipe(List<RecipeInfo> recipes) {
if (recipes == null || recipes.isEmpty()) {
return null;
}
for (RecipeInfo info : recipes) {
if (info.isCraftingRecipe()) {
return info;
}
}
return recipes.get(0);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private static Optional<IRecipeLayoutDrawable<Object>> createLayout(
IRecipeManager recipeManager,
IRecipeCategory<?> category,
Object recipeObj,
IFocusFactory focusFactory
) {
return (Optional) recipeManager.createRecipeLayoutDrawable(
(IRecipeCategory) category,
recipeObj,
focusFactory.getEmptyFocusGroup()
);
}
private static RecipeInfo extractRecipeInfo(Object recipeObj, IRecipeLayoutDrawable<Object> layout, boolean isCrafting) {
try {
ResourceLocation recipeId = extractRecipeId(recipeObj);
if (recipeId == null) {
return null;
}
IRecipeSlotsView slotsView = layout.getRecipeSlotsView();
List<List<GenericStack>> inputs = new ArrayList<>();
List<GenericStack> outputs = new ArrayList<>();
for (IRecipeSlotView slot : slotsView.getSlotViews(RecipeIngredientRole.INPUT)) {
List<GenericStack> slotStacks = new ArrayList<>();
for (ITypedIngredient<?> typed : slot.getAllIngredientsList()) {
if (typed == null) {
continue;
}
GenericStack stack = convertToGenericStack(typed);
if (stack != null) {
slotStacks.add(stack);
}
}
inputs.add(slotStacks);
}
for (IRecipeSlotView slot : slotsView.getSlotViews(RecipeIngredientRole.OUTPUT)) {
for (ITypedIngredient<?> typed : slot.getAllIngredientsList()) {
if (typed == null) {
continue;
}
GenericStack stack = convertToGenericStack(typed);
if (stack != null) {
outputs.add(stack);
}
}
}
return new RecipeInfo(recipeObj, recipeId, isCrafting, inputs, outputs);
} catch (Throwable t) {
return null;
}
}
private static ResourceLocation extractRecipeId(Object recipeObj) {
if (recipeObj == null) {
return null;
}
if (recipeObj instanceof RecipeHolder<?> holder) {
return holder.id();
}
try {
var m = recipeObj.getClass().getMethod("getId");
Object id = m.invoke(recipeObj);
if (id instanceof ResourceLocation rl) {
return rl;
}
} catch (Throwable ignored) {
}
try {
var m = recipeObj.getClass().getMethod("getRecipeUid");
Object id = m.invoke(recipeObj);
if (id instanceof ResourceLocation rl) {
return rl;
}
} catch (Throwable ignored) {
}
return null;
}
private static GenericStack convertToGenericStack(ITypedIngredient<?> typedIngredient) {
if (typedIngredient.getType() == VanillaTypes.ITEM_STACK) {
ItemStack itemStack = (ItemStack) typedIngredient.getIngredient();
if (!itemStack.isEmpty()) {
AEItemKey itemKey = AEItemKey.of(itemStack);
if (itemKey != null) {
return new GenericStack(itemKey, itemStack.getCount());
}
}
} else if (typedIngredient.getType() == NeoForgeTypes.FLUID_STACK) {
FluidStack fluidStack = (FluidStack) typedIngredient.getIngredient();
if (!fluidStack.isEmpty()) {
AEFluidKey fluidKey = AEFluidKey.of(fluidStack);
if (fluidKey != null) {
return new GenericStack(fluidKey, fluidStack.getAmount());
}
}
}
return null;
}
}

View File

@ -0,0 +1,98 @@
package com.extendedae_plus.util;
import appeng.api.stacks.AEFluidKey;
import appeng.api.stacks.AEItemKey;
import appeng.api.stacks.GenericStack;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* 配方完整信息包含配方ID输入槽位候选项输出
*/
public class RecipeInfo {
private final Object recipeBase;
private final ResourceLocation recipeId;
private final boolean craftingRecipe;
private final List<List<GenericStack>> inputs;
private final List<GenericStack> outputs;
public RecipeInfo(
Object recipeBase,
ResourceLocation recipeId,
boolean craftingRecipe,
List<List<GenericStack>> inputs,
List<GenericStack> outputs
) {
this.recipeBase = recipeBase;
this.recipeId = recipeId;
this.craftingRecipe = craftingRecipe;
this.inputs = inputs;
this.outputs = outputs;
}
public Object getRecipeBase() {
return recipeBase;
}
public ResourceLocation getRecipeId() {
return recipeId;
}
public boolean isCraftingRecipe() {
return craftingRecipe;
}
public List<List<GenericStack>> getInputs() {
return inputs;
}
public List<GenericStack> getOutputs() {
return outputs;
}
public List<ItemStack> selectBestInputs(Map<Item, Integer> bookmarkPriorities) {
List<ItemStack> selected = new ArrayList<>();
for (List<GenericStack> slotOptions : inputs) {
if (slotOptions.isEmpty()) {
selected.add(ItemStack.EMPTY);
continue;
}
GenericStack best = slotOptions.get(0);
int bestPriority = getPriority(best, bookmarkPriorities);
for (int i = 1; i < slotOptions.size(); i++) {
GenericStack option = slotOptions.get(i);
int priority = getPriority(option, bookmarkPriorities);
if (priority < bestPriority) {
bestPriority = priority;
best = option;
}
}
selected.add(toItemStack(best));
}
return selected;
}
private int getPriority(GenericStack stack, Map<Item, Integer> priorities) {
if (stack.what() instanceof AEItemKey itemKey) {
return priorities.getOrDefault(itemKey.getItem(), Integer.MAX_VALUE);
}
return Integer.MAX_VALUE;
}
private ItemStack toItemStack(GenericStack stack) {
if (stack.what() instanceof AEItemKey itemKey) {
return itemKey.toStack((int) stack.amount());
}
if (stack.what() instanceof AEFluidKey) {
return GenericStack.wrapInItemStack(stack);
}
return ItemStack.EMPTY;
}
}

View File

@ -0,0 +1,220 @@
package com.extendedae_plus.util.uploadPattern;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.inventories.InternalInventory;
import appeng.api.networking.IGrid;
import appeng.helpers.patternprovider.PatternContainer;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.util.inv.FilteredInternalInventory;
import appeng.util.inv.filter.IAEItemFilter;
import com.extendedae_plus.menu.locator.CuriosItemLocator;
import com.extendedae_plus.util.wireless.WirelessTerminalLocator;
import de.mari_023.ae2wtlib.api.registration.WTDefinition;
import de.mari_023.ae2wtlib.api.terminal.WTMenuHost;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* Ctrl+Q 临时样板缓存与上传逻辑pending provider upload
*/
public final class CtrlQPendingUploadUtil {
private static final String PENDING_DATA_KEY = "eap_ctrlq_pending_provider_upload_id";
private static final String PENDING_STACK_KEY = "eap_ctrlq_pending_provider_upload_stack";
private CtrlQPendingUploadUtil() {
}
public static String beginPendingCtrlQUpload(ServerPlayer player, ItemStack pattern) {
if (player == null || pattern == null || pattern.isEmpty() || !PatternDetailsHelper.isEncodedPattern(pattern)) {
return null;
}
clearPendingCtrlQUpload(player);
String id = UUID.randomUUID().toString();
player.getPersistentData().putString(PENDING_DATA_KEY, id);
player.getPersistentData().put(PENDING_STACK_KEY, pattern.saveOptional(player.registryAccess()));
return id;
}
public static void clearPendingCtrlQUpload(ServerPlayer player) {
if (player == null) return;
player.getPersistentData().remove(PENDING_DATA_KEY);
player.getPersistentData().remove(PENDING_STACK_KEY);
}
public static boolean hasPendingCtrlQPattern(ServerPlayer player) {
if (player == null) return false;
String id = player.getPersistentData().getString(PENDING_DATA_KEY);
if (id == null || id.isBlank()) return false;
return !getPendingCtrlQPattern(player).isEmpty();
}
public static boolean uploadPendingCtrlQPattern(ServerPlayer player, long providerId) {
if (player == null) return false;
ItemStack pending = getPendingCtrlQPattern(player);
if (pending.isEmpty()) return false;
ItemStack remain = insertPatternIntoProviderFromPlayerNetwork(player, pending, providerId);
if (remain.getCount() >= pending.getCount()) {
return false;
}
if (remain.isEmpty()) {
clearPendingCtrlQUpload(player);
} else {
player.getPersistentData().put(PENDING_STACK_KEY, remain.saveOptional(player.registryAccess()));
}
return true;
}
public static List<PatternContainer> listAvailableProvidersFromPlayerNetwork(ServerPlayer player) {
return listAvailableProvidersFromGrid(findPlayerGrid(player));
}
public static IGrid findPlayerGrid(ServerPlayer player) {
WirelessTerminalLocator.LocatedTerminal located = WirelessTerminalLocator.find(player);
ItemStack terminal = located.stack;
if (terminal.isEmpty()) {
return null;
}
WirelessTerminalItem wt = terminal.getItem() instanceof WirelessTerminalItem t ? t : null;
if (wt != null) {
return wt.getLinkedGrid(terminal, player.serverLevel(), null);
}
String curiosSlotId = located.getCuriosSlotId();
int curiosIndex = located.getCuriosIndex();
if (curiosSlotId != null && curiosIndex >= 0) {
try {
WTDefinition def = WTDefinition.ofOrNull(terminal);
if (def == null) return null;
WTMenuHost wtHost = def.wTMenuHostFactory().create(
def.item(),
player,
new CuriosItemLocator(curiosSlotId, curiosIndex),
(p, sub) -> {
}
);
if (wtHost == null || wtHost.getActionableNode() == null) return null;
return wtHost.getActionableNode().getGrid();
} catch (Throwable ignored) {
return null;
}
}
return null;
}
public static List<PatternContainer> listAvailableProvidersFromGrid(IGrid grid) {
List<PatternContainer> list = new ArrayList<>();
if (grid == null) return list;
try {
for (var machineClass : grid.getMachineClasses()) {
if (PatternContainer.class.isAssignableFrom(machineClass)) {
@SuppressWarnings("unchecked")
Class<? extends PatternContainer> containerClass = (Class<? extends PatternContainer>) machineClass;
for (var container : grid.getActiveMachines(containerClass)) {
if (container == null || !container.isVisibleInTerminal()) continue;
InternalInventory inv = container.getTerminalPatternInventory();
if (inv == null || inv.size() <= 0) continue;
boolean hasEmpty = false;
for (int i = 0; i < inv.size(); i++) {
if (inv.getStackInSlot(i).isEmpty()) {
hasEmpty = true;
break;
}
}
if (hasEmpty) list.add(container);
}
}
}
} catch (Throwable ignored) {
}
return list;
}
private static ItemStack getPendingCtrlQPattern(ServerPlayer player) {
if (player == null) return ItemStack.EMPTY;
String id = player.getPersistentData().getString(PENDING_DATA_KEY);
if (id == null || id.isBlank()) return ItemStack.EMPTY;
CompoundTag data = player.getPersistentData();
if (!data.contains(PENDING_STACK_KEY)) return ItemStack.EMPTY;
CompoundTag stackTag = data.getCompound(PENDING_STACK_KEY);
ItemStack stack = ItemStack.parseOptional(player.registryAccess(), stackTag);
if (stack.isEmpty() || !PatternDetailsHelper.isEncodedPattern(stack)) {
clearPendingCtrlQUpload(player);
return ItemStack.EMPTY;
}
return stack;
}
private static ItemStack insertPatternIntoProviderFromPlayerNetwork(ServerPlayer player, ItemStack pattern, long providerId) {
if (player == null || pattern == null || pattern.isEmpty() || !PatternDetailsHelper.isEncodedPattern(pattern)) {
return pattern == null ? ItemStack.EMPTY : pattern;
}
int index = decodeProviderIndex(providerId);
if (index < 0) return pattern;
List<PatternContainer> providers = listAvailableProvidersFromPlayerNetwork(player);
if (index >= providers.size()) return pattern;
PatternContainer target = providers.get(index);
if (target == null) return pattern;
ItemStack remain = pattern.copy();
for (PatternContainer container : buildSameNameTryList(providers, target)) {
InternalInventory inv = container.getTerminalPatternInventory();
if (inv == null || inv.size() <= 0) continue;
ItemStack nextRemain = new FilteredInternalInventory(inv, new CtrlQPatternFilter()).addItems(remain.copy());
if (nextRemain.getCount() < remain.getCount()) {
remain = nextRemain;
if (remain.isEmpty()) {
return ItemStack.EMPTY;
}
}
}
return remain;
}
private static int decodeProviderIndex(long providerId) {
if (providerId >= 0) return -1;
long idx = -1L - providerId;
if (idx > Integer.MAX_VALUE) return -1;
return (int) idx;
}
private static List<PatternContainer> buildSameNameTryList(List<PatternContainer> all, PatternContainer target) {
String targetName = ExtendedAEPatternUploadUtil.getProviderDisplayName(target);
List<PatternContainer> tryList = new ArrayList<>();
tryList.add(target);
for (PatternContainer container : all) {
if (container == null || container == target) continue;
String name = ExtendedAEPatternUploadUtil.getProviderDisplayName(container);
if (name != null && name.equals(targetName)) {
tryList.add(container);
}
}
return tryList;
}
private static class CtrlQPatternFilter implements IAEItemFilter {
@Override
public boolean allowExtract(InternalInventory inv, int slot, int amount) {
return true;
}
@Override
public boolean allowInsert(InternalInventory inv, int slot, ItemStack stack) {
return !stack.isEmpty() && PatternDetailsHelper.isEncodedPattern(stack);
}
}
}

View File

@ -517,6 +517,67 @@ public class ExtendedAEPatternUploadUtil {
return false; return false;
} }
/**
* 将给定的已编码样板直接上传到装配矩阵不依赖编码终端槽位
*
* @param player 服务器玩家
* @param pattern 已编码样板
* @param grid AE 网络
* @return 是否成功插入矩阵
*/
public static boolean uploadPatternToMatrix(ServerPlayer player, ItemStack pattern, IGrid grid) {
if (player == null || grid == null || pattern == null || pattern.isEmpty() || !PatternDetailsHelper.isEncodedPattern(pattern)) {
return false;
}
IPatternDetails details = PatternDetailsHelper.decodePattern(pattern, player.level());
if (!(details instanceof AECraftingPattern
|| details instanceof AESmithingTablePattern
|| details instanceof AEStonecuttingPattern)) {
sendMessage(player, "extendedae_plus.upload_to_matrix.fail");
return false;
}
if (matrixContainsPattern(grid, pattern)) {
if (player != null) {
player.sendSystemMessage(Component.translatable("extendedae_plus.message.matrix.duplicate"));
}
return false;
}
List<InternalInventory> inventories = findAllMatrixPatternInventories(grid);
if (!inventories.isEmpty()) {
for (InternalInventory inv : inventories) {
if (inv == null) continue;
ItemStack toInsert = pattern.copy();
ItemStack remain = inv.addItems(toInsert);
if (remain.getCount() < toInsert.getCount()) {
sendMessage(player, "extendedae_plus.upload_to_matrix.success");
return true;
}
}
}
List<?> handlers = findAllMatrixPatternHandlers(grid);
if (!handlers.isEmpty()) {
for (Object cap : handlers) {
ItemStack toInsert = pattern.copy();
ItemStack remain = insertIntoAnySlot(cap, toInsert);
if (remain.getCount() < toInsert.getCount()) {
sendMessage(player, "extendedae_plus.upload_to_matrix.success");
return true;
}
}
}
if (inventories.isEmpty() && handlers.isEmpty()) {
sendMessage(player, "extendedae_plus.upload_to_matrix.fail_no_matrix");
} else {
sendMessage(player, "extendedae_plus.upload_to_matrix.fail_full");
}
return false;
}
/** /**
* 在给定 AE Grid 中收集所有已成型且在线的装配矩阵图样模块的用于外部插入的内部库存 * 在给定 AE Grid 中收集所有已成型且在线的装配矩阵图样模块的用于外部插入的内部库存
* 优先使用 TileAssemblerMatrixPattern#getExposedInventory仅允许插入且已带AE过滤规则 * 优先使用 TileAssemblerMatrixPattern#getExposedInventory仅允许插入且已带AE过滤规则

View File

@ -61,6 +61,14 @@
"extendedae_plus.screen.remove_mapping": "Remove Mapping", "extendedae_plus.screen.remove_mapping": "Remove Mapping",
"extendedae_plus.screen.cn_name": "Chinese Name", "extendedae_plus.screen.cn_name": "Chinese Name",
"extendedae_plus.button.choose_provider": "Upload Pattern", "extendedae_plus.button.choose_provider": "Upload Pattern",
"key.extendedae_plus.create_pattern": "Create Pattern (Ctrl+Q)",
"key.categories.extendedae_plus": "ExtendedAE Plus",
"message.extendedae_plus.hover_item_first": "Hover an item first.",
"message.extendedae_plus.no_recipes_found": "No related recipes found.",
"message.extendedae_plus.recipe_not_found": "Recipe not found.",
"message.extendedae_plus.no_blank_pattern": "No blank pattern available.",
"message.extendedae_plus.pattern_creation_failed": "Failed to create encoded pattern.",
"message.extendedae_plus.no_network": "Unable to access AE network.",
"extendedae_plus.pattern.hovertext.player": "Encoded by %s", "extendedae_plus.pattern.hovertext.player": "Encoded by %s",
"item.extendedae_plus.entity_speed_ticker": "Entity Accelerator", "item.extendedae_plus.entity_speed_ticker": "Entity Accelerator",
@ -231,4 +239,4 @@
"extendedae_plus.jade.network.offline": "Offline", "extendedae_plus.jade.network.offline": "Offline",
"extendedae_plus.screen.global_controller_title": "Pattern Provider Status Controller" "extendedae_plus.screen.global_controller_title": "Pattern Provider Status Controller"
} }

View File

@ -61,6 +61,14 @@
"extendedae_plus.screen.remove_mapping": "删除映射", "extendedae_plus.screen.remove_mapping": "删除映射",
"extendedae_plus.screen.cn_name": "中文名", "extendedae_plus.screen.cn_name": "中文名",
"extendedae_plus.button.choose_provider":"上传样板", "extendedae_plus.button.choose_provider":"上传样板",
"key.extendedae_plus.create_pattern": "快速创建样板 (Ctrl+Q)",
"key.categories.extendedae_plus": "扩展AE Plus",
"message.extendedae_plus.hover_item_first": "请先将鼠标悬停在物品上。",
"message.extendedae_plus.no_recipes_found": "未找到相关配方。",
"message.extendedae_plus.recipe_not_found": "未找到该配方。",
"message.extendedae_plus.no_blank_pattern": "没有可用空白样板。",
"message.extendedae_plus.pattern_creation_failed": "创建编码样板失败。",
"message.extendedae_plus.no_network": "无法访问 AE 网络。",
"extendedae_plus.pattern.hovertext.player": "由 %s 编码", "extendedae_plus.pattern.hovertext.player": "由 %s 编码",
"item.extendedae_plus.entity_speed_ticker": "实体加速器", "item.extendedae_plus.entity_speed_ticker": "实体加速器",
@ -226,4 +234,4 @@
"extendedae_plus.jade.network.offline": "设备离线", "extendedae_plus.jade.network.offline": "设备离线",
"extendedae_plus.screen.global_controller_title": "样板供应器状态控制器" "extendedae_plus.screen.global_controller_title": "样板供应器状态控制器"
} }