为样板编码终端添加上传供应器选择按钮

This commit is contained in:
GaLicn 2025-08-16 00:35:08 +08:00
parent 2dbea6c15c
commit 92dd02ea85
8 changed files with 667 additions and 5 deletions

View File

@ -0,0 +1,155 @@
package com.extendedae_plus.client.ui;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.UploadEncodedPatternToProviderC2SPacket;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.components.Button;
import net.minecraft.client.gui.screens.Screen;
import net.minecraft.network.chat.Component;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 简单的供应器选择弹窗
* 展示若干个可点击的供应器条目点击后发送带 providerId 的上传请求
*/
public class ProviderSelectScreen extends Screen {
private final Screen parent;
// 原始数据
private final List<Long> ids;
private final List<String> names;
private final List<Integer> emptySlots;
// 分组后的数据同名合并
private final List<Long> gIds = new ArrayList<>(); // 代表条目使用的 providerId选择空位数最多的那个
private final List<String> gNames = new ArrayList<>(); // 分组名供应器名称
private final List<Integer> gTotalSlots = new ArrayList<>(); // 该名称下供应器空位总和
private final List<Integer> gCount = new ArrayList<>(); // 该名称下供应器数量
private int page = 0;
private static final int PAGE_SIZE = 6;
private final List<Button> entryButtons = new ArrayList<>();
public ProviderSelectScreen(Screen parent, List<Long> ids, List<String> names, List<Integer> emptySlots) {
super(Component.translatable("extendedae_plus.screen.choose_provider.title"));
this.parent = parent;
this.ids = ids;
this.names = names;
this.emptySlots = emptySlots;
buildGroups();
}
@Override
protected void init() {
this.clearWidgets();
entryButtons.clear();
int start = page * PAGE_SIZE;
int end = Math.min(start + PAGE_SIZE, gIds.size());
int centerX = this.width / 2;
int startY = this.height / 2 - 70;
int buttonWidth = 240;
int buttonHeight = 20;
int gap = 5;
for (int i = start; i < end; i++) {
int idx = i;
String label = buildLabel(idx);
Button btn = Button.builder(Component.literal(label), b -> onChoose(idx))
.bounds(centerX - buttonWidth / 2, startY + (i - start) * (buttonHeight + gap), buttonWidth, buttonHeight)
.build();
entryButtons.add(btn);
this.addRenderableWidget(btn);
}
// 分页按钮
int navY = startY + PAGE_SIZE * (buttonHeight + gap) + 10;
Button prev = Button.builder(Component.literal("<"), b -> changePage(-1))
.bounds(centerX - 60, navY, 20, 20)
.build();
Button next = Button.builder(Component.literal(">"), b -> changePage(1))
.bounds(centerX + 40, navY, 20, 20)
.build();
prev.active = page > 0;
next.active = (page + 1) * PAGE_SIZE < gIds.size();
this.addRenderableWidget(prev);
this.addRenderableWidget(next);
// 关闭按钮
Button close = Button.builder(Component.translatable("gui.cancel"), b -> onClose())
.bounds(centerX - 40, navY + 30, 80, 20)
.build();
this.addRenderableWidget(close);
}
private void changePage(int delta) {
int newPage = page + delta;
if (newPage < 0) return;
if (newPage * PAGE_SIZE >= Math.max(1, gIds.size())) return;
page = newPage;
init();
}
private String buildLabel(int idx) {
String name = gNames.get(idx);
int totalSlots = gTotalSlots.get(idx);
int count = gCount.get(idx);
// 不显示具体 id显示合并统计名称总空位x数量
return name + " (" + totalSlots + ") x" + count;
}
private void onChoose(int idx) {
if (idx < 0 || idx >= gIds.size()) return;
long providerId = gIds.get(idx);
ModNetwork.CHANNEL.sendToServer(new UploadEncodedPatternToProviderC2SPacket(providerId));
this.onClose();
}
@Override
public void onClose() {
Minecraft.getInstance().setScreen(parent);
}
@Override
public boolean isPauseScreen() {
return false;
}
private void buildGroups() {
// 使用 LinkedHashMap 保持首次出现顺序
Map<String, Group> map = new LinkedHashMap<>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
long id = ids.get(i);
int slots = emptySlots.get(i);
Group g = map.computeIfAbsent(name, k -> new Group());
g.count++;
g.totalSlots += Math.max(0, slots);
// 挑选空位最多的作为代表 id若并列保留先到者
if (slots > g.bestSlots) {
g.bestSlots = slots;
g.bestId = id;
}
}
for (Map.Entry<String, Group> e : map.entrySet()) {
String name = e.getKey();
Group g = e.getValue();
gNames.add(name);
gIds.add(g.bestId);
gTotalSlots.add(g.totalSlots);
gCount.add(g.count);
}
}
private static class Group {
long bestId = Long.MIN_VALUE;
int bestSlots = Integer.MIN_VALUE;
int totalSlots = 0;
int count = 0;
}
}

View File

@ -0,0 +1,63 @@
package com.extendedae_plus.mixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.gui.components.Tooltip;
import net.minecraft.client.gui.components.Button;
import net.minecraft.network.chat.Component;
import net.minecraft.world.entity.player.Inventory;
import appeng.client.gui.Icon;
import appeng.client.gui.AEBaseScreen;
import appeng.client.gui.me.items.PatternEncodingTermScreen;
import appeng.client.gui.style.ScreenStyle;
import appeng.client.gui.widgets.IconButton;
import appeng.menu.me.items.PatternEncodingTermMenu;
import appeng.menu.AEBaseMenu;
import com.extendedae_plus.network.ModNetwork;
import com.extendedae_plus.network.UploadEncodedPatternToProviderC2SPacket;
/**
* 在图样编码终端右侧工具栏加入一个上传按钮
* 点击后把当前已编码样板上传到任意可用的样板供应器服务端自动选择
*/
@Mixin(AEBaseScreen.class)
public abstract class PatternEncodingTermScreenMixin<T extends AEBaseMenu> {
// 使用 @Shadow 访问 AEBaseScreen 的受保护方法
@Shadow
protected abstract <B extends Button> B addToLeftToolbar(B button);
@Unique
private boolean extendedae_plus$uploadBtnAdded = false;
@Inject(method = "init", at = @At("HEAD"))
private void extendedae_plus$addUploadButton(CallbackInfo ci) {
// 仅在图样编码终端界面中添加按钮
if (!(((Object) this) instanceof PatternEncodingTermScreen)) {
return;
}
// 幂等避免每次 init() 都重复添加按钮
if (extendedae_plus$uploadBtnAdded) {
return;
}
var uploadBtn = new IconButton(btn -> ModNetwork.CHANNEL
.sendToServer(new com.extendedae_plus.network.RequestProvidersListC2SPacket())) {
@Override
protected Icon getIcon() {
return Icon.ARROW_UP;
}
};
uploadBtn.setTooltip(Tooltip.create(Component.translatable("extendedae_plus.button.choose_provider")));
// 直接调用 @Shadow 方法避免跨包 protected 访问问题
this.addToLeftToolbar(uploadBtn);
extendedae_plus$uploadBtnAdded = true;
}
}

View File

@ -7,6 +7,7 @@ import net.minecraftforge.network.NetworkDirection;
import com.extendedae_plus.ExtendedAEPlus;
import com.extendedae_plus.network.PullFromJeiOrCraftC2SPacket;
import com.extendedae_plus.network.UploadEncodedPatternToProviderC2SPacket;
public class ModNetwork {
private static final String PROTOCOL_VERSION = "1";
@ -37,6 +38,24 @@ public class ModNetwork {
.decoder(PullFromJeiOrCraftC2SPacket::decode)
.consumerNetworkThread(PullFromJeiOrCraftC2SPacket::handle)
.add();
CHANNEL.messageBuilder(UploadEncodedPatternToProviderC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(UploadEncodedPatternToProviderC2SPacket::encode)
.decoder(UploadEncodedPatternToProviderC2SPacket::decode)
.consumerNetworkThread(UploadEncodedPatternToProviderC2SPacket::handle)
.add();
CHANNEL.messageBuilder(RequestProvidersListC2SPacket.class, nextId(), NetworkDirection.PLAY_TO_SERVER)
.encoder(RequestProvidersListC2SPacket::encode)
.decoder(RequestProvidersListC2SPacket::decode)
.consumerNetworkThread(RequestProvidersListC2SPacket::handle)
.add();
CHANNEL.messageBuilder(ProvidersListS2CPacket.class, nextId(), NetworkDirection.PLAY_TO_CLIENT)
.encoder(ProvidersListS2CPacket::encode)
.decoder(ProvidersListS2CPacket::decode)
.consumerNetworkThread(ProvidersListS2CPacket::handle)
.add();
}
private static int nextId() { return id++; }

View File

@ -0,0 +1,65 @@
package com.extendedae_plus.network;
import net.minecraft.client.Minecraft;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.api.distmarker.OnlyIn;
import net.minecraftforge.network.NetworkEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import com.extendedae_plus.client.ui.ProviderSelectScreen;
/**
* S2C: 返回可见且有空位的样板供应器列表客户端弹窗展示供用户选择
*/
public class ProvidersListS2CPacket {
private final List<Long> ids;
private final List<String> names;
private final List<Integer> emptySlots;
public ProvidersListS2CPacket(List<Long> ids, List<String> names, List<Integer> emptySlots) {
this.ids = ids;
this.names = names;
this.emptySlots = emptySlots;
}
public static void encode(ProvidersListS2CPacket msg, FriendlyByteBuf buf) {
buf.writeVarInt(msg.ids.size());
for (int i = 0; i < msg.ids.size(); i++) {
buf.writeLong(msg.ids.get(i));
buf.writeUtf(msg.names.get(i));
buf.writeVarInt(msg.emptySlots.get(i));
}
}
public static ProvidersListS2CPacket decode(FriendlyByteBuf buf) {
int size = buf.readVarInt();
List<Long> ids = new ArrayList<>(size);
List<String> names = new ArrayList<>(size);
List<Integer> slots = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
ids.add(buf.readLong());
names.add(buf.readUtf());
slots.add(buf.readVarInt());
}
return new ProvidersListS2CPacket(ids, names, slots);
}
public static void handle(ProvidersListS2CPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
ctx.enqueueWork(() -> handleClient(msg));
ctx.setPacketHandled(true);
}
@OnlyIn(Dist.CLIENT)
private static void handleClient(ProvidersListS2CPacket msg) {
var mc = Minecraft.getInstance();
if (mc == null) return;
System.out.println("[EAE+][Client] ProvidersListS2C received, size=" + msg.ids.size());
var current = mc.screen;
mc.setScreen(new ProviderSelectScreen(current, msg.ids, msg.names, msg.emptySlots));
}
}

View File

@ -0,0 +1,77 @@
package com.extendedae_plus.network;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
import net.minecraft.server.level.ServerPlayer;
import appeng.menu.me.items.PatternEncodingTermMenu;
import appeng.menu.implementations.PatternAccessTermMenu;
import appeng.helpers.patternprovider.PatternContainer;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
import com.extendedae_plus.util.ExtendedAEPatternUploadUtil;
/**
* C2S: 请求当前终端可见的样板供应器列表用于弹窗选择
*/
public class RequestProvidersListC2SPacket {
public RequestProvidersListC2SPacket() {}
public static void encode(RequestProvidersListC2SPacket msg, FriendlyByteBuf buf) {}
public static RequestProvidersListC2SPacket decode(FriendlyByteBuf buf) { return new RequestProvidersListC2SPacket(); }
public static void handle(RequestProvidersListC2SPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
ctx.enqueueWork(() -> {
ServerPlayer player = ctx.getSender();
if (player == null) return;
if (!(player.containerMenu instanceof PatternEncodingTermMenu encMenu)) return;
System.out.println("[EAE+][Server] RequestProvidersListC2S from " + player.getGameProfile().getName());
// 优先若玩家也打开了样板访问终端则用 byId 方式精确服务器ID
PatternAccessTermMenu accessMenu = ExtendedAEPatternUploadUtil.getPatternAccessMenu(player);
if (accessMenu != null) {
List<Long> ids = ExtendedAEPatternUploadUtil.getAllProviderIds(accessMenu);
List<Long> filteredIds = new ArrayList<>();
List<String> names = new ArrayList<>();
List<Integer> slots = new ArrayList<>();
for (Long id : ids) {
if (id == null) continue;
if (!ExtendedAEPatternUploadUtil.isProviderAvailable(id, accessMenu)) continue;
int empty = ExtendedAEPatternUploadUtil.getAvailableSlots(id, accessMenu);
if (empty <= 0) continue; // 只列出有空位的
filteredIds.add(id);
names.add(ExtendedAEPatternUploadUtil.getProviderDisplayName(id, accessMenu));
slots.add(empty);
}
System.out.println("[EAE+][Server] Providers via accessMenu: size=" + filteredIds.size());
ModNetwork.CHANNEL.sendTo(new ProvidersListS2CPacket(filteredIds, names, slots), player.connection.connection, net.minecraftforge.network.NetworkDirection.PLAY_TO_CLIENT);
return;
}
// 回退基于编码终端所在网络枚举供应器负数ID编码索引encodedId = -1 - index
List<PatternContainer> containers = ExtendedAEPatternUploadUtil.listAvailableProvidersFromGrid(encMenu);
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);
}
System.out.println("[EAE+][Server] Providers via grid-fallback: size=" + idxIds.size());
ModNetwork.CHANNEL.sendTo(new ProvidersListS2CPacket(idxIds, names, slots), player.connection.connection, net.minecraftforge.network.NetworkDirection.PLAY_TO_CLIENT);
});
ctx.setPacketHandled(true);
}
}

View File

@ -0,0 +1,48 @@
package com.extendedae_plus.network;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraftforge.network.NetworkEvent;
import java.util.function.Supplier;
import net.minecraft.server.level.ServerPlayer;
import appeng.menu.me.items.PatternEncodingTermMenu;
import com.extendedae_plus.util.ExtendedAEPatternUploadUtil;
/**
* C2S: 请求将图样编码终端的已编码样板上传到指定的样板供应器由客户端选择
*/
public class UploadEncodedPatternToProviderC2SPacket {
private final long providerId;
public UploadEncodedPatternToProviderC2SPacket(long providerId) {
this.providerId = providerId;
}
public static void encode(UploadEncodedPatternToProviderC2SPacket msg, FriendlyByteBuf buf) {
buf.writeLong(msg.providerId);
}
public static UploadEncodedPatternToProviderC2SPacket decode(FriendlyByteBuf buf) {
return new UploadEncodedPatternToProviderC2SPacket(buf.readLong());
}
public static void handle(UploadEncodedPatternToProviderC2SPacket msg, Supplier<NetworkEvent.Context> ctxSupplier) {
var ctx = ctxSupplier.get();
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) {
ExtendedAEPatternUploadUtil.uploadFromEncodingMenuToProvider(player, menu, msg.providerId);
} else {
int index = (int) (-1L - msg.providerId);
ExtendedAEPatternUploadUtil.uploadFromEncodingMenuToProviderByIndex(player, menu, index);
}
});
ctx.setPacketHandled(true);
}
}

View File

@ -2,10 +2,14 @@ package com.extendedae_plus.util;
import appeng.api.inventories.InternalInventory;
import appeng.api.crafting.PatternDetailsHelper;
import appeng.api.crafting.IPatternDetails;
import appeng.api.networking.IGrid;
import appeng.api.networking.IGridNode;
import appeng.helpers.patternprovider.PatternContainer;
import appeng.menu.implementations.PatternAccessTermMenu;
import appeng.menu.me.items.PatternEncodingTermMenu;
import appeng.crafting.pattern.AECraftingPattern;
import appeng.core.definitions.AEItems;
import appeng.util.inv.FilteredInternalInventory;
import appeng.util.inv.filter.IAEItemFilter;
import net.minecraft.server.level.ServerPlayer;
@ -18,10 +22,6 @@ import java.util.Set;
import java.util.List;
import java.util.ArrayList;
import appeng.menu.me.items.PatternEncodingTermMenu;
import appeng.core.definitions.AEItems;
import appeng.api.crafting.IPatternDetails;
import appeng.crafting.pattern.AECraftingPattern;
import com.glodblock.github.extendedae.common.tileentities.matrix.TileAssemblerMatrixBase;
import net.minecraftforge.common.capabilities.ForgeCapabilities;
import net.minecraftforge.items.IItemHandler;
@ -632,4 +632,238 @@ public class ExtendedAEPatternUploadUtil {
return "未知终端类型";
}
}
/**
* AE2 的图样编码终端菜单上传当前已编码图样至当前网络中任意可用的样板供应器
* 策略
* 1) 仅当 encoded 槽位存在有效编码样板时执行
* 2) 通过 menu.getNetworkNode() 获取 IGrid遍历在线的 PatternContainer
* 3) 仅选择在终端中可见isVisibleInTerminal且库存存在空位的供应器
* 4) 使用 AE2 的标准 FilteredInternalInventory + Pattern 过滤器尝试插入
* 5) 成功后清空 encoded 槽位返回 true否则返回 false
*/
public static boolean uploadFromEncodingMenuToAnyProvider(ServerPlayer player, PatternEncodingTermMenu menu) {
if (player == null || menu == null) {
return false;
}
// 读取已编码槽位的物品通过 accessor
var encodedSlot = ((com.extendedae_plus.mixin.accessor.PatternEncodingTermMenuAccessor) (Object) menu)
.epp$getEncodedPatternSlot();
ItemStack stack = encodedSlot.getItem();
if (stack.isEmpty() || !PatternDetailsHelper.isEncodedPattern(stack)) {
return false;
}
// 获取 AE 网络
IGridNode node = menu.getNetworkNode();
if (node == null) {
return false;
}
IGrid grid = node.getGrid();
if (grid == null) {
return false;
}
// 遍历在线的 PatternContainer寻找第一个可见且有空位的供应器
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) {
continue;
}
// AE2 样板过滤规则尝试插入
var filtered = new FilteredInternalInventory(inv, new ExtendedAEPatternFilter());
ItemStack toInsert = stack.copy();
ItemStack remain = filtered.addItems(toInsert);
if (remain.getCount() < toInsert.getCount()) {
int inserted = toInsert.getCount() - remain.getCount();
stack.shrink(inserted);
if (stack.isEmpty()) {
encodedSlot.set(ItemStack.EMPTY);
} else {
encodedSlot.set(stack);
}
return true;
}
}
}
}
} catch (Throwable t) {
// 忽略异常以避免噪声
}
return false;
}
/**
* 将图样编码终端的已编码图样上传到指定的样板供应器通过 providerId 定位
*/
public static boolean uploadFromEncodingMenuToProvider(ServerPlayer player, PatternEncodingTermMenu menu, long providerId) {
if (player == null || menu == null) {
return false;
}
var encodedSlot = ((com.extendedae_plus.mixin.accessor.PatternEncodingTermMenuAccessor) (Object) menu)
.epp$getEncodedPatternSlot();
ItemStack stack = encodedSlot.getItem();
if (stack.isEmpty() || !PatternDetailsHelper.isEncodedPattern(stack)) {
return false;
}
PatternAccessTermMenu accessMenu = getPatternAccessMenu(player);
if (accessMenu == null) {
return false;
}
PatternContainer container = getPatternContainerById(accessMenu, providerId);
if (container == null || !container.isVisibleInTerminal()) {
return false;
}
InternalInventory inv = container.getTerminalPatternInventory();
if (inv == null || inv.size() <= 0) {
return false;
}
var filtered = new FilteredInternalInventory(inv, new ExtendedAEPatternFilter());
ItemStack toInsert = stack.copy();
ItemStack remain = filtered.addItems(toInsert);
if (remain.getCount() < toInsert.getCount()) {
int inserted = toInsert.getCount() - remain.getCount();
stack.shrink(inserted);
if (stack.isEmpty()) {
encodedSlot.set(ItemStack.EMPTY);
} else {
encodedSlot.set(stack);
}
return true;
}
return false;
}
/**
* 列出当前菜单中所有供应器的服务器ID原样返回 byId key 集合
*/
public static java.util.List<Long> getAllProviderIds(PatternAccessTermMenu menu) {
java.util.List<Long> result = new java.util.ArrayList<>();
if (menu == null) return result;
try {
java.lang.reflect.Field byIdField = findByIdField(menu.getClass());
if (byIdField == null) return result;
byIdField.setAccessible(true);
@SuppressWarnings("unchecked")
java.util.Map<Long, Object> byId = (java.util.Map<Long, Object>) byIdField.get(menu);
if (byId != null) {
result.addAll(byId.keySet());
}
} catch (Throwable ignored) {
}
return result;
}
/**
* 基于编码终端菜单的 AE Grid 遍历列出可在终端中可见且有空位的供应器容器
* 返回顺序稳定 grid machineClasses 顺序再按 activeMachines 迭代顺序
*/
public static List<PatternContainer> listAvailableProvidersFromGrid(PatternEncodingTermMenu menu) {
List<PatternContainer> list = new ArrayList<>();
if (menu == null) return list;
try {
IGridNode node = menu.getNetworkNode();
if (node == null) return list;
IGrid grid = node.getGrid();
if (grid == null) return list;
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;
}
/** 获取供应器显示名(优先组名) */
public static String getProviderDisplayName(PatternContainer container) {
if (container == null) return "未知供应器";
try {
var group = container.getTerminalGroup();
if (group != null) return group.name().getString();
} catch (Throwable ignored) {
}
return "样板供应器";
}
/** 计算供应器空槽位数量 */
public static int getAvailableSlots(PatternContainer container) {
if (container == null) return -1;
InternalInventory inv = container.getTerminalPatternInventory();
if (inv == null) return -1;
int available = 0;
for (int i = 0; i < inv.size(); i++) {
if (inv.getStackInSlot(i).isEmpty()) available++;
}
return available;
}
/**
* 基于索引的定向上传使用 listAvailableProvidersFromGrid(menu) 的顺序
* 将编码槽样板插入到第 index 个供应器
*/
public static boolean uploadFromEncodingMenuToProviderByIndex(ServerPlayer player, PatternEncodingTermMenu menu, int index) {
if (player == null || menu == null || index < 0) return false;
List<PatternContainer> list = listAvailableProvidersFromGrid(menu);
if (index >= list.size()) return false;
var container = list.get(index);
if (container == null) return false;
var encodedSlot = ((com.extendedae_plus.mixin.accessor.PatternEncodingTermMenuAccessor) (Object) menu)
.epp$getEncodedPatternSlot();
ItemStack stack = encodedSlot.getItem();
if (stack.isEmpty() || !PatternDetailsHelper.isEncodedPattern(stack)) {
return false;
}
InternalInventory inv = container.getTerminalPatternInventory();
if (inv == null || inv.size() <= 0) return false;
var filtered = new FilteredInternalInventory(inv, new ExtendedAEPatternFilter());
ItemStack toInsert = stack.copy();
ItemStack remain = filtered.addItems(toInsert);
if (remain.getCount() < toInsert.getCount()) {
int inserted = toInsert.getCount() - remain.getCount();
stack.shrink(inserted);
if (stack.isEmpty()) {
encodedSlot.set(ItemStack.EMPTY);
} else {
encodedSlot.set(stack);
}
return true;
}
return false;
}
}

View File

@ -8,7 +8,8 @@
"SlotGridLayoutMixin",
"GuiExPatternTerminalMixin",
"HighlightButtonMixin",
"PickFromWirelessMixin"
"PickFromWirelessMixin",
"PatternEncodingTermScreenMixin"
],
"mixins": [
"ContainerExPatternProviderMixin",