/** * Copyright (c) 2005-2017, KoLmafia development team * http://kolmafia.sourceforge.net/ * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * [1] Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * [2] Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in * the documentation and/or other materials provided with the * distribution. * [3] Neither the name "KoLmafia" nor the names of its contributors may * be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ package net.sourceforge.kolmafia.persistence; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import net.sourceforge.kolmafia.AdventureResult; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLConstants.CraftingType; import net.sourceforge.kolmafia.KoLConstants.MafiaState; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.KoLmafiaCLI; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.objectpool.IntegerPool; import net.sourceforge.kolmafia.objectpool.ItemPool; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.request.CombineMeatRequest; import net.sourceforge.kolmafia.request.CreateItemRequest; import net.sourceforge.kolmafia.utilities.StringUtilities; public class ItemFinder { public static final int ANY_MATCH = 1; public static final int FOOD_MATCH = 2; public static final int BOOZE_MATCH = 3; public static final int SPLEEN_MATCH = 4; public static final int USE_MATCH = 5; public static final int CREATE_MATCH = 6; public static final int UNTINKER_MATCH = 7; public static final int EQUIP_MATCH = 8; public static final int CANDY_MATCH = 9; public static final int ABSORB_MATCH = 10; public static final int ROBO_MATCH = 11; public static final List<String> getMatchingNames( String searchString ) { return ItemDatabase.getMatchingNames( searchString ); } public static final String getFirstMatchingItemName( List<String> nameList, String searchString ) { return ItemFinder.getFirstMatchingItemName( nameList, searchString, ItemFinder.ANY_MATCH ); } public static final String getFirstMatchingItemName( List<String> nameList, String searchString, int filterType ) { if ( nameList == null || nameList.isEmpty() ) { return null; } // Filter the list ItemFinder.filterNameList( nameList, filterType ); if ( nameList.isEmpty() ) { return null; } // If there are multiple matches, such that one is a substring of the // others, choose the shorter one, on the grounds that the user would // have included part of the unique section of the longer name if that // was the item they actually intended. This makes it easier to refer // to non-clockwork in-a-boxes, and DoD potions by flavor. while ( nameList.size() >= 2 ) { String name0 = nameList.get( 0 ); String name1 = nameList.get( 1 ); if ( name0.contains( name1 ) ) { nameList.remove( 0 ); } else if ( name1.contains( name0 ) ) { nameList.remove( 1 ); } else break; } // If a single item remains, that's it! if ( nameList.size() == 1 ) { return ItemDatabase.getCanonicalName( nameList.get( 0 ) ); } // Remove duplicate names that all refer to the same item? Set<Integer> itemIdSet = new HashSet<Integer>(); int pseudoItems = 0; for ( int i = 0; i < nameList.size(); ++i ) { int itemId = ItemDatabase.getItemId( nameList.get( i ) ); if ( itemId == -1 ) { pseudoItems += 1; } else { itemIdSet.add( IntegerPool.get( itemId ) ); } } if ( ( pseudoItems + itemIdSet.size() ) == 1 ) { return ItemDatabase.getCanonicalName( nameList.get( 0 ) ); } String itemName; String rv = null; // Candy hearts, snowcones and cupcakes take precedence over // all the other items in the game, IF exactly one such item // matches. for ( int i = 0; i < nameList.size(); ++i ) { itemName = nameList.get( i ); if ( !itemName.startsWith( "pix" ) && itemName.endsWith( "candy heart" ) ) { if ( rv != null ) return ""; rv = ItemDatabase.getCanonicalName( itemName ); } } for ( int i = 0; i < nameList.size(); ++i ) { itemName = nameList.get( i ); if ( !itemName.startsWith( "abo" ) && !itemName.startsWith( "yel" ) && itemName.endsWith( "snowcone" ) ) { if ( rv != null ) return ""; rv = ItemDatabase.getCanonicalName( itemName ); } } for ( int i = 0; i < nameList.size(); ++i ) { itemName = nameList.get( i ); if ( itemName.endsWith( "cupcake" ) ) { if ( rv != null ) return ""; rv = ItemDatabase.getCanonicalName( itemName ); } } if ( rv != null ) return rv; // If we get here, there is not a single matching item return ""; } private static final void filterNameList( List<String> nameList, int filterType ) { if ( filterType != ItemFinder.FOOD_MATCH && filterType != ItemFinder.BOOZE_MATCH && filterType != ItemFinder.SPLEEN_MATCH && filterType != ItemFinder.CANDY_MATCH ) { // First, check to see if there are an HP/MP restores // in the list of matches. If there are, only return // the restorative items (the others are irrelevant). ArrayList<String> restoreList = new ArrayList<String>(); for ( int i = 0; i < nameList.size(); ++i ) { String itemName = nameList.get( i ); int itemId = ItemDatabase.getItemId( itemName ); if ( RestoresDatabase.isRestore( itemId ) ) { restoreList.add( itemName ); } } if ( !restoreList.isEmpty() ) { nameList.clear(); nameList.addAll( restoreList ); } } // Check for consumption filters when matching against the // item name. Iterator<String> nameIterator = nameList.iterator(); while ( nameIterator.hasNext() ) { String itemName = nameIterator.next(); int itemId = ItemDatabase.getItemId( itemName ); if ( filterType == ItemFinder.CREATE_MATCH || filterType == ItemFinder.UNTINKER_MATCH ) { CraftingType mixMethod = ConcoctionDatabase.getMixingMethod( itemId, itemName ); boolean condition = ( filterType == ItemFinder.CREATE_MATCH ) ? ( mixMethod == CraftingType.NOCREATE && CombineMeatRequest.getCost( itemId ) == 0 ) : ( mixMethod != CraftingType.COMBINE && mixMethod != CraftingType.JEWELRY ); ItemFinder.conditionalRemove( nameIterator, condition ); continue; } int useType = ItemDatabase.getConsumptionType( itemId ); switch ( filterType ) { case ItemFinder.FOOD_MATCH: ItemFinder.conditionalRemove( nameIterator, useType != KoLConstants.CONSUME_EAT && useType != KoLConstants.CONSUME_FOOD_HELPER ); break; case ItemFinder.BOOZE_MATCH: ItemFinder.conditionalRemove( nameIterator, useType != KoLConstants.CONSUME_DRINK && useType != KoLConstants.CONSUME_DRINK_HELPER ); break; case ItemFinder.SPLEEN_MATCH: ItemFinder.conditionalRemove( nameIterator, useType != KoLConstants.CONSUME_SPLEEN ); break; case ItemFinder.EQUIP_MATCH: switch ( useType ) { case KoLConstants.EQUIP_FAMILIAR: case KoLConstants.EQUIP_ACCESSORY: case KoLConstants.EQUIP_HAT: case KoLConstants.EQUIP_PANTS: case KoLConstants.EQUIP_SHIRT: case KoLConstants.EQUIP_WEAPON: case KoLConstants.EQUIP_OFFHAND: case KoLConstants.EQUIP_CONTAINER: case KoLConstants.CONSUME_STICKER: case KoLConstants.CONSUME_CARD: case KoLConstants.CONSUME_FOLDER: case KoLConstants.CONSUME_BOOTSKIN: case KoLConstants.CONSUME_BOOTSPUR: case KoLConstants.CONSUME_SIXGUN: break; default: nameIterator.remove(); } break; case ItemFinder.CANDY_MATCH: ItemFinder.conditionalRemove( nameIterator, !ItemDatabase.isCandyItem( itemId ) ); break; case ItemFinder.ABSORB_MATCH: ItemFinder.conditionalRemove( nameIterator, ( ItemDatabase.getNoobSkillId( itemId ) == -1 && !( ItemDatabase.isEquipment( itemId ) && !ItemDatabase.isFamiliarEquipment( itemId ) ) ) ); break; case ItemFinder.ROBO_MATCH: ItemFinder.conditionalRemove( nameIterator, itemId < ItemPool.LITERAL_GRASSHOPPER || itemId > ItemPool.PHIL_COLLINS || Preferences.getString( "_roboDrinks" ).contains( itemName ) ); break; case ItemFinder.USE_MATCH: ItemFinder.conditionalRemove( nameIterator, !ItemDatabase.isUsable( itemId ) ); break; } } if ( nameList.size() == 1 || filterType == ItemFinder.CREATE_MATCH || filterType == ItemFinder.UNTINKER_MATCH ) { return; } // Never match against (non-quest) untradeable items not available // in NPC stores when other items are possible. // This can be overridden by adding "matchable" as a secondary // use; this is needed for untradeables that do need to be // explicitly referred to, and have names similar to other items // (such as the NS Tower keys). // If this process results in filtering EVERYTHING in our list, that's not helpful. // Make a backup of nameList to restore from in such a case. List<String> nameListCopy = new ArrayList<String>(nameList); nameIterator = nameList.iterator(); while ( nameIterator.hasNext() ) { String itemName = nameIterator.next(); int itemId = ItemDatabase.getItemId( itemName ); conditionalRemove( nameIterator, itemId != -1 && !ItemDatabase.getAttribute( itemId, ItemDatabase.ATTR_TRADEABLE | ItemDatabase.ATTR_MATCHABLE | ItemDatabase.ATTR_QUEST ) && !NPCStoreDatabase.contains( itemId ) ); } // restore from last step iff we filtered _everything_ if ( nameList.isEmpty() ) { nameList.addAll( nameListCopy ); } } private static final void conditionalRemove( Iterator<String> iterator, boolean condition ) { if ( condition ) { iterator.remove(); } } /** * Utility method which determines the first item which matches the given parameter string. Note that the string may * also specify an item quantity before the string. */ public static final AdventureResult getFirstMatchingItem( String parameters ) { return ItemFinder.getFirstMatchingItem( parameters, true, null, ItemFinder.ANY_MATCH ); } public static final AdventureResult getFirstMatchingItem( String parameters, int filterType ) { return ItemFinder.getFirstMatchingItem( parameters, true, null, filterType ); } public static final AdventureResult getFirstMatchingItem( String parameters, boolean errorOnFailure ) { return ItemFinder.getFirstMatchingItem( parameters, errorOnFailure, null, ItemFinder.ANY_MATCH ); } public static final AdventureResult getFirstMatchingItem( String parameters, boolean errorOnFailure, int filterType ) { return getFirstMatchingItem( parameters, errorOnFailure, null, filterType ); } public static final AdventureResult getFirstMatchingItem( String parameters, boolean errorOnFailure, List<AdventureResult> sourceList, int filterType ) { // Ignore spaces and tabs in front of the parameter string parameters = parameters.trim(); // If there are no valid strings passed in, return if ( parameters.length() == 0 ) { if ( errorOnFailure ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Need to provide an item to match." ); } return null; } // Find the item id int itemCount = 1; int itemId = -1; // Allow the person to ask for all of the item from the source if ( parameters.charAt( 0 ) == '*' ) { itemCount = 0; parameters = parameters.substring( 1 ).trim(); } List<String> matchList; if ( parameters.contains( "\u00B6" ) || parameters.contains( "[" ) ) { // At least one item is specified by item ID if ( parameters.contains( "," ) ) { // We can't parse multiple items of this sort if ( errorOnFailure ) { KoLmafia.updateDisplay( MafiaState.ERROR, "More than one item specified by item ID." ); } return null; } int spaceIndex = parameters.indexOf( ' ' ); if ( spaceIndex != -1 ) { String itemCountString = parameters.substring( 0, spaceIndex ); if ( StringUtilities.isNumeric( itemCountString ) ) { itemCount = StringUtilities.parseInt( itemCountString ); parameters = parameters.substring( spaceIndex + 1 ).trim(); } } // KoL has an item whose name includes a pilcrow // character. Handle it String name = parameters; // If the pilcrow character is first, it is followed by an item ID if ( name.startsWith( "\u00B6" ) ) { itemId = StringUtilities.parseInt( parameters.substring( 1 ) ); } else if ( name.startsWith( "[" ) ) { int index = name.indexOf( "]" ); if ( index == -1 ) { return null; } itemId = StringUtilities.parseInt( name.substring( 1, index ) ); } else if ( ItemDatabase.getItemId( parameters, 1 ) == -1 ) { // This is not the item with a pilcrow character if ( errorOnFailure ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Unknown item " + name ); } return null; } matchList = new ArrayList<String>(); if ( itemId != -1 ) { matchList.add( "[" + itemId + "]" ); } else { matchList.add( name ); } } else if ( ItemDatabase.getItemId( parameters, 1 ) != -1 ) { // The entire parameter is a single item matchList = new ArrayList<String>(); matchList.add( ItemDatabase.getCanonicalName( ItemDatabase.getItemId( parameters, 1 ) ) ); } else { int spaceIndex = parameters.indexOf( ' ' ); if ( spaceIndex != -1 ) { String itemCountString = parameters.substring( 0, spaceIndex ); if ( StringUtilities.isNumeric( itemCountString ) ) { itemCount = StringUtilities.parseInt( itemCountString ); parameters = parameters.substring( spaceIndex + 1 ).trim(); } } // This is not right for "1 seal tooth, 2 turtle totem, 3 stolen accordion" // since the first count is trimmed off matchList = ItemFinder.getMatchingNames( parameters ); } String itemName = ItemFinder.getFirstMatchingItemName( matchList, parameters, filterType ); if ( itemName == null ) { if ( errorOnFailure ) { String error; switch ( filterType ) { case ANY_MATCH: default: error = " has no matches."; break; case FOOD_MATCH: error = " cannot be eaten."; break; case BOOZE_MATCH: error = " cannot be drunk."; break; case SPLEEN_MATCH: error = " cannot be chewed."; break; case USE_MATCH: error = " cannot be used."; break; case CREATE_MATCH: error = " cannot be created."; break; case UNTINKER_MATCH: error = " cannot be untinkered."; break; case EQUIP_MATCH: error = " cannot be equipped."; break; case CANDY_MATCH: error = " is not candy."; break; case ABSORB_MATCH: error = " cannot be absorbed."; break; } KoLmafia.updateDisplay( MafiaState.ERROR, "[" + parameters + "]" + error ); } return null; } if ( itemName.equals( "" ) ) { if ( errorOnFailure ) { RequestLogger.printList( matchList ); RequestLogger.printLine(); KoLmafia.updateDisplay( MafiaState.ERROR, "[" + parameters + "] has too many matches." ); } return null; } AdventureResult firstMatch = null; if ( itemId != -1 ) { firstMatch = ItemPool.get( itemId, itemCount ); } else { firstMatch = ItemPool.get( itemName, itemCount ); } // The result also depends on the number of items which // are available in the given match area. int matchCount; if ( filterType == ItemFinder.CREATE_MATCH ) { boolean skipNPCs = Preferences.getBoolean( "autoSatisfyWithNPCs" ) && itemCount <= 0; if ( skipNPCs ) { // Let '*' and negative counts be interpreted // relative to the quantity that can be created // with on-hand ingredients. Preferences.setBoolean( "autoSatisfyWithNPCs", false ); ConcoctionDatabase.refreshConcoctionsNow(); } CreateItemRequest instance = CreateItemRequest.getInstance( firstMatch ); matchCount = instance == null ? 0 : instance.getQuantityPossible(); if ( skipNPCs ) { Preferences.setBoolean( "autoSatisfyWithNPCs", true ); ConcoctionDatabase.refreshConcoctionsNow(); } } else if ( sourceList == null ) { // Default to number in inventory if count was "*" (all) // or negative (all but that many) and no list was given. matchCount = itemCount <= 0 ? firstMatch.getCount( KoLConstants.inventory ) : 1; } else { matchCount = firstMatch.getCount( sourceList ); } // If the person wants all except a certain quantity, update // the item count. if ( itemCount <= 0 ) { itemCount = matchCount + itemCount; firstMatch = firstMatch.getInstance( itemCount ); } else if ( matchCount < itemCount && sourceList != null ) { if ( errorOnFailure ) { KoLmafia.updateDisplay( MafiaState.ERROR, "[" + ( itemCount > 1 ? itemCount + " " : "" ) + firstMatch.getName() + "] requested, but " + ( matchCount == 0 ? "none" : "only " + matchCount ) + " available." ); } return null; } if ( KoLmafiaCLI.isExecutingCheckOnlyCommand ) { KoLmafia.updateDisplay( firstMatch == null ? "No match" : firstMatch.toString() ); return null; } return itemCount <= 0 ? null : firstMatch; } public static AdventureResult[] getMatchingItemList( String itemList ) { return ItemFinder.getMatchingItemList( itemList, true, null, ItemFinder.ANY_MATCH ); } public static AdventureResult[] getMatchingItemList( String itemList, boolean errorOnFailure ) { return ItemFinder.getMatchingItemList( itemList, errorOnFailure, null, ItemFinder.ANY_MATCH ); } public static AdventureResult[] getMatchingItemList( String itemList, List<AdventureResult> sourceList ) { return ItemFinder.getMatchingItemList( itemList, true, sourceList, ItemFinder.ANY_MATCH ); } public static AdventureResult[] getMatchingItemList( String itemList, boolean errorOnFailure, List<AdventureResult> sourceList ) { return ItemFinder.getMatchingItemList( itemList, errorOnFailure, sourceList, ItemFinder.ANY_MATCH ); } public static AdventureResult[] getMatchingItemList( String itemList, boolean errorOnFailure, List<AdventureResult> sourceList, int filterType ) { AdventureResult firstMatch = ItemFinder.getFirstMatchingItem( itemList, false, sourceList, filterType ); if ( firstMatch != null ) { AdventureResult[] items = new AdventureResult[ 1 ]; items[ 0 ] = firstMatch; return items; } String[] itemNames = itemList.split( "\\s*,\\s*" ); boolean isMeatMatch = false; ArrayList<AdventureResult> items = new ArrayList<AdventureResult>(); for ( String name : itemNames ) { isMeatMatch = false; if ( name.endsWith( " meat" ) ) { String amountString = name.substring( 0, name.length() - 5 ).trim(); if ( amountString.equals( "*" ) || StringUtilities.isNumeric( amountString ) ) { isMeatMatch = true; int amount = 0; if ( !amountString.equals( "*" ) ) { amount = StringUtilities.parseInt( amountString ); } if ( amount <= 0 ) { amount += sourceList == KoLConstants.storage ? KoLCharacter.getStorageMeat() : sourceList == KoLConstants.closet ? KoLCharacter.getClosetMeat() : KoLCharacter.getAvailableMeat(); } firstMatch = new AdventureResult( AdventureResult.MEAT, amount ); } } if ( !isMeatMatch ) { firstMatch = ItemFinder.getFirstMatchingItem( name, errorOnFailure, sourceList, filterType ); } if ( firstMatch != null ) { AdventureResult.addResultToList( items, firstMatch ); } } AdventureResult[] result = new AdventureResult[ items.size() ]; return items.toArray( result ); } }