/** * 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.request; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.EnumSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.AdventureResult; import net.sourceforge.kolmafia.KoLAdventure; 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.RequestLogger; import net.sourceforge.kolmafia.SpecialOutfit; import net.sourceforge.kolmafia.objectpool.Concoction; import net.sourceforge.kolmafia.objectpool.ConcoctionPool; import net.sourceforge.kolmafia.objectpool.ItemPool; import net.sourceforge.kolmafia.persistence.ConcoctionDatabase; import net.sourceforge.kolmafia.persistence.ItemDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.session.EquipmentManager; import net.sourceforge.kolmafia.session.InventoryManager; import net.sourceforge.kolmafia.session.ResultProcessor; import net.sourceforge.kolmafia.session.StoreManager; import net.sourceforge.kolmafia.utilities.AdventureResultArray; import net.sourceforge.kolmafia.utilities.StringUtilities; public class CreateItemRequest extends GenericRequest implements Comparable<CreateItemRequest> { public static final GenericRequest REDIRECT_REQUEST = new GenericRequest( "inventory.php?action=message" ); public static final Pattern ITEMID_PATTERN = Pattern.compile( "item\\d?=(\\d+)" ); private static final Pattern QUANTITY_PATTERN = Pattern.compile( "(quantity|qty)=(\\d+)" ); public static final Pattern TARGET_PATTERN = Pattern.compile( "target=(\\d+)" ); public static final Pattern MODE_PATTERN = Pattern.compile( "mode=([^&]+)" ); public static final Pattern CRAFT_PATTERN_1 = Pattern.compile( "[\\&\\?](?:a|b)=(\\d+)" ); public static final Pattern CRAFT_PATTERN_2 = Pattern.compile( "steps\\[\\]=(\\d+),(\\d+)" ); public static final Pattern CRAFT_COMMENT_PATTERN = Pattern.compile( "<!-- ?cr:(\\d+)x(-?\\d+),(-?\\d+)=(\\d+) ?-->" ); // 1=quantity, 2,3=items used, 4=result (redundant) public static final Pattern DISCOVERY_PATTERN = Pattern.compile( "descitem\\((\\d+)\\);" ); public static final Pattern JACKHAMMER_PATTERN = Pattern.compile( "jackhammer lets you finish your smithing in record time" ); public static final Pattern AUTO_ANVIL_PATTERN = Pattern.compile( "auto-anvil handles some of the smithing" ); public static final Pattern THORS_PLIERS_PATTERN = Pattern.compile( "use Thor's Pliers to do the job super fast" ); public static final Pattern RAPID_PROTOTYPING_PATTERN = Pattern.compile( "That rapid prototyping programming you downloaded is really paying dividends!" ); public static final AdventureResult TENDER_HAMMER = ItemPool.get( ItemPool.TENDER_HAMMER, 1 ); public static final AdventureResult GRIMACITE_HAMMER = ItemPool.get( ItemPool.GRIMACITE_HAMMER, 1 ); public Concoction concoction; public AdventureResult createdItem; private String name; private int itemId; private CraftingType mixingMethod; private EnumSet<CraftingRequirements> requirements; protected int beforeQuantity; private int yield; protected int quantityNeeded, quantityPossible, quantityPullable; /** * Constructs a new <code>CreateItemRequest</code> with nothing known other than the form to use. This is used * by descendant classes to avoid weird type-casting problems, as it assumes that there is no known way for the item * to be created. * * @param formSource The form to be used for the item creation * @param conc The Concoction for the item to be handled */ public CreateItemRequest( final String formSource, final Concoction conc ) { super( formSource ); this.concoction = conc; this.itemId = conc.getItemId(); this.name = conc.getName(); this.mixingMethod = CraftingType.SUBCLASS; this.requirements = conc.getRequirements(); // is this right? this.calculateYield(); } private CreateItemRequest( final Concoction conc ) { this( "", conc ); this.mixingMethod = conc.getMixingMethod(); } private void calculateYield() { this.yield = this.concoction.getYield(); this.createdItem = this.concoction.getItem().getInstance( this.yield ); } public void reconstructFields() { String formSource = "craft.php"; String action = "craft"; String mode = null; switch ( this.mixingMethod ) { case COMBINE: case ACOMBINE: case JEWELRY: mode = "combine"; break; case MIX: case MIX_FANCY: mode = "cocktail"; break; case COOK: case COOK_FANCY: mode = "cook"; break; case SMITH: case SSMITH: mode = "smith"; break; case ROLLING_PIN: formSource = "inv_use.php"; break; case MALUS: formSource = "guild.php"; action = "malussmash"; break; default: // For everything else, simply force the datastring to // be rebuilt before the request is submitted this.setDataChanged(); return; } this.constructURLString( formSource ); this.addFormField( "action", action ); if ( mode != null ) { this.addFormField( "mode", mode ); this.addFormField( "ajax", "1" ); } } public static final CreateItemRequest getInstance( final int itemId ) { return CreateItemRequest.getInstance( ConcoctionPool.get( itemId ), true ); } public static final CreateItemRequest getInstance( final int itemId, final boolean rNINP ) { return CreateItemRequest.getInstance( ConcoctionPool.get( itemId ), rNINP ); } public static final CreateItemRequest getInstance( final String name ) { return CreateItemRequest.getInstance( ConcoctionPool.get( name ), true ); } public static final CreateItemRequest getInstance( final AdventureResult item ) { return CreateItemRequest.getInstance( ConcoctionPool.get( item ), true ); } public static final CreateItemRequest getInstance( final AdventureResult item, final boolean rNINP ) { return CreateItemRequest.getInstance( ConcoctionPool.get( item ), rNINP ); } public static final CreateItemRequest getInstance( final Concoction conc, final boolean returnNullIfNotPermitted ) { if ( conc == null ) { ConcoctionDatabase.excuse = null; return null; } CreateItemRequest instance = conc.getRequest(); if ( instance == null ) { ConcoctionDatabase.excuse = null; return null; } if ( instance instanceof CombineMeatRequest ) { return instance; } // If the item creation process is not permitted, then return // null to indicate that it is not possible to create the item. if ( returnNullIfNotPermitted ) { if ( Preferences.getBoolean( "unknownRecipe" + conc.getItemId() ) ) { ConcoctionDatabase.excuse = "That item requires a recipe. If you've already learned it, visit the crafting discoveries page in the relay browser to let KoLmafia know about it."; return null; } Concoction concoction = instance.concoction; if ( !ConcoctionDatabase.checkPermittedMethod( concoction ) ) { // checkPermittedMethod set the excuse return null; } } return instance; } // This API should only be called by Concoction.getRequest(), which // is responsible for caching the instances. public static final CreateItemRequest constructInstance( final Concoction conc ) { if ( conc == null ) { return null; } int itemId = conc.getItemId(); if ( CombineMeatRequest.getCost( itemId ) > 0 ) { return new CombineMeatRequest( conc ); } // Return the appropriate subclass of item which will be // created. switch ( conc.getMixingMethod() ) { case NOCREATE: return null; case STILL: return new StillRequest( conc ); case STARCHART: return new StarChartRequest( conc ); case SUGAR_FOLDING: return new SugarSheetRequest( conc ); case PIXEL: return new PixelRequest( conc ); case GNOME_TINKER: return new GnomeTinkerRequest( conc ); case STAFF: return new ChefStaffRequest( conc ); case SUSHI: return new SushiRequest( conc ); case SINGLE_USE: return new SingleUseRequest( conc ); case MULTI_USE: return new MultiUseRequest( conc ); case CRIMBO05: return new Crimbo05Request( conc ); case CRIMBO06: return new Crimbo06Request( conc ); case CRIMBO07: return new Crimbo07Request( conc ); case CRIMBO12: return new Crimbo12Request( conc ); case CRIMBO16: return new Crimbo16Request( conc ); case PHINEAS: return new PhineasRequest( conc ); case CLIPART: return new ClipArtRequest( conc ); case JARLS: return new JarlsbergRequest( conc ); case GRANDMA: return new GrandmaRequest( conc ); case CHEMCLASS: case ARTCLASS: case SHOPCLASS: return new KOLHSRequest( conc ); case BEER: return new BeerGardenRequest( conc ); case JUNK: return new JunkMagazineRequest( conc ); case WINTER: return new WinterGardenRequest( conc ); case RUMPLE: return new RumpleRequest( conc ); case FIVE_D: return new FiveDPrinterRequest( conc ); case VYKEA: return new VYKEARequest( conc ); case DUTYFREE: return new AirportRequest( conc ); case FLOUNDRY: return new FloundryRequest( conc ); case TERMINAL: return new TerminalExtrudeRequest( conc ); case BARREL: return new BarrelShrineRequest( conc ); case WAX: return new WaxGlobRequest( conc ); case SPANT: return new SpantRequest( conc ); default: return new CreateItemRequest( conc ); } } @Override public boolean equals( final Object o ) { if ( o instanceof CreateItemRequest ) { return this.compareTo( (CreateItemRequest) o ) == 0; } return false; } @Override public int hashCode() { return this.name != null ? this.name.toLowerCase().hashCode() : 0; } public int compareTo( final CreateItemRequest o ) { return o == null ? -1 : this.getName().compareToIgnoreCase( ( (CreateItemRequest) o ).getName() ); } /** * Runs the item creation request. Note that if another item needs to be created for the request to succeed, this * method will fail. */ @Override public void run() { if ( GenericRequest.abortIfInFightOrChoice() ) { return; } if ( !KoLmafia.permitsContinue() || this.quantityNeeded <= 0 ) { return; } // Acquire all needed ingredients if ( this.mixingMethod != CraftingType.SUBCLASS && this.mixingMethod != CraftingType.ROLLING_PIN && !this.makeIngredients() ) { return; } // Save outfit in case we need to equip something - like a Grimacite hammer SpecialOutfit.createImplicitCheckpoint(); while ( this.quantityNeeded > 0 && KoLmafia.permitsContinue() ) { if ( !this.autoRepairBoxServant() ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Auto-repair was unsuccessful." ); break; } this.reconstructFields(); this.beforeQuantity = this.createdItem.getCount( KoLConstants.inventory ); switch ( this.mixingMethod ) { case SUBCLASS: super.run(); if ( this.responseCode == 302 && this.redirectLocation.startsWith( "inventory" ) ) { CreateItemRequest.REDIRECT_REQUEST.constructURLString( this.redirectLocation ).run(); } break; case ROLLING_PIN: this.makeDough(); break; case COINMASTER: this.makeCoinmasterPurchase(); break; default: this.combineItems(); break; } // Certain creations are used immediately. if ( this.noCreation() ) { break; } // Figure out how many items were created int createdQuantity = this.createdItem.getCount( KoLConstants.inventory ) - this.beforeQuantity; // If we created none, log error and stop iterating if ( createdQuantity == 0 ) { // If the subclass didn't detect the failure, do so here. if ( KoLmafia.permitsContinue() ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Creation failed, no results detected." ); } break; } KoLmafia.updateDisplay( "Successfully created " + this.getName() + " (" + createdQuantity + ")" ); this.quantityNeeded -= createdQuantity; } SpecialOutfit.restoreImplicitCheckpoint(); } public boolean noCreation() { return false; } private static final AdventureResult[][] DOUGH_DATA = { // input, tool, output { ItemPool.get( ItemPool.DOUGH, 1 ), ItemPool.get( ItemPool.ROLLING_PIN, 1 ), ItemPool.get( ItemPool.FLAT_DOUGH, 1 ), }, { ItemPool.get( ItemPool.FLAT_DOUGH, 1 ), ItemPool.get( ItemPool.UNROLLING_PIN, 1 ), ItemPool.get( ItemPool.DOUGH, 1 ), }, }; public void makeDough() { AdventureResult input = null; AdventureResult tool = null; AdventureResult output = null; // Find the array row and load the // correct tool/input/output data. for ( AdventureResult[] row : CreateItemRequest.DOUGH_DATA ) { output = row[ 2 ]; if ( this.itemId == output.getItemId() ) { tool = row[ 1 ]; input = row[ 0 ]; break; } } if ( tool == null ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Can't deduce correct tool to use." ); return; } int needed = this.quantityNeeded; // See how many of the ingredient are already available int available = InventoryManager.getAccessibleCount( input ); // See how many of those we should pull into inventory int retrieve = Math.min( available, needed ); // See how many we need to purchase int purchase = needed - retrieve; // Pull accessible ingredients into inventory if ( !InventoryManager.retrieveItem( input.getInstance( retrieve ) ) ) { // This should not happen, but cope. purchase = needed - input.getCount( KoLConstants.inventory ); } if ( purchase > 0 ) { // If we got here, we don't have all of the ingredient that we need // It's always cheaper to buy wads of dough, so just do that. // Using makePurchases directly because retrieveItem does not handle this recursion gracefully. AdventureResult dough = ItemPool.get( ItemPool.DOUGH, purchase ); ArrayList<PurchaseRequest> results = StoreManager.searchNPCs( dough ); KoLmafia.makePurchases( results, results.toArray( new PurchaseRequest[0] ), purchase, false, 50 ); // And if we are making wads of dough, that reduces how many we need if ( output.getItemId() == ItemPool.DOUGH ) { needed -= purchase; } } // Purchasing might have satisfied our needs if ( needed == 0 ) { return; } // If we don't have the correct tool, and the person wishes to // create more than 10 dough, then notify the person that they // should purchase a tool before continuing. if ( needed > 10 && !InventoryManager.retrieveItem( tool ) ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Please purchase a " + tool.getName() + " first." ); return; } // If we have the correct tool, use it to create the dough. if ( InventoryManager.hasItem( tool ) && InventoryManager.retrieveItem( tool ) ) { KoLmafia.updateDisplay( "Using " + tool.getName() + "..." ); UseItemRequest.getInstance( tool ).run(); return; } // Without the right tool, we must knead the dough by hand. String name = output.getName(); UseItemRequest request = UseItemRequest.getInstance( input ); for ( int i = 1; KoLmafia.permitsContinue() && i <= needed; ++i ) { KoLmafia.updateDisplay( "Creating " + name + " (" + i + " of " + needed + ")..." ); request.run(); } } public void makeCoinmasterPurchase() { PurchaseRequest request = this.concoction.getPurchaseRequest(); if ( request == null ) { return; } request.setLimit( this.quantityNeeded ); request.run(); } /** * Helper routine which actually does the item combination. */ private void combineItems() { String path = this.getPath(); String quantityField = "quantity"; this.calculateYield(); AdventureResult[] ingredients = ConcoctionDatabase.getIngredients( this.concoction.getIngredients() ); if ( ingredients.length == 1 ) { if ( this.getAdventuresUsed() > KoLCharacter.getAdventuresLeft() ) { KoLmafia.updateDisplay( MafiaState.ERROR, "Ran out of adventures." ); return; } // If there is only one ingredient, then it probably // only needs a "whichitem" field added to the request. this.addFormField( "whichitem", String.valueOf( ingredients[ 0 ].getItemId() ) ); } else if ( path.equals( "craft.php" ) ) { quantityField = "qty"; this.addFormField( "a", String.valueOf( ingredients[ 0 ].getItemId() ) ); this.addFormField( "b", String.valueOf( ingredients[ 1 ].getItemId() ) ); } else { for ( int i = 0; i < ingredients.length; ++i ) { this.addFormField( "item" + ( i + 1 ), String.valueOf( ingredients[ i ].getItemId() ) ); } } int quantity = ( this.quantityNeeded + this.yield - 1 ) / this.yield; this.addFormField( quantityField, String.valueOf( quantity ) ); KoLmafia.updateDisplay( "Creating " + this.name + " (" + this.quantityNeeded + ")..." ); super.run(); } @Override public void processResults() { if ( CreateItemRequest.parseGuildCreation( this.getURLString(), this.responseText ) ) { return; } CreateItemRequest.parseCrafting( this.getURLString(), this.responseText ); // Check to see if box-servant was overworked and exploded. if ( this.responseText.contains( "Smoke" ) ) { KoLmafia.updateDisplay( "Your box servant has escaped!" ); } } public static int parseCrafting( final String location, final String responseText ) { if ( !location.startsWith( "craft.php" ) ) { return 0; } if ( location.contains( "action=pulverize" ) ) { return PulverizeRequest.parseResponse( location, responseText ); } Matcher m = MODE_PATTERN.matcher( location ); String mode = m.find() ? m.group(1) : ""; if ( mode.equals( "discoveries" ) ) { m = DISCOVERY_PATTERN.matcher( responseText ); while ( m.find() ) { int id = ItemDatabase.getItemIdFromDescription( m.group( 1 ) ); String pref = "unknownRecipe" + id; if ( id > 0 && Preferences.getBoolean( pref ) ) { KoLmafia.updateDisplay( "You know the recipe for " + ItemDatabase.getItemName( id ) ); Preferences.setBoolean( pref, false ); ConcoctionDatabase.setRefreshNeeded( true ); } } return 0; } boolean paste = mode.equals( "combine" ) && ( !KoLCharacter.knollAvailable() || KoLCharacter.inZombiecore() ); int created = 0; m = CRAFT_COMMENT_PATTERN.matcher( responseText ); while ( m.find() ) { // item ids can be -1, if crafting uses a single item int qty = StringUtilities.parseInt( m.group( 1 ) ); int item1 = StringUtilities.parseInt( m.group( 2 ) ); int item2 = StringUtilities.parseInt( m.group( 3 ) ); if ( item1 > 0 ) { ResultProcessor.processItem( item1, -qty ); } if ( item2 > 0 ) { ResultProcessor.processItem( item2, -qty ); } if ( paste ) { ResultProcessor.processItem( ItemPool.MEAT_PASTE, -qty ); } if ( item1 < 0 ) { RequestLogger.updateSessionLog( "Crafting used " + qty + ItemDatabase.getItemName( item2 ) ); } else if ( item2 < 0 ) { RequestLogger.updateSessionLog( "Crafting used " + qty + ItemDatabase.getItemName( item1 ) ); } else { RequestLogger.updateSessionLog( "Crafting used " + qty + " each of " + ItemDatabase.getItemName( item1 ) + " and " + ItemDatabase.getItemName( item2 ) ); } String pref = "unknownRecipe" + m.group( 4 ); if ( Preferences.getBoolean( pref ) ) { KoLmafia.updateDisplay( "(You apparently already knew this recipe.)" ); Preferences.setBoolean( pref, false ); ConcoctionDatabase.setRefreshNeeded( true ); } if ( ItemDatabase.isFancyItem( item1 ) || ItemDatabase.isFancyItem( item2 ) ) { if ( mode.equals( "cook" ) && KoLCharacter.hasChef() ) { Preferences.increment( "chefTurnsUsed", qty ); } else if ( mode.equals( "cocktail" ) && KoLCharacter.hasBartender() ) { Preferences.increment( "bartenderTurnsUsed", qty ); } } created = qty; } if ( responseText.contains( "Smoke" ) ) { String servant = "servant"; if ( mode.equals( "cook" ) ) { servant = "chef"; KoLCharacter.setChef( false ); } else if ( mode.equals( "cocktail" ) ) { servant = "bartender"; KoLCharacter.setBartender( false ); } RequestLogger.updateSessionLog( "Your " + servant + " blew up" ); } if ( mode.equals( "smith" ) ) { int turnsSaved = 0; // Remove from Jackhammer, then Warbear Anvil, then Thor's Pliers Matcher freeTurn = JACKHAMMER_PATTERN.matcher( responseText ); while ( freeTurn.find() ) { int jackhammerTurnsSaved = Math.min( 3 - Preferences.getInteger( "_legionJackhammerCrafting" ), created ); Preferences.increment( "_legionJackhammerCrafting", created, 3, false ); turnsSaved += jackhammerTurnsSaved; } freeTurn = AUTO_ANVIL_PATTERN.matcher( responseText ); while ( freeTurn.find() ) { int autoAnvilTurnsSaved = Math.min( 5 - Preferences.getInteger( "_warbearAutoAnvilCrafting" ), created - turnsSaved ); Preferences.increment( "_warbearAutoAnvilCrafting", created - turnsSaved, 5, false ); turnsSaved += autoAnvilTurnsSaved; } freeTurn = THORS_PLIERS_PATTERN.matcher( responseText ); while ( freeTurn.find() ) { int thorsPliersTurnsSaved = Math.min( 10 - Preferences.getInteger( "_thorsPliersCrafting" ), created - turnsSaved ); Preferences.increment( "_thorsPliersCrafting", created - turnsSaved, 10, false ); turnsSaved += thorsPliersTurnsSaved; } freeTurn = RAPID_PROTOTYPING_PATTERN.matcher( responseText ); while ( freeTurn.find() ) { Preferences.increment( "_rapidPrototypingUsed", created - turnsSaved, 5, false ); } } else { Matcher freeTurn = RAPID_PROTOTYPING_PATTERN.matcher( responseText ); while ( freeTurn.find() ) { Preferences.increment( "_rapidPrototypingUsed", created, 5, false ); } } return created; } public static boolean parseGuildCreation( final String urlString, final String responseText ) { if ( !urlString.startsWith( "guild.php" ) ) { return false; } // If nothing was created, don't deal with ingredients if ( !responseText.contains( "You acquire" ) ) { return true; } int multiplier = 1; // Using the Malus uses 5 ingredients at a time if ( urlString.contains( "action=malussmash" ) ) { multiplier = 5; } else { return true; } AdventureResult [] ingredients = CreateItemRequest.findIngredients( urlString ); int quantity = CreateItemRequest.getQuantity( urlString, ingredients, multiplier ); for ( int i = 0; i < ingredients.length; ++i ) { AdventureResult item = ingredients[i]; ResultProcessor.processItem( item.getItemId(), -quantity ); } return true; } private boolean autoRepairBoxServant() { return CreateItemRequest.autoRepairBoxServant( this.mixingMethod, this.requirements ); } public static boolean autoRepairBoxServant( final CraftingType mixingMethod, final EnumSet<CraftingRequirements> requirements ) { if ( KoLmafia.refusesContinue() ) { return false; } if ( requirements.contains( CraftingRequirements.HAMMER ) && !InventoryManager.retrieveItem( CreateItemRequest.TENDER_HAMMER ) ) { return false; } if ( requirements.contains( CraftingRequirements.GRIMACITE ) ) { AdventureResult hammer = CreateItemRequest.GRIMACITE_HAMMER; int slot = EquipmentManager.WEAPON; if ( !KoLCharacter.hasEquipped( hammer, slot ) && EquipmentManager.canEquip( hammer ) && InventoryManager.retrieveItem( hammer ) ) { ( new EquipmentRequest( hammer, slot ) ).run(); } return KoLCharacter.hasEquipped( hammer, slot ); } // If we are not cooking or mixing, or if we already have the // appropriate servant installed, we don't need to repair switch ( mixingMethod ) { case COOK_FANCY: // We need range installed to cook fancy foods. if ( !KoLCharacter.hasRange() ) { // Acquire and use a range if ( !InventoryManager.retrieveItem( ItemPool.RANGE ) ) { return false; } UseItemRequest.getInstance( ItemPool.get( ItemPool.RANGE, 1 ) ).run(); } // If we have a chef, fancy cooking is now free if ( KoLCharacter.hasChef() ) { return true; } break; case MIX_FANCY: // We need a cocktail kit installed to mix fancy // drinks. if ( !KoLCharacter.hasCocktailKit() ) { // Acquire and use cocktail kit if ( !InventoryManager.retrieveItem( ItemPool.COCKTAIL_KIT ) ) { return false; } UseItemRequest.getInstance( ItemPool.get( ItemPool.COCKTAIL_KIT, 1 ) ).run(); } // If we have a bartender, fancy mixing is now free if ( KoLCharacter.hasBartender() || KoLCharacter.hasSkill( "Cocktail Magic" ) ) { return true; } break; case SMITH: return ( KoLCharacter.knollAvailable() && !KoLCharacter.inZombiecore() ) || InventoryManager.retrieveItem( CreateItemRequest.TENDER_HAMMER ); case SSMITH: return InventoryManager.retrieveItem( CreateItemRequest.TENDER_HAMMER ); default: return true; } boolean autoRepairSuccessful = false; // If they want to auto-repair, make sure that the appropriate // item is available in their inventory switch ( mixingMethod ) { case COOK_FANCY: autoRepairSuccessful = CreateItemRequest.useBoxServant( ItemPool.CHEF, ItemPool.CLOCKWORK_CHEF ); break; case MIX_FANCY: autoRepairSuccessful = CreateItemRequest.useBoxServant( ItemPool.BARTENDER, ItemPool.CLOCKWORK_BARTENDER ); break; } return autoRepairSuccessful && KoLmafia.permitsContinue(); } private static boolean useBoxServant( final int servant, final int clockworkServant ) { // We have no box servant. if ( !Preferences.getBoolean( "autoRepairBoxServants" ) ) { // We don't want to autorepair. It's OK if we don't // require one and have turns available to craft. return !Preferences.getBoolean( "requireBoxServants" ) && ( KoLCharacter.getAdventuresLeft() > 0 || ConcoctionDatabase.getFreeCraftingTurns() > 0 ); } // We want to autorepair. // First, check to see if a box servant is available // for usage, either normally, or through some form // of creation. int usedServant; if ( InventoryManager.hasItem( clockworkServant, false ) ) { usedServant = clockworkServant; } else if ( InventoryManager.hasItem( servant, true ) ) { usedServant = servant; } else if ( InventoryManager.canUseMall() || InventoryManager.canUseClanStash() ) { usedServant = servant; } else { // We can't autorepair. It's still OK if we are willing // to cook without a box servant and have turns // available to craft. return !Preferences.getBoolean( "requireBoxServants" ) && ( KoLCharacter.getAdventuresLeft() > 0 || ConcoctionDatabase.getFreeCraftingTurns() > 0 ); } // Once you hit this point, you're guaranteed to // have the servant in your inventory, so attempt // to repair the box servant. UseItemRequest.getInstance( ItemPool.get( usedServant, 1 ) ).run(); return servant == ItemPool.CHEF ? KoLCharacter.hasChef() : KoLCharacter.hasBartender(); } public boolean makeIngredients() { KoLmafia.updateDisplay( "Verifying ingredients for " + this.name + " (" + this.quantityNeeded + ")..." ); this.calculateYield(); int yield = this.yield; // If this is a combining request, you need meat paste as well. if ( ( this.mixingMethod == CraftingType.COMBINE || this.mixingMethod == CraftingType.ACOMBINE || this.mixingMethod == CraftingType.JEWELRY ) && ( !KoLCharacter.knollAvailable() || KoLCharacter.inZombiecore() ) ) { int pasteNeeded = this.concoction.getMeatPasteNeeded( this.quantityNeeded + this.concoction.initial ); AdventureResult paste = ItemPool.get( ItemPool.MEAT_PASTE, pasteNeeded ); if ( !InventoryManager.retrieveItem( paste ) ) { return false; } } AdventureResult[] ingredients = (AdventureResult[]) ConcoctionDatabase.getIngredients( this.concoction.getIngredients() ).clone(); // Sort ingredients by their creatability, so that if the overall creation // is going to fail, it should do so immediately, without wasted effort. Arrays.sort( ingredients, new Comparator() { public int compare( Object o1, Object o2 ) { Concoction left = ConcoctionPool.get( (AdventureResult) o1 ); if ( left == null ) return -1; Concoction right = ConcoctionPool.get( (AdventureResult) o2 ); if ( right == null ) return 1; return left.creatable - right.creatable; } } ); // Retrieve all the ingredients AdventureResult [] retrievals = new AdventureResult[ ingredients.length ]; for ( int i = 0; i < ingredients.length; ++i ) { // First, calculate the multiplier that's needed // for this ingredient to avoid not making enough // intermediate ingredients and getting an error. int multiplier = 0; for ( int j = 0; j < ingredients.length; ++j ) { if ( ingredients[ i ].getItemId() == ingredients[ j ].getItemId() ) { multiplier += ingredients[ j ].getCount(); } } // Then, make enough of the ingredient in order // to proceed with the concoction. int quantity = this.quantityNeeded * multiplier; if ( yield > 1 ) { quantity = ( quantity + yield - 1 ) / yield; } AdventureResult retrieval = ingredients[ i ].getInstance( quantity ); if ( !InventoryManager.retrieveItem( retrieval ) ) { return false; } retrievals[ i ] = retrieval; } // clockwork clockwise dome = clockwork widget + flange + spring // clockwork widget = flange + cog + sprocket // // Creating a clockwork counterclockwise dome retrieves a flange and a spring and recurses // Creating a clockwork widget sees that it has a flange, retrieves a cog and a socket, // makes the widget and returns // The previously retrieved flange is no longer present for the clockwork counterclockwise dome // // The issue arises when an ingredient is needed both for this creation and another // ingredient and we no longer have a necessary ingredient in inventory after, supposedly, // retrieving all the ingredients // Make another pass to get ingredients that were consumed // while creating other ingredients. for ( int i = 0; i < ingredients.length; ++i ) { if ( !InventoryManager.retrieveItem( retrievals[ i ] ) ) { return false; } } return true; } /** * Returns the item Id for the item created by this request. * * @return The item Id of the item being created */ public int getItemId() { return this.itemId; } /** * Returns the name of the item created by this request. * * @return The name of the item being created */ public String getName() { return this.name; } /** * Returns the quantity of items to be created by this request if it were to run right now. */ public int getQuantityNeeded() { return this.quantityNeeded; } /** * Sets the quantity of items to be created by this request. This method is used whenever the original quantity * intended by the request changes. */ public void setQuantityNeeded( final int quantityNeeded ) { this.quantityNeeded = quantityNeeded; } /** * Returns the quantity of items that could be created with available ingredients. */ public int getQuantityPossible() { return this.quantityPossible; } /** * Sets the quantity of items that could be created. This is set by * refreshConcoctions. */ public void setQuantityPossible( final int quantityPossible ) { this.quantityPossible = quantityPossible; } /** * Returns the quantity of items that could be pulled with the current budget. */ public int getQuantityPullable() { return this.quantityPullable; } /** * Sets the quantity of items that could be pulled. This is set by * refreshConcoctions. */ public void setQuantityPullable( final int quantityPullable ) { this.quantityPullable = quantityPullable; } /** * Returns the string form of this item creation request. This displays the item name, and the amount that will be * created by this request. * * @return The string form of this request */ @Override public String toString() { return this.getName() + " (" + this.getQuantityPossible() + ")"; } /** * An alternative method to doing adventure calculation is determining how many adventures are used by the given * request, and subtract them after the request is done. * * @return The number of adventures used by this request. */ @Override public int getAdventuresUsed() { return CreateItemRequest.getAdventuresUsed( this ); } public static int getAdventuresUsed( final GenericRequest request ) { String urlString = request.getURLString(); String path = request.getPath(); CraftingType mixingMethod = CreateItemRequest.findMixingMethod( urlString ); if ( mixingMethod == CraftingType.NOCREATE ) { return 0; } int multiplier = CreateItemRequest.getMultiplier( mixingMethod ); if ( multiplier == 0 ) { return 0; } AdventureResult [] ingredients = CreateItemRequest.findIngredients( urlString ); int quantity = CreateItemRequest.getQuantity( urlString, ingredients, multiplier ); return CreateItemRequest.getAdventuresUsed( mixingMethod, quantity ); } private static final CraftingType findMixingMethod( final String urlString ) { if ( urlString.startsWith( "guild.php" ) ) { return CraftingType.NOCREATE; } Concoction concoction = CreateItemRequest.findConcoction( urlString ); return concoction == null ? CraftingType.NOCREATE: concoction.getMixingMethod(); } private static final Concoction findConcoction( final String urlString ) { if ( urlString.startsWith( "craft.php" ) ) { if ( urlString.contains( "target" ) ) { Matcher targetMatcher = CreateItemRequest.TARGET_PATTERN.matcher( urlString ); if ( targetMatcher.find() ) { return null; } return ConcoctionPool.get( StringUtilities.parseInt( targetMatcher.group( 1 ) ) ); } return ConcoctionPool.findConcoction( CreateItemRequest.findIngredients( urlString ) ); } return null; } private static int getMultiplier( final CraftingType mixingMethod ) { switch ( mixingMethod ) { case SMITH: return ( KoLCharacter.knollAvailable() && !KoLCharacter.inZombiecore() ) ? 0 : 1; case SSMITH: return 1; case COOK_FANCY: return KoLCharacter.hasChef() ? 0 : 1; case MIX_FANCY: return KoLCharacter.hasBartender() ? 0 : 1; } return 0; } private static int getAdventuresUsed( final CraftingType mixingMethod, final int quantityNeeded ) { switch ( mixingMethod ) { case SMITH: case SSMITH: return Math.max( 0, ( quantityNeeded - ConcoctionDatabase.getFreeCraftingTurns() - ConcoctionDatabase.getFreeSmithingTurns() - ConcoctionDatabase.getFreeSmithJewelTurns() ) ); case COOK_FANCY: case MIX_FANCY: return Math.max( 0, ( quantityNeeded - ConcoctionDatabase.getFreeCraftingTurns() ) ); } return 0; } public static final boolean registerRequest( final boolean isExternal, final String urlString ) { // Delegate to subclasses if appropriate if ( urlString.startsWith( "shop.php" ) ) { // Let shop.php creation methods register themselves return false; } if ( urlString.startsWith( "volcanoisland.php" ) ) { return PhineasRequest.registerRequest( urlString ); } if ( urlString.startsWith( "sushi.php" ) ) { return SushiRequest.registerRequest( urlString ); } if ( urlString.startsWith( "crimbo07.php" ) ) { return Crimbo07Request.registerRequest( urlString ); } if ( urlString.contains( "action=makestaff" ) ) { return ChefStaffRequest.registerRequest( urlString ); } if ( urlString.contains( "action=makepaste" ) || urlString.contains( "action=makestuff" ) ) { return CombineMeatRequest.registerRequest( urlString ); } if ( urlString.startsWith( "multiuse.php" ) ) { if ( MultiUseRequest.registerRequest( urlString ) ) { return true; } if ( SingleUseRequest.registerRequest( urlString ) ) { return true; } return false; } if ( urlString.startsWith( "gnomes.php" ) ) { return GnomeTinkerRequest.registerRequest( urlString ); } if ( urlString.startsWith( "inv_use.php" ) ) { if ( MultiUseRequest.registerRequest( urlString ) ) { return true; } if ( SingleUseRequest.registerRequest( urlString ) ) { return true; } Matcher whichMatcher = CreateItemRequest.WHICHITEM_PATTERN.matcher( urlString ); if ( !whichMatcher.find() ) { return false; } int tool = StringUtilities.parseInt( whichMatcher.group( 1 ) ); int ingredient = -1; switch ( tool ) { case ItemPool.ROLLING_PIN: ingredient = ItemPool.DOUGH; break; case ItemPool.UNROLLING_PIN: ingredient = ItemPool.FLAT_DOUGH; break; default: return false; } RequestLogger.updateSessionLog(); RequestLogger.updateSessionLog( "Use " + ItemDatabase.getDisplayName( tool ) ); // *** Should do this after we get response text back AdventureResult item = ItemPool.get( ingredient ); int quantity = item.getCount( KoLConstants.inventory ); ResultProcessor.processItem( ingredient, 0 - quantity ); return true; } // Now that we know it's not a special subclass instance, // all we do is parse out the ingredients which were used // and then print the attempt to the screen. String command = null; int multiplier = 1; boolean usesTurns = false; if ( urlString.startsWith( "craft.php" ) ) { if ( urlString.contains( "action=pulverize" ) ) { return false; } if ( !urlString.contains( "action=craft" ) ) { return true; } if ( urlString.contains( "mode=combine" ) ) { command = "Combine"; } else if ( urlString.contains( "mode=cocktail" ) ) { command = "Mix"; usesTurns = !KoLCharacter.hasBartender() && !KoLCharacter.hasSkill( "Cocktail Magic" ); } else if ( urlString.contains( "mode=cook" ) ) { command = "Cook"; usesTurns = !KoLCharacter.hasChef(); } else if ( urlString.contains( "mode=smith" ) ) { command = "Smith"; usesTurns = true; } else if ( urlString.contains( "mode=jewelry" ) ) { command = "Ply"; usesTurns = true; } else { // Take credit for all visits to crafting return true; } } else if ( urlString.startsWith( "guild.php" ) ) { if ( urlString.contains( "action=malussmash" ) ) { command = "Pulverize"; multiplier = 5; } else { return false; } } else { return false; } String line = CreateItemRequest.getCreationCommand( command, urlString, multiplier, usesTurns ); RequestLogger.updateSessionLog(); RequestLogger.updateSessionLog( line ); // *** We should deduct ingredients after we have verified that the creation worked. AdventureResult [] ingredients = CreateItemRequest.findIngredients( urlString ); int quantity = CreateItemRequest.getQuantity( urlString, ingredients, multiplier ); CreateItemRequest.useIngredients( urlString, ingredients, quantity ); return true; } public static final String getCreationCommand( final String command, final String urlString, final int multiplier, final boolean usesTurns ) { StringBuilder buffer = new StringBuilder(); if ( usesTurns ) { buffer.append( "[" ); buffer.append( String.valueOf( KoLAdventure.getAdventureCount() ) ); buffer.append( "] " ); } buffer.append( command ); AdventureResult [] ingredients = CreateItemRequest.findIngredients( urlString ); int quantity = CreateItemRequest.getQuantity( urlString, ingredients, multiplier ); for ( int i = 0; i < ingredients.length; ++i ) { AdventureResult item = ingredients[i]; if ( item.getItemId() == 0 ) { continue; } buffer.append( ' ' ); if ( i > 0 ) { buffer.append( "+ " ); } buffer.append( quantity ); buffer.append( ' ' ); buffer.append( item.getName() ); } return buffer.toString(); } public static final AdventureResult [] findIngredients( final String urlString ) { if ( urlString.startsWith( "craft.php" ) && urlString.contains( "target" ) ) { // Crafting is going to make an item from ingredients. // Return the ingredients we think will be used. Matcher targetMatcher = CreateItemRequest.TARGET_PATTERN.matcher( urlString ); if ( !targetMatcher.find() ) { return null; } int itemId = StringUtilities.parseInt( targetMatcher.group( 1 ) ); return ConcoctionDatabase.getIngredients( itemId ); } Matcher matcher = urlString.startsWith( "craft.php" ) ? CreateItemRequest.CRAFT_PATTERN_1.matcher( urlString ) : CreateItemRequest.ITEMID_PATTERN.matcher( urlString ); AdventureResultArray ingredients = new AdventureResultArray(); while ( matcher.find() ) { ingredients.add( CreateItemRequest.getIngredient( matcher.group(1) ) ); } return ingredients.toArray(); } private static final AdventureResult getIngredient( final String itemId ) { return ItemPool.get( StringUtilities.parseInt( itemId ), 1 ); } public static final int getQuantity( final String urlString, final AdventureResult [] ingredients, int multiplier ) { if ( !urlString.contains( "max=on" ) && !urlString.contains( "smashall=1" ) && !urlString.contains( "makeall=on" ) ) { Matcher matcher = CreateItemRequest.QUANTITY_PATTERN.matcher( urlString ); return matcher.find() ? StringUtilities.parseInt( matcher.group( 2 ) ) * multiplier : multiplier; } int quantity = Integer.MAX_VALUE; for ( int i = 0; i < ingredients.length; ++i ) { AdventureResult item = ingredients[i]; quantity = Math.min( item.getCount( KoLConstants.inventory ) / multiplier, quantity ); } return quantity * multiplier; } private static final void useIngredients( final String urlString, AdventureResult [] ingredients, int quantity ) { // Let crafting tell us which ingredients it used and remove // them from inventory after the fact. if ( urlString.startsWith( "craft.php" ) ) { return; } // Similarly,.we deal with ingredients from guild tools later if ( urlString.startsWith( "guild.php" ) ) { return; } // If we have no ingredients, nothing to do if ( ingredients == null ) { return; } for ( int i = 0; i < ingredients.length; ++i ) { AdventureResult item = ingredients[i]; ResultProcessor.processItem( item.getItemId(), 0 - quantity ); } if ( urlString.contains( "mode=combine" ) ) { ResultProcessor.processItem( ItemPool.MEAT_PASTE, 0 - quantity ); } } }