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..60dd16c 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,14 @@ 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 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(ignored + ? "gui.ae2powertools.crafter.nbt_matching.ignored.desc" + : "gui.ae2powertools.crafter.nbt_matching.strict.desc")); } if (!tooltip.isEmpty()) { @@ -1013,6 +1056,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 +1185,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 +1357,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..c576497 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 @@ -487,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. - * - * @param candidates List of all candidates wanting to craft - * @return Map of ItemStackKey to available quantity in network + * Fetches the AE2 storage list once and reuses it for all lookups. + * + *

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<>(); @@ -499,17 +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; - ItemStackKey key = new ItemStackKey(item); - if (!pool.containsKey(key)) { + // Key includes matching mode (strict vs ignore-NBT). + ItemStackKey key = new ItemStackKey(item, ignoreNbt); + + // 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); - pool.put(key, found != null ? found.getStackSize() : 0L); + available = found != null ? found.getStackSize() : 0L; } + + pool.put(key, available); } } @@ -517,43 +548,103 @@ 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(); + for (CrafterRecipeInfo.IngredientInfo ingredient : candidate.info.getConsumedItems()) { IAEItemStack item = ingredient.getItem(); if (item == null) continue; - ItemStackKey key = new ItemStackKey(item); - resourceUsers.computeIfAbsent(key, k -> new ArrayList<>()).add(candidate); + ItemStackKey key = new ItemStackKey(item, ignoreNbt); long needed = calculateItemsNeededForCrafts(ingredient, effectiveMaxBatchSize); + + resourceUsers.computeIfAbsent(key, k -> new ArrayList<>()).add(candidate); 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()) { @@ -566,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); - 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); @@ -588,16 +677,18 @@ 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; - 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); @@ -605,19 +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); + 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 @@ -627,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); @@ -641,12 +738,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; @@ -718,17 +814,35 @@ 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) { + 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; @@ -751,17 +865,42 @@ 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; + private final boolean ignoreNbt; - ItemStackKey(IAEItemStack item) { + ItemStackKey(IAEItemStack item, boolean ignoreNbt) { this.item = item; + this.ignoreNbt = ignoreNbt; } @Override @@ -769,14 +908,65 @@ 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(); + h = 31 * h + stack.getMetadata(); + h = 31 * h + (ignoreNbt ? 1 : 0); + + if (!ignoreNbt && stack.hasTagCompound()) { + h = 31 * h + stack.getTagCompound().hashCode(); + } + + return h; + } + } + + /** + * 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; } } @@ -902,15 +1092,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 +1340,173 @@ 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; + } + + /** + * 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 @@ -1549,6 +1918,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). */ 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..b05c484 100644 --- a/src/main/resources/assets/ae2powertools/lang/en_us.lang +++ b/src/main/resources/assets/ae2powertools/lang/en_us.lang @@ -219,6 +219,10 @@ 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.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.nbt_matching.ignored.desc=Match items by item+meta only (ignore NBT data) # GUI - Batch diff --git a/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V1.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V1.png new file mode 100644 index 0000000..a4b4879 Binary files /dev/null and b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V1.png differ diff --git a/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V2.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V2.png new file mode 100644 index 0000000..6ab8228 Binary files /dev/null and b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_i_V2.png differ diff --git a/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V1.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V1.png new file mode 100644 index 0000000..88fa3d3 Binary files /dev/null and b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V1.png differ diff --git a/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V2.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V2.png new file mode 100644 index 0000000..14d2ef9 Binary files /dev/null and b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_ii_V2.png differ diff --git a/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V1.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V1.png new file mode 100644 index 0000000..1b71520 Binary files /dev/null and b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V1.png differ diff --git a/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V2.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V2.png new file mode 100644 index 0000000..9d4d25f Binary files /dev/null and b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iii_V2.png differ diff --git a/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V1.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V1.png new file mode 100644 index 0000000..ab1fb45 Binary files /dev/null and b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V1.png differ diff --git a/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V2.png b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V2.png new file mode 100644 index 0000000..e153c29 Binary files /dev/null and b/src/main/resources/assets/ae2powertools/textures/items/modern_crafter_speed_upgrade_iv_V2.png differ