/** * 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.session; import java.io.File; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.AdventureResult; import net.sourceforge.kolmafia.FamiliarData; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLConstants.CraftingRequirements; import net.sourceforge.kolmafia.KoLConstants.CraftingType; import net.sourceforge.kolmafia.KoLConstants.MafiaState; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.KoLmafiaASH; import net.sourceforge.kolmafia.KoLmafiaCLI; import net.sourceforge.kolmafia.Modifiers; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.RequestThread; import net.sourceforge.kolmafia.SpecialOutfit; import net.sourceforge.kolmafia.listener.ItemListenerRegistry; import net.sourceforge.kolmafia.listener.PreferenceListenerRegistry; import net.sourceforge.kolmafia.objectpool.Concoction; import net.sourceforge.kolmafia.objectpool.ConcoctionPool; import net.sourceforge.kolmafia.objectpool.IntegerPool; import net.sourceforge.kolmafia.objectpool.ItemPool; import net.sourceforge.kolmafia.persistence.CoinmastersDatabase; import net.sourceforge.kolmafia.persistence.ConcoctionDatabase; import net.sourceforge.kolmafia.persistence.DebugDatabase; import net.sourceforge.kolmafia.persistence.EquipmentDatabase; import net.sourceforge.kolmafia.persistence.ItemDatabase; import net.sourceforge.kolmafia.persistence.NPCStoreDatabase; import net.sourceforge.kolmafia.persistence.RestoresDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.request.ApiRequest; import net.sourceforge.kolmafia.request.ClanStashRequest; import net.sourceforge.kolmafia.request.ClosetRequest; import net.sourceforge.kolmafia.request.CombineMeatRequest; import net.sourceforge.kolmafia.request.CreateItemRequest; import net.sourceforge.kolmafia.request.EquipmentRequest; import net.sourceforge.kolmafia.request.FamiliarRequest; import net.sourceforge.kolmafia.request.GenericRequest; import net.sourceforge.kolmafia.request.HermitRequest; import net.sourceforge.kolmafia.request.PurchaseRequest; import net.sourceforge.kolmafia.request.StandardRequest; import net.sourceforge.kolmafia.request.StorageRequest; import net.sourceforge.kolmafia.request.UntinkerRequest; import net.sourceforge.kolmafia.request.UseItemRequest; import net.sourceforge.kolmafia.session.Limitmode; import net.sourceforge.kolmafia.swingui.GenericFrame; import net.sourceforge.kolmafia.textui.Interpreter; import net.sourceforge.kolmafia.textui.parsetree.Value; import net.sourceforge.kolmafia.utilities.AdventureResultArray; import net.sourceforge.kolmafia.utilities.InputFieldUtilities; import net.sourceforge.kolmafia.utilities.StringUtilities; import org.json.JSONException; import org.json.JSONObject; public abstract class InventoryManager { private static final int BULK_PURCHASE_AMOUNT = 30; private static int askedAboutCrafting = 0; private static boolean cloverProtectionEnabled = true; public static void resetInventory() { KoLConstants.inventory.clear(); } public static void refresh() { // Retrieve the contents of inventory via api.php RequestThread.postRequest( new ApiRequest( "inventory" ) ); } public static final void parseInventory( final JSONObject JSON ) { if ( JSON == null ) { return; } ArrayList<AdventureResult> items = new ArrayList<AdventureResult>(); ArrayList<AdventureResult> unlimited = new ArrayList<AdventureResult>(); try { // {"1":"1","2":"1" ... } Iterator< ? > keys = JSON.keys(); while ( keys.hasNext() ) { String key = (String) keys.next(); int itemId = StringUtilities.parseInt( key ); int count = JSON.getInt( key ); String name = ItemDatabase.getItemDataName( itemId ); if ( name == null ) { // Fetch descid from api.php?what=item // and register new item. ItemDatabase.registerItem( itemId ); } if ( Limitmode.limitItem( itemId ) ) { unlimited.add( ItemPool.get( itemId, count ) ); } else { items.add( ItemPool.get( itemId, count ) ); } } } catch ( JSONException e ) { ApiRequest.reportParseError( "inventory", JSON.toString(), e ); return; } KoLConstants.inventory.clear(); KoLConstants.inventory.addAll( items ); KoLConstants.unlimited.clear(); KoLConstants.unlimited.addAll( unlimited ); EquipmentManager.updateEquipmentLists(); ConcoctionDatabase.refreshConcoctions(); PreferenceListenerRegistry.firePreferenceChanged( "(hats)" ); } public static final int getCount( final int itemId ) { return InventoryManager.getCount( ItemPool.get( itemId, 1 ) ); } public static final int getCount( final AdventureResult item ) { return item.getCount( KoLConstants.inventory ); } public static final boolean hasItem( final int itemId ) { return InventoryManager.hasItem( itemId, false ); } public static final boolean hasItem( final int itemId, final boolean shouldCreate ) { return InventoryManager.hasItem( ItemPool.get( itemId, 1 ), shouldCreate ); } public static final boolean hasItem( final AdventureResult item ) { return InventoryManager.hasItem( item, false ); } public static final boolean hasItem( final AdventureResult item, final boolean shouldCreate ) { int count = InventoryManager.getAccessibleCount( item ); if ( shouldCreate ) { CreateItemRequest creation = CreateItemRequest.getInstance( item ); if ( creation != null ) { count += creation.getQuantityPossible(); } } return count > 0 && count >= item.getCount(); } public static final int getAccessibleCount( final int itemId ) { return InventoryManager.getAccessibleCount( ItemPool.get( itemId, 1 ) ); } public static final int getAccessibleCount( final AdventureResult item ) { if ( item == null ) { return 0; } int itemId = item.getItemId(); if ( itemId <= 0 ) { return 0; } // Agree with what retrieveItem looks at if ( itemId == HermitRequest.WORTHLESS_ITEM.getItemId() ) { return HermitRequest.getWorthlessItemCount( true ); } // If this item is restricted, ignore it entirely. if ( !StandardRequest.isAllowed( "Items", item.getName() ) ) { return 0; } int count = item.getCount( KoLConstants.inventory ); // Items in closet might be accessible, but if the user has // marked items in the closet as out-of-bounds, honor that. if ( InventoryManager.canUseCloset() ) { count += item.getCount( KoLConstants.closet ); } // Free Pulls from Hagnk's are always accessible count += item.getCount( KoLConstants.freepulls ); // Storage and your clan stash are always accessible // once you are out of Ronin or have freed the king, // but the user can mark either as out-of-bounds if ( InventoryManager.canUseStorage() ) { count += item.getCount( KoLConstants.storage ); } if ( InventoryManager.canUseClanStash() ) { count += item.getCount( ClanManager.getStash() ); } count += InventoryManager.getEquippedCount( item ); for ( FamiliarData current: KoLCharacter.getFamiliarList() ) { if ( !current.equals( KoLCharacter.getFamiliar() ) && current.getItem() != null && current.getItem().equals( item ) ) { ++count; } } return count; } public static final int getEquippedCount( final int itemId ) { return InventoryManager.getEquippedCount( ItemPool.get( itemId, 1 ) ); } public static final int getEquippedCount( final AdventureResult item ) { int count = 0; for ( int i = 0; i <= EquipmentManager.FAMILIAR; ++i ) { AdventureResult equipment = EquipmentManager.getEquipment( i ); if ( equipment != null && equipment.getItemId() == item.getItemId() ) { ++count; } } return count; } public static final boolean retrieveItem( final int itemId ) { return InventoryManager.retrieveItem( ItemPool.get( itemId, 1 ), true, true ); } public static final boolean retrieveItem( final int itemId, final boolean isAutomated ) { return InventoryManager.retrieveItem( ItemPool.get( itemId, 1 ), isAutomated, true ); } public static final boolean retrieveItem( final int itemId, final boolean isAutomated, final boolean useEquipped ) { return InventoryManager.retrieveItem( ItemPool.get( itemId, 1 ), isAutomated, useEquipped ); } public static final boolean retrieveItem( final int itemId, final int count ) { return InventoryManager.retrieveItem( ItemPool.get( itemId, count ), true, true ); } public static final boolean retrieveItem( final int itemId, final int count, final boolean isAutomated ) { return InventoryManager.retrieveItem( ItemPool.get( itemId, count ), isAutomated, true ); } public static final boolean retrieveItem( final int itemId, final int count, final boolean isAutomated, final boolean useEquipped ) { return InventoryManager.retrieveItem( ItemPool.get( itemId, count ), isAutomated, useEquipped ); } public static final boolean retrieveItem( final String itemName ) { return InventoryManager.retrieveItem( ItemPool.get( itemName, 1 ), true, true ); } public static final boolean retrieveItem( final String itemName, final boolean isAutomated ) { return InventoryManager.retrieveItem( ItemPool.get( itemName, 1 ), isAutomated, true ); } public static final boolean retrieveItem( final String itemName, final boolean isAutomated, final boolean useEquipped ) { return InventoryManager.retrieveItem( ItemPool.get( itemName, 1 ), isAutomated, useEquipped ); } public static final boolean retrieveItem( final String itemName, final int count ) { return InventoryManager.retrieveItem( ItemPool.get( itemName, count ), true, true ); } public static final boolean retrieveItem( final String itemName, final int count, final boolean isAutomated ) { return InventoryManager.retrieveItem( ItemPool.get( itemName, count ), isAutomated, true ); } public static final boolean retrieveItem( final String itemName, final int count, final boolean isAutomated, final boolean useEquipped ) { return InventoryManager.retrieveItem( ItemPool.get( itemName, count ), isAutomated, useEquipped ); } public static final boolean retrieveItem( final AdventureResult item ) { return InventoryManager.retrieveItem( item, true, true ); } public static final boolean retrieveItem( final AdventureResult item, final boolean isAutomated ) { return InventoryManager.retrieveItem( item, isAutomated, true ); } public static final boolean retrieveItem( final AdventureResult item, final boolean isAutomated, final boolean useEquipped ) { String rv = InventoryManager.retrieveItem( item, isAutomated, useEquipped, false ); if ( rv == null ) { return false; } if ( rv.equals( "" ) ) { if ( EquipmentDatabase.isHat( item ) ) { PreferenceListenerRegistry.firePreferenceChanged( "(hats)" ); } return true; } RequestLogger.printLine( "INTERNAL ERROR: retrieveItem returned string when not simulating!" ); return true; } public static final String simRetrieveItem( final int itemId ) { return InventoryManager.simRetrieveItem( ItemPool.get( itemId, 1 ), true ); } public static final String simRetrieveItem( final int itemId, final boolean isAutomated ) { return InventoryManager.simRetrieveItem( ItemPool.get( itemId, 1 ), isAutomated ); } public static final String simRetrieveItem( final String itemName ) { return InventoryManager.simRetrieveItem( ItemPool.get( itemName, 1 ), true ); } public static final String simRetrieveItem( final String itemName, final boolean isAutomated ) { return InventoryManager.simRetrieveItem( ItemPool.get( itemName, 1 ), isAutomated ); } public static final String simRetrieveItem( final AdventureResult item ) { return InventoryManager.simRetrieveItem( item, true ); } public static final String simRetrieveItem( final AdventureResult item, final boolean isAutomated ) { return InventoryManager.simRetrieveItem( item, isAutomated, true ); } public static final String simRetrieveItem( final AdventureResult item, final boolean isAutomated, final boolean useEquipped ) { String rv = InventoryManager.retrieveItem( item, isAutomated, useEquipped, true ); if ( rv == null || rv.equals( "" ) ) { RequestLogger.printLine( "INTERNAL ERROR: retrieveItem didn't return string when simulating!" ); return "buggy"; } return rv; } private static final String retrieveItem( final AdventureResult item, final boolean isAutomated, final boolean useEquipped, final boolean sim ) { // if we're simulating, we don't need to waste time disabling/enabling clover protection if ( sim ) { return InventoryManager.doRetrieveItem( item, isAutomated, useEquipped, sim ); } try { InventoryManager.setCloverProtection( false ); return InventoryManager.doRetrieveItem( item, isAutomated, useEquipped, false ); } finally { // Restore clover protection InventoryManager.setCloverProtection( true ); } } // When called with sim=true, retrieveItem should return a non-empty string // indicating how at least some quantity of the item would be retrieved. // There are two distinguished return values: "have" indicates trivial // success, "fail" indicates unavoidable failure. No side-effects, please! // When called with sim=false, it should return "" for success (equivalent // to the previous return value of true), null for failure (previously false). private static final String doRetrieveItem( final AdventureResult item, final boolean isAutomated, final boolean useEquipped, final boolean sim ) { int itemId = item.getItemId(); if ( itemId < 0 ) { // See if it is a Coin Master token. Concoction concoction = ConcoctionPool.get( item ); String property = concoction != null ? concoction.property : null; if ( property == null ) { if ( sim ) { return "fail"; } KoLmafia.updateDisplay( MafiaState.ERROR, "Don't know how to retrieve a " + item.getName() ); return null; } int have = Preferences.getInteger( property ); int need = item.getCount() - have; if ( need > 0 ) { if ( sim ) { return "fail"; } KoLmafia.updateDisplay( MafiaState.ERROR, "You need " + need + " more " + item.getName() + " to continue." ); return null; } return sim ? "have" : ""; } if ( itemId == 0 ) { return sim ? "pretend to have" : ""; } if ( itemId == HermitRequest.WORTHLESS_ITEM.getItemId() ) { // Retrieve worthless items using special techniques. if ( sim ) { return "chewing gum"; } try { SpecialOutfit.createImplicitCheckpoint(); return InventoryManager.retrieveWorthlessItems( item ) ? "" : null; } finally { SpecialOutfit.restoreImplicitCheckpoint(); } } // If it is a virtual item, see if we already bought it if ( ItemDatabase.isVirtualItem( itemId ) ) { if ( ItemDatabase.haveVirtualItem( itemId ) ) { return sim ? "have" : ""; } } int availableCount = item.getCount( KoLConstants.inventory ); int missingCount = item.getCount() - availableCount; // If you already have enough of the given item, then return // from this method. if ( missingCount <= 0 ) { return sim ? "have" : ""; } // Handle the bridge by untinkering the abridged dictionary // You can have at most one of these. if ( itemId == ItemPool.BRIDGE ) { if ( InventoryManager.hasItem( ItemPool.ABRIDGED ) ) { if ( sim ) { return "untinker"; } RequestThread.postRequest( new UntinkerRequest( ItemPool.ABRIDGED, 1 ) ); } if ( sim ) { return "fail"; } return item.getCount( KoLConstants.inventory ) > 0 ? "" : null; } boolean isRestricted = !StandardRequest.isAllowed( "Items", item.getName() ); CreateItemRequest creator = CreateItemRequest.getInstance( item ); // If this item is restricted, we might be able to create it. // If we can't, give up now; we cannot obtain it in any way. if ( isRestricted && creator == null ) { return sim ? "fail" : null; } // Don't waste time checking familiars and equipment for // restricted items or non-equipment. if ( !isRestricted && ItemDatabase.isEquipment( itemId ) ) { for ( FamiliarData current: KoLCharacter.getFamiliarList() ) { if ( current.getItem() != null && current.getItem().equals( item ) ) { if ( sim ) { return "steal"; } KoLmafia.updateDisplay( "Stealing " + item.getName() + " from " + current.getName() + " the " + current.getRace() + "..." ); FamiliarRequest request = new FamiliarRequest( current, EquipmentRequest.UNEQUIP ); RequestThread.postRequest( request ); if ( --missingCount <= 0 ) { return ""; } // Keep going; generic familiar equipment might // be retrievable from multiple familiars. } } } if ( !isRestricted && ItemDatabase.isEquipment( itemId ) && useEquipped ) { for ( int i = EquipmentManager.HAT; i <= EquipmentManager.FAMILIAR; ++i ) { // If you are dual-wielding the target item, // remove the one in the offhand slot first // since taking from the weapon slot will drop // the offhand weapon. int slot = i == EquipmentManager.WEAPON ? EquipmentManager.OFFHAND : i == EquipmentManager.OFFHAND ? EquipmentManager.WEAPON : i; if ( EquipmentManager.getEquipment( slot ).equals( item ) ) { if ( sim ) { return "remove"; } SpecialOutfit.replaceEquipmentInSlot( EquipmentRequest.UNEQUIP, slot ); RequestThread.postRequest( new EquipmentRequest( EquipmentRequest.UNEQUIP, slot ) ); if ( --missingCount <= 0 ) { return ""; } } } } // Attempt to pull the item from the closet. boolean shouldUseCloset = InventoryManager.canUseCloset(); if ( shouldUseCloset ) { int itemCount = item.getCount( KoLConstants.closet ); if ( itemCount > 0 ) { if ( sim ) { return "uncloset"; } int retrieveCount = Math.min( itemCount, missingCount ); RequestThread.postRequest( new ClosetRequest( ClosetRequest.CLOSET_TO_INVENTORY, item.getInstance( retrieveCount ) ) ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } } // If the item is a free pull from Hagnk's, pull it if ( !isRestricted ) { int itemCount = item.getCount( KoLConstants.freepulls ); if ( itemCount > 0 ) { if ( sim ) { return "free pull"; } int retrieveCount = Math.min( itemCount, missingCount ); RequestThread.postRequest( new StorageRequest( StorageRequest.STORAGE_TO_INVENTORY, item.getInstance( retrieveCount ) ) ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } } // Attempt to pull the items out of storage, if you are out of // ronin and the user wishes to use storage if ( !isRestricted && InventoryManager.canUseStorage() ) { int itemCount = item.getCount( KoLConstants.storage ); if ( itemCount > 0 ) { if ( sim ) { return "pull"; } int retrieveCount = Math.min( itemCount, missingCount ); RequestThread.postRequest( new StorageRequest( StorageRequest.STORAGE_TO_INVENTORY, item.getInstance( retrieveCount ) ) ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } } // Attempt to pull the item from the clan stash, if it is // available there and the user wishes to use the stash if ( !isRestricted && InventoryManager.canUseClanStash() ) { int itemCount = item.getCount( ClanManager.getStash() ); if ( itemCount > 0 ) { if ( sim ) { return "unstash"; } int retrieveCount = Math.min( itemCount, InventoryManager.getPurchaseCount( itemId, missingCount ) ); RequestThread.postRequest( new ClanStashRequest( item.getInstance( retrieveCount ), ClanStashRequest.STASH_TO_ITEMS ) ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } } // From here on, we will consider buying the item. Decide if we // want to use only NPCs or if the mall is possible. boolean shouldUseNPCStore = InventoryManager.canUseNPCStores( item ); boolean forceNoMall = isRestricted; if ( !forceNoMall ) { if ( shouldUseNPCStore ) { // If Price from NPC store is 100 or below and available, never try mall. int NPCPrice = NPCStoreDatabase.availablePrice( itemId ); int autosellPrice = ItemDatabase.getPriceById( itemId ); if ( NPCPrice > 0 && NPCPrice <= Math.max( 100, autosellPrice * 2 ) ) { forceNoMall = true; } } // Things that we can construct out of pure Meat cannot // possibly be cheaper to buy. if ( creator != null && creator instanceof CombineMeatRequest ) { forceNoMall = true; } } boolean shouldUseMall = !forceNoMall && InventoryManager.canUseMall( item ); boolean scriptSaysBuy = false; // Attempt to create the item from existing ingredients (if // possible). The user's buyScript can kick in here and force // it to be purchased, rather than created Concoction concoction = ConcoctionPool.get( item ); boolean asked = false; if ( creator != null && creator.getQuantityPossible() > 0 ) { if ( sim ) { return shouldUseMall ? "create or buy" : "create"; } if ( !forceNoMall ) { boolean defaultBuy = shouldUseMall && InventoryManager.cheaperToBuy( item, missingCount ); scriptSaysBuy = InventoryManager.invokeBuyScript( item, missingCount, 2, defaultBuy ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } if ( !scriptSaysBuy ) { // Prompt about adventures if we make it here. creator.setQuantityNeeded( Math.min( missingCount, creator.getQuantityPossible() ) ); if ( isAutomated && concoction != null && concoction.getAdventuresNeeded( missingCount, true ) > 0 ) { if ( !InventoryManager.allowTurnConsumption( creator ) ) { return null; } asked = true; } RequestThread.postRequest( creator ); if ( ItemDatabase.isVirtualItem( itemId ) && ItemDatabase.haveVirtualItem( itemId ) ) { return ""; } missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } if ( !KoLmafia.permitsContinue() ) { return null; } } } // A ten-leaf clover can be created (by using a disassembled // clover) or purchased from the Hermit (if he has any in // stock. We tried the former above. Now try the latter. boolean shouldUseCoinmasters = InventoryManager.canUseCoinmasters(); if ( shouldUseCoinmasters && KoLConstants.hermitItems.contains( item ) && ( !shouldUseMall || InventoryManager.currentWorthlessItemCost() < StoreManager.getMallPrice( item ) ) ) { int itemCount = itemId == ItemPool.TEN_LEAF_CLOVER ? HermitRequest.cloverCount() : PurchaseRequest.MAX_QUANTITY; if ( itemCount > 0 ) { if ( sim ) { return "hermit"; } int retrieveCount = Math.min( itemCount, missingCount ); RequestThread.postRequest( new HermitRequest( itemId, retrieveCount ) ); } missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } // Try to purchase the item from the mall, if the user wishes // to autosatisfy through purchases, and we have none of the // ingredients needed to create the item if ( shouldUseMall && !scriptSaysBuy && !InventoryManager.hasAnyIngredient( itemId ) ) { if ( sim ) { return "create or buy"; } if ( creator == null ) { scriptSaysBuy = true; } else { boolean defaultBuy = InventoryManager.cheaperToBuy( item, missingCount ); scriptSaysBuy = InventoryManager.invokeBuyScript( item, missingCount, 0, defaultBuy ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } } if ( shouldUseNPCStore || scriptSaysBuy ) { if ( sim ) { return shouldUseNPCStore ? "buy from NPC" : "buy"; } // If buying from the mall will leave the item in storage, use only NPCs boolean onlyNPC = forceNoMall || !InventoryManager.canUseMall(); ArrayList<PurchaseRequest> results = onlyNPC ? StoreManager.searchNPCs( item ) : StoreManager.searchMall( item ); KoLmafia.makePurchases( results, results.toArray( new PurchaseRequest[0] ), InventoryManager.getPurchaseCount( itemId, missingCount ), isAutomated, 0 ); if ( !onlyNPC ) { StoreManager.updateMallPrice( item, results ); } missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } // Use budgeted pulls if the item is available from storage. if ( !isRestricted && !KoLCharacter.canInteract() && !KoLCharacter.isHardcore() ) { int pullCount = Math.min( item.getCount( KoLConstants.storage ), ConcoctionDatabase.getPullsBudgeted() ); if ( pullCount > 0 ) { if ( sim ) { return "pull"; } pullCount = Math.min( pullCount, item.getCount() ); int newbudget = ConcoctionDatabase.getPullsBudgeted() - pullCount; RequestThread.postRequest( new StorageRequest( StorageRequest.STORAGE_TO_INVENTORY, item.getInstance( pullCount ) ) ); ConcoctionDatabase.setPullsBudgeted( newbudget ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } } CraftingType mixingMethod = ConcoctionDatabase.getMixingMethod( item ); switch ( itemId ) { case ItemPool.DOUGH: case ItemPool.DISASSEMBLED_CLOVER: case ItemPool.JOLLY_BRACELET: scriptSaysBuy = true; break; default: scriptSaysBuy = creator == null || mixingMethod == CraftingType.NOCREATE; break; } if ( creator != null && mixingMethod != CraftingType.NOCREATE ) { if ( sim ) { return shouldUseMall ? "create or buy" : "create"; } boolean defaultBuy = scriptSaysBuy || shouldUseMall && InventoryManager.cheaperToBuy( item, missingCount ); scriptSaysBuy = InventoryManager.invokeBuyScript( item, missingCount, 1, defaultBuy ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } // If it's creatable, and you have at least one ingredient, see // if you can make it via recursion. if ( creator != null && mixingMethod != CraftingType.NOCREATE && !scriptSaysBuy ) { boolean makeFromComponents = true; if ( isAutomated && creator.getQuantityPossible() > 0 ) { // Speculate on how much the items needed to make the creation would cost. // Do not retrieve if the average meat spend to make one of the item // exceeds the user's autoBuyPriceLimit. float meatSpend = InventoryManager.priceToMake( item, missingCount, 0, true, true ) / missingCount; int autoBuyPriceLimit = Preferences.getInteger( "autoBuyPriceLimit" ); if ( meatSpend > autoBuyPriceLimit ) { makeFromComponents = false; KoLmafia.updateDisplay( MafiaState.ERROR, "The average amount of meat spent on components (" + KoLConstants.COMMA_FORMAT.format( meatSpend ) + ") for one " + item.getName() + " exceeds autoBuyPriceLimit (" + KoLConstants.COMMA_FORMAT.format( autoBuyPriceLimit ) + ")" ); // If making it from components was cheaper than buying the final product, and we // couldn't afford to make it, don't bother trying to buy the final product. shouldUseMall = false; } } if ( makeFromComponents ) { if ( sim ) { return "create"; } // Second place to check for adventure usage. Make sure we didn't already ask above. creator.setQuantityNeeded( missingCount ); if ( !asked && isAutomated && concoction != null && creator != null && concoction.getAdventuresNeeded( missingCount, true ) > 0 ) { if ( !InventoryManager.allowTurnConsumption( creator ) ) { return null; } asked = true; } RequestThread.postRequest( creator ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } if ( !KoLmafia.permitsContinue() && isAutomated ) { return null; } } } // All other options have been exhausted. Buy the remaining // items from the mall. if ( shouldUseMall ) { if ( sim ) { return "buy"; } ArrayList<PurchaseRequest> results = StoreManager.searchMall( item ); KoLmafia.makePurchases( results, results.toArray( new PurchaseRequest[0] ), InventoryManager.getPurchaseCount( itemId, missingCount ), isAutomated, 0 ); StoreManager.updateMallPrice( item, results ); missingCount = item.getCount() - item.getCount( KoLConstants.inventory ); if ( missingCount <= 0 ) { return ""; } } // We were unable to obtain as many of the item as the user desired. // Fail now. if ( sim ) { return "fail"; } KoLmafia.updateDisplay( MafiaState.ERROR, "You need " + missingCount + " more " + item.getName() + " to continue." ); return null; } // *** Start of worthless item handling private static final AdventureResult[] WORTHLESS_ITEMS = new AdventureResult[] { ItemPool.get( ItemPool.WORTHLESS_TRINKET, 1 ), ItemPool.get( ItemPool.WORTHLESS_GEWGAW, 1 ), ItemPool.get( ItemPool.WORTHLESS_KNICK_KNACK, 1 ), }; private static final AdventureResult[] STARTER_ITEMS = new AdventureResult[] { // A hat and a weapon for all six classes ItemPool.get( ItemPool.SEAL_HELMET, 1 ), ItemPool.get( ItemPool.SEAL_CLUB, 1 ), ItemPool.get( ItemPool.HELMET_TURTLE, 1 ), ItemPool.get( ItemPool.TURTLE_TOTEM, 1 ), ItemPool.get( ItemPool.RAVIOLI_HAT, 1 ), ItemPool.get( ItemPool.PASTA_SPOON, 1 ), ItemPool.get( ItemPool.HOLLANDAISE_HELMET, 1 ), ItemPool.get( ItemPool.SAUCEPAN, 1 ), ItemPool.get( ItemPool.DISCO_MASK, 1 ), ItemPool.get( ItemPool.DISCO_BALL, 1 ), ItemPool.get( ItemPool.MARIACHI_HAT, 1 ), ItemPool.get( ItemPool.STOLEN_ACCORDION, 1 ), // One pair of pants ItemPool.get( ItemPool.OLD_SWEATPANTS, 1 ), }; private static boolean retrieveWorthlessItems( final AdventureResult item ) { int count = HermitRequest.getWorthlessItemCount( false ); int needed = item.getCount(); if ( count >= needed ) { return true; } // Figure out if you already have enough items in the closet if ( InventoryManager.canUseCloset() ) { InventoryManager.transferWorthlessItems( false ); count = HermitRequest.getWorthlessItemCount(); if ( count >= needed ) { return true; } } // Figure out if you already have enough items in storage if ( InventoryManager.canUseStorage() ) { InventoryManager.pullWorthlessItems(); count = HermitRequest.getWorthlessItemCount(); if ( count >= needed ) { return true; } } while ( count < needed && InventoryManager.hasItem( HermitRequest.SUMMON_SCROLL ) ) { // Read a single 31337 scroll RequestThread.postRequest( UseItemRequest.getInstance( HermitRequest.SUMMON_SCROLL ) ); // If we now have a hermit script in inventory, read it if ( InventoryManager.hasItem( HermitRequest.HACK_SCROLL ) ) { RequestThread.postRequest( UseItemRequest.getInstance( HermitRequest.HACK_SCROLL ) ); } count = HermitRequest.getWorthlessItemCount(); } if ( count >= needed ) { return true; } // Do not refresh concoctions while we transfer sewer items around. ConcoctionDatabase.deferRefresh( true ); // If the character has any of the starter items, retrieve them to improve // the probability of getting worthless items. int missingStarterItemCount = InventoryManager.STARTER_ITEMS.length - InventoryManager.getStarterItemCount(); if ( missingStarterItemCount > 0 ) { InventoryManager.transferChewingGumItems( InventoryManager.STARTER_ITEMS, true, false ); missingStarterItemCount = InventoryManager.STARTER_ITEMS.length - InventoryManager.getStarterItemCount(); } // If you can interact with players, use the server-friendlier version of gum // retrieval that pre-computes a total amount of gum and retrieves it all // at once to start. if ( KoLCharacter.canInteract() ) { // To save server hits, retrieve all the gum needed rather than constantly // purchase small amounts of gum. int totalGumCount = missingStarterItemCount + needed - count; if ( InventoryManager.retrieveItem( ItemPool.CHEWING_GUM, totalGumCount ) ) { if ( needed - count <= 3 ) { InventoryManager.transferWorthlessItems( true ); RequestThread.postRequest( UseItemRequest.getInstance( ItemPool.CHEWING_GUM, totalGumCount ) ); } else { while ( needed - count > 0 ) { int gumCount = missingStarterItemCount == 0 ? Math.min( needed - count, 3 ) : missingStarterItemCount + 3; // Put the worthless items into the closet and then use the chewing gum int closetCount = InventoryManager.transferWorthlessItems( true ); RequestThread.postRequest( UseItemRequest.getInstance( ItemPool.CHEWING_GUM, gumCount ) ); // Recalculate how many worthless items are still needed and how many starter // items are now present in the inventory (if it's zero already, no additional // computations are needed). int inventoryCount = HermitRequest.getWorthlessItemCount(); count = inventoryCount + closetCount; if ( missingStarterItemCount != 0 ) { missingStarterItemCount = InventoryManager.STARTER_ITEMS.length - InventoryManager.getStarterItemCount(); } } } // Pull the worthless items back out of the closet. count = InventoryManager.transferWorthlessItems( false ); } } // Otherwise, go ahead and hit the server a little harder in order to retrieve // the worthless items. else { if ( InventoryManager.retrieveItem( ItemPool.CHEWING_GUM, needed - count ) ) { while ( count < needed ) { int gumUseCount = 1; // If you are missing at most one starter item, it is optimal // to use three chewing gums instead of one. if ( missingStarterItemCount <= 1 ) { gumUseCount = Math.min( needed - count, 3 ); } AdventureResult gum = ItemPool.get( ItemPool.CHEWING_GUM, gumUseCount ); if ( gum.getCount( KoLConstants.inventory ) < gumUseCount && !InventoryManager.retrieveItem( ItemPool.CHEWING_GUM, needed - count ) ) { break; } // Closet your existing worthless items (since they will reduce // the probability of you getting more) and then use the gum. int closetCount = InventoryManager.transferWorthlessItems( true ); RequestThread.postRequest( UseItemRequest.getInstance( gum ) ); int inventoryCount = HermitRequest.getWorthlessItemCount(); count = inventoryCount + closetCount; } // Pull the worthless items back out of the closet. count = InventoryManager.transferWorthlessItems( false ); } } // Now that we have (possibly) gotten more sewer items, refresh ConcoctionDatabase.deferRefresh( false ); if ( count < needed ) { KoLmafia.updateDisplay( MafiaState.ABORT, "Unable to acquire " + item.getCount() + " worthless items." ); } return count >= needed; } private static int getStarterItemCount() { int starterItemCount = 0; for ( int i = 0; i < InventoryManager.STARTER_ITEMS.length; ++i ) { AdventureResult item = InventoryManager.STARTER_ITEMS[ i ]; if ( item.getCount( KoLConstants.inventory ) > 0 || KoLCharacter.hasEquipped( item ) ) { ++starterItemCount; } } return starterItemCount; } private static void transferChewingGumItems( final AdventureResult[] items, final boolean moveOne, final boolean moveToCloset ) { List<AdventureResult> source = moveToCloset ? KoLConstants.inventory : KoLConstants.closet; List<AdventureResult> destination = moveToCloset ? KoLConstants.closet : KoLConstants.inventory; AdventureResultArray attachmentList = new AdventureResultArray(); for ( int i = 0; i < items.length; ++i ) { AdventureResult item = items[ i ]; if ( moveOne && !moveToCloset && ( item.getCount( destination ) > 0 || KoLCharacter.hasEquipped( item ) ) ) { continue; } int itemCount = item.getCount( source ); if ( itemCount > 0 ) { attachmentList.add( ItemPool.get( item.getItemId(), moveOne ? 1 : itemCount ) ); } } if ( !attachmentList.isEmpty() ) { int moveType = moveToCloset ? ClosetRequest.INVENTORY_TO_CLOSET : ClosetRequest.CLOSET_TO_INVENTORY; RequestThread.postRequest( new ClosetRequest( moveType, attachmentList.toArray() ) ); } } private static int transferWorthlessItems( final boolean moveToCloset ) { InventoryManager.transferChewingGumItems( InventoryManager.WORTHLESS_ITEMS, false, moveToCloset ); List<AdventureResult> destination = moveToCloset ? KoLConstants.closet : KoLConstants.inventory; int trinketCount = HermitRequest.TRINKET.getCount( destination ); int gewgawCount = HermitRequest.GEWGAW.getCount( destination ); int knickKnackCount = HermitRequest.KNICK_KNACK.getCount( destination ); return trinketCount + gewgawCount + knickKnackCount; } private static int pullWorthlessItems() { int trinketCount = HermitRequest.TRINKET.getCount( KoLConstants.storage ); int gewgawCount = HermitRequest.GEWGAW.getCount( KoLConstants.storage ); int knickKnackCount = HermitRequest.KNICK_KNACK.getCount( KoLConstants.storage ); AdventureResult[] items = new AdventureResult[] { HermitRequest.TRINKET.getInstance( trinketCount ), HermitRequest.GEWGAW.getInstance( gewgawCount ), HermitRequest.KNICK_KNACK.getInstance( knickKnackCount ) }; RequestThread.postRequest( new StorageRequest( StorageRequest.STORAGE_TO_INVENTORY, items ) ); return trinketCount + gewgawCount + knickKnackCount; } /* 16 possible sewer items: 6 classes * 1 hat 6 classes * 1 weapon 1 pants 3 worthless items Items can be in inventory or equipped. Unless you have all 16 possible items, using a piece of gum will retrieve one you don't have yet. If you have all the non-worthless items, you are guaranteed to get a worthless item. If are missing some non-worthless items, whether you get a worthless item or one of the missing non-worthless-items is probabilistic. Assume you have no worthless items in inventory Let X = number of non-worthless sewer items you have. Given X, what is the expected # of gums needed to get a worthless item? Consider X = 13. Of the ( 16 - 13 ) = 3 possible items, 3 are your goal and ( 3 - 3 ) = 0 are not your goal. E(13) = 3/3 * 1 + 0/3 = 1.0 Consider X = 12. Of the ( 16 - 12 ) = 4 possible items, 3 are your goal and ( 4 - 3 ) = 1 are not your goal. You have a 3/4 chance of getting your goal with the first piece of gum. If you don't get one, you have used 1 gum, now have 13 sewer items and will use another piece of gum. E(12) = 3/4 * 1 + 1/4 * ( 1 + E(13) ) = .75 + 0.50 = 1.25 Consider X = 11. Of the ( 16 - 11 ) = 5 possible items, 3 are your goal and ( 5 - 3 ) = 2 are not your goal. You have a 3/5 chance of getting your goal with the first piece of gum. If you don't get one, you have used 1 gum, now have 12 sewer items and will use another piece of gum. E(11) = 3/5 * 1 + 2/5 * ( 1 + E(12) ) = .60 + 0.90 = 1.50 This generalizes: E(X) = 3/(16-X) + (13-X)/(16-X) * ( 1 + E(X + 1 ) ) Rearranging terms: E(X) = 1 + ( 13 - X ) * E( X + 1 ) / ( 16 - X ) This little ASH program calculates this: float [14] factors; factors[ 13 ] = 1.0; for x from 12 downto 0 { float f2 = ( 13.0 - x ) * factors[ x + 1] / (16.0 - x ); factors[ x ] = 1.0 + f2; } for i from 0 to 13 { float px = factors[ i ] ; print( i + ": " + px + " gum = " + ceil( 50.0 * px ) + " Meat" ); } Resulting in this: 0: 4.25 gum = 213 Meat 1: 4.0 gum = 200 Meat 2: 3.75 gum = 188 Meat 3: 3.5 gum = 175 Meat 4: 3.25 gum = 163 Meat 5: 3.0 gum = 150 Meat 6: 2.75 gum = 138 Meat 7: 2.5 gum = 125 Meat 8: 2.25 gum = 113 Meat 9: 2.0 gum = 100 Meat 10: 1.75 gum = 88 Meat 11: 1.5 gum = 75 Meat 12: 1.25 gum = 63 Meat 13: 1.0 gum = 50 Meat From this table, I derive the following formula for expected # of chewing gum needed to retrieve a worthless item: E(X) = ( 17 - X ) / 4 Cost(X) = 12.5 * ( 17 - X ) Meat */ public static PurchaseRequest CHEWING_GUM = NPCStoreDatabase.getPurchaseRequest( ItemPool.CHEWING_GUM ); public static int currentWorthlessItemCost() { int x = InventoryManager.getStarterItemCount(); int gumPrice = InventoryManager.CHEWING_GUM.getPrice(); return (int) Math.ceil( ( gumPrice / 4.0 ) * ( 17 - x ) ); } // *** End of worthless item handling private static boolean invokeBuyScript( final AdventureResult item, final int quantity, final int ingredientLevel, final boolean defaultBuy ) { String scriptName = Preferences.getString( "buyScript" ).trim(); if ( scriptName.length() == 0 ) { return defaultBuy; } List<File> scriptFiles = KoLmafiaCLI.findScriptFile( scriptName ); Interpreter interpreter = KoLmafiaASH.getInterpreter( scriptFiles ); if ( interpreter != null ) { File scriptFile = scriptFiles.get( 0 ); KoLmafiaASH.logScriptExecution( "Starting buy script: ", scriptFile.getName(), interpreter ); Value v = interpreter.execute( "main", new String[] { item.getName(), String.valueOf( quantity ), String.valueOf( ingredientLevel ), String.valueOf( defaultBuy ) } ); KoLmafiaASH.logScriptExecution( "Finished buy script: ", scriptFile.getName(), interpreter ); return v != null && v.intValue() != 0; } return defaultBuy; } private static boolean cheaperToBuy( final AdventureResult item, final int quantity ) { if ( !ItemDatabase.isTradeable( item.getItemId() ) ) { return false; } int mallPrice = StoreManager.getMallPrice( item, 7.0f ) * quantity; if ( mallPrice <= 0 ) { return false; } int makePrice = InventoryManager.priceToMake( item, quantity, 0, false ); if ( makePrice == Integer.MAX_VALUE ) { return true; } if ( mallPrice / 2 < makePrice && makePrice / 2 < mallPrice ) { // Less than a 2:1 ratio, we should check more carefully mallPrice = StoreManager.getMallPrice( item ) * quantity; if ( mallPrice <= 0 ) { return false; } makePrice = InventoryManager.priceToMake( item, quantity, 0, true ); if ( makePrice == Integer.MAX_VALUE ) { return true; } } if ( Preferences.getBoolean( "debugBuy" ) ) { RequestLogger.printLine( "\u262F " + item.getInstance( quantity ) + " mall=" + mallPrice + " make=" + makePrice ); } return mallPrice < makePrice; } private static int itemValue( final AdventureResult item, final boolean exact ) { float factor = Preferences.getFloat( "valueOfInventory" ); if ( factor <= 0.0f ) { return 0; } int lower = 0; int autosell = ItemDatabase.getPriceById( item.getItemId() ); int upper = Math.max( 0, autosell ); if ( factor <= 1.0f ) { return lower + (int) ( ( upper - lower ) * factor ); } factor -= 1.0f; lower = upper; int mall = StoreManager.getMallPrice( item, exact ? 0.0f : 7.0f ); if ( mall > Math.max( 100, 2 * Math.abs( autosell ) ) ) { upper = Math.max( lower, mall ); } if ( factor <= 1.0f ) { return lower + (int) ( ( upper - lower ) * factor ); } factor -= 1.0f; upper = Math.max( lower, mall ); return lower + (int) ( ( upper - lower ) * factor ); } private static int priceToAcquire( final AdventureResult item, int quantity, final int level, final boolean exact, final boolean mallPriceOnly ) { int price = 0; int onhand = Math.min( quantity, item.getCount( KoLConstants.inventory ) ); if ( onhand > 0 ) { if ( item.getItemId() != ItemPool.PLASTIC_SWORD ) { price = mallPriceOnly ? 0 : InventoryManager.itemValue( item, exact ); } price *= onhand; quantity -= onhand; if ( quantity == 0 ) { if ( Preferences.getBoolean( "debugBuy" ) ) { RequestLogger.printLine( "\u262F " + item.getInstance( onhand ) + " onhand=" + price ); } return price; } } int mallPrice = StoreManager.getMallPrice( item, exact ? 0.0f : 7.0f ) * quantity; if ( mallPrice <= 0 ) { mallPrice = Integer.MAX_VALUE; } else { mallPrice += price; } int makePrice = InventoryManager.priceToMake( item, quantity, level, exact, mallPriceOnly ); if ( makePrice != Integer.MAX_VALUE ) { makePrice += price; } if ( !exact && mallPrice / 2 < makePrice && makePrice / 2 < mallPrice ) { // Less than a 2:1 ratio, we should check more carefully return InventoryManager.priceToAcquire( item, quantity, level, true, mallPriceOnly ); } if ( Preferences.getBoolean( "debugBuy" ) ) { RequestLogger.printLine( "\u262F " + item.getInstance( quantity ) + " mall=" + mallPrice + " make=" + makePrice ); } return Math.min( mallPrice, makePrice ); } private static int priceToMake( final AdventureResult item, final int quantity, final int level, final boolean exact, final boolean mallPriceOnly ) { int id = item.getItemId(); int meatCost = CombineMeatRequest.getCost( id ); if ( meatCost > 0 ) { return meatCost * quantity; } CraftingType method = ConcoctionDatabase.getMixingMethod( item ); EnumSet<CraftingRequirements> requirements = ConcoctionDatabase.getRequirements( id ); if ( level > 10 || !ConcoctionDatabase.isPermittedMethod( method, requirements ) ) { return Integer.MAX_VALUE; } int price = ConcoctionDatabase.getCreationCost( method ); int yield = ConcoctionDatabase.getYield( id ); int madeQuantity = ( quantity + yield - 1 ) / yield; AdventureResult ingredients[] = ConcoctionDatabase.getIngredients( id ); for ( int i = 0; i < ingredients.length; ++i ) { AdventureResult ingredient = ingredients[ i ]; int needed = ingredient.getCount() * madeQuantity; int ingredientPrice = InventoryManager.priceToAcquire( ingredient, needed, level + 1, exact, mallPriceOnly ); if ( ingredientPrice == Integer.MAX_VALUE ) { return ingredientPrice; } price += ingredientPrice; } return price * quantity / ( yield * madeQuantity ); } private static int priceToMake( final AdventureResult item, final int qty, final int level, final boolean exact ) { return InventoryManager.priceToMake( item, qty, level, exact, false ); } private static int getPurchaseCount( final int itemId, final int missingCount ) { if ( missingCount >= InventoryManager.BULK_PURCHASE_AMOUNT || !KoLCharacter.canInteract() || KoLCharacter.getAvailableMeat() < 5000 ) { return missingCount; } if ( InventoryManager.shouldBulkPurchase( itemId ) ) { return InventoryManager.BULK_PURCHASE_AMOUNT; } return missingCount; } private static final boolean hasAnyIngredient( final int itemId ) { return InventoryManager.hasAnyIngredient( itemId, null ); } private static final boolean hasAnyIngredient( final int itemId, HashSet<Integer> seen ) { if ( itemId < 0 ) { return false; } switch ( itemId ) { case ItemPool.MEAT_PASTE: return KoLCharacter.getAvailableMeat() >= 10; case ItemPool.MEAT_STACK: return KoLCharacter.getAvailableMeat() >= 100; case ItemPool.DENSE_STACK: return KoLCharacter.getAvailableMeat() >= 1000; } AdventureResult[] ingredients = ConcoctionDatabase.getStandardIngredients( itemId ); boolean shouldUseCloset = InventoryManager.canUseCloset(); for ( int i = 0; i < ingredients.length; ++i ) { AdventureResult ingredient = ingredients[ i ]; // An item is immediately available if it is in your // inventory, or in your closet. if ( KoLConstants.inventory.contains( ingredient ) ) { return true; } if ( shouldUseCloset && KoLConstants.closet.contains( ingredient ) ) { return true; } } Integer key = IntegerPool.get( itemId ); if ( seen == null ) { seen = new HashSet<Integer>(); } else if ( seen.contains( key ) ) { return false; } seen.add( key ); for ( int i = 0; i < ingredients.length; ++i ) { // An item is immediately available if you have the // ingredients for a substep. if ( InventoryManager.hasAnyIngredient( ingredients[ i ].getItemId(), seen ) ) { return true; } } return false; } private static boolean shouldBulkPurchase( final int itemId ) { // We always bulk purchase certain specific items. switch ( itemId ) { case ItemPool.REMEDY: // soft green echo eyedrop antidote case ItemPool.TINY_HOUSE: case ItemPool.DRASTIC_HEALING: case ItemPool.ANTIDOTE: return true; } if ( !KoLmafia.isAdventuring() ) { return false; } // We bulk purchase consumable items if we are // auto-adventuring. if ( RestoresDatabase.isRestore( itemId ) ) { return true; } return false; } public static boolean itemAvailable( final AdventureResult item ) { if ( item == null ) { return false; } return InventoryManager.itemAvailable( item.getItemId() ); } public static boolean itemAvailable( final int itemId ) { return InventoryManager.hasItem( itemId ) || InventoryManager.canUseStorage( itemId ) || InventoryManager.canUseMall( itemId ) || InventoryManager.canUseNPCStores( itemId ) || InventoryManager.canUseCoinmasters( itemId ) || InventoryManager.canUseClanStash( itemId ) || InventoryManager.canUseCloset( itemId ); } public static boolean canUseMall( final AdventureResult item ) { if ( item == null ) { return false; } return InventoryManager.canUseMall( item.getItemId() ); } public static boolean canUseMall( final int itemId ) { return ItemDatabase.isTradeable( itemId ) && InventoryManager.canUseMall(); } public static boolean canUseMall() { return KoLCharacter.canInteract() && Preferences.getBoolean( "autoSatisfyWithMall" ) && !Limitmode.limitMall(); } public static boolean canUseNPCStores( final AdventureResult item ) { if ( item == null ) { return false; } return InventoryManager.canUseNPCStores( item.getItemId() ); } public static boolean canUseNPCStores( final int itemId ) { return InventoryManager.canUseNPCStores() && NPCStoreDatabase.contains( itemId ); } public static boolean canUseNPCStores() { return Preferences.getBoolean( "autoSatisfyWithNPCs" ) && !Limitmode.limitNPCStores(); } public static boolean canUseCoinmasters( final AdventureResult item ) { if ( item == null ) { return false; } return InventoryManager.canUseCoinmasters( item.getItemId() ); } public static boolean canUseCoinmasters( final int itemId ) { return InventoryManager.canUseCoinmasters() && CoinmastersDatabase.contains( itemId ); } public static boolean canUseCoinmasters() { return Preferences.getBoolean( "autoSatisfyWithCoinmasters" ) && !Limitmode.limitCoinmasters(); } public static boolean canUseClanStash( final AdventureResult item ) { if ( item == null ) { return false; } boolean canUseStash = InventoryManager.canUseClanStash(); if ( canUseStash && !ClanManager.isStashRetrieved() ) { RequestThread.postRequest( new ClanStashRequest() ); } return canUseStash && item.getCount( ClanManager.getStash() ) > 0; } public static boolean canUseClanStash( final int itemId ) { AdventureResult item = ItemPool.get( itemId, 1 ); return InventoryManager.canUseClanStash( item ); } public static boolean canUseClanStash() { return KoLCharacter.canInteract() && Preferences.getBoolean( "autoSatisfyWithStash" ) && KoLCharacter.hasClan() && !Limitmode.limitClan(); } public static boolean canUseCloset( final AdventureResult item ) { if ( item == null ) { return false; } return InventoryManager.canUseCloset() && item.getCount( KoLConstants.closet ) > 0; } public static boolean canUseCloset( final int itemId ) { AdventureResult item = ItemPool.get( itemId, 1 ); return InventoryManager.canUseCloset( item ); } public static boolean canUseCloset() { return Preferences.getBoolean( "autoSatisfyWithCloset" ) && !Limitmode.limitCampground(); } public static boolean canUseStorage( final AdventureResult item ) { if ( item == null ) { return false; } return InventoryManager.canUseStorage() && item.getCount( KoLConstants.storage ) > 0; } public static boolean canUseStorage( final int itemId ) { AdventureResult item = ItemPool.get( itemId, 1 ); return InventoryManager.canUseStorage( item ); } public static boolean canUseStorage() { return KoLCharacter.canInteract() && Preferences.getBoolean( "autoSatisfyWithStorage" ) && !Limitmode.limitStorage(); } public static final void fireInventoryChanged( final int itemId ) { ItemListenerRegistry.fireItemChanged( itemId ); } private static Pattern COT_PATTERN = Pattern.compile( "Current Occupant:.*?<b>.* the (.*?)</b>" ); public static final AdventureResult CROWN_OF_THRONES = ItemPool.get( ItemPool.HATSEAT, 1 ); public static final void checkCrownOfThrones() { // If we are wearing the Crown of Thrones, we've already seen // which familiar is riding in it if ( KoLCharacter.hasEquipped( InventoryManager.CROWN_OF_THRONES, EquipmentManager.HAT ) ) { return; } // The Crown of Thrones is not trendy, but double check anyway AdventureResult item = InventoryManager.CROWN_OF_THRONES; if ( !StandardRequest.isAllowed( "Items", item.getName() ) ) { return; } // See if we have a Crown of Thrones in inventory or closet int count = item.getCount( KoLConstants.inventory ) + item.getCount( KoLConstants.closet ); if ( count == 0 ) { return; } // See which familiar is riding in it. String descId = ItemDatabase.getDescriptionId( ItemPool.HATSEAT ); GenericRequest req = new GenericRequest( "desc_item.php?whichitem=" + descId ); RequestThread.postRequest( req ); Matcher matcher = InventoryManager.COT_PATTERN.matcher( req.responseText ); if ( matcher.find() ) { String race = matcher.group( 1 ); KoLCharacter.setEnthroned( KoLCharacter.findFamiliar( race ) ); } } public static final AdventureResult BUDDY_BJORN = ItemPool.get( ItemPool.BUDDY_BJORN, 1 ); public static final void checkBuddyBjorn() { // If we are wearing the Bjorn Buddy, we've already seen // which familiar is riding in it if ( KoLCharacter.hasEquipped( InventoryManager.BUDDY_BJORN, EquipmentManager.CONTAINER ) ) { return; } // Check if the Buddy Bjorn is Trendy AdventureResult item = InventoryManager.BUDDY_BJORN; if ( !StandardRequest.isAllowed( "Items", item.getName() ) ) { return; } // See if we have a Buddy Bjorn in inventory or closet int count = item.getCount( KoLConstants.inventory ) + item.getCount( KoLConstants.closet ); if ( count == 0 ) { return; } // See which familiar is riding in it. String descId = ItemDatabase.getDescriptionId( ItemPool.BUDDY_BJORN ); GenericRequest req = new GenericRequest( "desc_item.php?whichitem=" + descId ); RequestThread.postRequest( req ); // COT_PATTERN works for this Matcher matcher = InventoryManager.COT_PATTERN.matcher( req.responseText ); if ( matcher.find() ) { String race = matcher.group( 1 ); KoLCharacter.setBjorned( KoLCharacter.findFamiliar( race ) ); } } public static final AdventureResult NO_HAT = ItemPool.get( ItemPool.NO_HAT, 1 ); public static final void checkNoHat() { String mod = Preferences.getString( "_noHatModifier" ); if ( !KoLCharacter.hasEquipped( InventoryManager.NO_HAT, EquipmentManager.HAT ) && !KoLConstants.inventory.contains( InventoryManager.NO_HAT ) ) { return; } if ( mod == "" ) { String rawText = DebugDatabase.rawItemDescriptionText( ItemDatabase.getDescriptionId( ItemPool.NO_HAT ), true ); mod = DebugDatabase.parseItemEnchantments( rawText, new ArrayList<String>(), KoLConstants.EQUIP_HAT ); Preferences.setString( "_noHatModifier", mod ); } Modifiers.overrideModifier( "Item:[" + ItemPool.NO_HAT + "]", mod ); } public static final AdventureResult JICK_SWORD = ItemPool.get( ItemPool.JICK_SWORD, 1 ); public static final void checkJickSword() { String mod = Preferences.getString( "jickSwordModifier" ); if ( mod != "" ) { Modifiers.overrideModifier( "Item:[" + ItemPool.JICK_SWORD + "]", mod ); return; } if ( !KoLCharacter.hasEquipped( InventoryManager.JICK_SWORD, EquipmentManager.WEAPON ) && !KoLConstants.inventory.contains( InventoryManager.JICK_SWORD ) ) { // There are other places it could be, but it only needs to be // checked once ever, and if the sword isn't being used then // it can be checked later return; } if ( mod == "" ) { String rawText = DebugDatabase.rawItemDescriptionText( ItemDatabase.getDescriptionId( ItemPool.JICK_SWORD ), true ); mod = DebugDatabase.parseItemEnchantments( rawText, new ArrayList<String>(), KoLConstants.EQUIP_WEAPON ); Preferences.setString( "jickSwordModifier", mod ); Modifiers.overrideModifier( "Item:[" + ItemPool.JICK_SWORD + "]", mod ); } } private static final AdventureResult GOLDEN_MR_ACCESSORY = ItemPool.get( ItemPool.GOLDEN_MR_ACCESSORY, 1 ); public static void countGoldenMrAccesories() { int oldCount = Preferences.getInteger( "goldenMrAccessories" ); int newCount = InventoryManager.GOLDEN_MR_ACCESSORY.getCount( KoLConstants.inventory ) + InventoryManager.GOLDEN_MR_ACCESSORY.getCount( KoLConstants.closet ) + InventoryManager.GOLDEN_MR_ACCESSORY.getCount( KoLConstants.storage ) + InventoryManager.GOLDEN_MR_ACCESSORY.getCount( KoLConstants.collection ) + InventoryManager.getEquippedCount( InventoryManager.GOLDEN_MR_ACCESSORY ); // A Golden Mr. Accessory cannot be traded or discarded. Once // you purchase one, it's yours forever more. if ( newCount > oldCount ) { if ( oldCount == 0 ) { ResponseTextParser.learnSkill( "The Smile of Mr. A." ); } Preferences.setInteger( "goldenMrAccessories", newCount ); } } private static boolean allowTurnConsumption( final CreateItemRequest creator ) { if ( !GenericFrame.instanceExists() ) { return true; } if ( !InventoryManager.askAboutCrafting( creator ) ) { return false; } return true; } public static boolean askAboutCrafting( final CreateItemRequest creator ) { if ( creator.getQuantityNeeded() < 1 ) { return true; } // Allow the user to permanently squash this prompt. if ( Preferences.getInteger( "promptAboutCrafting" ) < 1 ) { return true; } // If we've already nagged, don't nag. Unless the user wants us to nag. Then, nag. if ( InventoryManager.askedAboutCrafting == KoLCharacter.getUserId() && Preferences.getInteger( "promptAboutCrafting" ) < 2 ) { return true; } // See if we have enough free crafting turns available int freeCrafts = ConcoctionDatabase.getFreeCraftingTurns(); int count = creator.getQuantityNeeded(); int needed = creator.concoction.getAdventuresNeeded( count ); CraftingType mixingMethod = creator.concoction.getMixingMethod(); if ( mixingMethod == CraftingType.JEWELRY ) { freeCrafts += ConcoctionDatabase.getFreeSmithJewelTurns(); } if ( mixingMethod == CraftingType.SMITH || mixingMethod == CraftingType.SSMITH ) { freeCrafts += ConcoctionDatabase.getFreeSmithingTurns(); freeCrafts += ConcoctionDatabase.getFreeSmithJewelTurns(); } if ( needed <= freeCrafts ) { return true; } // We could cast Inigo's automatically here, but nah. Let the user do that. String itemName = creator.getName(); StringBuffer message = new StringBuffer(); if ( freeCrafts > 0 ) { message.append( "You will run out of free crafting turns before you finished crafting " ); } else { int craftingAdvs = needed - freeCrafts; message.append( "You are about to spend " ); message.append( craftingAdvs ); message.append( " adventure" ); if ( craftingAdvs > 1 ) { message.append( "s" ); } message.append( " crafting " ); } message.append( itemName ); message.append( " (" ); message.append( count - creator.concoction.getInitial() ); message.append( "). Are you sure?" ); if ( !InputFieldUtilities.confirm( message.toString() ) ) { return false; } InventoryManager.askedAboutCrafting = KoLCharacter.getUserId(); return true; } public static boolean cloverProtectionActive() { return InventoryManager.cloverProtectionEnabled && Preferences.getBoolean( "cloverProtectActive" ); } // Accessory function just to _temporarily_ disable clover protection so that messing with preferences is unnecessary. private static void setCloverProtection( boolean enabled ) { InventoryManager.cloverProtectionEnabled = enabled; } }