处理书签分支完成

This commit is contained in:
GaLi 2026-02-28 17:59:40 +08:00
parent ab7664e4a6
commit b87b74929e
6 changed files with 423 additions and 160 deletions

View File

@ -5,8 +5,10 @@ import com.extendedae_plus.client.ModKeybindings;
import com.extendedae_plus.init.ModNetwork;
import com.extendedae_plus.integration.jei.JeiRuntimeProxy;
import com.extendedae_plus.network.pattern.CreateCtrlQPatternC2SPacket;
import com.extendedae_plus.network.provider.RequestProvidersListC2SPacket;
import com.extendedae_plus.util.RecipeFinderUtil;
import com.extendedae_plus.util.RecipeInfo;
import com.extendedae_plus.util.uploadPattern.RecipeTypeNameConfig;
import mezz.jei.api.constants.RecipeTypes;
import mezz.jei.api.constants.VanillaTypes;
import mezz.jei.api.ingredients.ITypedIngredient;
@ -15,6 +17,7 @@ import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.client.event.ScreenEvent;
import net.minecraftforge.eventbus.api.SubscribeEvent;
@ -273,7 +276,112 @@ public class CtrlQPatternKeyHandler {
* @param recipeBookmark 配方书签对象
*/
private static void handleProcessingRecipeBookmark(Object recipeBookmark) {
System.out.println("chuli");
try {
var getRecipeUidMethod = recipeBookmark.getClass().getMethod("getRecipeUid");
net.minecraft.resources.ResourceLocation recipeId =
(net.minecraft.resources.ResourceLocation) getRecipeUidMethod.invoke(recipeBookmark);
if (recipeId == null) {
return;
}
Minecraft mc = Minecraft.getInstance();
if (mc.level == null) {
return;
}
var recipeManager = mc.level.getRecipeManager();
var recipeOpt = recipeManager.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 = RecipeFinderUtil.findRecipesByIngredient(
JeiRuntimeProxy.getIngredientUnderMouse().orElse(null)
);
if (recipeInfos.isEmpty()) {
var getRecipeOutputMethod = recipeBookmark.getClass().getMethod("getRecipeOutput");
Object recipeOutput = getRecipeOutputMethod.invoke(recipeBookmark);
if (recipeOutput instanceof ITypedIngredient<?> typedIngredient) {
recipeInfos = RecipeFinderUtil.findRecipesByIngredient(typedIngredient);
}
}
if (recipeInfos.isEmpty()) {
if (mc.player != null) {
mc.player.displayClientMessage(
Component.translatable("message.extendedae_plus.no_recipes_found"),
true
);
}
return;
}
RecipeInfo matchingRecipeInfo = null;
for (RecipeInfo info : recipeInfos) {
if (info.getRecipe().getId().equals(recipeId)) {
matchingRecipeInfo = info;
break;
}
}
if (matchingRecipeInfo == null) {
matchingRecipeInfo = recipeInfos.get(0);
}
List<ItemStack> selectedIngredients = selectIngredientsWithJeiPriority(matchingRecipeInfo);
List<ItemStack> selectedOutputs = convertOutputsToItemStacks(matchingRecipeInfo);
ModNetwork.CHANNEL.sendToServer(new CreateCtrlQPatternC2SPacket(
recipeId,
matchingRecipeInfo.isCraftingRecipe(),
selectedIngredients,
selectedOutputs,
true
));
ModNetwork.CHANNEL.sendToServer(new RequestProvidersListC2SPacket());
} catch (Exception e) {
e.printStackTrace();
}
}
private static void setLastProcessingNameFromRecipe(Object recipeBase) {
String name = null;
if (recipeBase instanceof Recipe<?> recipe) {
name = RecipeTypeNameConfig.mapRecipeTypeToSearchKey(recipe);
} else if (recipeBase != null
&& "com.gregtechceu.gtceu.api.recipe.GTRecipe".equals(recipeBase.getClass().getName())) {
name = RecipeTypeNameConfig.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 = RecipeTypeNameConfig.mapGTCEuRecipeToSearchKey(inner);
} catch (Throwable ignored) {
}
}
if (name == null || name.isBlank()) {
name = RecipeTypeNameConfig.deriveSearchKeyFromUnknownRecipe(recipeBase);
}
if (name != null && !name.isBlank()) {
RecipeTypeNameConfig.setLastProcessingName(name);
}
}
/**

View File

@ -9,7 +9,7 @@ import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
/**
* C2S: 请求将图样编码终端的已编码样板上传到指定的样板供应器由客户端选择
* C2S: Request uploading an encoded pattern to a selected provider.
*/
public class UploadEncodedPatternToProviderC2SPacket {
private final long providerId;
@ -31,15 +31,23 @@ public class UploadEncodedPatternToProviderC2SPacket {
ctx.enqueueWork(() -> {
ServerPlayer player = ctx.getSender();
if (player == null) return;
if (!(player.containerMenu instanceof PatternEncodingTermMenu menu)) return;
// 支持两种模式
// 1) providerId >= 0: 访问终端 byId 模式
// 2) providerId < 0: 索引模式由列表回退路径生成index = -1 - providerId
if (msg.providerId >= 0) {
ProviderUploadUtil.uploadFromEncodingMenuToProvider(player, menu, msg.providerId);
} else {
int index = (int) (-1L - msg.providerId);
ProviderUploadUtil.uploadFromEncodingMenuToProviderByIndex(player, menu, index);
// Prefer pending Ctrl+Q pattern upload when present.
if (ProviderUploadUtil.hasPendingCtrlQPattern(player)) {
if (ProviderUploadUtil.uploadPendingCtrlQPattern(player, msg.providerId)) {
return;
}
}
if (player.containerMenu instanceof PatternEncodingTermMenu menu) {
// 1) providerId >= 0: byId mode from access terminal
// 2) providerId < 0: index mode, index = -1 - providerId
if (msg.providerId >= 0) {
ProviderUploadUtil.uploadFromEncodingMenuToProvider(player, menu, msg.providerId);
} else {
int index = (int) (-1L - msg.providerId);
ProviderUploadUtil.uploadFromEncodingMenuToProviderByIndex(player, menu, index);
}
}
});
ctx.setPacketHandled(true);

View File

@ -1,11 +1,5 @@
package com.extendedae_plus.network.pattern;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import com.extendedae_plus.util.wireless.WirelessTerminalLocator;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.networking.IGrid;
import appeng.api.networking.energy.IEnergyService;
@ -17,6 +11,8 @@ import appeng.core.definitions.AEItems;
import appeng.items.tools.powered.WirelessCraftingTerminalItem;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.me.helpers.PlayerSource;
import com.extendedae_plus.util.uploadPattern.ProviderUploadUtil;
import com.extendedae_plus.util.wireless.WirelessTerminalLocator;
import de.mari_023.ae2wtlib.terminal.WTMenuHost;
import de.mari_023.ae2wtlib.wut.WTDefinition;
import de.mari_023.ae2wtlib.wut.WUTHandler;
@ -31,24 +27,31 @@ import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeManager;
import net.minecraftforge.network.NetworkEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
/**
* C2S: Ctrl+Q快速创建样板数据包
*
* <p>
* 从客户端发送配方ID选择的材料和输出到服务器服务器消耗空白样板并创建编码样板掉落到玩家脚下</p>
* C2S: Ctrl+Q quick-create pattern request.
*/
public class CreateCtrlQPatternC2SPacket {
private final ResourceLocation recipeId;
private final boolean isCraftingPattern;
private final List<ItemStack> selectedIngredients;
private final List<ItemStack> outputs; // 输出材料物品或包装的流体
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 encode(CreateCtrlQPatternC2SPacket msg, FriendlyByteBuf buf) {
@ -62,22 +65,27 @@ public class CreateCtrlQPatternC2SPacket {
for (ItemStack stack : msg.outputs) {
buf.writeItem(stack);
}
buf.writeBoolean(msg.openProviderSelector);
}
public static CreateCtrlQPatternC2SPacket decode(FriendlyByteBuf buf) {
ResourceLocation recipeId = buf.readResourceLocation();
boolean isCraftingPattern = buf.readBoolean();
int ingredientCount = buf.readInt();
List<ItemStack> ingredients = new ArrayList<>();
for (int i = 0; i < ingredientCount; i++) {
ingredients.add(buf.readItem());
}
int outputCount = buf.readInt();
List<ItemStack> outputs = new ArrayList<>();
for (int i = 0; i < outputCount; i++) {
outputs.add(buf.readItem());
}
return new CreateCtrlQPatternC2SPacket(recipeId, isCraftingPattern, ingredients, outputs);
boolean openProviderSelector = buf.readableBytes() > 0 && buf.readBoolean();
return new CreateCtrlQPatternC2SPacket(recipeId, isCraftingPattern, ingredients, outputs, openProviderSelector);
}
public static void handle(CreateCtrlQPatternC2SPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
@ -88,64 +96,49 @@ public class CreateCtrlQPatternC2SPacket {
return;
}
// 1. 验证配方存在
RecipeManager recipeManager = player.level().getRecipeManager();
var recipeOpt = recipeManager.byKey(msg.recipeId);
if (recipeOpt.isEmpty()) {
player.displayClientMessage(
Component.translatable("message.extendedae_plus.recipe_not_found"),
false
);
player.displayClientMessage(Component.translatable("message.extendedae_plus.recipe_not_found"), false);
return;
}
Recipe<?> recipe = recipeOpt.get();
// 2. 消耗空白样板
if (!consumeBlankPattern(player)) {
player.displayClientMessage(
Component.translatable("message.extendedae_plus.no_blank_pattern"),
false
);
player.displayClientMessage(Component.translatable("message.extendedae_plus.no_blank_pattern"), false);
return;
}
// 3. 创建样板
ItemStack pattern = createPattern(recipe, 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
);
player.displayClientMessage(Component.translatable("message.extendedae_plus.pattern_creation_failed"), false);
return;
}
if (msg.openProviderSelector) {
String pendingId = ProviderUploadUtil.beginPendingCtrlQUpload(player, pattern);
if (pendingId == null) {
if (!player.getInventory().add(pattern)) {
player.drop(pattern, false);
}
}
return;
}
// 4. 交付样板优先放入背包满了再掉落
if (!player.getInventory().add(pattern)) {
player.drop(pattern, false);
}
});
ctx.setPacketHandled(true);
}
/**
* 消耗空白样板优先从AE网络提取网络无货才从玩家背包消耗
*
* @param player 玩家
* @return 是否成功消耗
*/
private static boolean consumeBlankPattern(ServerPlayer player) {
// 1. 尝试从AE网络提取需要玩家持有无线终端
if (tryExtractFromNetwork(player)) {
return true;
}
// 2. 网络提取失败从背包消耗
Inventory inventory = player.getInventory();
for (int i = 0; i < inventory.getContainerSize(); i++) {
ItemStack stack = inventory.getItem(i);
@ -155,27 +148,19 @@ public class CreateCtrlQPatternC2SPacket {
}
}
return false; // 未找到
return false;
}
/**
* 尝试从AE网络提取空白样板
*
* @param player 玩家
* @return 是否成功提取
*/
private static boolean tryExtractFromNetwork(ServerPlayer player) {
// 定位玩家身上的无线终端
WirelessTerminalLocator.LocatedTerminal located = WirelessTerminalLocator.find(player);
ItemStack terminal = located.stack;
if (terminal.isEmpty()) {
return false; // 没有无线终端
return false;
}
IGrid grid;
boolean usedWtHost;
// 若来自 Curios优先通过 ae2wtlib WTMenuHost 获取量子桥网络
String curiosSlotId = located.getCuriosSlotId();
int curiosIndex = located.getCuriosIndex();
@ -208,7 +193,6 @@ public class CreateCtrlQPatternC2SPacket {
return false;
}
} else {
// Curios AE2 原生路径处理
WirelessCraftingTerminalItem wct = terminal.getItem() instanceof WirelessCraftingTerminalItem c ? c : null;
WirelessTerminalItem wt = wct != null ? wct : (terminal.getItem() instanceof WirelessTerminalItem t ? t : null);
if (wt == null) {
@ -219,60 +203,41 @@ public class CreateCtrlQPatternC2SPacket {
return false;
}
if (!wt.hasPower(player, 0.5, terminal)) {
return false; // 能量不足
return false;
}
usedWtHost = false;
}
// 从网络提取空白样板
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, // 只提取1个
new PlayerSource(player)
energy,
storage,
blankPatternKey,
1,
new PlayerSource(player)
);
if (extracted > 0) {
// 提取成功消耗无线终端能量
if (usedWtHost) {
// WTMenuHost 已在 drainPower 中处理能量消耗
} else {
// 原生 AE2 扣能
if (!usedWtHost) {
WirelessCraftingTerminalItem wct2 = terminal.getItem() instanceof WirelessCraftingTerminalItem c2 ? c2 : null;
WirelessTerminalItem wt2 = wct2 != null ? wct2 : (terminal.getItem() instanceof WirelessTerminalItem t2 ? t2 : null);
if (wt2 != null) {
wt2.usePower(player, 0.5, terminal);
}
}
// 确保写回终端若位于 Curios 等需要显式写回的容器
located.commit();
return true;
}
return false; // 网络中没有空白样板
return false;
}
/**
* 从配方创建样板支持物品和流体
*
* @param recipe 配方
* @param isCrafting 是否为合成样板
* @param selectedIngredients 客户端选择的材料应用JEI优先级后流体已包装为 GenericStack.wrapInItemStack
* @param selectedOutputs 客户端传递的输出材料物品或包装的流体
* @param player 玩家
* @return 编码的样板物品
*/
private static ItemStack createPattern(Recipe<?> recipe, boolean isCrafting, List<ItemStack> selectedIngredients, List<ItemStack> selectedOutputs, ServerPlayer player) {
try {
if (isCrafting && recipe instanceof CraftingRecipe craftingRecipe) {
// ===== 合成样板创建路径 =====
// 准备9格工作台输入3x3布局
ItemStack[] inputs = new ItemStack[9];
for (int i = 0; i < 9; i++) {
if (i < selectedIngredients.size()) {
@ -282,80 +247,59 @@ public class CreateCtrlQPatternC2SPacket {
}
}
// 准备输出
ItemStack output = recipe.getResultItem(player.level().registryAccess()).copy();
// 使用 encodeCraftingPattern 创建合成样板
// 直接传递 CraftingRecipe 对象而非 RecipeHolder
ItemStack encodedPattern = PatternDetailsHelper.encodeCraftingPattern(
craftingRecipe,
inputs,
output,
true, // allowSubstitutes - 允许替代材料
false // allowFluidSubstitutes - 不允许流体替代
craftingRecipe,
inputs,
output,
true,
false
);
// 添加编码玩家信息到NBT
encodedPattern.getOrCreateTag().putString("encodePlayer", player.getName().getString());
return encodedPattern;
} else {
// ===== 处理样板创建路径支持物品和流体=====
List<GenericStack> inputs = new ArrayList<>();
List<GenericStack> outputs = new ArrayList<>();
// 处理输入 - 使用客户端传入的材料选择支持流体
for (ItemStack item : selectedIngredients) {
if (!item.isEmpty()) {
// 尝试解包 GenericStack流体会被包装在特殊的 ItemStack
GenericStack genericStack = GenericStack.unwrapItemStack(item);
if (genericStack != null) {
// 这是一个包装的 GenericStack可能是流体
inputs.add(genericStack);
} else {
// 普通物品
AEItemKey itemKey = AEItemKey.of(item);
if (itemKey != null) {
inputs.add(new GenericStack(itemKey, item.getCount()));
}
}
}
}
// 处理输出 - 使用客户端传入的输出支持流体
for (ItemStack item : selectedOutputs) {
if (!item.isEmpty()) {
// 尝试解包 GenericStack流体会被包装在特殊的 ItemStack
GenericStack genericStack = GenericStack.unwrapItemStack(item);
if (genericStack != null) {
// 这是一个包装的 GenericStack可能是流体
outputs.add(genericStack);
} else {
// 普通物品
AEItemKey itemKey = AEItemKey.of(item);
if (itemKey != null) {
outputs.add(new GenericStack(itemKey, item.getCount()));
}
}
}
}
// 使用 encodeProcessingPattern 创建处理样板
ItemStack encodedPattern = PatternDetailsHelper.encodeProcessingPattern(
inputs.toArray(new GenericStack[0]),
outputs.toArray(new GenericStack[0])
);
// 添加编码玩家信息到NBT
encodedPattern.getOrCreateTag().putString("encodePlayer", player.getName().getString());
return encodedPattern;
}
List<GenericStack> inputs = new ArrayList<>();
List<GenericStack> outputs = new ArrayList<>();
for (ItemStack item : selectedIngredients) {
if (!item.isEmpty()) {
GenericStack genericStack = GenericStack.unwrapItemStack(item);
if (genericStack != null) {
inputs.add(genericStack);
} else {
AEItemKey itemKey = AEItemKey.of(item);
if (itemKey != null) {
inputs.add(new GenericStack(itemKey, item.getCount()));
}
}
}
}
for (ItemStack item : selectedOutputs) {
if (!item.isEmpty()) {
GenericStack genericStack = GenericStack.unwrapItemStack(item);
if (genericStack != null) {
outputs.add(genericStack);
} else {
AEItemKey itemKey = AEItemKey.of(item);
if (itemKey != null) {
outputs.add(new GenericStack(itemKey, item.getCount()));
}
}
}
}
ItemStack encodedPattern = PatternDetailsHelper.encodeProcessingPattern(
inputs.toArray(new GenericStack[0]),
outputs.toArray(new GenericStack[0])
);
encodedPattern.getOrCreateTag().putString("encodePlayer", player.getName().getString());
return encodedPattern;
} catch (Exception e) {
return ItemStack.EMPTY;
}
}
}
}

View File

@ -6,6 +6,7 @@ import appeng.menu.me.items.PatternEncodingTermMenu;
import com.extendedae_plus.init.ModNetwork;
import com.extendedae_plus.util.PatternProviderDataUtil;
import com.extendedae_plus.util.PatternTerminalUtil;
import com.extendedae_plus.util.uploadPattern.ProviderUploadUtil;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.server.level.ServerPlayer;
import net.minecraftforge.network.NetworkEvent;
@ -29,6 +30,27 @@ public class RequestProvidersListC2SPacket {
ctx.enqueueWork(() -> {
ServerPlayer player = ctx.getSender();
if (player == null) return;
// Ctrl+Q pending 模式不依赖编码终端直接基于玩家网络给出列表负数索引 ID
if (ProviderUploadUtil.hasPendingCtrlQPattern(player)) {
List<PatternContainer> containers = ProviderUploadUtil.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 = PatternProviderDataUtil.getAvailableSlots(c);
if (empty <= 0) continue;
long encodedId = -1L - i;
idxIds.add(encodedId);
names.add(PatternProviderDataUtil.getProviderDisplayName(c));
slots.add(empty);
}
ModNetwork.CHANNEL.sendTo(new ProvidersListS2CPacket(idxIds, names, slots), player.connection.connection, net.minecraftforge.network.NetworkDirection.PLAY_TO_CLIENT);
return;
}
if (!(player.containerMenu instanceof PatternEncodingTermMenu encMenu)) return;
// 优先若玩家也打开了样板访问终端则用 byId 方式精确服务器ID

View File

@ -158,13 +158,20 @@ public final class PatternTerminalUtil {
* 返回顺序稳定 grid machineClasses 顺序再按 activeMachines 迭代顺序
*/
public static List<PatternContainer> listAvailableProvidersFromGrid(PatternEncodingTermMenu menu) {
List<PatternContainer> list = new ArrayList<>();
if (menu == null) return list;
if (menu == null) return new ArrayList<>();
try {
IGridNode node = menu.getNetworkNode();
if (node == null) return list;
IGrid grid = node.getGrid();
if (grid == null) return list;
if (node == null) return new ArrayList<>();
return listAvailableProvidersFromGrid(node.getGrid());
} catch (Throwable ignored) {
return new ArrayList<>();
}
}
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")
@ -175,7 +182,10 @@ public final class PatternTerminalUtil {
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 (inv.getStackInSlot(i).isEmpty()) {
hasEmpty = true;
break;
}
}
if (hasEmpty) list.add(container);
}

View File

@ -2,7 +2,10 @@ package com.extendedae_plus.util.uploadPattern;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.inventories.InternalInventory;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNode;
import appeng.helpers.patternprovider.PatternContainer;
import appeng.items.tools.powered.WirelessTerminalItem;
import appeng.menu.implementations.PatternAccessTermMenu;
import appeng.menu.me.items.PatternEncodingTermMenu;
import appeng.util.inv.FilteredInternalInventory;
@ -10,10 +13,16 @@ import appeng.util.inv.filter.IAEItemFilter;
import com.extendedae_plus.mixin.ae2.accessor.PatternEncodingTermMenuAccessor;
import com.extendedae_plus.util.PatternProviderDataUtil;
import com.extendedae_plus.util.PatternTerminalUtil;
import com.extendedae_plus.util.wireless.WirelessTerminalLocator;
import de.mari_023.ae2wtlib.terminal.WTMenuHost;
import de.mari_023.ae2wtlib.wut.WTDefinition;
import de.mari_023.ae2wtlib.wut.WUTHandler;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.item.ItemStack;
import java.util.List;
import java.util.UUID;
/**
* 与样板供应器provider上传相关的工具类
@ -24,6 +33,9 @@ import java.util.List;
* 其中使用 PatternTerminalUtil 提供的反射/容器访问工具
*/
public final class ProviderUploadUtil {
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 ProviderUploadUtil() {}
/**
@ -221,6 +233,165 @@ public final class ProviderUploadUtil {
return false;
}
/**
* 缓存 Ctrl+Q 生成的待上传样板不放入玩家背包
*/
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.copy().save(new CompoundTag()));
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();
}
/**
* pending Ctrl+Q 样板上传到玩家网络中的目标 provider负数索引 ID
*/
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.save(new CompoundTag()));
}
return true;
}
/**
* 列出玩家无线终端网络中的可用 provider顺序与负数索引上传保持一致
*/
public static List<PatternContainer> listAvailableProvidersFromPlayerNetwork(ServerPlayer player) {
IGrid grid = findPlayerGrid(player);
return PatternTerminalUtil.listAvailableProvidersFromGrid(grid);
}
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.of(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 ExtendedAEPatternFilter()).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 = PatternProviderDataUtil.getProviderDisplayName(target);
List<PatternContainer> tryList = new java.util.ArrayList<>();
tryList.add(target);
for (PatternContainer container : all) {
if (container == null || container == target) continue;
String name = PatternProviderDataUtil.getProviderDisplayName(container);
if (name != null && name.equals(targetName)) {
tryList.add(container);
}
}
return tryList;
}
private static IGrid findPlayerGrid(ServerPlayer player) {
WirelessTerminalLocator.LocatedTerminal located = WirelessTerminalLocator.find(player);
ItemStack terminal = located.stack;
if (terminal.isEmpty()) {
return null;
}
String curiosSlotId = located.getCuriosSlotId();
int curiosIndex = located.getCuriosIndex();
if (curiosSlotId != null && curiosIndex >= 0) {
try {
String current = WUTHandler.getCurrentTerminal(terminal);
WTDefinition def = WUTHandler.wirelessTerminals.get(current);
if (def != null) {
WTMenuHost wtHost = def.wTMenuHostFactory().create(player, null, terminal, (p, sub) -> {});
if (wtHost != null) {
IGridNode node = wtHost.getActionableNode();
if (node != null) {
return node.getGrid();
}
}
}
} catch (Exception ignored) {
return null;
}
} else {
WirelessTerminalItem wt = terminal.getItem() instanceof WirelessTerminalItem t ? t : null;
if (wt != null) {
return wt.getLinkedGrid(terminal, player.serverLevel(), player);
}
}
return null;
}
/**
* ExtendedAE兼容的样板过滤器
* 使用AE2的PatternDetailsHelper进行样板验证