基础移植
This commit is contained in:
parent
675123e8e9
commit
236a1fbe2d
39
src/main/java/com/extendedae_plus/client/ModKeybindings.java
Normal file
39
src/main/java/com/extendedae_plus/client/ModKeybindings.java
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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 模式
|
||||||
|
|
|
||||||
227
src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java
Normal file
227
src/main/java/com/extendedae_plus/util/RecipeFinderUtil.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/main/java/com/extendedae_plus/util/RecipeInfo.java
Normal file
98
src/main/java/com/extendedae_plus/util/RecipeInfo.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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过滤规则)。
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "样板供应器状态控制器"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user