diff --git a/src/api/java/appeng/api/config/FuzzyMode.java b/src/api/java/appeng/api/config/FuzzyMode.java index 84c36c78f..d8353fac5 100644 --- a/src/api/java/appeng/api/config/FuzzyMode.java +++ b/src/api/java/appeng/api/config/FuzzyMode.java @@ -23,15 +23,38 @@ package appeng.api.config; +public enum FuzzyMode { + /** + * Matches any item from undamaged to including 0% durability, but not negative durability (where item damage + * exceeds maxdamage). + */ + IGNORE_ALL(-1), + /** + * Matches items that have less than 100% durability (that is, at least 1 damage point) if a damaged item is used as + * the filter, or undamaged items otherwise. + */ + PERCENT_99(0), + /** + * If an item with less than 75% durability is used as the filter, items with less than 75% durability are matched. + * Otherwise items with 75% durability or more are matched. + */ + PERCENT_75(25), + /** + * If an item with less than 50% durability is used as the filter, items with less than 50% durability are matched. + * Otherwise items with 50% durability or more are matched. + */ + PERCENT_50(50), + /** + * If an item with less than 25% durability is used as the filter, items with less than 50% durability are matched. + * Otherwise items with 25% durability or more are matched. + */ + PERCENT_25(75); -public enum FuzzyMode -{ - // Note that percentage damaged, is the inverse of percentage durability. - IGNORE_ALL( -1 ), - PERCENT_99( 0 ), - PERCENT_75( 25 ), - PERCENT_50( 50 ), - PERCENT_25( 75 ); + public final float breakPoint; + /** + * Note this is percentage "damaged". It's the inverse of percentage durability. + */ + public final float percentage; public final float breakPoint; public final float percentage; diff --git a/src/main/java/appeng/util/UUIDMatcher.java b/src/main/java/appeng/util/UUIDMatcher.java deleted file mode 100644 index 2a6c92353..000000000 --- a/src/main/java/appeng/util/UUIDMatcher.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * This file is part of Applied Energistics 2. - * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. - * - * Applied Energistics 2 is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Applied Energistics 2 is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Applied Energistics 2. If not, see . - */ - -package appeng.util; - - -import java.util.regex.Pattern; - - -/** - * Regex wrapper for {@link java.util.UUID}s to not rely on try catch - */ -public final class UUIDMatcher -{ - /** - * String which is the regular expression for {@link java.util.UUID}s - */ - private static final String UUID_REGEX = "[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}"; - - /** - * Pattern which pre-compiles the {@link appeng.util.UUIDMatcher#UUID_REGEX} - */ - private static final Pattern PATTERN = Pattern.compile( UUID_REGEX ); - - /** - * Checks if a potential {@link java.util.UUID} is an {@link java.util.UUID} by applying a regular expression on it. - * - * @param potential to be checked potential {@link java.util.UUID} - * - * @return true, if the potential {@link java.util.UUID} is indeed an {@link java.util.UUID} - */ - public boolean isUUID( final CharSequence potential ) - { - return PATTERN.matcher( potential ).matches(); - } -} diff --git a/src/main/java/appeng/util/item/AESharedItemStack.java b/src/main/java/appeng/util/item/AESharedItemStack.java index 5ce9e784d..700e5b6b1 100644 --- a/src/main/java/appeng/util/item/AESharedItemStack.java +++ b/src/main/java/appeng/util/item/AESharedItemStack.java @@ -25,15 +25,13 @@ import com.google.common.base.Preconditions; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import appeng.api.config.FuzzyMode; +final class AESharedItemStack { - -final class AESharedItemStack implements Comparable -{ - private static final NBTTagCompound LOW_TAG = new NBTTagCompound(); - private static final NBTTagCompound HIGH_TAG = new NBTTagCompound(); + private final ItemStack itemStack; + private final int itemId; + private final int itemDamage; + private final int hashCode; private final ItemStack itemStack; private final int itemId; @@ -53,228 +51,44 @@ final class AESharedItemStack implements Comparable return new Bounds( this.itemStack, fuzzy, ignoreMeta ); } - ItemStack getDefinition() - { - return this.itemStack; - } - - int getItemDamage() - { - return this.itemDamage; - } + ItemStack getDefinition() { + return this.itemStack; + } int getItemID() { return this.itemId; } - @Override - public int hashCode() - { - return this.hashCode; - } + @Override + public int hashCode() { + return this.hashCode; + } - @Override - public boolean equals( final Object obj ) - { - if( obj instanceof AESharedItemStack ) - { - final AESharedItemStack other = (AESharedItemStack) obj; + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof AESharedItemStack)) { + return false; + } - Preconditions.checkState( this.itemStack.getCount() == 1, "ItemStack#getCount() has to be 1" ); - Preconditions.checkArgument( other.getDefinition().getCount() == 1, "ItemStack#getCount() has to be 1" ); + final AESharedItemStack other = (AESharedItemStack) obj; + Preconditions.checkState(this.itemStack.getCount() == 1, "ItemStack#getCount() has to be 1"); + Preconditions.checkArgument(other.getDefinition().getCount() == 1, "ItemStack#getCount() has to be 1"); - if( this.itemStack == other.itemStack ) - { - return true; - } - return ItemStack.areItemStacksEqual( this.itemStack, other.itemStack ); - } - return false; - } + if (this.itemStack == other.itemStack) { + return true; + } + return ItemStack.areItemStacksEqual(this.itemStack, other.itemStack); + } - @Override - public int compareTo( final AESharedItemStack b ) - { - Preconditions.checkState( this.itemStack.getCount() == 1, "ItemStack#getCount() has to be 1" ); - Preconditions.checkArgument( b.getDefinition().getCount() == 1, "ItemStack#getCount() has to be 1" ); + private int makeHashCode() { + return Objects.hash( + this.itemId, + this.itemDamage, + this.itemStack.hasTag() ? this.itemStack.getTag() : 0); + } - if( this.itemStack == b.getDefinition() ) - { - return 0; - } - - final int id = this.itemId - b.itemId; - if( id != 0 ) - { - return id; - } - - final int damageValue = this.itemDamage - b.itemDamage; - if( damageValue != 0 ) - { - return damageValue; - } - - final int nbt = this.compareNBT( b.getDefinition() ); - if( nbt != 0 ) - { - return nbt; - } - - if( !this.itemStack.areCapsCompatible( b.getDefinition() ) ) - { - return System.identityHashCode( this.itemStack ) - System.identityHashCode( b.getDefinition() ); - } - return 0; - } - - private int compareNBT( final ItemStack b ) - { - if( this.itemStack.getTagCompound() == b.getTagCompound() ) - { - return 0; - } - if( this.itemStack.getTagCompound() == LOW_TAG || b.getTagCompound() == HIGH_TAG ) - { - return -1; - } - if( this.itemStack.getTagCompound() == HIGH_TAG || b.getTagCompound() == LOW_TAG ) - { - return 1; - } - return System.identityHashCode( this.itemStack.getTagCompound() ) - System.identityHashCode( b.getTagCompound() ); - } - - private int makeHashCode() - { - return Objects.hash( this.itemId, this.itemDamage, this.itemStack.hasTagCompound() ? this.itemStack.getTagCompound() : 0 ); - } - - /** - * Creates the lower and upper bounds for a specific shared itemstack. - */ - public static final class Bounds - { - /** - * Bounds enforced by {@link ItemStack#isEmpty()} - */ - private static final int MIN_DAMAGE_VALUE = 0; - private static final int MAX_DAMAGE_VALUE = 65535; - - private final AESharedItemStack lower; - private final AESharedItemStack upper; - - public Bounds( final ItemStack stack, final FuzzyMode fuzzy, final boolean ignoreMeta ) - { - Preconditions.checkState( !stack.isEmpty(), "ItemStack#isEmpty() has to be false" ); - Preconditions.checkState( stack.getCount() == 1, "ItemStack#getCount() has to be 1" ); - - final NBTTagCompound tag = stack.hasTagCompound() ? stack.getTagCompound() : null; - - this.lower = this.makeLowerBound( stack, tag, fuzzy, ignoreMeta ); - this.upper = this.makeUpperBound( stack, tag, fuzzy, ignoreMeta ); - } - - public AESharedItemStack lower() - { - return this.lower; - } - - public AESharedItemStack upper() - { - return this.upper; - } - - private AESharedItemStack makeLowerBound( final ItemStack itemStack, final NBTTagCompound tag, final FuzzyMode fuzzy, final boolean ignoreMeta ) - { - final ItemStack newDef = itemStack.copy(); - - if( ignoreMeta ) - { - newDef.setItemDamage( MIN_DAMAGE_VALUE ); - newDef.setTagCompound( tag ); - } - else - { - if( newDef.getItem().isDamageable() ) - { - if (itemStack.getMaxDamage() == 0) - { - newDef.setItemDamage(itemStack.getItemDamage() ); - } - else if ( fuzzy == FuzzyMode.IGNORE_ALL ) - { - newDef.setItemDamage( MIN_DAMAGE_VALUE ); - } - else if( fuzzy == FuzzyMode.PERCENT_99 ) - { - if( itemStack.getItemDamage() == MIN_DAMAGE_VALUE ) - { - newDef.setItemDamage( MIN_DAMAGE_VALUE ); - } - else - { - newDef.setItemDamage( MIN_DAMAGE_VALUE + 1 ); - } - } - else - { - final int breakpoint = fuzzy.calculateBreakPoint( itemStack.getMaxDamage() ); - final int damage = breakpoint <= itemStack.getItemDamage() ? breakpoint : 0; - newDef.setItemDamage( damage ); - } - } - newDef.setTagCompound( LOW_TAG ); - } - - return new AESharedItemStack( newDef ); - } - - private AESharedItemStack makeUpperBound( final ItemStack itemStack, final NBTTagCompound tag, final FuzzyMode fuzzy, final boolean ignoreMeta ) - { - final ItemStack newDef = itemStack.copy(); - - if( ignoreMeta ) - { - newDef.setItemDamage( MAX_DAMAGE_VALUE ); - newDef.setTagCompound( tag ); - } - else - { - if( newDef.getItem().isDamageable() ) - { - if (itemStack.getMaxDamage() == 0) - { - newDef.setItemDamage(itemStack.getItemDamage() ); - } - else if ( fuzzy == FuzzyMode.IGNORE_ALL ) - { - newDef.setItemDamage( itemStack.getMaxDamage() + 1 ); - } - else if( fuzzy == FuzzyMode.PERCENT_99 ) - { - if( itemStack.getItemDamage() == MIN_DAMAGE_VALUE ) - { - newDef.setItemDamage( MIN_DAMAGE_VALUE ); - } - else - { - newDef.setItemDamage( itemStack.getMaxDamage() + 1 ); - } - } - else - { - final int breakpoint = fuzzy.calculateBreakPoint( itemStack.getMaxDamage() ); - final int damage = itemStack.getItemDamage() < breakpoint ? breakpoint - 1 : itemStack.getMaxDamage() + 1; - newDef.setItemDamage( damage ); - } - } - newDef.setTagCompound( HIGH_TAG ); - } - - return new AESharedItemStack( newDef ); - } - - } } diff --git a/src/main/java/appeng/util/item/FuzzyItemVariantList.java b/src/main/java/appeng/util/item/FuzzyItemVariantList.java new file mode 100644 index 000000000..270a3df07 --- /dev/null +++ b/src/main/java/appeng/util/item/FuzzyItemVariantList.java @@ -0,0 +1,179 @@ +/* + * This file is part of Applied Energistics 2. + * Copyright (c) 2021, TeamAppliedEnergistics, All rights reserved. + * + * Applied Energistics 2 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Applied Energistics 2 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Applied Energistics 2. If not, see . + */ + +package appeng.util.item; + +import java.util.Collection; +import java.util.Comparator; +import java.util.Map; + +import com.google.common.base.Preconditions; + +import net.minecraft.item.ItemStack; + +import it.unimi.dsi.fastutil.objects.Object2ObjectAVLTreeMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectSortedMap; + +import appeng.api.config.FuzzyMode; +import appeng.api.storage.data.IAEItemStack; + +/** + * This variant list is optimized for damageable items, and supports selecting durability ranges with + * {@link #findFuzzy(IAEItemStack, FuzzyMode)}. + */ +class FuzzyItemVariantList extends ItemVariantList { + + static final SharedStackComparator COMPARATOR = new SharedStackComparator(); + + // NOTE: We only use Object as they key here so we can pass our special DamageBounds to the subMap method. + // We NEVER put any keys in this map that are not AESharedItemStacks. + private final Object2ObjectSortedMap records = new Object2ObjectAVLTreeMap<>(COMPARATOR); + + @Override + public Collection findFuzzy(final IAEItemStack filter, final FuzzyMode fuzzy) { + ItemStack itemStack = filter.getDefinition(); + + ItemDamageBound lowerBound = makeLowerBound(itemStack, fuzzy); + ItemDamageBound upperBound = makeUpperBound(itemStack, fuzzy); + Preconditions.checkState(lowerBound.itemDamage > upperBound.itemDamage); + + return this.records.subMap(lowerBound, upperBound).values(); + } + + @SuppressWarnings("unchecked") + @Override + Map getRecords() { + // We ensure on our end that we NEVER use anything but AESharedItemStack as the key in this map + return (Map) (Object) this.records; + } + + static class ItemDamageBound { + final int itemDamage; + + public ItemDamageBound(int itemDamage) { + this.itemDamage = itemDamage; + } + } + + /** + * This comparator creates a strict and total ordering over all {@link AESharedItemStack} of the same item. To + * support selecting ranges of durability, it is defined for type {@link Object} and also accepts + * {@link ItemDamageBound} as an argument to compare against. + */ + static class SharedStackComparator implements Comparator { + @Override + public int compare(Object a, Object b) { + // Either argument can either be a damage bound or a shared item stack + // Since we never put damage bounds into the map as keys, only one + // of the two arguments can possibly be a bound + ItemDamageBound boundA = null; + AESharedItemStack stackA = null; + int itemDamageA; + if (a instanceof ItemDamageBound) { + boundA = (ItemDamageBound) a; + itemDamageA = boundA.itemDamage; + } else { + stackA = (AESharedItemStack) a; + itemDamageA = stackA.getItemDamage(); + } + ItemDamageBound boundB = null; + AESharedItemStack stackB = null; + int itemDamageB; + if (b instanceof ItemDamageBound) { + boundB = (ItemDamageBound) b; + itemDamageB = boundB.itemDamage; + } else { + stackB = (AESharedItemStack) b; + itemDamageB = stackB.getItemDamage(); + } + + // When either argument is a damage bound, we just compare the damage values because it is used + // only to get a certain damage range out of the map. + if (boundA != null || boundB != null) { + return Integer.compare(itemDamageB, itemDamageA); + } + + ItemStack itemStackA = stackA.getDefinition(); + ItemStack itemStackB = stackB.getDefinition(); + Preconditions.checkState(itemStackA.getCount() == 1, "ItemStack#getCount() has to be 1"); + Preconditions.checkArgument(itemStackB.getCount() == 1, "ItemStack#getCount() has to be 1"); + + if (itemStackA == itemStackB) { + return 0; + } + + // Damaged items are sorted before undamaged items + final int damageValue = Integer.compare(itemDamageB, itemDamageA); + if (damageValue != 0) { + return damageValue; + } + + // As a final tie breaker, order by the object identity of the item stack + // While this will order seemingly at random, we only need the order of + // damage values to be predictable, while still having to satisfy the + // complete order requirements of the sorted map + return Long.compare(System.identityHashCode(itemStackA), System.identityHashCode(itemStackB)); + } + } + + /** + * Minecraft reverses the damage values. So anything with a damage of 0 is undamaged and increases the more damaged + * the item is. + *

+ * Further the used subMap follows [MAX_DAMAGE, MIN_DAMAGE), so to include undamaged items, we have to start with a + * lower damage value than 0, while it is fine to use {@link ItemStack#getMaxDamage()} for the upper bound. + */ + private static final int MIN_DAMAGE_VALUE = -1; + + /* + * Keep in mind that the stack order is from most damaged to least damaged, so this lower bound will actually be a + * higher number than the upper bound. + */ + static ItemDamageBound makeLowerBound(final ItemStack stack, final FuzzyMode fuzzy) { + Preconditions.checkState(stack.isDamageable(), "ItemStack#isDamageable() has to be true"); + + int damage; + if (fuzzy == FuzzyMode.IGNORE_ALL) { + damage = stack.getMaxDamage(); + } else { + final int breakpoint = fuzzy.calculateBreakPoint(stack.getMaxDamage()); + damage = stack.getDamage() <= breakpoint ? breakpoint : stack.getMaxDamage(); + } + + return new ItemDamageBound(damage); + } + + /* + * Keep in mind that the stack order is from most damaged to least damaged, so this upper bound will actually be a + * lower number than the lower bound. It also is exclusive. + */ + static ItemDamageBound makeUpperBound(final ItemStack stack, final FuzzyMode fuzzy) { + Preconditions.checkState(stack.isDamageable(), "ItemStack#isDamageable() has to be true"); + + int damage; + if (fuzzy == FuzzyMode.IGNORE_ALL) { + damage = MIN_DAMAGE_VALUE; + } else { + final int breakpoint = fuzzy.calculateBreakPoint(stack.getMaxDamage()); + damage = stack.getDamage() <= breakpoint ? MIN_DAMAGE_VALUE : breakpoint; + } + + return new ItemDamageBound(damage); + } + +} diff --git a/src/main/java/appeng/util/item/ItemList.java b/src/main/java/appeng/util/item/ItemList.java index c8e22b796..e50cd73b7 100644 --- a/src/main/java/appeng/util/item/ItemList.java +++ b/src/main/java/appeng/util/item/ItemList.java @@ -22,9 +22,10 @@ package appeng.util.item; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.ConcurrentModificationException; import java.util.Iterator; -import java.util.NavigableMap; -import java.util.concurrent.ConcurrentSkipListMap; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicInteger; import net.minecraftforge.oredict.OreDictionary; @@ -34,20 +35,23 @@ import appeng.api.storage.data.IItemList; import appeng.util.item.AESharedItemStack.Bounds; -public final class ItemList implements IItemList -{ + private final Reference2ObjectMap records = new Reference2ObjectOpenHashMap<>(); + /** + * We increment this version field everytime an attempt to mutate this item list (or potentially one of its + * sub-lists) is made. Iterators will copy the version when they are created and compare it against the current + * version whenever they advance to trigger a {@link ConcurrentModificationException}. + */ + private final AtomicInteger version = new AtomicInteger(0); - private final NavigableMap records = new ConcurrentSkipListMap<>(); + @Override + public IAEItemStack findPrecise(final IAEItemStack itemStack) { + if (itemStack == null) { + return null; + } - @Override - public void add( final IAEItemStack option ) - { - if( option == null ) - { - return; - } - - final IAEItemStack st = this.records.get( ( (AEItemStack) option ).getSharedStack() ); + ItemVariantList record = this.records.get(itemStack.getItem()); + return record != null ? record.findPrecise(itemStack) : null; + } if( st != null ) { @@ -55,37 +59,41 @@ public final class ItemList implements IItemList return; } - final IAEItemStack opt = option.copy(); + ItemVariantList record = this.records.get(filter.getItem()); + return record != null ? record.findFuzzy(filter, fuzzy) : Collections.emptyList(); + } this.putItemRecord( opt ); } - @Override - public IAEItemStack findPrecise( final IAEItemStack itemStack ) - { - if( itemStack == null ) - { - return null; - } + @Override + public void add(final IAEItemStack itemStack) { + version.incrementAndGet(); + + if (itemStack == null) { + return; + } return this.records.get( ( (AEItemStack) itemStack ).getSharedStack() ); } - @Override - public Collection findFuzzy( final IAEItemStack filter, final FuzzyMode fuzzy ) - { - if( filter == null ) - { - return Collections.emptyList(); - } + @Override + public void addStorage(final IAEItemStack itemStack) { + version.incrementAndGet(); + + if (itemStack == null) { + return; + } final AEItemStack ais = (AEItemStack) filter; - return ais.getOre().map( or -> - { - if( or.getAEEquivalents().size() == 1 ) - { - final IAEItemStack is = or.getAEEquivalents().get( 0 ); + @Override + public void addCrafting(final IAEItemStack itemStack) { + version.incrementAndGet(); + + if (itemStack == null) { + return; + } return this.findFuzzyDamage( is, fuzzy, is.getItemDamage() == OreDictionary.WILDCARD_VALUE ); } @@ -93,10 +101,13 @@ public final class ItemList implements IItemList { final Collection output = new ArrayList<>(); - for( final IAEItemStack is : or.getAEEquivalents() ) - { - output.addAll( this.findFuzzyDamage( is, fuzzy, is.getItemDamage() == OreDictionary.WILDCARD_VALUE ) ); - } + @Override + public void addRequestable(final IAEItemStack itemStack) { + version.incrementAndGet(); + + if (itemStack == null) { + return; + } return output; } @@ -117,7 +128,12 @@ public final class ItemList implements IItemList return; } - final IAEItemStack st = this.records.get( ( (AEItemStack) option ).getSharedStack() ); + @Override + public int size() { + int size = 0; + for (ItemVariantList entry : records.values()) { + size += entry.size(); + } if( st != null ) { @@ -125,105 +141,78 @@ public final class ItemList implements IItemList return; } - final IAEItemStack opt = option.copy(); + @Override + public Iterator iterator() { + return new ChainedIterator(this.records.values().iterator(), version); + } this.putItemRecord( opt ); } - /* - * public void clean() { Iterator i = iterator(); while (i.hasNext()) { StackType AEI = - * i.next(); if ( !AEI.isMeaningful() ) i.remove(); } } - */ + private ItemVariantList getOrCreateRecord(Item item) { + return this.records.computeIfAbsent(item, this::makeRecordMap); + } - @Override - public void addCrafting( final IAEItemStack option ) - { - if( option == null ) - { - return; - } + private ItemVariantList makeRecordMap(Item item) { + if (item.isDamageable()) { + return new FuzzyItemVariantList(); + } else { + return new NormalItemVariantList(); + } + } - final IAEItemStack st = this.records.get( ( (AEItemStack) option ).getSharedStack() ); + /** + * Iterates over multiple item lists as if they were one list. + */ + private static class ChainedIterator implements Iterator { - if( st != null ) - { - st.setCraftable( true ); - return; - } + private final AtomicInteger parentVersion; + private final int version; + private final Iterator parent; + private Iterator next; - final IAEItemStack opt = option.copy(); - opt.setStackSize( 0 ); - opt.setCraftable( true ); + public ChainedIterator(Iterator iterator, AtomicInteger parentVersion) { + this.parent = iterator; + this.parentVersion = parentVersion; + this.version = parentVersion.get(); + this.ensureItems(); + } - this.putItemRecord( opt ); - } + @Override + public boolean hasNext() { + return next != null && next.hasNext(); + } - @Override - public void addRequestable( final IAEItemStack option ) - { - if( option == null ) - { - return; - } + @Override + public IAEItemStack next() { + if (this.next == null) { + throw new NoSuchElementException(); + } + if (this.version != this.parentVersion.get()) { + throw new ConcurrentModificationException(); + } - final IAEItemStack st = this.records.get( ( (AEItemStack) option ).getSharedStack() ); + IAEItemStack result = this.next.next(); + this.ensureItems(); + return result; + } - if( st != null ) - { - st.setCountRequestable( st.getCountRequestable() + option.getCountRequestable() ); - return; - } + private void ensureItems() { + if (hasNext()) { + return; // Still items left in the current one + } - final IAEItemStack opt = option.copy(); - opt.setStackSize( 0 ); - opt.setCraftable( false ); - opt.setCountRequestable( option.getCountRequestable() ); + // Find the next iterator willing to return some items... + while (this.parent.hasNext()) { + this.next = this.parent.next().iterator(); - this.putItemRecord( opt ); - } + if (this.next.hasNext()) { + return; // Found one! + } + } - @Override - public IAEItemStack getFirstItem() - { - for( final IAEItemStack stackType : this ) - { - return stackType; - } - - return null; - } - - @Override - public int size() - { - return this.records.size(); - } - - @Override - public Iterator iterator() - { - return new MeaningfulItemIterator<>( this.records.values().iterator() ); - } - - @Override - public void resetStatus() - { - for( final IAEItemStack i : this ) - { - i.reset(); - } - } - - private IAEItemStack putItemRecord( final IAEItemStack itemStack ) - { - return this.records.put( ( (AEItemStack) itemStack ).getSharedStack(), itemStack ); - } - - private Collection findFuzzyDamage( final IAEItemStack filter, final FuzzyMode fuzzy, final boolean ignoreMeta ) - { - final AEItemStack itemStack = (AEItemStack) filter; - final Bounds bounds = itemStack.getSharedStack().getBounds( fuzzy, ignoreMeta ); - - return this.records.subMap( bounds.lower(), true, bounds.upper(), true ).descendingMap().values(); - } + // No more items + this.next = null; + } + } } diff --git a/src/main/java/appeng/util/item/ItemModList.java b/src/main/java/appeng/util/item/ItemModList.java deleted file mode 100644 index 60268f679..000000000 --- a/src/main/java/appeng/util/item/ItemModList.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * This file is part of Applied Energistics 2. - * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. - * - * Applied Energistics 2 is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Applied Energistics 2 is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Applied Energistics 2. If not, see . - */ - -package appeng.util.item; - - -import java.util.Collection; - -import appeng.api.AEApi; -import appeng.api.config.FuzzyMode; -import appeng.api.storage.channels.IItemStorageChannel; -import appeng.api.storage.data.IAEItemStack; -import appeng.api.storage.data.IItemContainer; - - -public class ItemModList implements IItemContainer -{ - - private final IItemContainer backingStore; - private final IItemContainer overrides = AEApi.instance().storage().getStorageChannel( IItemStorageChannel.class ).createList(); - - public ItemModList( final IItemContainer backend ) - { - this.backingStore = backend; - } - - @Override - public void add( final IAEItemStack option ) - { - IAEItemStack over = this.overrides.findPrecise( option ); - if( over == null ) - { - over = this.backingStore.findPrecise( option ); - if( over == null ) - { - this.overrides.add( option ); - } - else - { - option.add( over ); - this.overrides.add( option ); - } - } - else - { - this.overrides.add( option ); - } - } - - @Override - public IAEItemStack findPrecise( final IAEItemStack i ) - { - final IAEItemStack over = this.overrides.findPrecise( i ); - if( over == null ) - { - return this.backingStore.findPrecise( i ); - } - return over; - } - - @Override - public Collection findFuzzy( final IAEItemStack input, final FuzzyMode fuzzy ) - { - return this.overrides.findFuzzy( input, fuzzy ); - } - - @Override - public boolean isEmpty() - { - return this.overrides.isEmpty() && this.backingStore.isEmpty(); - } -} diff --git a/src/main/java/appeng/util/item/ItemVariantList.java b/src/main/java/appeng/util/item/ItemVariantList.java new file mode 100644 index 000000000..01af28937 --- /dev/null +++ b/src/main/java/appeng/util/item/ItemVariantList.java @@ -0,0 +1,118 @@ +/* + * This file is part of Applied Energistics 2. + * Copyright (c) 2021, TeamAppliedEnergistics, All rights reserved. + * + * Applied Energistics 2 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Applied Energistics 2 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Applied Energistics 2. If not, see . + */ + +package appeng.util.item; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +import appeng.api.config.FuzzyMode; +import appeng.api.storage.data.IAEItemStack; + +/** + * Stores variants of a single type of {@link net.minecraft.item.Item}, i.e. versions with different durability, or + * different NBT or capabilities. + */ +abstract class ItemVariantList { + + public void add(final IAEItemStack option) { + final IAEItemStack st = this.getRecords().get(((AEItemStack) option).getSharedStack()); + + if (st != null) { + st.add(option); + return; + } + + final IAEItemStack opt = option.copy(); + + this.putItemRecord(opt); + } + + public IAEItemStack findPrecise(final IAEItemStack itemStack) { + return this.getRecords().get(((AEItemStack) itemStack).getSharedStack()); + } + + public void addStorage(final IAEItemStack option) { + final IAEItemStack st = this.getRecords().get(((AEItemStack) option).getSharedStack()); + + if (st != null) { + st.incStackSize(option.getStackSize()); + return; + } + + final IAEItemStack opt = option.copy(); + + this.putItemRecord(opt); + } + + public void addCrafting(final IAEItemStack option) { + final IAEItemStack st = this.getRecords().get(((AEItemStack) option).getSharedStack()); + + if (st != null) { + st.setCraftable(true); + return; + } + + final IAEItemStack opt = option.copy(); + opt.setStackSize(0); + opt.setCraftable(true); + + this.putItemRecord(opt); + } + + public void addRequestable(final IAEItemStack option) { + final IAEItemStack st = this.getRecords().get(((AEItemStack) option).getSharedStack()); + + if (st != null) { + st.setCountRequestable(st.getCountRequestable() + option.getCountRequestable()); + return; + } + + final IAEItemStack opt = option.copy(); + opt.setStackSize(0); + opt.setCraftable(false); + opt.setCountRequestable(option.getCountRequestable()); + + this.putItemRecord(opt); + } + + public int size() { + int size = 0; + for (IAEItemStack entry : getRecords().values()) { + if (entry.isMeaningful()) { + size++; + } + } + + return size; + } + + public Iterator iterator() { + return new MeaningfulItemIterator<>(this.getRecords().values()); + } + + private void putItemRecord(final IAEItemStack itemStack) { + this.getRecords().put(((AEItemStack) itemStack).getSharedStack(), itemStack); + } + + abstract Map getRecords(); + + public abstract Collection findFuzzy(final IAEItemStack filter, final FuzzyMode fuzzy); + +} diff --git a/src/main/java/appeng/util/item/MeaningfulItemIterator.java b/src/main/java/appeng/util/item/MeaningfulItemIterator.java index 7d7c8b016..310d23fb3 100644 --- a/src/main/java/appeng/util/item/MeaningfulItemIterator.java +++ b/src/main/java/appeng/util/item/MeaningfulItemIterator.java @@ -1,6 +1,6 @@ /* * This file is part of Applied Energistics 2. - * Copyright (c) 2013 - 2015, AlgorithmX2, All rights reserved. + * Copyright (c) 2021, TeamAppliedEnergistics, All rights reserved. * * Applied Energistics 2 is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by @@ -18,44 +18,30 @@ package appeng.util.item; - +import java.util.Collection; import java.util.Iterator; import java.util.NoSuchElementException; import appeng.api.storage.data.IAEItemStack; +/** + * This iterator will only return items from a collection that are meaningful (w.r.t. + * {@link IAEItemStack#isMeaningful()}. Items that are not meaningful are automatically removed from the collection as + * it is being iterated. + */ +public class MeaningfulItemIterator implements Iterator { + private final Iterator parent; + private T next; -public class MeaningfulItemIterator implements Iterator -{ + public MeaningfulItemIterator(final Collection collection) { + this.parent = collection.iterator(); + this.next = seekNext(); + } - private final Iterator parent; - private T next; - - public MeaningfulItemIterator( final Iterator iterator ) - { - this.parent = iterator; - } - - @Override - public boolean hasNext() - { - while( this.parent.hasNext() ) - { - this.next = this.parent.next(); - - if( this.next.isMeaningful() ) - { - return true; - } - else - { - this.parent.remove(); // self cleaning :3 - } - } - - this.next = null; - return false; - } + @Override + public boolean hasNext() { + return this.next != null; + } @Override public T next() @@ -65,12 +51,22 @@ public class MeaningfulItemIterator implements Iterator< throw new NoSuchElementException(); } - return this.next; - } + T result = this.next; + this.next = this.seekNext(); + return result; + } - @Override - public void remove() - { - this.parent.remove(); - } + private T seekNext() { + while (this.parent.hasNext()) { + T item = this.parent.next(); + + if (item.isMeaningful()) { + return item; + } else { + this.parent.remove(); + } + } + + return null; + } } diff --git a/src/main/java/appeng/util/item/NormalItemVariantList.java b/src/main/java/appeng/util/item/NormalItemVariantList.java new file mode 100644 index 000000000..89a92a155 --- /dev/null +++ b/src/main/java/appeng/util/item/NormalItemVariantList.java @@ -0,0 +1,51 @@ +/* + * This file is part of Applied Energistics 2. + * Copyright (c) 2021, TeamAppliedEnergistics, All rights reserved. + * + * Applied Energistics 2 is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Applied Energistics 2 is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Applied Energistics 2. If not, see . + */ + +package appeng.util.item; + +import java.util.Collection; +import java.util.Map; + +import it.unimi.dsi.fastutil.objects.Reference2ObjectMap; +import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap; + +import appeng.api.config.FuzzyMode; +import appeng.api.storage.data.IAEItemStack; + +/** + * This variant list is optimized for items that cannot be damaged and thus do not support querying durability ranges + * via {@link #findFuzzy(IAEItemStack, FuzzyMode)}. + */ +class NormalItemVariantList extends ItemVariantList { + + private final Reference2ObjectMap records = new Reference2ObjectOpenHashMap<>(); + + @Override + Map getRecords() { + return this.records; + } + + /** + * For items that do not support durability, we just return all variants to a fuzzy search. + */ + @Override + public Collection findFuzzy(IAEItemStack filter, FuzzyMode fuzzy) { + return this.getRecords().values(); + } + +} diff --git a/src/test/java/appeng/util/EnumCyclerTest.java b/src/test/java/appeng/util/EnumCyclerTest.java new file mode 100644 index 000000000..b00998551 --- /dev/null +++ b/src/test/java/appeng/util/EnumCyclerTest.java @@ -0,0 +1,91 @@ +package appeng.util; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.EnumSet; + +import org.junit.jupiter.api.Test; + +class EnumCyclerTest { + + @Test + void testRotateEnumForwardWithOnlySomeValidOptions() { + EnumSet validOptions = EnumSet.of(RotateTestEnum.A, RotateTestEnum.C, RotateTestEnum.E); + assertThat(EnumCycler.rotateEnum(RotateTestEnum.A, false, validOptions)).isEqualTo(RotateTestEnum.C); + assertThat(EnumCycler.rotateEnum(RotateTestEnum.C, false, validOptions)).isEqualTo(RotateTestEnum.E); + assertThat(EnumCycler.rotateEnum(RotateTestEnum.E, false, validOptions)).isEqualTo(RotateTestEnum.A); + } + + @Test + void testRotateEnumBackwardsWithOnlySomeValidOptions() { + EnumSet validOptions = EnumSet.of(RotateTestEnum.A, RotateTestEnum.C, RotateTestEnum.E); + assertThat(EnumCycler.rotateEnum(RotateTestEnum.A, true, validOptions)).isEqualTo(RotateTestEnum.E); + assertThat(EnumCycler.rotateEnum(RotateTestEnum.C, true, validOptions)).isEqualTo(RotateTestEnum.A); + assertThat(EnumCycler.rotateEnum(RotateTestEnum.E, true, validOptions)).isEqualTo(RotateTestEnum.C); + } + + /** + * When there are no valid options, the function should reject the arguments. + */ + @Test + void testRotateEnumNoValidOptions() { + assertThrows(IllegalArgumentException.class, () -> { + EnumCycler.rotateEnum(TestEnum.A, false, EnumSet.noneOf(TestEnum.class)); + }); + } + + /** + * When the current enum literal is not part of the valid options, it should just skip to the next valid option + * instead. + */ + @Test + void testRotateEnumCurrentIsNotAValidOption() { + assertThat(EnumCycler.rotateEnum(TestEnum.B, false, EnumSet.of(TestEnum.A))).isEqualTo(TestEnum.A); + assertThat(EnumCycler.rotateEnum(TestEnum.B, true, EnumSet.of(TestEnum.A))).isEqualTo(TestEnum.A); + } + + /** + * When there's only one valid option, it should just rotate back to it. + */ + @Test + void testRotateEnumOnlyOneValidOption() { + assertThat(EnumCycler.rotateEnum(TestEnum.A, false, EnumSet.of(TestEnum.A))).isEqualTo(TestEnum.A); + assertThat(EnumCycler.rotateEnum(TestEnum.A, true, EnumSet.of(TestEnum.A))).isEqualTo(TestEnum.A); + } + + @Test + void testNext() { + assertThat(EnumCycler.next(TestEnum.A)).isEqualTo(TestEnum.B); + assertThat(EnumCycler.next(TestEnum.B)).isEqualTo(TestEnum.C); + assertThat(EnumCycler.next(TestEnum.C)).isEqualTo(TestEnum.A); + assertThat(EnumCycler.next(SingleLiteralEnum.A)).isEqualTo(SingleLiteralEnum.A); + } + + @Test + void testPrev() { + assertThat(EnumCycler.prev(TestEnum.A)).isEqualTo(TestEnum.C); + assertThat(EnumCycler.prev(TestEnum.B)).isEqualTo(TestEnum.A); + assertThat(EnumCycler.prev(TestEnum.C)).isEqualTo(TestEnum.B); + assertThat(EnumCycler.prev(SingleLiteralEnum.A)).isEqualTo(SingleLiteralEnum.A); + } + + enum TestEnum { + A, + B, + C + } + + enum RotateTestEnum { + A, + B, + C, + D, + E + } + + enum SingleLiteralEnum { + A + } + +} \ No newline at end of file diff --git a/src/test/java/appeng/util/UUIDMatcherTest.java b/src/test/java/appeng/util/UUIDMatcherTest.java deleted file mode 100644 index 69e3c6d13..000000000 --- a/src/test/java/appeng/util/UUIDMatcherTest.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * This file is part of Applied Energistics 2. - * Copyright (c) 2013 - 2014, AlgorithmX2, All rights reserved. - * - * Applied Energistics 2 is free software: you can redistribute it and/or modify - * it under the terms of the GNU Lesser General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * Applied Energistics 2 is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with Applied Energistics 2. If not, see . - */ - -package appeng.util; - - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -import org.junit.Test; - - -/** - * Tests for {@link UUIDMatcher} - */ -public final class UUIDMatcherTest -{ - private static final String IS_UUID = "03ba29a1-d6bd-32ba-90b2-375e4d65abc9"; - private static final String NO_UUID = "no"; - private static final String INVALID_UUID = "g3ba29a1-d6bd-32ba-90b2-375e4d65abc9"; - - private final UUIDMatcher matcher; - - public UUIDMatcherTest() - { - this.matcher = new UUIDMatcher(); - } - - @Test - public void testUUID_shouldPass() - { - assertTrue( this.matcher.isUUID( IS_UUID ) ); - } - - @Test - public void testNoUUD_shouldPass() - { - assertFalse( this.matcher.isUUID( NO_UUID ) ); - } - - @Test - public void testInvalidUUID_shouldPass() - { - assertFalse( this.matcher.isUUID( INVALID_UUID ) ); - } -} diff --git a/src/test/java/appeng/util/WideReadableNumberConverterTest.java b/src/test/java/appeng/util/WideReadableNumberConverterTest.java index eb57ea6eb..f48d4929d 100644 --- a/src/test/java/appeng/util/WideReadableNumberConverterTest.java +++ b/src/test/java/appeng/util/WideReadableNumberConverterTest.java @@ -21,7 +21,7 @@ package appeng.util; import static org.junit.Assert.assertEquals; -import org.junit.Test; +import org.junit.jupiter.api.Test; /** diff --git a/src/test/java/appeng/util/helpers/P2PHelperTest.java b/src/test/java/appeng/util/helpers/P2PHelperTest.java index e18c35828..9cb6e1bb3 100644 --- a/src/test/java/appeng/util/helpers/P2PHelperTest.java +++ b/src/test/java/appeng/util/helpers/P2PHelperTest.java @@ -22,7 +22,7 @@ package appeng.util.helpers; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; -import org.junit.Test; +import org.junit.jupiter.api.Test; import appeng.api.util.AEColor; diff --git a/src/test/java/appeng/util/item/AESharedItemStackTest.java b/src/test/java/appeng/util/item/AESharedItemStackTest.java new file mode 100644 index 000000000..262dacaa9 --- /dev/null +++ b/src/test/java/appeng/util/item/AESharedItemStackTest.java @@ -0,0 +1,78 @@ +package appeng.util.item; + +import java.util.IdentityHashMap; +import java.util.Map; + +import com.google.common.testing.EqualsTester; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.util.registry.Bootstrap; +import net.minecraft.util.text.StringTextComponent; + +class AESharedItemStackTest { + + @BeforeAll + static void bootstrap() { + Bootstrap.register(); + } + + // Test stack -> Name for debugging the tests + final Map stacks = new IdentityHashMap<>(); + + AESharedItemStackTest() { + TestItemWithCaps TEST_ITEM = new TestItemWithCaps(); + + ItemStack nameTag1 = new ItemStack(TEST_ITEM); + stacks.put(new AESharedItemStack(nameTag1), "no-nbt"); + + // NBT + ItemStack nameTag2 = new ItemStack(TEST_ITEM); + nameTag2.setDisplayName(new StringTextComponent("Hello World")); + stacks.put(new AESharedItemStack(nameTag2), "nbt1"); + + // Different NBT + ItemStack nameTag3 = new ItemStack(TEST_ITEM); + nameTag3.setDisplayName(new StringTextComponent("ABCDEFGH")); + stacks.put(new AESharedItemStack(nameTag3), "nbt2"); + + // NBT + Cap + CompoundNBT capNbt = new CompoundNBT(); + capNbt.putInt("Parent", 1); + ItemStack nameTag4 = new ItemStack(TEST_ITEM, 1, capNbt); + nameTag4.setDisplayName(new StringTextComponent("Hello World")); + stacks.put(new AESharedItemStack(nameTag4), "nbt1+cap1"); + + // NBT + Different Cap + CompoundNBT capNbt2 = new CompoundNBT(); + capNbt2.putInt("Parent", 123); + ItemStack nameTag5 = new ItemStack(TEST_ITEM, 1, capNbt2); + nameTag5.setDisplayName(new StringTextComponent("Hello World")); + stacks.put(new AESharedItemStack(nameTag5), "nbt1+cap2"); + } + + /** + * Tests equality between shared item stacks. + */ + @Test + void testEquals() { + EqualsTester tester = new EqualsTester(); + for (AESharedItemStack stack : stacks.keySet()) { + // Add the stack, and a pristine copy of the stack + tester.addEqualityGroup(stack, new AESharedItemStack(stack.getDefinition().copy())); + } + + // Test that using the same item stack instance makes two separate shared stacks equal + ItemStack itemStack = new ItemStack(Items.CRAFTING_TABLE); + tester.addEqualityGroup( + new AESharedItemStack(itemStack), + new AESharedItemStack(itemStack)); + + tester.testEquals(); + } + +} \ No newline at end of file diff --git a/src/test/java/appeng/util/item/FuzzyItemVariantListTest.java b/src/test/java/appeng/util/item/FuzzyItemVariantListTest.java new file mode 100644 index 000000000..cf0885992 --- /dev/null +++ b/src/test/java/appeng/util/item/FuzzyItemVariantListTest.java @@ -0,0 +1,157 @@ +package appeng.util.item; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.Arrays; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; + +import appeng.api.config.FuzzyMode; + +public class FuzzyItemVariantListTest { + + @Test + void testOrderForDamagedItems() { + // Diamond Sword @ 100% durability + ItemStack undamagedSword = new ItemStack(Items.DIAMOND_SWORD); + AESharedItemStack undamagedStack = new AESharedItemStack(undamagedSword); + + // Unenchanted Diamond Sword @ 0% durability + ItemStack damagedSword = new ItemStack(Items.DIAMOND_SWORD); + damagedSword.setDamage(damagedSword.getMaxDamage()); + AESharedItemStack damagedStack = new AESharedItemStack(damagedSword); + + // Create a list of stacks and sort by their natural order + AESharedItemStack[] stacks = new AESharedItemStack[] { + damagedStack, undamagedStack + }; + Arrays.sort(stacks, FuzzyItemVariantList.COMPARATOR); + assertThat(stacks).containsExactly(damagedStack, undamagedStack); + } + + @Nested + class Bounds { + final ItemStack stack = new ItemStack(Items.DIAMOND_SWORD); + final ItemStack damagedStack; + + { + damagedStack = stack.copy(); + damagedStack.setDamage(damagedStack.getMaxDamage()); + } + + @Test + void testIgnoreAll() { + DamageBounds bounds = new DamageBounds(stack, FuzzyMode.IGNORE_ALL); + assertEquals(stack.getMaxDamage(), bounds.lower.itemDamage); + assertEquals(-1, bounds.upper.itemDamage); + } + + /** + * PERCENT_99 with an undamaged item should select only undamaged items, which translates to a damage range of + * [0, -1). + */ + @Test + void test99PercentDurabilityWithUndamagedItem() { + DamageBounds bounds = new DamageBounds(stack, FuzzyMode.PERCENT_99); + assertEquals(0, bounds.lower.itemDamage); + assertEquals(-1, bounds.upper.itemDamage); + } + + /** + * PERCENT_99 with a damaged item should select only damaged items, which translates to a damage range of + * [maxDmg, 0). + */ + @Test + void test99PercentDurabilityWithDamagedItem() { + DamageBounds bounds = new DamageBounds(damagedStack, FuzzyMode.PERCENT_99); + assertEquals(stack.getMaxDamage(), bounds.lower.itemDamage); + assertEquals(0, bounds.upper.itemDamage); + } + + /** + * PERCENT_75 with an undamaged item should select items that have 75% or more durability, which should + * translate to a damage range of [0.25*maxDmg, -1). + */ + @Test + void test75PercentWithUndamagedItem() { + DamageBounds bounds = new DamageBounds(stack, FuzzyMode.PERCENT_75); + assertEquals((int) (0.25 * stack.getMaxDamage()), bounds.lower.itemDamage); + assertEquals(-1, bounds.upper.itemDamage); + } + + /** + * PERCENT_75 with a damaged item should select items that have less than 75% durability, which should translate + * to a damage range of [maxDmg, 0.25*maxDmg). + */ + @Test + void test75PercentWithDamagedItem() { + DamageBounds bounds = new DamageBounds(damagedStack, FuzzyMode.PERCENT_75); + assertEquals(stack.getMaxDamage(), bounds.lower.itemDamage); + assertEquals((int) (0.25 * stack.getMaxDamage()), bounds.upper.itemDamage); + } + + /** + * PERCENT_50 with an undamaged item should select items that have 50% or more durability, which should + * translate to a damage range of [0.50*maxDmg, -1). + */ + @Test + void test50PercentWithUndamagedItem() { + DamageBounds bounds = new DamageBounds(stack, FuzzyMode.PERCENT_50); + assertEquals((int) (0.50 * stack.getMaxDamage()), bounds.lower.itemDamage); + assertEquals(-1, bounds.upper.itemDamage); + } + + /** + * PERCENT_50 with a damaged item should select items that have less than 50% durability, which should translate + * to a damage range of [maxDmg, 0.50*maxDmg). + */ + @Test + void test50PercentWithDamagedItem() { + DamageBounds bounds = new DamageBounds(damagedStack, FuzzyMode.PERCENT_50); + assertEquals(stack.getMaxDamage(), bounds.lower.itemDamage); + assertEquals((int) (0.50 * stack.getMaxDamage()), bounds.upper.itemDamage); + } + + /** + * PERCENT_25 with an undamaged item should select items that have 25% or more durability, which should + * translate to a damage range of [0.75*maxDmg, -1). + */ + @Test + void test25PercentWithUndamagedItem() { + DamageBounds bounds = new DamageBounds(stack, FuzzyMode.PERCENT_25); + assertEquals((int) (0.75 * stack.getMaxDamage()), bounds.lower.itemDamage); + assertEquals(-1, bounds.upper.itemDamage); + } + + /** + * PERCENT_25 with a damaged item should select items that have less than 25% durability, which should translate + * to a damage range of [maxDmg, 0.75*maxDmg). + */ + @Test + void test25PercentWithDamagedItem() { + DamageBounds bounds = new DamageBounds(damagedStack, FuzzyMode.PERCENT_25); + assertEquals(stack.getMaxDamage(), bounds.lower.itemDamage); + assertEquals((int) (0.75 * stack.getMaxDamage()), bounds.upper.itemDamage); + } + + } + + private static class DamageBounds { + final FuzzyItemVariantList.ItemDamageBound lower; + final FuzzyItemVariantList.ItemDamageBound upper; + + public DamageBounds(ItemStack stack, FuzzyMode mode) { + lower = FuzzyItemVariantList.makeLowerBound(stack, mode); + upper = FuzzyItemVariantList.makeUpperBound(stack, mode); + + // This may be counter intuitive, but the map is sorted in descending order of item damage + assertThat(lower.itemDamage).isGreaterThan(upper.itemDamage); + } + } + +} diff --git a/src/test/java/appeng/util/item/ItemListTest.java b/src/test/java/appeng/util/item/ItemListTest.java new file mode 100644 index 000000000..2d3dcfa0b --- /dev/null +++ b/src/test/java/appeng/util/item/ItemListTest.java @@ -0,0 +1,452 @@ +package appeng.util.item; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterators; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import net.minecraft.util.text.StringTextComponent; + +import appeng.api.config.FuzzyMode; +import appeng.api.storage.data.IAEItemStack; + +public class ItemListTest { + + ItemList itemList = new ItemList(); + + /** + * add should merge item stacks by adding stored/requestable counts, and setting craftable if it wasn't set before. + */ + @Test + public void testAddMergesAllStackProperties() { + itemList.add(diamondSword(100, 1, 0, false)); + assertPreciseStackProperties(diamondSwordFilter(100), 1, 0, false); + + itemList.add(diamondSword(100, 0, 1, false)); + assertPreciseStackProperties(diamondSwordFilter(100), 1, 1, false); + + itemList.add(diamondSword(100, 0, 0, true)); + assertPreciseStackProperties(diamondSwordFilter(100), 1, 1, true); + } + + /** + * addStorage only considers {@link IAEItemStack#getStackSize()} and ignores other properties when merging stacks, + * but inherits all properties when it's adding a new item. + */ + @Test + public void testAddStorageForNewItem() { + // TODO: This might actually be incorrect, given how addrequestable et al behave + itemList.addStorage(diamondSword(100, 1, 1, true)); + assertPreciseStackProperties(diamondSwordFilter(100), 1, 1, true); + } + + @Test + public void testAddStorageForExistingItem() { + itemList.addStorage(diamondSword(100, 1, 0, false)); + itemList.addStorage(diamondSword(100, 1, 2, true)); + assertPreciseStackProperties(diamondSwordFilter(100), 2, 0, false); + } + + /** + * addRequestable only considers {@link IAEItemStack#getCountRequestable()} and sets the stored amount to 0 and + * craftable to false when adding an item. + */ + @Test + public void testAddRequestableForNewItem() { + itemList.addRequestable(diamondSword(100, 1, 2, true)); + assertPreciseStackProperties(diamondSwordFilter(100), 0, 2, false); + } + + /** + * addRequestable only considers {@link IAEItemStack#getCountRequestable()} when merging into an existing item. + */ + @Test + public void testAddRequestableForExistingItem() { + itemList.addRequestable(diamondSword(100, 0, 1, false)); + itemList.addRequestable(diamondSword(100, 2, 1, true)); + assertPreciseStackProperties(diamondSwordFilter(100), 0, 2, false); + } + + /** + * addCraftable only considers {@link IAEItemStack#isCraftable()} and sets the stored and requestable amounts to 0 + * when adding an item. + */ + @Test + public void testAddCraftingForNewItem() { + itemList.addCrafting(diamondSword(100, 1, 2, true)); + // TODO: I think it is unintended that the requestable amount is used + assertPreciseStackProperties(diamondSwordFilter(100), 0, 2, true); + } + + /** + * addRequestable only considers {@link IAEItemStack#getCountRequestable()} when merging into an existing item. + */ + @Test + public void testAddCraftingForExistingItem() { + itemList.addCrafting(diamondSword(100, 0, 0, false)); + itemList.addCrafting(diamondSword(100, 1, 2, true)); + assertPreciseStackProperties(diamondSwordFilter(100), 0, 0, true); + } + + /** + * an empty craftable stack still creates an entry in the list + */ + @Test + public void testAddEmptyStackThatIsCraftable() { + itemList.add(diamondSword(100, 0, 0, true)); + assertPreciseStackProperties(diamondSwordFilter(100), 0, 0, true); + } + + /** + * check that craftable isn't accidentally reset to false when merging stacks + */ + @Test + public void testAddDoesNotResetCraftableBackToFalse() { + itemList.add(diamondSword(100, 0, 0, true)); + itemList.add(diamondSword(100, 1, 0, false)); + assertPreciseStackProperties(diamondSwordFilter(100), 1, 0, true); + } + + /** + * stacks for the same item, but different damage values should not be merged + */ + @Test + public void testAddDoesNotMergeAcrossDamageValues() { + itemList.add(diamondSword(100, 1, 0, false)); + itemList.add(diamondSword(99, 1, 0, false)); + assertPreciseStackProperties(diamondSwordFilter(100), 1, 0, false); + assertPreciseStackProperties(diamondSwordFilter(99), 1, 0, false); + } + + private void assertPreciseStackProperties(IAEItemStack stack, long stored, long requestable, boolean craftable) { + IAEItemStack storedStack = itemList.findPrecise(stack); + assertEquals(stored, storedStack.getStackSize(), "stored amount"); + assertEquals(requestable, storedStack.getCountRequestable(), "requestable amount"); + assertEquals(craftable, storedStack.isCraftable(), "craftable"); + } + + /** + * Even if the stack has no stored or requestable amounts, it should be returned by the item list if it is + * craftable. + */ + @Test + public void testSizeAndIterateForEmptyButCraftableStack() { + itemList.add(diamondSword(100, 0, 0, true)); + assertListContent(diamondSword(100, 0, 0, true)); + } + + /** + * If the stack is not craftable, an empty stack is actually ignored. + */ + @Test + public void testEmptyStackIsIgnored() { + itemList.add(diamondSword(100, 0, 0, false)); + assertListContent(); + } + + @Test + public void testResetStatus() { + itemList.add(diamondSword(100, 1, 0, false)); + itemList.add(nameTag(1, 0, false)); + assertEquals(2, itemList.size()); + + itemList.resetStatus(); + + assertListContent(); // The list should now be empty + } + + /** + * Tests that iteration across multiple items and variations of those items works. + */ + @Test + public void testIterateAcrossMultipleItems() { + // Add damaged variants of the same item, including NBT variants + AEItemStack sword1 = diamondSword(100, 1, 0, false); + itemList.add(sword1); + AEItemStack sword2 = diamondSword(50, 1, 0, false); + itemList.add(sword2); + AEItemStack sword3 = diamondSword(25, 1, 0, false); + itemList.add(sword3); + AEItemStack sword4 = diamondSword(100, "master sword", 1, 0, false); + itemList.add(sword4); + // And a non-damagable item with different NBT + AEItemStack nameTag1 = nameTag(1, 0, false); + itemList.add(nameTag1); + AEItemStack nameTag2 = nameTag("bob", 1, 0, false); + itemList.add(nameTag2); + + assertListContent(sword1, sword2, sword3, sword4, nameTag1, nameTag2); + } + + @Test + void testConcurrentModificationByAddingItemType() { + AEItemStack sword = diamondSword(100, 1, 0, false); + itemList.add(sword); + AEItemStack nameTag = nameTag(1, 0, false); + itemList.add(nameTag); + AEItemStack craftingTable = AEItemStack.fromItemStack(new ItemStack(Items.CRAFTING_TABLE)); + + assertThrows(ConcurrentModificationException.class, () -> { + Iterator it = itemList.iterator(); + itemList.add(craftingTable); + assertThat(Iterators.toArray(it, IAEItemStack.class)).containsOnly(sword, nameTag); + }); + } + + /** + * Regression test for broken iterators that mutated state in {@see Iterator#hasNext}. This was the case for both + * the top-level and sub-iterator. + */ + @Test + public void testIteratorHasNextDoesNotSkipItems() { + itemList.add(diamondSword(100, 1, 0, false)); + itemList.add(diamondSword(50, 1, 0, false)); + + Iterator it = itemList.iterator(); + assertTrue(it.hasNext()); + assertTrue(it.hasNext()); + assertTrue(it.hasNext()); + } + + @Test + public void testGetFirstItemForEmptyList() { + assertNull(itemList.getFirstItem()); + } + + @Test + public void testGetFirstItem() { + AEItemStack itemStack = diamondSword(100, 1, 0, false); + itemList.add(itemStack); + // The order is no longer well defined w.r.t. the hashmap + assertEquals(itemStack, itemList.getFirstItem()); + } + + @Nested + class FindFuzzyDamageableItems { + + // Swords to cover all durability values + AEItemStack swordAbove100 = diamondSword(101, 1, 0, false); + AEItemStack[] swords = new AEItemStack[101]; + // Filters for inverting the filter as needed + AEItemStack undamagedFilter = diamondSwordFilter(100); + AEItemStack damagedFilter = diamondSwordFilter(0); + + @BeforeEach + void addItems() { + itemList.add(swordAbove100); + for (int i = 0; i <= 100; i++) { + swords[i] = diamondSword(i, 1, 0, false); + assertEquals(i, getDurabilityPercent(swords[i])); + itemList.add(swords[i]); + } + } + + @Test + public void testIgnoreAllWithUndamagedFilter() { + assertReturnedDurabilities(undamagedFilter, FuzzyMode.IGNORE_ALL, 0, 100, false); + } + + @Test + public void testIgnoreAllWithDamagedFilter() { + assertReturnedDurabilities(damagedFilter, FuzzyMode.IGNORE_ALL, 0, 100, false); + } + + @Test + public void testPercent99WithUndamagedFilter() { + assertReturnedDurabilities(undamagedFilter, FuzzyMode.PERCENT_99, 100, 100, false); + } + + @Test + public void testPercent99WithDamagedFilter() { + assertReturnedDurabilities(damagedFilter, FuzzyMode.PERCENT_99, 0, 99, false); + } + + @Test + public void testPercent75WithUndamagedFilter() { + assertReturnedDurabilities(undamagedFilter, FuzzyMode.PERCENT_75, 75, 100, false); + } + + @Test + public void testPercent75WithDamagedFilter() { + assertReturnedDurabilities(damagedFilter, FuzzyMode.PERCENT_75, 0, 74, false); + } + + @Test + public void testPercent50WithUndamagedFilter() { + assertReturnedDurabilities(undamagedFilter, FuzzyMode.PERCENT_50, 50, 100, false); + } + + @Test + public void testPercent50WithDamagedFilter() { + assertReturnedDurabilities(damagedFilter, FuzzyMode.PERCENT_50, 0, 49, false); + } + + @Test + public void testPercent25WithUndamagedFilter() { + assertReturnedDurabilities(undamagedFilter, FuzzyMode.PERCENT_25, 25, 100, false); + } + + @Test + public void testPercent25WithDamagedFilter() { + assertReturnedDurabilities(damagedFilter, FuzzyMode.PERCENT_25, 0, 24, false); + } + + private void assertReturnedDurabilities(IAEItemStack filter, FuzzyMode fuzzyMode, int minDurabilityInclusive, + int maxDurabilityInclusive, boolean above100) { + Collection items = itemList.findFuzzy(filter, fuzzyMode); + + // Build a list of the durabilities that got returned + List durabilities = items.stream().map(this::getDurabilityPercent) + .sorted() + .collect(Collectors.toList()); + + // Build a sorted list of the durabilities we expect + List expectedDurabilities = new ArrayList<>(); + for (int i = minDurabilityInclusive; i <= maxDurabilityInclusive; i++) { + expectedDurabilities.add(getDurabilityPercent(swords[i])); + } + if (above100) { + expectedDurabilities.add(getDurabilityPercent(swordAbove100)); + } + expectedDurabilities.sort(Integer::compare); + + assertEquals(expectedDurabilities, durabilities); + } + + private int getDurabilityPercent(IAEItemStack stack) { + if (stack == swordAbove100) { + return 101; + } + return (int) ((1.0f - (stack.getItemDamage() / (float) stack.getDefinition().getMaxDamage())) * 100); + } + } + + @Test + void testFindFuzzyForNormalItems() { + AEItemStack item1 = nameTag(null, 1, 0, false); + itemList.add(item1); + AEItemStack item2 = nameTag("name1", 1, 0, false); + itemList.add(item2); + AEItemStack item3 = nameTag("name2", 1, 0, false); + itemList.add(item3); + // Add another item to ensure this is not returned + itemList.add(AEItemStack.fromItemStack(new ItemStack(Items.CRAFTING_TABLE))); + + for (FuzzyMode fuzzyMode : FuzzyMode.values()) { + Collection result = itemList.findFuzzy(nameTag(null, 0, 0, false), fuzzyMode); + assertThat(result).containsOnly(item1, item2, item3); + } + } + + /** + * Tests how ItemList behaves w.r.t. null arguments, given that {@link AEItemStack#fromItemStack(ItemStack)} can + * return null for an empty stack, this sometimes leaks into method parameters. As such, methods should behave as if + * an empty item stack was passed. + */ + @Nested + class NullArguments { + @BeforeEach + void addItem() { + itemList.add(diamondSword(100, 1, 0, false)); + } + + @Test + void testFindFuzzy() { + assertThat(itemList.findFuzzy(null, FuzzyMode.PERCENT_99)).isEmpty(); + } + + @Test + void testFindPrecise() { + assertThat(itemList.findPrecise(null)).isNull(); + } + + @Test + void testAdd() { + assertThat(itemList.size()).isEqualTo(1); + itemList.add(null); + assertThat(itemList.size()).isEqualTo(1); + } + + @Test + void testAddStorage() { + assertThat(itemList.size()).isEqualTo(1); + itemList.addStorage(null); + assertThat(itemList.size()).isEqualTo(1); + } + + @Test + void testAddRequestable() { + assertThat(itemList.size()).isEqualTo(1); + itemList.addRequestable(null); + assertThat(itemList.size()).isEqualTo(1); + } + + @Test + void testAddCrafting() { + assertThat(itemList.size()).isEqualTo(1); + itemList.addCrafting(null); + assertThat(itemList.size()).isEqualTo(1); + } + } + + private void assertListContent(AEItemStack... stacks) { + assertEquals(stacks.length == 0, itemList.isEmpty(), "isEmpty"); + assertEquals(stacks.length, itemList.size()); + assertEquals(ImmutableSet.copyOf(stacks), ImmutableSet.copyOf(itemList)); + } + + private AEItemStack diamondSwordFilter(int durabilityPercent) { + return diamondSword(durabilityPercent, 0, 0, false); + } + + private AEItemStack diamondSword(int durabilityPercent, long stored, long requestable, boolean craftable) { + return diamondSword(durabilityPercent, null, stored, requestable, craftable); + } + + private AEItemStack diamondSword(int durabilityPercent, String customName, long stored, long requestable, + boolean craftable) { + ItemStack is = new ItemStack(Items.DIAMOND_SWORD); + if (customName != null) { + is.setDisplayName(new StringTextComponent(customName)); + } + int damage = (int) ((100 - durabilityPercent) / 100.0f * is.getMaxDamage()); + is.setDamage(damage); + AEItemStack ais = AEItemStack.fromItemStack(is); + ais.setStackSize(stored); + ais.setCountRequestable(requestable); + ais.setCraftable(craftable); + return ais; + } + + // customName can be used to create items that differ in NBT + private AEItemStack nameTag(long stored, long requestable, boolean craftable) { + return nameTag(null, stored, requestable, craftable); + } + + private AEItemStack nameTag(String customName, long stored, long requestable, boolean craftable) { + ItemStack is = new ItemStack(Items.NAME_TAG); + if (customName != null) { + is.setDisplayName(new StringTextComponent(customName)); + } + AEItemStack ais = AEItemStack.fromItemStack(is); + ais.setStackSize(stored); + ais.setCountRequestable(requestable); + ais.setCraftable(craftable); + return ais; + } + +} \ No newline at end of file diff --git a/src/test/java/appeng/util/item/TestItemWithCaps.java b/src/test/java/appeng/util/item/TestItemWithCaps.java new file mode 100644 index 000000000..9ece399ae --- /dev/null +++ b/src/test/java/appeng/util/item/TestItemWithCaps.java @@ -0,0 +1,60 @@ +package appeng.util.item; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompoundNBT; +import net.minecraft.nbt.IntNBT; +import net.minecraft.util.Direction; +import net.minecraftforge.common.capabilities.Capability; +import net.minecraftforge.common.capabilities.ICapabilityProvider; +import net.minecraftforge.common.util.INBTSerializable; +import net.minecraftforge.common.util.LazyOptional; + +public class TestItemWithCaps extends Item { + public TestItemWithCaps() { + super(new Properties()); + setRegistryName("appliedenergistics2:test_item"); + } + + @Nullable + @Override + public ICapabilityProvider initCapabilities(ItemStack stack, @Nullable CompoundNBT nbt) { + if (nbt == null) { + return null; + } else { + return new CapabilityProvider(); + } + } + + /** + * Simple capability provider that just has a single counter value to produce different NBT. + */ + public static class CapabilityProvider implements ICapabilityProvider, INBTSerializable { + private int counter; + + @Nonnull + @Override + public LazyOptional getCapability(@Nonnull Capability cap, @Nullable Direction side) { + return LazyOptional.empty(); + } + + @Nonnull + @Override + public LazyOptional getCapability(@Nonnull Capability cap) { + return LazyOptional.empty(); + } + + @Override + public IntNBT serializeNBT() { + return IntNBT.valueOf(counter); + } + + @Override + public void deserializeNBT(IntNBT nbt) { + counter = nbt.getInt(); + } + } +}