From ec412ade380c3364338afdcf97eab11266267883 Mon Sep 17 00:00:00 2001 From: NuanKi Date: Wed, 4 Mar 2026 19:28:21 -0600 Subject: [PATCH 1/7] Add Ignore NBT feature to AutoCrafter for item matching --- .../features/crafter/CrafterEntry.java | 19 ++ .../features/crafter/GuiAutoCrafter.java | 60 +++++ .../crafter/PacketCrafterStateSync.java | 3 + .../crafter/PacketToggleCrafterIgnoreNbt.java | 63 +++++ .../features/crafter/TileAutoCrafter.java | 235 ++++++++++++++++-- .../network/PowerToolsNetwork.java | 2 + .../assets/ae2powertools/lang/en_us.lang | 2 + 7 files changed, 358 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/ae2powertools/features/crafter/PacketToggleCrafterIgnoreNbt.java diff --git a/src/main/java/com/ae2powertools/features/crafter/CrafterEntry.java b/src/main/java/com/ae2powertools/features/crafter/CrafterEntry.java index 72b8e25..2e5f7ce 100644 --- a/src/main/java/com/ae2powertools/features/crafter/CrafterEntry.java +++ b/src/main/java/com/ae2powertools/features/crafter/CrafterEntry.java @@ -68,6 +68,12 @@ public class CrafterEntry { */ private long targetQuantity; + /** + * If true, consumed inputs for this entry are matched in the ME network ignoring NBT (item + meta only). + * This does NOT affect catalyst slot validation. + */ + private boolean ignoreNbt; + // ==================== ERROR DETAILS ==================== /** @@ -149,6 +155,7 @@ public CrafterEntry() { this.lastCraftTick = 0; this.metricsTotal = 0; this.metricsError = 0; + this.ignoreNbt = false; } /** @@ -471,6 +478,7 @@ public NBTTagCompound writeToNBT() { tag.setInteger("state", state.ordinal()); tag.setLong("targetQty", targetQuantity); tag.setLong("lastCraft", lastCraftTick); + tag.setBoolean("ignoreNbt", ignoreNbt); // Save pending outputs as a list if (!pendingOutputs.isEmpty()) { @@ -525,6 +533,7 @@ public void readFromNBT(NBTTagCompound tag) { if (targetQuantity == 0) targetQuantity = Long.MAX_VALUE; lastCraftTick = tag.getLong("lastCraft"); + ignoreNbt = tag.getBoolean("ignoreNbt"); // Load pending outputs pendingOutputs.clear(); @@ -616,4 +625,14 @@ public IAEItemStack[] getInputGrid() { // Fall back to synced data (client-side) return syncedInputGrid; } + + // --- Ignore NBT --- + + public boolean isIgnoreNbt() { + return ignoreNbt; + } + + public void setIgnoreNbt(boolean ignoreNbt) { + this.ignoreNbt = ignoreNbt; + } } diff --git a/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java b/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java index f0f8297..ba69c37 100644 --- a/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java +++ b/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java @@ -114,6 +114,13 @@ public class GuiAutoCrafter extends GuiContainer { private boolean overviewBtnHovered = false; private boolean overviewCloseBtnHovered = false; + // Ignore NBT toggle button + private static final int IGNORE_NBT_BTN_X = 150; + private static final int IGNORE_NBT_BTN_Y = STATE_INDICATOR_Y; + + // Hover state for Ignore NBT button + private boolean ignoreNbtBtnHovered = false; + // Hovered elements private int hoveredRecipeSlot = -1; private int hoveredOverviewRow = -1; @@ -144,6 +151,7 @@ public class GuiAutoCrafter extends GuiContainer { private final double[] syncedOccupancy = new double[TileAutoCrafter.ENTRY_COUNT]; private final double[] syncedErrorRate = new double[TileAutoCrafter.ENTRY_COUNT]; private final List> syncedErrorDetails = new ArrayList<>(); + private final boolean[] syncedIgnoreNbt = new boolean[TileAutoCrafter.ENTRY_COUNT]; // Catalyst info per entry (slot index -> expected item) private final List> syncedCatalystInfo = new ArrayList<>(); @@ -336,6 +344,7 @@ private void drawAE2Buttons(int mouseX, int mouseY) { if (!overviewMode) { drawPageNavigationButtons(mouseX, mouseY); drawBatchSpeedButtons(mouseX, mouseY); + drawIgnoreNbtButton(mouseX, mouseY); } } @@ -469,6 +478,32 @@ private void drawSquareButton(int x, int y, int size, String text, boolean enabl fontRenderer.drawStringWithShadow(text, textX, textY, textColor); } + /** + * Draws the per-entry Ignore NBT toggle button. + * This is a small square vanilla-style button that toggles fuzzy NBT matching for the current recipe entry. + */ + private void drawIgnoreNbtButton(int mouseX, int mouseY) { + int x = guiLeft + IGNORE_NBT_BTN_X; + int y = guiTop + IGNORE_NBT_BTN_Y; + + int page = getCurrentPage(); + + // Only enable if this entry has a recipe/pattern to act on + boolean enabled = hasDisplayData(page); + + ignoreNbtBtnHovered = enabled + && mouseX >= x && mouseX < x + PAGE_BTN_SIZE + && mouseY >= y && mouseY < y + PAGE_BTN_SIZE; + + // Draw button with "N" label (NBT) + drawSquareButton(x, y, PAGE_BTN_SIZE, "N", enabled, ignoreNbtBtnHovered); + + // Visual ON indicator (subtle green overlay) + if (enabled && isIgnoreNbtEnabled(page)) { + drawRect(x + 1, y + 1, x + PAGE_BTN_SIZE - 1, y + PAGE_BTN_SIZE - 1, 0x3000FF00); + } + } + /** * Draws tooltips for custom buttons. */ @@ -493,6 +528,10 @@ private void drawAE2ButtonTooltips(int mouseX, int mouseY) { FormatUtil.formatTimeTicks(container.syncSpeedTicks))); tooltip.add(""); tooltip.add(TextFormatting.DARK_GRAY + I18n.format("gui.ae2powertools.crafter.speed.explanation")); + } else if (ignoreNbtBtnHovered) { + boolean on = isIgnoreNbtEnabled(getCurrentPage()); + tooltip.add(I18n.format("gui.ae2powertools.crafter.ignore_nbt") + ": " + (on ? "ON" : "OFF")); + tooltip.add(TextFormatting.DARK_GRAY + I18n.format("gui.ae2powertools.crafter.ignore_nbt.desc")); } if (!tooltip.isEmpty()) { @@ -1013,6 +1052,14 @@ protected void mouseClicked(int mouseX, int mouseY, int mouseButton) throws IOEx openSpeedDialog(); return; } + + // Ignore NBT toggle button + if (ignoreNbtBtnHovered) { + int page = getCurrentPage(); + PowerToolsNetwork.INSTANCE.sendToServer(new PacketToggleCrafterIgnoreNbt( + container.getTile().getPos(), page)); + return; + } } // Handle result right-click to disable @@ -1134,6 +1181,9 @@ public void handleStateSync(NBTTagCompound data) { syncedStates[i] = CrafterState.NO_PATTERN; } + // Per-entry ignore NBT setting + syncedIgnoreNbt[i] = entryTag.getBoolean("ignoreNbt"); + // Metrics syncedMetricsTotal[i] = entryTag.getLong("metricsTotal"); long metricsError = entryTag.getLong("metricsError"); @@ -1303,4 +1353,14 @@ private int getSyncedBatchSize() { private int getSyncedEffectiveBatchSize() { return hasSyncedData ? syncedEffectiveBatchSize : container.syncEffectiveBatchSize; } + + /** Gets whether Ignore NBT is enabled for an entry. */ + private boolean isIgnoreNbtEnabled(int entryIndex) { + if (hasSyncedData && entryIndex >= 0 && entryIndex < TileAutoCrafter.ENTRY_COUNT) { + return syncedIgnoreNbt[entryIndex]; + } + + CrafterEntry entry = container.getTile().getEntry(entryIndex); + return entry != null && entry.isIgnoreNbt(); + } } diff --git a/src/main/java/com/ae2powertools/features/crafter/PacketCrafterStateSync.java b/src/main/java/com/ae2powertools/features/crafter/PacketCrafterStateSync.java index 3dd0cf5..be78f9c 100644 --- a/src/main/java/com/ae2powertools/features/crafter/PacketCrafterStateSync.java +++ b/src/main/java/com/ae2powertools/features/crafter/PacketCrafterStateSync.java @@ -66,6 +66,9 @@ public static PacketCrafterStateSync fromTile(TileAutoCrafter tile) { entryTag.setInteger("state", entry.getState().ordinal()); entryTag.setBoolean("enabled", entry.isEnabled()); + // Per-entry ignore NBT setting + entryTag.setBoolean("ignoreNbt", entry.isIgnoreNbt()); + // Metrics entryTag.setLong("metricsTotal", entry.getMetricsTotal()); entryTag.setLong("metricsError", entry.getMetricsError()); diff --git a/src/main/java/com/ae2powertools/features/crafter/PacketToggleCrafterIgnoreNbt.java b/src/main/java/com/ae2powertools/features/crafter/PacketToggleCrafterIgnoreNbt.java new file mode 100644 index 0000000..c7c5663 --- /dev/null +++ b/src/main/java/com/ae2powertools/features/crafter/PacketToggleCrafterIgnoreNbt.java @@ -0,0 +1,63 @@ +package com.ae2powertools.features.crafter; + +import io.netty.buffer.ByteBuf; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.tileentity.TileEntity; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; +import net.minecraftforge.fml.common.network.simpleimpl.IMessage; +import net.minecraftforge.fml.common.network.simpleimpl.IMessageHandler; +import net.minecraftforge.fml.common.network.simpleimpl.MessageContext; + +/** + * Packet to toggle the per-entry Ignore NBT setting for the AutoCrafter. + */ +public class PacketToggleCrafterIgnoreNbt implements IMessage { + + private BlockPos pos; + private int entryIndex; + + public PacketToggleCrafterIgnoreNbt() {} + + public PacketToggleCrafterIgnoreNbt(BlockPos pos, int entryIndex) { + this.pos = pos; + this.entryIndex = entryIndex; + } + + @Override + public void fromBytes(ByteBuf buf) { + int x = buf.readInt(); + int y = buf.readInt(); + int z = buf.readInt(); + this.pos = new BlockPos(x, y, z); + this.entryIndex = buf.readInt(); + } + + @Override + public void toBytes(ByteBuf buf) { + buf.writeInt(pos.getX()); + buf.writeInt(pos.getY()); + buf.writeInt(pos.getZ()); + buf.writeInt(entryIndex); + } + + public static class Handler implements IMessageHandler { + + @Override + public IMessage onMessage(PacketToggleCrafterIgnoreNbt message, MessageContext ctx) { + EntityPlayerMP player = ctx.getServerHandler().player; + World world = player.world; + + player.getServerWorld().addScheduledTask(() -> { + TileEntity te = world.getTileEntity(message.pos); + if (te instanceof TileAutoCrafter) { + TileAutoCrafter crafter = (TileAutoCrafter) te; + crafter.toggleIgnoreNbt(message.entryIndex); + } + }); + + return null; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java b/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java index 7db995b..50ab957 100644 --- a/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java +++ b/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java @@ -417,11 +417,14 @@ private void validateAllEntries() { boolean hasAllInputs = true; List missingInputs = new ArrayList<>(); + boolean ignoreNbt = candidate.entry.isIgnoreNbt(); + for (CrafterRecipeInfo.IngredientInfo ingredient : candidate.info.getConsumedItems()) { IAEItemStack item = ingredient.getItem(); if (item == null) continue; - ItemStackKey key = new ItemStackKey(item); + ItemStackKey key = new ItemStackKey(item, ignoreNbt); + long available = sharedPool.getOrDefault(key, 0L); long needed = calculateItemsNeededForCrafts(ingredient, 1); // At least 1 craft @@ -488,7 +491,7 @@ private static class CraftCandidate { /** * Pre-initialize the shared resource pool with all items needed by all candidates. * Fetches the storage list ONCE and lookups are done against that cached list. - * + * * @param candidates List of all candidates wanting to craft * @return Map of ItemStackKey to available quantity in network */ @@ -501,14 +504,24 @@ private Map initializeSharedPool(List candid // Collect all unique items needed by all candidates for (CraftCandidate candidate : candidates) { + boolean ignoreNbt = candidate.entry.isIgnoreNbt(); + for (CrafterRecipeInfo.IngredientInfo ingredient : candidate.info.getConsumedItems()) { IAEItemStack item = ingredient.getItem(); if (item == null) continue; - ItemStackKey key = new ItemStackKey(item); + ItemStackKey key = new ItemStackKey(item, ignoreNbt); if (!pool.containsKey(key)) { - IAEItemStack found = storageList.findPrecise(item); - pool.put(key, found != null ? found.getStackSize() : 0L); + + long available; + if (ignoreNbt) { + available = getNetworkQuantityIgnoringNbt(item, storageList); + } else { + IAEItemStack found = storageList.findPrecise(item); + available = found != null ? found.getStackSize() : 0L; + } + + pool.put(key, available); } } } @@ -541,11 +554,15 @@ private List allocateResourcesFairly(List candi Map totalDemand = new HashMap<>(); for (CraftCandidate candidate : candidates) { + + boolean ignoreNbt = candidate.entry.isIgnoreNbt(); + for (CrafterRecipeInfo.IngredientInfo ingredient : candidate.info.getConsumedItems()) { IAEItemStack item = ingredient.getItem(); if (item == null) continue; - ItemStackKey key = new ItemStackKey(item); + ItemStackKey key = new ItemStackKey(item, ignoreNbt); + resourceUsers.computeIfAbsent(key, k -> new ArrayList<>()).add(candidate); long needed = calculateItemsNeededForCrafts(ingredient, effectiveMaxBatchSize); totalDemand.merge(key, needed, Long::sum); @@ -567,7 +584,7 @@ private List allocateResourcesFairly(List candi if (demand <= available) { // Not contested - each candidate gets what they need for (CraftCandidate user : users) { - CrafterRecipeInfo.IngredientInfo ing = findIngredient(user.info, key); + CrafterRecipeInfo.IngredientInfo ing = findIngredient(user.info, key, user.entry.isIgnoreNbt()); if (ing != null) { allocations.put(user, calculateItemsNeededForCrafts(ing, effectiveMaxBatchSize)); } @@ -588,11 +605,14 @@ private List allocateResourcesFairly(List candi int finalCrafts = effectiveMaxBatchSize; List limitingFactors = new ArrayList<>(); + boolean ignoreNbt = candidate.entry.isIgnoreNbt(); + for (CrafterRecipeInfo.IngredientInfo ingredient : candidate.info.getConsumedItems()) { IAEItemStack item = ingredient.getItem(); if (item == null) continue; - ItemStackKey key = new ItemStackKey(item); + ItemStackKey key = new ItemStackKey(item, ignoreNbt); + Map allocations = itemAllocations.get(key); if (allocations == null) continue; @@ -616,8 +636,9 @@ private List allocateResourcesFairly(List candi candidate.entry.setBatchSizeTracking(effectiveMaxBatchSize, finalCrafts); if (finalCrafts > 0) { - // Deduct from shared pool - deductFromPool(candidate.info, pool, finalCrafts); + + deductFromPool(candidate.info, pool, finalCrafts, candidate.entry.isIgnoreNbt()); + simulations.add(new CraftSimulation(candidate.entry, candidate.info, finalCrafts)); // If crafts were reduced, add info about why @@ -641,12 +662,11 @@ private List allocateResourcesFairly(List candi * Find the ingredient info for a specific item in a recipe. */ @Nullable - private CrafterRecipeInfo.IngredientInfo findIngredient(CrafterRecipeInfo info, ItemStackKey key) { + private CrafterRecipeInfo.IngredientInfo findIngredient(CrafterRecipeInfo info, ItemStackKey key, boolean ignoreNbt) { for (CrafterRecipeInfo.IngredientInfo ingredient : info.getConsumedItems()) { IAEItemStack item = ingredient.getItem(); if (item == null) continue; - - if (new ItemStackKey(item).equals(key)) return ingredient; + if (new ItemStackKey(item, ignoreNbt).equals(key)) return ingredient; } return null; @@ -723,12 +743,13 @@ private void updateEntryState(CrafterEntry entry, CrafterState newState) { * For most items: deduct 1 item per craft. * For DURABILITY items: deduct based on how many items are actually needed. */ - private void deductFromPool(CrafterRecipeInfo info, Map pool, int crafts) { + private void deductFromPool(CrafterRecipeInfo info, Map pool, int crafts, boolean ignoreNbt) { for (CrafterRecipeInfo.IngredientInfo ingredient : info.getConsumedItems()) { IAEItemStack item = ingredient.getItem(); if (item == null) continue; - ItemStackKey key = new ItemStackKey(item); + ItemStackKey key = new ItemStackKey(item, ignoreNbt); + long current = pool.getOrDefault(key, 0L); long itemsToDeduct; @@ -759,9 +780,11 @@ private void deductFromPool(CrafterRecipeInfo info, Map pool */ private static class ItemStackKey { private final IAEItemStack item; + private final boolean ignoreNbt; - ItemStackKey(IAEItemStack item) { + ItemStackKey(IAEItemStack item, boolean ignoreNbt) { this.item = item; + this.ignoreNbt = ignoreNbt; } @Override @@ -769,14 +792,31 @@ public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof ItemStackKey)) return false; ItemStackKey other = (ItemStackKey) obj; - return item.isSameType(other.item); + + if (this.ignoreNbt != other.ignoreNbt) return false; + + ItemStack a = this.item.createItemStack(); + ItemStack b = other.item.createItemStack(); + if (a.getItem() != b.getItem()) return false; + if (a.getMetadata() != b.getMetadata()) return false; + + if (this.ignoreNbt) return true; + + return this.item.isSameType(other.item); } @Override public int hashCode() { - // Use item + meta for hash, NBT comparison in equals ItemStack stack = item.createItemStack(); - return stack.getItem().hashCode() ^ (stack.getMetadata() * 31); + int h = stack.getItem().hashCode() ^ (stack.getMetadata() * 31); + + h = 31 * h + (ignoreNbt ? 1 : 0); + + if (!ignoreNbt && stack.hasTagCompound()) { + h = 31 * h + stack.getTagCompound().hashCode(); + } + + return h; } } @@ -862,7 +902,7 @@ private int getLeftoverDurability(CrafterEntry entry, int recipeSlotIndex, IAEIt * @param crafts Number of individual crafts to perform */ private boolean extractInputs(CrafterEntry entry, CrafterRecipeInfo info, int crafts) { - // First simulate all extractions + List toExtract = new ArrayList<>(); for (CrafterRecipeInfo.IngredientInfo ingredient : info.getConsumedItems()) { @@ -902,15 +942,27 @@ private boolean extractInputs(CrafterEntry entry, CrafterRecipeInfo info, int cr if (request.getStackSize() > 0) toExtract.add(request); } - // Simulate to verify we can extract everything - // Note: This can fail if another crafter extracted between calculateAvailableBatches and now + boolean ignoreNbt = entry.isIgnoreNbt(); + for (IAEItemStack request : toExtract) { - IAEItemStack result = tryExtractFromNetwork(request, Actionable.SIMULATE); - if (result == null || result.getStackSize() < request.getStackSize()) return false; + boolean ok; + if (ignoreNbt) { + ok = extractFromNetworkIgnoringNbt(request, Actionable.SIMULATE); + } else { + IAEItemStack result = tryExtractFromNetwork(request, Actionable.SIMULATE); + ok = result != null && result.getStackSize() >= request.getStackSize(); + } + + if (!ok) return false; } - // Actually extract - for (IAEItemStack request : toExtract) tryExtractFromNetwork(request, Actionable.MODULATE); + for (IAEItemStack request : toExtract) { + if (ignoreNbt) { + extractFromNetworkIgnoringNbt(request, Actionable.MODULATE); + } else { + tryExtractFromNetwork(request, Actionable.MODULATE); + } + } return true; } @@ -1138,6 +1190,35 @@ private long getNetworkQuantity(IAEItemStack item) { } } + /** + * Counts items in the network ignoring NBT (matches by item + meta only). + * This is used for availability checks and batch allocation when Ignore NBT is enabled. + */ + private long getNetworkQuantityIgnoringNbt(IAEItemStack template, @Nullable IItemList storageList) { + if (template == null || storageList == null) return 0L; + + ItemStack wanted = template.createItemStack(); + if (wanted.isEmpty()) return 0L; + + net.minecraft.item.Item wantedItem = wanted.getItem(); + int wantedMeta = wanted.getMetadata(); + + long total = 0L; + for (IAEItemStack stored : storageList) { + if (stored == null || stored.getStackSize() <= 0) continue; + + ItemStack storedStack = stored.createItemStack(); + if (storedStack.isEmpty()) continue; + + if (storedStack.getItem() != wantedItem) continue; + if (storedStack.getMetadata() != wantedMeta) continue; + + total += stored.getStackSize(); + } + + return total; + } + // ==================== FAKE PLAYER ==================== @Nullable @@ -1549,6 +1630,16 @@ public void toggleEntry(int index) { markDirty(); } + public void toggleIgnoreNbt(int index) { + if (index < 0 || index >= entries.size()) return; + + CrafterEntry entry = entries.get(index); + entry.setIgnoreNbt(!entry.isIgnoreNbt()); + + needsSync = true; + markDirty(); + } + /** * Clear an entry (remove pattern and reset). */ @@ -1945,4 +2036,96 @@ public void validate() { super.validate(); gridProxy.validate(); } + + /** + * Extracts items from the AE2 network ignoring NBT (matches by item + meta only). + * Uses the network inventory list to pull from any NBT variant of the same base item. + * + * For SIMULATE: returns true if enough items exist. + * For MODULATE: actually extracts items (best-effort). + */ + private boolean extractFromNetworkIgnoringNbt(IAEItemStack request, Actionable mode) { + if (request == null || request.getStackSize() <= 0) return true; + + try { + IGrid grid = gridProxy.getGrid(); + if (grid == null) return false; + + IStorageGrid storage = grid.getCache(IStorageGrid.class); + if (storage == null) return false; + + IMEMonitor inv = storage.getInventory( + AEApi.instance().storage().getStorageChannel(IItemStorageChannel.class)); + + IItemList list = inv.getStorageList(); + if (list == null) return false; + + ItemStack wanted = request.createItemStack(); + if (wanted.isEmpty()) return false; + + net.minecraft.item.Item wantedItem = wanted.getItem(); + int wantedMeta = wanted.getMetadata(); + + // IMPORTANT: + // We must NOT iterate the live AE2 ItemList iterator while extracting, because extraction + // mutates the underlying list and will trigger ConcurrentModificationException. + // + // Strategy: + // - SIMULATE: just count available items (no extraction / no mutation) + // - MODULATE: snapshot matching stacks first, then extract using the snapshot + if (mode == Actionable.SIMULATE) { + long available = 0L; + + for (IAEItemStack stored : list) { + if (stored == null || stored.getStackSize() <= 0) continue; + + ItemStack storedStack = stored.createItemStack(); + if (storedStack.isEmpty()) continue; + + if (storedStack.getItem() != wantedItem) continue; + if (storedStack.getMetadata() != wantedMeta) continue; + + available += stored.getStackSize(); + if (available >= request.getStackSize()) return true; + } + + return available >= request.getStackSize(); + } + + // MODULATE path: snapshot matching entries before extracting to avoid CME + List snapshot = new ArrayList<>(); + for (IAEItemStack stored : list) { + if (stored == null || stored.getStackSize() <= 0) continue; + + ItemStack storedStack = stored.createItemStack(); + if (storedStack.isEmpty()) continue; + + if (storedStack.getItem() != wantedItem) continue; + if (storedStack.getMetadata() != wantedMeta) continue; + + snapshot.add(stored.copy()); + } + + long remaining = request.getStackSize(); + + for (IAEItemStack stored : snapshot) { + if (stored == null || stored.getStackSize() <= 0) continue; + + long toTake = Math.min(remaining, stored.getStackSize()); + + IAEItemStack takeReq = stored.copy(); + takeReq.setStackSize(toTake); + + IAEItemStack extracted = inv.extractItems(takeReq, Actionable.MODULATE, actionSource); + long got = extracted != null ? extracted.getStackSize() : 0; + + remaining -= got; + if (remaining <= 0) return true; + } + + return remaining <= 0; + } catch (GridAccessException e) { + return false; + } + } } diff --git a/src/main/java/com/ae2powertools/network/PowerToolsNetwork.java b/src/main/java/com/ae2powertools/network/PowerToolsNetwork.java index e42d245..f092d53 100644 --- a/src/main/java/com/ae2powertools/network/PowerToolsNetwork.java +++ b/src/main/java/com/ae2powertools/network/PowerToolsNetwork.java @@ -13,6 +13,7 @@ import com.ae2powertools.features.crafter.PacketSetCrafterPage; import com.ae2powertools.features.crafter.PacketSetCrafterSpeed; import com.ae2powertools.features.crafter.PacketToggleCrafterEntry; +import com.ae2powertools.features.crafter.PacketToggleCrafterIgnoreNbt; /** @@ -50,5 +51,6 @@ public static void init() { INSTANCE.registerMessage(PacketReturnToCrafterGui.Handler.class, PacketReturnToCrafterGui.class, packetId++, Side.SERVER); INSTANCE.registerMessage(PacketRequestCrafterSync.Handler.class, PacketRequestCrafterSync.class, packetId++, Side.SERVER); INSTANCE.registerMessage(PacketCrafterStateSync.Handler.class, PacketCrafterStateSync.class, packetId++, Side.CLIENT); + INSTANCE.registerMessage(PacketToggleCrafterIgnoreNbt.Handler.class, PacketToggleCrafterIgnoreNbt.class, packetId++, Side.SERVER); } } diff --git a/src/main/resources/assets/ae2powertools/lang/en_us.lang b/src/main/resources/assets/ae2powertools/lang/en_us.lang index b0697ad..2fb32ec 100644 --- a/src/main/resources/assets/ae2powertools/lang/en_us.lang +++ b/src/main/resources/assets/ae2powertools/lang/en_us.lang @@ -219,6 +219,8 @@ gui.ae2powertools.crafter.page=Page %d/%d gui.ae2powertools.crafter.empty_slot=Slot %d - Empty gui.ae2powertools.crafter.click_to_view=Left-click to view gui.ae2powertools.crafter.right_click_toggle=Right-click to toggle +gui.ae2powertools.crafter.ignore_nbt=Ignore input item's NBT +gui.ae2powertools.crafter.ignore_nbt.desc=Match items by item+meta only (ignore nbt data) # GUI - Batch From aa4a3e0e18a4bfe2279ee232642adac2ceb5d982 Mon Sep 17 00:00:00 2001 From: NuanKi Date: Wed, 4 Mar 2026 19:43:55 -0600 Subject: [PATCH 2/7] Added variants for the crafter speed upgrades --- .../items/crafter_speed_upgrade_i_V1.png | Bin 0 -> 1348 bytes .../items/crafter_speed_upgrade_i_V2.png | Bin 0 -> 1370 bytes .../items/crafter_speed_upgrade_ii_V1.png | Bin 0 -> 1353 bytes .../items/crafter_speed_upgrade_ii_V2.png | Bin 0 -> 1370 bytes .../items/crafter_speed_upgrade_iii_V1.png | Bin 0 -> 1353 bytes .../items/crafter_speed_upgrade_iii_V2.png | Bin 0 -> 1370 bytes .../items/crafter_speed_upgrade_iv_V1.png | Bin 0 -> 1353 bytes .../items/crafter_speed_upgrade_iv_V2.png | Bin 0 -> 1370 bytes 8 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_i_V1.png create mode 100644 src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_i_V2.png create mode 100644 src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V1.png create mode 100644 src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V2.png create mode 100644 src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V1.png create mode 100644 src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V2.png create mode 100644 src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iv_V1.png create mode 100644 src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iv_V2.png diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_i_V1.png b/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_i_V1.png new file mode 100644 index 0000000000000000000000000000000000000000..a4b48794324a16eae3e145b6ec6f5b0a1b29d04b GIT binary patch literal 1348 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WChAG1^9%x`Ugk(fl#zx5D-QA2Swe#_vqgJ#|IA|J9y~$0U&w!_<=*m?tug! z-3N*vI<{ol+NI0a9stQ6zkmPHlI3fcf<)FV0itDVD(hMj(sB~ha{Ys&mx8#h74A4AMxq?Mi0!3A_6KBaxoRyfCBT!T=P+TQ5VRl7btIUL1Qmd9J9^ImNbZcdOi{g>3 zf<@J`6K2UyoSm4SQ&HclaCED}k*%hd4ki{3fdbc=sbwCsf0 z(yNx6SU8$mI0zJ1ORZimy?VK+rDI}xu8F1Nbvvh3irV+?J-&bMab4~0T{e|b8Z4{;4JWnEM{QfI}E~%$MaXD00kvWTq8&;)h{7N8mfBVz-` z1rT#VdRP}g%$f#dg8&oIBqp%RAWI7%3#!Y|zyKu6W_qP~Q-6^)P+fzki(`m|f9S>B z>RARNY!6zZTo#8edd9nH#hOL2sk@?XHy+MgoyWXo?X>^@!^=8+S) z39A6pTP7wBE>?~OY}^l$zT4cseU~fB@v_1N6OMyRm%X*IVP<%>E6&6#^{~TIudk~j zR$uYu*{a;IVO9K9sk1y1Tf;Wq&=6kr)Y)N{m_um#9DzfcsdG5bX>8VsZpyLoxa{wc zFWATVaE9iojhZa^<-5K;Wpr43>rspFp{Yqs!Cg}|=bpd5ci!t~l3A{8oi@{gR^67r zv*X=#W`k36JYGjknz?XY%dK2NNt_^72a>6}&odGKb(YSo@exb$sp_5Uv> zzh*YZM@ec{**UFjKlk_j^XC`s4kz7QRoLgJ-sz+GG{@}sY~@1&wg9BdFHXl8y{PI`@A4AMxq?Mi0!3A_6KBaxoRyfCBT!T=P+TQ5VRl7btIUL1Qmd9J9^ImNbnAZx$;$c` z#UonhTO%#xirJ25@y27_oteXGLJtqMoBnp!%TSU3m*^-i2^YT;;N>9C1GP-@k3 zg`->igQ8_8%$8oY+{D7s)WSiaxLRuUa_QB}O)VW0({oKM9k1Itty0v!ckl82dygyY zTPx~XT7KW=88g_ArodYV`ks9Hd=IN=$zyaj2GDtD9 zGB5&JUO+4jWrLD}1|u_AoC(M_WMpCx1kzDJoY~F-76*A41QLK4q#s73+seQIOimNn z8CZa742+Bo7#Bdy1?gd305NMCkPQM%K$DojDuXO7fGnskLjwblESu?-;!XWU)Tl=T947 z>EaRIs=c;!dwtcj%K7&jL!Y^vjMYq=@BRMJ_x(KE6caZuY@ITFdZo8-TkiVjznjM=WJgl)BxY3#f`*Jt1NJ%MbCR%YIcI`#Ya9@WHkvwW)d z#?|p1U$N!n26nUD`PJ9?`J3e>e`HRXxU5}y%f7cU|Ep?ba*WSBTPyWEqkhr}F)v@W z$@l-w=kZxQO>;|c+0hn>-RkL+SMQG&VE1j2Jhn~OK#fPZai>h*T!VA<xEoL7%rYw0dhJ^L-+m_^lP>*b?|0z+=}a4Kp&t>AyO zBhdZq(M4Bc^a3Bh{JAq?)nc~|?1=%Z%-0-UbSXk_;@2-J+X7fk`45=*OqxFZY0$d3 z_@`Qu{MNC@LvMFTdIUdT`*%~!y0G=@S6{V|q#;2EOsDV
mdKI;Vst0AhShM*si- literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V1.png b/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V1.png new file mode 100644 index 0000000000000000000000000000000000000000..88fa3d356fb2c2e5d8f8b874a71ef339ef1e28d7 GIT binary patch literal 1353 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WChBx2Ka=y`Ugk(fl#zx5D-QA2Swe#_vqgJ#|IA|J9y~$0U&w!_<=*m?tug! z-3N*vI<{ol+NI0a9stQ6zkmPHlI3fcf<)FV0itDVD(hMj(sB~ha{Ys&mx8#h74A4AMxq?Mi0!3A_6KBaxoRyfCBT!T=P+TQ5VRl7btIUL1Qmd9J9^ImNbZcdOi{g>3 zf<@J`6K2UyoSm4SQ&HclaCED}k*%hd4ki{3fdbc=sbwCsf0 z(yNx6SU8$mI0zJ1ORZimy?VK+rDI}xu8E~1L;oL!5}+?0-@o^`vc8p}{7gk%3q$zK zg!CL>$fSl%=>k&WB|(0{|7ijk_9dG>23o;c;1OBOz`%DHgc*M}Gi0LikMUMb$xUt|qb*XZfu7-Hcc zdNH?pmVpS{gO(_l#i5Iy@orkNW>IYFuBh9Mhx1nFF>hHL_5Z*BP1Q%<=S6=w7Q%u~utR+=(imvy>#LI6h?mf7@kJ;dY-gnLf zt=)TfU(6_(&%`UXx+ikll$)D;^7^ay{#$X`KE>nUO6FvK{@UxG<-RL3tPwkLTyWJ@ zsh#t_|21T-TQGg9m3(98<#|4JC3gb34y{}_`_&%i#J)K`R&)Kz_@=M;VA$+Ad;7b; zmoGm^=c>zeUGm~5$5Oed@B8P^XaCK!xpMI#ORKpmMy*pOpM28)-JJFEhK1azi;@i$ zkDdE^_Q|{ZaxQhUiESG*xq3JcyCmfC9xoF}-ETcdiCNppAe1ZFPcnH;=Fz(bCmZCI zTvl%3oNO3+B&4M~Nz?QG{qzsq9a_74TXk0&ND4bm>AJ}261cYORk!QHeKCt}oqnCY zFTmCGKMT|TfVGm{t}D02C_R4an!iA3)%?axf#do4tEX6hY$2`ddqW0$o^JrpvalUDPlj0@7p*0pD(yNZ{3#3 Q&Y*

FVdQ&MBb@0EKi)X8-^I literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V2.png b/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V2.png new file mode 100644 index 0000000000000000000000000000000000000000..14d2ef973b6fb898e73b24a37820b4b708fae870 GIT binary patch literal 1370 zcmcIieN57M82=(Fk|tQK0&&5brlt-B4Cm^gVMZw^qBHRYHbtFsHndCdCVJv^dij=> z4K>XdC=|64Q|pXS;L@~RSvI|solc$WI=j2hu64zIf7ShS|KGFc<@0==m+!vYQx(UH z-nxaj1pu&>dz2%9k&Kud8hTDwwG{?bp&Caus409wihXfu(Y}0d+^*XX;kS`GV)Hi$Ub#x}1!DQhJONc!>?;af^f-T9G(IeP%hYss0 z`xh6N?TgC_Rb_FhTD>h$4EF;tpA>cuh7&~sUV`I)62PxS;ou|KlSfY!0^m807|K>~ zH4~DAqNwDegaT>NX^A`w#7S~LFACvB3CWC*(2&s3YPX#;uxU6enJeXT1A!Y%TmTW} z0#FbggXaASBd6exLVw_$APz-20WTO+pyi%%{=g$2Mnnt^ng2Kb_f-JkIn`r;Gwh*; z(Wt2Qk9(0Bm-dglE<%im6WrwVp;HjSgy_>$D3yv-T^X~5H|pe(u&SFWS+ zNz*vv5gIzlkQfv`sV2U`52Wv6J;`jrn**|1urvORkK)PRxTnwSO!;25*q<-uPF@do zE;v0p^IK%u5u*jWPn|dItlg0(8foJx4?dit%~pGCH|{r_x}{RCD%$MjiL0FZX#vu< z;~VDJcRhwElABj5&o?zGj>mp2>tB0Y(Z6s2xXa`lD6L+lWDslmXkwGRH8kMDq-g#} zLfhr^v&vl?gs_u^jkhv?+1xb$@$U8uH}Kr|_dTZ3#{|Wg<3$LXos(odL@KXZXpI?r zeJ9?v<67=~%H-R(cYGy%ni%DG>7|~wtAqCkFyXSck~ti%Vh`c3-180EGkXWeUZgsd zUy|}^4^|DtwH(#C?{^-H^=q`hZ}W)hycjT8^Mysd`}V%<#-6((JSJ96unq-N{XDjp zkC19xsWYn6^r)Rr)nYxv+7f0>7WuvNpJwAJoNIEo1niTEr|NIzzSElpG-qFq@v+4m zFMM_uO9@;l#w`uUM^9TiW>Z7rm)GBnBOl|TY6GR>B_@D1T;!kXdTjnuxR{{EgPOoC_H+$n7d)je&FYi6CQ7~Xhg z<8}z`SY0F)XFtENwS!~1=gO=)M#>(=G~;SI=`|J1>JDaWv;@C46>+JXeP?8e*pfR^n8z6iZ{}xbUA*=g>s{KoW8^2ul@m1(oSLk literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V1.png b/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V1.png new file mode 100644 index 0000000000000000000000000000000000000000..1b715202af21252ab9a73eecbc12442b7fa16426 GIT binary patch literal 1353 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WChBx2Ka=y`Ugk(fl#zx5D-QA2Swe#_vqgJ#|IA|J9y~$0U&w!_<=*m?tug! z-3N*vI<{ol+NI0a9stQ6zkmPHlI3fcf<)FV0itDVD(hMj(sB~ha{Ys&mx8#h74A4AMxq?Mi0!3A_6KBaxoRyfCBT!T=P+TQ5VRl7btIUL1Qmd9J9^ImNbZcdOi{g>3 zf<@J`6K2UyoSm4SQ&HclaCED}k*%hd4ki{3fdbc=sbwCsf0 z(yNx6SU8$mI0zJ1ORZimy?VK+rDI}xu8E~1!}buA1t zV-nJHfFX0L^2R4160_W3oCO|{#S9F5he4R}c>anMprB-lYeY$K zep*R+Vo@qXKw@TIiJqTph=Qq}p`M{(7l+t6prReA5uRzDo>~kXKn^Q|6eBAGBar0< z#L`eUC<$mVGK0mLfNVoXCI&$u9R0w;}F>4x-4FXI+lbFCNgDfq8ET}F+0|SsOo9UI}P5njIKy{6tE{-7< z{-GCht7jRAusvvra#Ap7rtXTm-FP@}bsqDUwNd~7``=W3uq9CkM_G{x{StvD#NASFyVD7Sea?ff1aUX*ShDuP8SUV z7KZFQ748)(?EIEPLFaY&)F@k4x1|%$U+-$kx+$>X?lXtT^f>~DG*jnrp3~T@6Wvr| zb0g25aj&C=T1)aWjpNG%_T0?6J)NClYf-bXA4hl>r;ldXvJ$)aa;tT-Q&&8ZRQnX+ zwX$q``Qu&jdySG($rGccd)0D1@oGyWDyIys>F5DNh=+^1i z+4}-qP5-kn?GIQh+3mV=Ta41^-6gjUUO%oI4DpTBzQ)_XZ?N^Rt}-P)%7T`+ip z%D3vzxm%;wo6r7QCBxs|_xQ$T;Tf!b$;TV@Z`WPfy8E%kv`yLUmz8I*zrU3EJ5fiu zp8ZY4v2C`tjd$DK5+}LTiL@n7Idk|x|E9M*XN2r;#RiI;S)3yFqximk!~gk$tMk@v Rnd}Ton4YeFF6*2UngCMlOPc@y literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V2.png b/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V2.png new file mode 100644 index 0000000000000000000000000000000000000000..9d4d25f7f167af92d7be62b4ffe6ea4012f4616f GIT binary patch literal 1370 zcmcIieNfVO82=(Fk|tPP1>yzQX=&!6K;d~g@Gzqk6wxzLgF8h%Wp=1ba3`8Lr^~mj zY^Y(rKw+qvSXwK>fMsd7-t2Odo1S`IYqqWBJZJ3tv-Z#a-*eB)=lMJ@-}~M@HSxTd zjT?v?000}ghd2Tl$%uKNq33kh+F(Ex3Sy!_-z4c3OkA_saclstHhBImaf3C__*h{y zKQWbusE8+kn#vcXI%Z~P9CLTaY_M0--B5K`6lH3$G+!c>@e@ToNLp2*Qj4=?VyVnWuEfigeoY!ulSU%V$IDCcg-TLm zYe`uZsZm2T=qT19iq!<1-zrpU$~}{hywtBzU-SW4{G`}XP?;cQ8^j2_OOii|Q) z_RP%8IcDY*s;ZJQwR%&K814t)-ODsB3`dIuyhP{!B!FLq!of$d#}6GX1i*U?F_gXF zQYIt`MbRlmi3QT46B2nAh?nGkUKGNM7Lpkup&_B6wH{k1VbhJQ6t0xZ4FVo8aRWq@ z8$dyH7@F@VjGV%56ndR^fjAW90(@Xhg_c|Gy3QjXMnnt^ng2Kb_f-JkIW=N{E9{|$ z(Wt17k9(0Bw~mjy&OwZb3*6+g-YE!YLiA}WluAXa?u@5}gAMWs*jLNtu!T&1(htsK z8rF8q+lVj@U*D!YHqt)6DbM;aD;ZsF+WV7xPS%Rk>0esY{j1`@Tl8||1so=&e<}}9 zsP*LXj`(sw902wE9$hm{7{yz>&@N-;c#^NpHiiZrxti!+;c&3fApC4g-r3Q*i&xS3 ztObXsNt|qSF2Qs#?o@KV-ErD5W*vZ|^j}yqgxaY4M%=td`*q_hmj$hsD zT5w`?^0$ccgC;9>mpX64RktNiG@|7x4?KE6ds^$Y*|f)4d|jnnP-q<$Nei6&>48%1 zktNIW9j{@ECC8hX$S<7rp$E zpuLcBO1W)`5O%z<`FiFr@84VgSiAYg1HAVCeTRAU2|;n&`6dL-&Pg^!k}7JZ+hcDp z-%4=ryqxsaHn>45L zOHw}V;ewI4n4>!V{no>A0nLsNTCdoybAdy3Us%=KZ|=%&?!68}Zg5*LkYgK2%q{r$bqm;~?isbXCz)iZsHYhe{v-dK8P z=XMGmSbYQ)=QuOHv6ExH>&~n>Ov)a`wBqWz=yjFM+D>MBj0C^A4AMxq?Mi0!3A_6KBaxoRyfCBT!T=P+TQ5VRl7btIUL1Qmd9J9^ImNbZcdOi{g>3 zf<@J`6K2UyoSm4SQ&HclaCED}k*%hd4ki{3fdbc=sbwCsf0 z(yNx6SU8$mI0zJ1ORZimy?VK+rDI}xu8F1Ne}>&>7^dC3_xS$3$CdT1w;7gH)V0iH zC`d@p0ftPd*7Zan60_W3oCO|{#S9F5he4R}c>anMprB-lYeY$K zep*R+Vo@qXKw@TIiJqTph=Qq}p`M{(7l+t6prReA5uRzDo>~kXKn^Q|6eBAGBar0< z#L`eUC<$mVGK0mLfNVoXCI&$u9R0w;}F>4x-4FXI+lbFCNgDfq8ET}F+0|SsOo9UI}P5njIKy{6tE{-7< z{-GCht7jRAusvvra#Ap7rtXTm-FP@}bsqDUwNd~7``=W3uq9CkM_G{x{StvD#NASFyVD7Sea?ff1aUX*ShDuP8SUV z7KZFQ748)(?EIEPLFaY&)F@k4x1|%$U+-$kx+$>X?lXtT^f>~DG*jnrp3~T@6Wvr| zb0g25aj&C=T1)aWjpNG%_T0?6J)NClYf-bXA4hl>r;ldXvJ$)aa;tT-Q&&8ZRQnX+ zwX$q``Qu&jdySG($rGccd)0D1@oGyWDyIys>F5DNh=+^1i z+4}-qP5-kn?GIQh+3mV=Ta41^-6gjUUO%oI4DpTBzQ)_XZ?N^Rt}-P)%7T`+ip z%D3vzxm%;wo6r7QCBxs|_xQ$T;Tf!b$;TV@Z`WPfy8E%kv`yLUmz8I*zrU3EJ5fiu zp8ZY4v2C`tjd$DK5+}LTiL@n7Idk|x|E9M*XN2r;#RiI;S)3yFqximk!~gk$tMk@v Rnd}Ton4YeFF6*2UngG5NO7Q>y literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iv_V2.png b/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iv_V2.png new file mode 100644 index 0000000000000000000000000000000000000000..e153c29d454d28f011aba7c5d91cdb556c548837 GIT binary patch literal 1370 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WChA`2l#}z`Ugk(fl#zx5D-QA2Swe#_vqgJ#|IA|J9y~$0U&w!_<=*m?tug! z-3N*vI<{ol+NI0a9stQ6zkmPHlI3fcf<)FV0itDVD(hMj(sB~ha{Ys&mx8#h74A4AMxq?Mi0!3A_6KBaxoRyfCBT!T=P+TQ5VRl7btIUL1Qmd9J9^ImNbnAbH-Iet% zibu8z7FEkmm?b-Lc4B(Y4TgCY^{om=w<;XjYHI0VV&NbN)H`vusfDA7rNbtMCaG1+ z6^?H44~mwZFk5=nauW+jQws-y;%ceY%cWN@H??$3OwTp3bi8iov`SI?-o3~7?>(-p zZ>^|nX%W&&NY4R=4?p9CIv^EU666>BpGJVezm<_6XbES5M`SSr1K(i~W;~w1A_XWY zS>hT|5}cn_Ql40p$`Fv4nOCCc=Nh75s%NNYXxPOeb`Gd$M{0y;ny0500|$`9${@wa z%D@O@c>%FBlnqJ>8jQ?faV8+!kdcW&5J*P>ab`OUSRCYG5J&)GkbW4AZYu)=FgZ-E0mQ6nKsE?40Zn28s|>QV0J5OE3=IrGvTUYTiZ}HaSp(H| zdAc};Sonut{2xEdL4y5*wULYe@;O!UJLjGf%07A~HH^7^tL|O4jMSh1|EpGaE_qjJ z`+@np-8_Ewi1pXA859lr6dD!;G%!eQVCXYDzj^D~wFx3T%uI(H1Z1R-9Xrg-upo1m z>*|ngIw7k<>_xJt?&@8*ky#+@EdN$FW9G26p*m)#TBLpoY={$gxSBFY;E-nO9L{qZ zlP?7-Nb_HN-G3lQu#fZMjLcITby@Pucb)ra-Qc$MD5rW$_F0x?BH7V9=lfOJo-}m!uQ%u~vuyxAx>6PBTZMo~8|88D? z+nj+rx9C0NvQ_m}rc%A@j1vwN%{A1VdMf(X9m)Cg;(DsY-%A=56i?os^5)yOL!bZd ze#^n|TJ@9sfnAp+EBF0>tHeTPwdsGwG&GM<* z8&}77e8rZN8`#Zq=T~3n=Wmvm{E<0j;<9$-E&JZa{I9B!$uU0jY^~JujQU9@#Jqgf zCg1-zpT}qMG|er&Wk*{icB`jPUcEnBfZex6^4KKa}Cbbmz%hXU292L zWqNFm(XohVwZ9r?9R(C!Ru*vtb6!1it);t2_3XENV-{7Ht(T7;3Jkf?!>OqCw1WT9 zjzIUbM;Bd*(F=V1^5@QoRg2v=uqOtrGGB9a(WMBziC@2@YztsD|Cq553(X=@I;V?cYr?>%!KrUwzd=k}vjp>9T4A?%3;|ha5_qcf9{{+;dUL zea_|92K@J@+~!^6a`YgZMW>fc`Y!g_{Jo1!dYdh}7Fo=7PZW55+H%HH$2~hASzopr0L7S1^#A|> literal 0 HcmV?d00001 From f3ff2d57b55e0d5ac545046e034e8ceddd39c0ab Mon Sep 17 00:00:00 2001 From: NuanKi Date: Thu, 5 Mar 2026 03:26:50 -0600 Subject: [PATCH 3/7] Update NBT matching terminology in AutoCrafter --- .../com/ae2powertools/features/crafter/GuiAutoCrafter.java | 4 +++- src/main/resources/assets/ae2powertools/lang/en_us.lang | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java b/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java index ba69c37..7300bff 100644 --- a/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java +++ b/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java @@ -530,7 +530,9 @@ private void drawAE2ButtonTooltips(int mouseX, int mouseY) { tooltip.add(TextFormatting.DARK_GRAY + I18n.format("gui.ae2powertools.crafter.speed.explanation")); } else if (ignoreNbtBtnHovered) { boolean on = isIgnoreNbtEnabled(getCurrentPage()); - tooltip.add(I18n.format("gui.ae2powertools.crafter.ignore_nbt") + ": " + (on ? "ON" : "OFF")); + tooltip.add(I18n.format(on + ? "gui.ae2powertools.crafter.nbt_matching.ignored" + : "gui.ae2powertools.crafter.nbt_matching.strict")); tooltip.add(TextFormatting.DARK_GRAY + I18n.format("gui.ae2powertools.crafter.ignore_nbt.desc")); } diff --git a/src/main/resources/assets/ae2powertools/lang/en_us.lang b/src/main/resources/assets/ae2powertools/lang/en_us.lang index 2fb32ec..683cd6c 100644 --- a/src/main/resources/assets/ae2powertools/lang/en_us.lang +++ b/src/main/resources/assets/ae2powertools/lang/en_us.lang @@ -219,8 +219,9 @@ gui.ae2powertools.crafter.page=Page %d/%d gui.ae2powertools.crafter.empty_slot=Slot %d - Empty gui.ae2powertools.crafter.click_to_view=Left-click to view gui.ae2powertools.crafter.right_click_toggle=Right-click to toggle -gui.ae2powertools.crafter.ignore_nbt=Ignore input item's NBT -gui.ae2powertools.crafter.ignore_nbt.desc=Match items by item+meta only (ignore nbt data) +gui.ae2powertools.crafter.nbt_matching.strict=NBT Matching: Strict +gui.ae2powertools.crafter.nbt_matching.ignored=NBT Matching: Ignored +gui.ae2powertools.crafter.ignore_nbt.desc=Ignores NBT when matching ME inputs (item + metadata only). # GUI - Batch From 4a9af0d0c864a930579de4e35da1f8f3a2453845 Mon Sep 17 00:00:00 2001 From: NuanKi Date: Thu, 5 Mar 2026 04:03:05 -0600 Subject: [PATCH 4/7] Readded comment --- .../com/ae2powertools/features/crafter/TileAutoCrafter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java b/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java index 50ab957..d65f71e 100644 --- a/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java +++ b/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java @@ -902,7 +902,7 @@ private int getLeftoverDurability(CrafterEntry entry, int recipeSlotIndex, IAEIt * @param crafts Number of individual crafts to perform */ private boolean extractInputs(CrafterEntry entry, CrafterRecipeInfo info, int crafts) { - + // First simulate all extractions List toExtract = new ArrayList<>(); for (CrafterRecipeInfo.IngredientInfo ingredient : info.getConsumedItems()) { From 4ef9c7a73696b31edf763b74e7fa9da1afc9948b Mon Sep 17 00:00:00 2001 From: NuanKi Date: Thu, 5 Mar 2026 05:15:54 -0600 Subject: [PATCH 5/7] Add localized strict/ignored NBT matching tooltips --- .../ae2powertools/features/crafter/GuiAutoCrafter.java | 8 +++++--- src/main/resources/assets/ae2powertools/lang/en_us.lang | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java b/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java index 7300bff..60dd16c 100644 --- a/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java +++ b/src/main/java/com/ae2powertools/features/crafter/GuiAutoCrafter.java @@ -529,11 +529,13 @@ private void drawAE2ButtonTooltips(int mouseX, int mouseY) { tooltip.add(""); tooltip.add(TextFormatting.DARK_GRAY + I18n.format("gui.ae2powertools.crafter.speed.explanation")); } else if (ignoreNbtBtnHovered) { - boolean on = isIgnoreNbtEnabled(getCurrentPage()); - tooltip.add(I18n.format(on + boolean ignored = isIgnoreNbtEnabled(getCurrentPage()); + tooltip.add(I18n.format(ignored ? "gui.ae2powertools.crafter.nbt_matching.ignored" : "gui.ae2powertools.crafter.nbt_matching.strict")); - tooltip.add(TextFormatting.DARK_GRAY + I18n.format("gui.ae2powertools.crafter.ignore_nbt.desc")); + tooltip.add(TextFormatting.DARK_GRAY + I18n.format(ignored + ? "gui.ae2powertools.crafter.nbt_matching.ignored.desc" + : "gui.ae2powertools.crafter.nbt_matching.strict.desc")); } if (!tooltip.isEmpty()) { diff --git a/src/main/resources/assets/ae2powertools/lang/en_us.lang b/src/main/resources/assets/ae2powertools/lang/en_us.lang index 683cd6c..b05c484 100644 --- a/src/main/resources/assets/ae2powertools/lang/en_us.lang +++ b/src/main/resources/assets/ae2powertools/lang/en_us.lang @@ -220,8 +220,9 @@ gui.ae2powertools.crafter.empty_slot=Slot %d - Empty gui.ae2powertools.crafter.click_to_view=Left-click to view gui.ae2powertools.crafter.right_click_toggle=Right-click to toggle gui.ae2powertools.crafter.nbt_matching.strict=NBT Matching: Strict +gui.ae2powertools.crafter.nbt_matching.strict.desc=Match items including NBT (exact match) gui.ae2powertools.crafter.nbt_matching.ignored=NBT Matching: Ignored -gui.ae2powertools.crafter.ignore_nbt.desc=Ignores NBT when matching ME inputs (item + metadata only). +gui.ae2powertools.crafter.nbt_matching.ignored.desc=Match items by item+meta only (ignore NBT data) # GUI - Batch From 7adda432215a70f68e36285d62756cc0281e7b1f Mon Sep 17 00:00:00 2001 From: NuanKi Date: Thu, 5 Mar 2026 16:31:44 -0600 Subject: [PATCH 6/7] Rename textures for crafter speed upgrades to use modern naming convention --- ...V1.png => modern_crafter_speed_upgrade_i_V1.png} | Bin ...V2.png => modern_crafter_speed_upgrade_i_V2.png} | Bin ...1.png => modern_crafter_speed_upgrade_ii_V1.png} | Bin ...2.png => modern_crafter_speed_upgrade_ii_V2.png} | Bin ....png => modern_crafter_speed_upgrade_iii_V1.png} | Bin ....png => modern_crafter_speed_upgrade_iii_V2.png} | Bin ...1.png => modern_crafter_speed_upgrade_iv_V1.png} | Bin ...2.png => modern_crafter_speed_upgrade_iv_V2.png} | Bin 8 files changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/assets/ae2powertools/textures/items/{crafter_speed_upgrade_i_V1.png => modern_crafter_speed_upgrade_i_V1.png} (100%) rename src/main/resources/assets/ae2powertools/textures/items/{crafter_speed_upgrade_i_V2.png => modern_crafter_speed_upgrade_i_V2.png} (100%) rename src/main/resources/assets/ae2powertools/textures/items/{crafter_speed_upgrade_ii_V1.png => modern_crafter_speed_upgrade_ii_V1.png} (100%) rename src/main/resources/assets/ae2powertools/textures/items/{crafter_speed_upgrade_ii_V2.png => modern_crafter_speed_upgrade_ii_V2.png} (100%) rename src/main/resources/assets/ae2powertools/textures/items/{crafter_speed_upgrade_iii_V1.png => modern_crafter_speed_upgrade_iii_V1.png} (100%) rename src/main/resources/assets/ae2powertools/textures/items/{crafter_speed_upgrade_iii_V2.png => modern_crafter_speed_upgrade_iii_V2.png} (100%) rename src/main/resources/assets/ae2powertools/textures/items/{crafter_speed_upgrade_iv_V1.png => modern_crafter_speed_upgrade_iv_V1.png} (100%) rename src/main/resources/assets/ae2powertools/textures/items/{crafter_speed_upgrade_iv_V2.png => modern_crafter_speed_upgrade_iv_V2.png} (100%) diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_i_V1.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V1.png similarity index 100% rename from src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_i_V1.png rename to src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V1.png diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_i_V2.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V2.png similarity index 100% rename from src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_i_V2.png rename to src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V2.png diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V1.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V1.png similarity index 100% rename from src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V1.png rename to src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V1.png diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V2.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V2.png similarity index 100% rename from src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_ii_V2.png rename to src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V2.png diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V1.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V1.png similarity index 100% rename from src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V1.png rename to src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V1.png diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V2.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V2.png similarity index 100% rename from src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iii_V2.png rename to src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V2.png diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iv_V1.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V1.png similarity index 100% rename from src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iv_V1.png rename to src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V1.png diff --git a/src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iv_V2.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V2.png similarity index 100% rename from src/main/resources/assets/ae2powertools/textures/items/crafter_speed_upgrade_iv_V2.png rename to src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V2.png From fc1955647696c63ce79a4158d2432ebe1e2cf04d Mon Sep 17 00:00:00 2001 From: NuanKi Date: Thu, 5 Mar 2026 17:51:53 -0600 Subject: [PATCH 7/7] Introduce per-entry ignore-NBT support in TileAutoCrafter and update the crafting allocation/extraction pipeline to handle mixed strict + ignore-NBT users. Highlights: - Add per-entry ignore-NBT toggle and propagate matching mode through candidate collection, validation, allocation, extraction, and pool reservation. - Extend ItemStackKey with matching-mode awareness (strict vs ignore-NBT) so resource accounting remains isolated and deterministic. - Add cross-reservation logic when strict entries reserve items, preventing ignore-NBT buckets from over-reporting remaining availability. - Add ignore-NBT network extraction path that aggregates compatible variants (same item+meta) and extracts safely via snapshot iteration. - Improve shared-pool initialization/allocation internals to reduce repeated ingredient lookups and improve behavior under contention. Docs: - Update/expand JavaDocs for 8 touched/added methods to describe matching semantics, allocation steps, and extraction behavior. --- .../features/crafter/TileAutoCrafter.java | 490 ++++++++++++------ 1 file changed, 343 insertions(+), 147 deletions(-) diff --git a/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java b/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java index d65f71e..c576497 100644 --- a/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java +++ b/src/main/java/com/ae2powertools/features/crafter/TileAutoCrafter.java @@ -490,10 +490,16 @@ private static class CraftCandidate { /** * Pre-initialize the shared resource pool with all items needed by all candidates. - * Fetches the storage list ONCE and lookups are done against that cached list. + * Fetches the AE2 storage list once and reuses it for all lookups. * - * @param candidates List of all candidates wanting to craft - * @return Map of ItemStackKey to available quantity in network + *

This supports the crafter's two matching modes:

+ *
    + *
  • Strict-NBT: uses {@code storageList.findPrecise(...)} for lookups.
  • + *
  • Ignore-NBT: builds a one-time {@code (Item, metadata)} index and uses it for lookups.
  • + *
+ * + * @param candidates list of all candidates wanting to craft + * @return map of {@link ItemStackKey} to the currently available quantity in the network for that key */ private Map initializeSharedPool(List candidates) { Map pool = new HashMap<>(); @@ -502,27 +508,39 @@ private Map initializeSharedPool(List candid IItemList storageList = getNetworkStorageList(); if (storageList == null) return pool; - // Collect all unique items needed by all candidates + // Built lazily: only constructed if we encounter at least one ignore-NBT candidate. + Map ignoreNbtIndex = null; + + // Walk all candidates and all consumed ingredients. for (CraftCandidate candidate : candidates) { boolean ignoreNbt = candidate.entry.isIgnoreNbt(); + // Build index once for ignore-NBT candidates. + if (ignoreNbt && ignoreNbtIndex == null) { + ignoreNbtIndex = buildIgnoreNbtAvailabilityIndex(storageList); + } + for (CrafterRecipeInfo.IngredientInfo ingredient : candidate.info.getConsumedItems()) { IAEItemStack item = ingredient.getItem(); if (item == null) continue; + // Key includes matching mode (strict vs ignore-NBT). ItemStackKey key = new ItemStackKey(item, ignoreNbt); - if (!pool.containsKey(key)) { - - long available; - if (ignoreNbt) { - available = getNetworkQuantityIgnoringNbt(item, storageList); - } else { - IAEItemStack found = storageList.findPrecise(item); - available = found != null ? found.getStackSize() : 0L; - } - pool.put(key, available); + // If already cached, skip repeated lookup. + if (pool.containsKey(key)) continue; + + long available; + if (ignoreNbt) { + // O(1) indexed lookup by item+meta. + available = getNetworkQuantityIgnoringNbtIndexed(item, ignoreNbtIndex); + } else { + // Exact AE2 lookup (strict type/NBT behavior). + IAEItemStack found = storageList.findPrecise(item); + available = found != null ? found.getStackSize() : 0L; } + + pool.put(key, available); } } @@ -530,29 +548,82 @@ private Map initializeSharedPool(List candid } /** - * Allocate resources fairly among competing entries. - * - * Fair allocation algorithm: - * 1. For each contested resource, divide available items equally among candidates that need it - * 2. Each candidate converts their item share to crafts (share รท items_per_craft) - * 3. A candidate's final crafts is the minimum across all its required resources - * - * This ensures that if two entries fight for the same resource, each gets an equal - * share of items. - * - * @param candidates List of entries wanting to craft (with their requested batch sizes) - * @param pool Shared resource pool (pre-initialized via initializeSharedPool) - * @param effectiveMaxBatchSize The pre-calculated effective max batch size - * @return List of simulations with fairly allocated craft counts + * Builds an availability index for ignore-NBT matching from an AE2 storage list. + * + *

Each entry in the storage list is converted to an {@link ItemStack} and grouped by + * {@code (Item, metadata)}. The index value is the sum of {@link IAEItemStack#getStackSize()} + * for all stored stacks in that bucket.

+ * + * @param storageList AE2 storage list to aggregate; may be {@code null} + * @return a map of {@code (Item, metadata) -> total available quantity}. Returns an empty map if + * {@code storageList} is {@code null}. + */ + private Map buildIgnoreNbtAvailabilityIndex(@Nullable IItemList storageList) { + Map index = new HashMap<>(); + if (storageList == null) return index; + + for (IAEItemStack stored : storageList) { + if (stored == null || stored.getStackSize() <= 0) continue; + + ItemStack storedStack = stored.createItemStack(); + if (storedStack.isEmpty()) continue; + + // Group all NBT variants under same item+meta bucket. + ItemMetaKey key = new ItemMetaKey(storedStack.getItem(), storedStack.getMetadata()); + index.merge(key, stored.getStackSize(), Long::sum); + } + + return index; + } + + /** + * Allocates shared network inputs across multiple crafting candidates and returns the resulting craft counts. + * + *

This method operates on the pre-initialized availability {@code pool} (see {@link #initializeSharedPool(List)}). + * It does not extract items; it only decides how many crafts each candidate is allowed to attempt and then deducts + * the corresponding reservations from {@code pool} via {@link #deductFromPool(CrafterRecipeInfo, Map, int, boolean)}.

+ * + *

Algorithm

+ *
    + *
  1. For every consumed ingredient of every candidate, compute that candidate's demand for + * {@code effectiveMaxBatchSize} crafts and group candidates by {@link ItemStackKey}.
  2. + *
  3. For each resource key: + *
      + *
    • If {@code totalDemand <= available}, allocate each candidate exactly its computed demand.
    • + *
    • If {@code totalDemand > available}, allocate {@code floor(available / userCount)} items to each user.
    • + *
    + *
  4. + *
  5. For each candidate, convert allocated item counts into a craft count per ingredient using + * {@link #calculateCraftsFromItems(CrafterRecipeInfo.IngredientInfo, long)} and take the minimum across + * all consumed ingredients (the bottleneck).
  6. + *
  7. Store batch tracking telemetry on the entry, then: + *
      + *
    • If {@code finalCrafts > 0}, reserve items by deducting from {@code pool} and return a {@link CraftSimulation}.
    • + *
    • If {@code finalCrafts == 0}, record error details and set the entry state to {@link CrafterState#MISSING_INPUT}.
    • + *
    + *
  8. + *
+ * + *

The allocation step uses equal per-user sharing for contested resources and does not redistribute any remainder + * from integer division.

+ * + * @param candidates entries that are eligible to craft this tick (already filtered for pattern/state/recipe validity) + * @param pool shared availability pool, mutated by this method to reflect reservations + * @param effectiveMaxBatchSize maximum crafts to attempt per candidate before allocation (config * user batch * upgrades) + * @return simulations containing the final allocated craft count for each candidate with {@code finalCrafts > 0} */ private List allocateResourcesFairly(List candidates, Map pool, int effectiveMaxBatchSize) { - // Step 1: For each resource, count how many candidates need it and check if contested - // A resource is contested if total demand exceeds supply + // Step 1: For each resource, count how many candidates need it and compute total demand + // A resource is contested if total demand exceeds supply. Map> resourceUsers = new HashMap<>(); Map totalDemand = new HashMap<>(); + // Resource -> (candidate -> that candidate's demand) + // This avoids recomputing demand later with repeated ingredient searches. + Map> demandPerCandidate = new HashMap<>(); + for (CraftCandidate candidate : candidates) { boolean ignoreNbt = candidate.entry.isIgnoreNbt(); @@ -562,15 +633,18 @@ private List allocateResourcesFairly(List candi if (item == null) continue; ItemStackKey key = new ItemStackKey(item, ignoreNbt); + long needed = calculateItemsNeededForCrafts(ingredient, effectiveMaxBatchSize); resourceUsers.computeIfAbsent(key, k -> new ArrayList<>()).add(candidate); - long needed = calculateItemsNeededForCrafts(ingredient, effectiveMaxBatchSize); totalDemand.merge(key, needed, Long::sum); + demandPerCandidate.computeIfAbsent(key, k -> new HashMap<>()).put(candidate, needed); } } - // Step 2: For each contested resource, calculate equal item share per candidate - // Key: resource -> candidate index -> allocated items + // Step 2: For each resource, allocate items to candidates + // - If not contested (total demand <= available), allocate each candidate exactly what they need + // - If contested (total demand > available), divide items equally among all users + // Key: resource -> candidate -> allocated items Map> itemAllocations = new HashMap<>(); for (Map.Entry> entry : resourceUsers.entrySet()) { @@ -583,16 +657,14 @@ private List allocateResourcesFairly(List candi if (demand <= available) { // Not contested - each candidate gets what they need - for (CraftCandidate user : users) { - CrafterRecipeInfo.IngredientInfo ing = findIngredient(user.info, key, user.entry.isIgnoreNbt()); - if (ing != null) { - allocations.put(user, calculateItemsNeededForCrafts(ing, effectiveMaxBatchSize)); - } - } + Map perCandidateDemand = demandPerCandidate.get(key); + if (perCandidateDemand != null) allocations.putAll(perCandidateDemand); } else { // Contested - divide equally among users - long sharePerUser = available / users.size(); - for (CraftCandidate user : users) allocations.put(user, sharePerUser); + long sharePerUser = users.isEmpty() ? 0L : (available / users.size()); + for (CraftCandidate user : users) { + allocations.put(user, sharePerUser); + } } itemAllocations.put(key, allocations); @@ -616,8 +688,7 @@ private List allocateResourcesFairly(List candi Map allocations = itemAllocations.get(key); if (allocations == null) continue; - Long allocated = allocations.get(candidate); - if (allocated == null) allocated = 0L; + long allocated = allocations.getOrDefault(candidate, 0L); // Convert item allocation to crafts int craftsFromAllocation = calculateCraftsFromItems(ingredient, allocated); @@ -625,20 +696,20 @@ private List allocateResourcesFairly(List candi if (craftsFromAllocation < finalCrafts) { long needed = calculateItemsNeededForCrafts(ingredient, effectiveMaxBatchSize); if (allocated < needed) { - limitingFactors.add(I18n.translateToLocalFormatted("gui.ae2powertools.crafter.error.limited_by", + limitingFactors.add(I18n.translateToLocalFormatted( + "gui.ae2powertools.crafter.error.limited_by", item.createItemStack().getDisplayName(), allocated, needed)); } } finalCrafts = Math.min(finalCrafts, craftsFromAllocation); } - // Update batch size tracking for occupancy calculation + // Keep telemetry data updated (used by occupancy/metrics UI). candidate.entry.setBatchSizeTracking(effectiveMaxBatchSize, finalCrafts); if (finalCrafts > 0) { - + // Deduct from shared pool deductFromPool(candidate.info, pool, finalCrafts, candidate.entry.isIgnoreNbt()); - simulations.add(new CraftSimulation(candidate.entry, candidate.info, finalCrafts)); // If crafts were reduced, add info about why @@ -648,7 +719,12 @@ private List allocateResourcesFairly(List candi } else { // No crafts possible - add all limiting factors as errors for (String factor : limitingFactors) candidate.entry.addErrorDetail(factor); - if (limitingFactors.isEmpty()) candidate.entry.addErrorDetail(I18n.translateToLocalFormatted("gui.ae2powertools.crafter.error.no_items_in_network")); + + if (limitingFactors.isEmpty()) { + candidate.entry.addErrorDetail(I18n.translateToLocalFormatted( + "gui.ae2powertools.crafter.error.no_items_in_network")); + } + updateEntryState(candidate.entry, CrafterState.MISSING_INPUT); // Record as error (no crafts possible) candidate.entry.recordMetrics(true, 0, 0); @@ -738,10 +814,27 @@ private void updateEntryState(CrafterEntry entry, CrafterState newState) { } /** - * Deduct items from the shared pool after allocating crafts to an entry. - * - * For most items: deduct 1 item per craft. - * For DURABILITY items: deduct based on how many items are actually needed. + * Deducts reserved inputs from the shared availability pool after an entry has been allocated + * a number of crafts. + * + *

The deduction is performed per consumed ingredient:

+ *
    + *
  • Non-durability ingredients: deducts {@code crafts} items (1 item per craft).
  • + *
  • DURABILITY ingredients: deducts the number of items required to supply + * {@code crafts * durabilityPerCraft} durability, computed using the template item's + * {@code maxDamage} with ceiling division.
  • + *
+ * + *

Cross-reservation rule: when {@code ignoreNbt} is {@code false} (strict matching), + * this method also deducts the same amount from the corresponding ignore-NBT bucket + * ({@code new ItemStackKey(item, true)}). This keeps the pooled model consistent by ensuring + * that items reserved for strict entries are not simultaneously considered available to + * ignore-NBT entries.

+ * + * @param info recipe info whose consumed ingredients will be deducted + * @param pool shared availability pool to mutate + * @param crafts number of crafts allocated to the entry + * @param ignoreNbt whether the allocation was for an ignore-NBT entry */ private void deductFromPool(CrafterRecipeInfo info, Map pool, int crafts, boolean ignoreNbt) { for (CrafterRecipeInfo.IngredientInfo ingredient : info.getConsumedItems()) { @@ -772,11 +865,34 @@ private void deductFromPool(CrafterRecipeInfo info, Map pool } pool.put(key, Math.max(0, current - itemsToDeduct)); + + // If this was a strict-NBT allocation, also reserve those items from the base (ignore-NBT) pool + // so ignore-NBT entries cannot starve strict entries under scarcity. + if (!ignoreNbt) { + ItemStackKey baseKey = new ItemStackKey(item, true); + long baseCurrent = pool.getOrDefault(baseKey, 0L); + pool.put(baseKey, Math.max(0, baseCurrent - itemsToDeduct)); + } } } /** - * Key for ItemStack identity in maps, using including NBT matching. + * Map key that defines how an {@link IAEItemStack} is grouped for shared-pool lookups. + * + *

The key always distinguishes by {@code ignoreNbt} first. Two keys are never equal if their + * {@code ignoreNbt} flags differ.

+ * + *

Equality then always requires the same {@code Item} and metadata (as obtained from + * {@link IAEItemStack#createItemStack()}).

+ * + *
    + *
  • If {@code ignoreNbt} is {@code true}, equality stops at {@code (Item, metadata)}.
  • + *
  • If {@code ignoreNbt} is {@code false}, equality additionally requires + * {@link IAEItemStack#isSameType(IAEItemStack)} to be {@code true}.
  • + *
+ * + *

This allows the shared pool to keep separate buckets for strict matching vs ignore-NBT matching, + * even when the base {@code Item + metadata} are the same.

*/ private static class ItemStackKey { private final IAEItemStack item; @@ -808,8 +924,8 @@ public boolean equals(Object obj) { @Override public int hashCode() { ItemStack stack = item.createItemStack(); - int h = stack.getItem().hashCode() ^ (stack.getMetadata() * 31); - + int h = stack.getItem().hashCode(); + h = 31 * h + stack.getMetadata(); h = 31 * h + (ignoreNbt ? 1 : 0); if (!ignoreNbt && stack.hasTagCompound()) { @@ -820,6 +936,40 @@ public int hashCode() { } } + /** + * Minimal map key for ignore-NBT indexing. + * + *

Represents an item solely by {@code (Item, metadata)} so that all NBT variants of the same base + * item are aggregated into one bucket.

+ * + *

Equality uses reference equality for {@link net.minecraft.item.Item} and value equality for + * the metadata.

+ */ + private static final class ItemMetaKey { + private final net.minecraft.item.Item item; + private final int meta; + + ItemMetaKey(net.minecraft.item.Item item, int meta) { + this.item = item; + this.meta = meta; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ItemMetaKey)) return false; + ItemMetaKey other = (ItemMetaKey) obj; + return this.item == other.item && this.meta == other.meta; + } + + @Override + public int hashCode() { + int h = item != null ? item.hashCode() : 0; + h = 31 * h + meta; + return h; + } + } + /** * Check if the entry has sufficient catalyst items in its internal inventory. * @@ -1219,6 +1369,144 @@ private long getNetworkQuantityIgnoringNbt(IAEItemStack template, @Nullable IIte return total; } + /** + * Extracts items from the AE2 network while ignoring NBT, matching only by {@code Item + metadata}. + * + *

The request is normalized via {@link IAEItemStack#createItemStack()}. Any stored stack whose + * {@code Item} and metadata match the normalized request is treated as an acceptable source, + * regardless of NBT differences.

+ * + *

Behavior

+ *
    + *
  • SIMULATE: iterates {@link IMEMonitor#getStorageList()} and sums {@link IAEItemStack#getStackSize()} + * across all matching stacks until the requested amount is reached or the list is exhausted.
  • + *
  • MODULATE: first collects copies of matching stacks into a snapshot + * (bounded once enough total quantity is represented), then optionally sorts the snapshot by size + * (largest-first), and finally extracts from the monitor until the request is satisfied or the + * snapshot is exhausted.
  • + *
+ * + *

The MODULATE path performs extraction based on a pre-extraction snapshot. This keeps iteration and + * extraction separate and ensures the set and order of candidates are determined before any items are removed.

+ * + * @param request requested stack; only {@code Item} and metadata are used (NBT is ignored) + * @param mode {@link Actionable#SIMULATE} to check availability, {@link Actionable#MODULATE} to extract + * @return

{@code true} if the requested amount is available (SIMULATE) or was fully extracted (MODULATE);

+ *

{@code false} if the network/storage list cannot be accessed or extraction did not obtain the full amount

+ */ + private boolean extractFromNetworkIgnoringNbt(IAEItemStack request, Actionable mode) { + if (request == null || request.getStackSize() <= 0) return true; + + try { + // Step 1: Resolve network inventory + IGrid grid = gridProxy.getGrid(); + if (grid == null) return false; + + IStorageGrid storage = grid.getCache(IStorageGrid.class); + if (storage == null) return false; + + IMEMonitor inv = storage.getInventory( + AEApi.instance().storage().getStorageChannel(IItemStorageChannel.class)); + + IItemList list = inv.getStorageList(); + if (list == null) return false; + + // Step 2: Normalize request to the base identity (Item + Metadata) + ItemStack wanted = request.createItemStack(); + if (wanted.isEmpty()) return false; + + net.minecraft.item.Item wantedItem = wanted.getItem(); + int wantedMeta = wanted.getMetadata(); + + // Step 3: SIMULATE path - just count matching availability across all NBT variants + if (mode == Actionable.SIMULATE) { + long available = 0L; + + for (IAEItemStack stored : list) { + if (stored == null || stored.getStackSize() <= 0) continue; + + ItemStack storedStack = stored.createItemStack(); + if (storedStack.isEmpty()) continue; + + if (storedStack.getItem() != wantedItem) continue; + if (storedStack.getMetadata() != wantedMeta) continue; + + available += stored.getStackSize(); + if (available >= request.getStackSize()) return true; + } + + return available >= request.getStackSize(); + } + + // Step 4: MODULATE path - snapshot matching entries before extracting to avoid CME + List snapshot = new ArrayList<>(); + long snapshotCount = 0L; + for (IAEItemStack stored : list) { + if (stored == null || stored.getStackSize() <= 0) continue; + + ItemStack storedStack = stored.createItemStack(); + if (storedStack.isEmpty()) continue; + + if (storedStack.getItem() != wantedItem) continue; + if (storedStack.getMetadata() != wantedMeta) continue; + + snapshot.add(stored.copy()); + snapshotCount += stored.getStackSize(); + if (snapshotCount >= request.getStackSize()) break; + } + + // Step 5: Prefer draining larger stacks first + if (snapshot.size() > 1) { + snapshot.sort((a, b) -> Long.compare(b.getStackSize(), a.getStackSize())); + } + + // Step 6: Extract until satisfied + long remaining = request.getStackSize(); + + for (IAEItemStack stored : snapshot) { + if (stored == null || stored.getStackSize() <= 0) continue; + + long toTake = Math.min(remaining, stored.getStackSize()); + + IAEItemStack takeReq = stored.copy(); + takeReq.setStackSize(toTake); + + IAEItemStack extracted = inv.extractItems(takeReq, Actionable.MODULATE, actionSource); + long got = extracted != null ? extracted.getStackSize() : 0; + + remaining -= got; + if (remaining <= 0) return true; + } + + return remaining <= 0; + } catch (GridAccessException e) { + return false; + } + } + + /** + * Returns the aggregated available quantity for {@code (Item, metadata)} while ignoring NBT, + * using a prebuilt index. + * + *

The lookup key is derived from {@code template.createItemStack()} and uses only + * {@code Item} and metadata.

+ * + * @param template AE item template identifying the {@code Item} and metadata to query (NBT is ignored) + * @param index prebuilt ignore-NBT availability index; may be {@code null} + * @return total available quantity for the matching {@code Item + metadata}, or {@code 0} if + * {@code template} cannot be resolved to a non-empty {@link ItemStack} or {@code index} is {@code null} + */ + private long getNetworkQuantityIgnoringNbtIndexed(IAEItemStack template, @Nullable Map index) { + if (template == null || index == null) return 0L; + + // Step 1: Normalize to base identity (Item + Metadata) + ItemStack wanted = template.createItemStack(); + if (wanted.isEmpty()) return 0L; + + // Step 2: O(1) aggregated lookup + return index.getOrDefault(new ItemMetaKey(wanted.getItem(), wanted.getMetadata()), 0L); + } + // ==================== FAKE PLAYER ==================== @Nullable @@ -2036,96 +2324,4 @@ public void validate() { super.validate(); gridProxy.validate(); } - - /** - * Extracts items from the AE2 network ignoring NBT (matches by item + meta only). - * Uses the network inventory list to pull from any NBT variant of the same base item. - * - * For SIMULATE: returns true if enough items exist. - * For MODULATE: actually extracts items (best-effort). - */ - private boolean extractFromNetworkIgnoringNbt(IAEItemStack request, Actionable mode) { - if (request == null || request.getStackSize() <= 0) return true; - - try { - IGrid grid = gridProxy.getGrid(); - if (grid == null) return false; - - IStorageGrid storage = grid.getCache(IStorageGrid.class); - if (storage == null) return false; - - IMEMonitor inv = storage.getInventory( - AEApi.instance().storage().getStorageChannel(IItemStorageChannel.class)); - - IItemList list = inv.getStorageList(); - if (list == null) return false; - - ItemStack wanted = request.createItemStack(); - if (wanted.isEmpty()) return false; - - net.minecraft.item.Item wantedItem = wanted.getItem(); - int wantedMeta = wanted.getMetadata(); - - // IMPORTANT: - // We must NOT iterate the live AE2 ItemList iterator while extracting, because extraction - // mutates the underlying list and will trigger ConcurrentModificationException. - // - // Strategy: - // - SIMULATE: just count available items (no extraction / no mutation) - // - MODULATE: snapshot matching stacks first, then extract using the snapshot - if (mode == Actionable.SIMULATE) { - long available = 0L; - - for (IAEItemStack stored : list) { - if (stored == null || stored.getStackSize() <= 0) continue; - - ItemStack storedStack = stored.createItemStack(); - if (storedStack.isEmpty()) continue; - - if (storedStack.getItem() != wantedItem) continue; - if (storedStack.getMetadata() != wantedMeta) continue; - - available += stored.getStackSize(); - if (available >= request.getStackSize()) return true; - } - - return available >= request.getStackSize(); - } - - // MODULATE path: snapshot matching entries before extracting to avoid CME - List snapshot = new ArrayList<>(); - for (IAEItemStack stored : list) { - if (stored == null || stored.getStackSize() <= 0) continue; - - ItemStack storedStack = stored.createItemStack(); - if (storedStack.isEmpty()) continue; - - if (storedStack.getItem() != wantedItem) continue; - if (storedStack.getMetadata() != wantedMeta) continue; - - snapshot.add(stored.copy()); - } - - long remaining = request.getStackSize(); - - for (IAEItemStack stored : snapshot) { - if (stored == null || stored.getStackSize() <= 0) continue; - - long toTake = Math.min(remaining, stored.getStackSize()); - - IAEItemStack takeReq = stored.copy(); - takeReq.setStackSize(toTake); - - IAEItemStack extracted = inv.extractItems(takeReq, Actionable.MODULATE, actionSource); - long got = extracted != null ? extracted.getStackSize() : 0; - - remaining -= got; - if (remaining <= 0) return true; - } - - return remaining <= 0; - } catch (GridAccessException e) { - return false; - } - } }