/**
* 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.Iterator;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.kolmafia.AdventureResult;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.KoLConstants.MafiaState;
import net.sourceforge.kolmafia.KoLmafia;
import net.sourceforge.kolmafia.Modifiers;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.listener.NamedListenerRegistry;
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.utilities.StringUtilities;
import org.json.JSONException;
import org.json.JSONObject;
public class StorageRequest
extends TransferItemRequest
{
private int moveType;
private boolean bulkTransfer;
public static final int REFRESH = 0;
public static final int EMPTY_STORAGE = 1;
public static final int STORAGE_TO_INVENTORY = 2;
public static final int PULL_MEAT_FROM_STORAGE = 3;
public static void refresh()
{
// To refresh storage, we get Meat and pulls from the main page
// and items from api.php
RequestThread.postRequest( new StorageRequest( REFRESH ) );
RequestThread.postRequest( new ApiRequest( "storage" ) );
StorageRequest.updateSettings();
}
public static void emptyStorage()
{
RequestThread.postRequest( new StorageRequest( EMPTY_STORAGE ) );
}
public static final void parseStorage( final JSONObject JSON )
{
if ( JSON == null )
{
return;
}
ArrayList<AdventureResult> items = new ArrayList<AdventureResult>();
ArrayList<AdventureResult> freepulls = new ArrayList<AdventureResult>();
ArrayList<AdventureResult> nopulls = 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 )
{
// api.php?what=item does not work for
// items in storage:
// "You don't own that item."
// ItemDatabase.registerItem( itemId );
continue;
}
AdventureResult item = ItemPool.get( itemId, count );
ArrayList list =
KoLCharacter.canInteract() ? items :
StorageRequest.isFreePull( item ) ? freepulls :
StorageRequest.isNoPull( item ) ? nopulls :
items;
list.add( item );
}
}
catch ( JSONException e )
{
ApiRequest.reportParseError( "storage", JSON.toString(), e );
return;
}
KoLConstants.storage.clear();
KoLConstants.storage.addAll( items );
KoLConstants.freepulls.clear();
KoLConstants.freepulls.addAll( freepulls );
KoLConstants.nopulls.clear();
KoLConstants.nopulls.addAll( nopulls );
if ( InventoryManager.canUseStorage() )
{
ConcoctionDatabase.refreshConcoctions();
}
}
public StorageRequest()
{
super( "storage.php" );
this.moveType = StorageRequest.REFRESH;
}
public StorageRequest( final int moveType )
{
this( moveType, new AdventureResult[ 0 ] );
this.moveType = moveType;
}
public StorageRequest( final int moveType, final int amount )
{
this( moveType, new AdventureResult( AdventureResult.MEAT, amount ) );
}
public StorageRequest( final int moveType, final AdventureResult attachment )
{
this( moveType, new AdventureResult[] { attachment } );
}
public StorageRequest( final int moveType, final AdventureResult[] attachments )
{
this( moveType, attachments, false );
}
public StorageRequest( final int moveType, final AdventureResult[] attachments, final boolean bulkTransfer )
{
super( "storage.php", attachments );
this.moveType = moveType;
this.bulkTransfer = bulkTransfer;
// Figure out the actual URL information based on the
// different request types.
switch ( moveType )
{
case REFRESH:
this.addFormField( "which", "5" );
break;
case EMPTY_STORAGE:
this.addFormField( "action", "pullall" );
this.source = KoLConstants.storage;
this.destination = KoLConstants.inventory;
break;
case STORAGE_TO_INVENTORY:
// storage.php?action=pull&whichitem1=1649&howmany1=1&pwd
this.addFormField( "action", "pull" );
this.addFormField( "ajax", "1" );
this.source = KoLConstants.storage;
this.destination = KoLConstants.inventory;
break;
case PULL_MEAT_FROM_STORAGE:
this.addFormField( "action", "takemeat" );
break;
}
}
@Override
protected boolean retryOnTimeout()
{
return this.moveType == StorageRequest.REFRESH;
}
public int getMoveType()
{
return this.moveType;
}
@Override
public String getItemField()
{
return "whichitem";
}
@Override
public String getQuantityField()
{
return "howmany";
}
@Override
public String getMeatField()
{
return "amt";
}
public List getItems()
{
List itemList = new ArrayList();
if ( this.attachments == null )
{
return itemList;
}
for ( int i = 0; i < this.attachments.length; ++i )
{
itemList.add( this.attachments[ i ] );
}
return itemList;
}
@Override
public int getCapacity()
{
return 11;
}
@Override
public boolean forceGETMethod()
{
return this.moveType == STORAGE_TO_INVENTORY;
}
@Override
public TransferItemRequest getSubInstance( final AdventureResult[] attachments )
{
return new StorageRequest( this.moveType, attachments, this.bulkTransfer );
}
@Override
public void run()
{
if ( KoLCharacter.isHardcore() )
{
switch ( this.moveType )
{
case EMPTY_STORAGE:
KoLmafia.updateDisplay( MafiaState.ERROR, "You cannot empty storage while in Hardcore." );
return;
case PULL_MEAT_FROM_STORAGE:
KoLmafia.updateDisplay( MafiaState.ERROR, "You cannot remove meat from storage while in Hardcore." );
return;
case STORAGE_TO_INVENTORY:
for ( int i = 0; i < this.attachments.length; i++ )
{
if ( !KoLConstants.freepulls.contains( this.attachments[i] ) )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "You cannot pull a " +
this.attachments[i].getName() + " in Hardcore." );
return;
}
}
break;
}
}
if ( KoLCharacter.inFistcore() && this.moveType == PULL_MEAT_FROM_STORAGE )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "You cannot remove meat from storage while in Fistcore." );
return;
}
if ( this.moveType == StorageRequest.STORAGE_TO_INVENTORY )
{
boolean nonNullItems = false;
for ( AdventureResult attachment : this.attachments )
{
if ( attachment != null )
{
nonNullItems = true;
break;
}
}
if ( !nonNullItems )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "No items could be removed from storage." );
return;
}
}
// Let TransferItemRequest handle it
super.run();
}
@Override
public void processResults()
{
switch ( this.moveType )
{
case StorageRequest.REFRESH:
StorageRequest.parseStorage( this.getURLString(), this.responseText );
return;
default:
super.processResults();
}
}
// <b>You have 178,634,761 meat in long-term storage.</b>
private static final Pattern STORAGEMEAT_PATTERN =
Pattern.compile( "<b>You have ([\\d,]+) meat in long-term storage.</b>" );
private static final Pattern STORAGEMEAT_FIST_PATTERN =
Pattern.compile( "thinking about the ([\\d,]+) you currently have" );
private static final Pattern PULLS_PATTERN = Pattern.compile( "<span class=\"pullsleft\">(\\d+)</span>" );
private static void parseStorage( final String urlString, final String responseText )
{
if ( !urlString.startsWith( "storage.php" ) )
{
return;
}
// On the main page - which=5 - Hagnk tells you how much meat
// you have in storage and how many pulls you have remaining.
//
// These data do not appear on the three item pages, and items
// do not appear on page 5.
if ( !urlString.contains( "which=5" ) )
{
return;
}
Matcher meatInStorageMatcher = KoLCharacter.inFistcore() ?
StorageRequest.STORAGEMEAT_FIST_PATTERN.matcher( responseText ) :
StorageRequest.STORAGEMEAT_PATTERN.matcher( responseText );
if ( meatInStorageMatcher.find() )
{
int meat = StringUtilities.parseInt( meatInStorageMatcher.group( 1 ) );
KoLCharacter.setStorageMeat( meat );
}
else if ( responseText.contains( "Hagnk doesn't have any of your meat" ) )
{
KoLCharacter.setStorageMeat( 0 );
}
Matcher pullsMatcher = StorageRequest.PULLS_PATTERN.matcher( responseText );
if ( pullsMatcher.find() )
{
ConcoctionDatabase.setPullsRemaining( StringUtilities.parseInt( pullsMatcher.group( 1 ) ) );
}
else if ( KoLCharacter.isHardcore() || !KoLCharacter.canInteract() )
{
ConcoctionDatabase.setPullsRemaining( 0 );
}
else
{
ConcoctionDatabase.setPullsRemaining( -1 );
}
return;
}
public static boolean isFreePull( final AdventureResult item )
{
// For now, special handling for the few items which are a free
// pull only for a specific path. If more path-specific free
// pulls are introduced, we'll define a "Free Pull Path"
// modifier or something.
int itemId = item.getItemId();
if ( ( itemId == ItemPool.BORIS_HELM || itemId == ItemPool.BORIS_HELM_ASKEW ) && !KoLCharacter.inAxecore() )
{
return false;
}
if ( ( itemId == ItemPool.JARLS_COSMIC_PAN || itemId == ItemPool.JARLS_PAN ) && !KoLCharacter.isJarlsberg() )
{
return false;
}
if ( ( itemId == ItemPool.PETE_JACKET || itemId == ItemPool.PETE_JACKET_COLLAR ) && !KoLCharacter.isSneakyPete() )
{
return false;
}
return Modifiers.getBooleanModifier( "Item", itemId, "Free Pull" );
}
public static boolean isNoPull( final AdventureResult item )
{
return Modifiers.getBooleanModifier( "Item", item.getItemId(), "No Pull" );
}
@Override
public boolean parseTransfer()
{
return StorageRequest.parseTransfer( this.getURLString(), this.responseText, this.bulkTransfer );
}
public static final boolean parseTransfer( final String urlString, final String responseText )
{
return StorageRequest.parseTransfer( urlString, responseText, false );
}
private static final Pattern ICHOR_PATTERN = Pattern.compile( "iqty=([\\d,]+)" );
public static final int ichorQuantity( final String urlString )
{
Matcher matcher = ICHOR_PATTERN.matcher( urlString );
return matcher.find() ? StringUtilities.parseInt( matcher.group( 1 ) ) : 0;
}
private static final boolean parseTransfer( final String urlString, final String responseText, final boolean bulkTransfer )
{
String action = GenericRequest.getAction( urlString );
if ( action == null )
{
StorageRequest.parseStorage( urlString, responseText );
return true;
}
if ( action.equals( "tossichor" ) )
{
// So generous, contributing 0 ichor to save the Kingdom.
// You don't have that much ichor. Good Try!
// You toss the ichor into the fissure and hear a distant voice, "Thanks a lot! We can save the Kingdom!"
if ( responseText.contains( "You toss the ichor into the fissure" ) )
{
int ichor = StorageRequest.ichorQuantity( urlString );
ResultProcessor.processResult( ItemPool.get( ItemPool.ELDRITCH_ICHOR, -ichor ) );
}
return true;
}
if ( action.equals( "pullall" ) )
{
// Hagnk leans back and yells something
// ugnigntelligible to a group of Knob Goblin teegnage
// delignquegnts, who go and grab all of your stuff
// from storage and bring it to you.
if ( responseText.contains( "go and grab all of your stuff" ) )
{
StorageRequest.emptyStorage( urlString );
KoLCharacter.updateStatus();
}
return true;
}
boolean transfer = false;
if ( action.equals( "takemeat" ) )
{
if ( responseText.contains( "Meat out of storage" ) )
{
StorageRequest.transferMeat( urlString );
transfer = true;
}
}
else if ( action.equals( "pull" ) )
{
if ( responseText.contains( "moved from storage to inventory" ) )
{
// Pull items from storage and/or freepulls
StorageRequest.transferItems( responseText, bulkTransfer );
transfer = true;
}
}
if ( !urlString.contains( "ajax=1" ) )
{
StorageRequest.parseStorage( urlString, responseText );
}
StorageRequest.updateSettings();
if ( transfer )
{
KoLCharacter.updateStatus();
}
return true;
}
public static final void emptyStorage( final String urlString )
{
KoLConstants.storage.clear();
KoLConstants.freepulls.clear();
KoLCharacter.setStorageMeat( 0 );
// Doing a "pull all" in Hagnk's does not tell
// you what went into inventory and what went
// into the closet.
InventoryManager.refresh();
ClosetRequest.refresh();
NamedListenerRegistry.fireChange( "(coinmaster)" );
// If we are still in a Trendy run or are pulling only
// "favorite things", we may have left items in storage.
if ( KoLCharacter.isTrendy() || KoLCharacter.getRestricted() || urlString.contains( "favonly=1" ) )
{
StorageRequest.refresh();
}
// Update settings
StorageRequest.updateSettings();
}
private static final void updateSettings()
{
if ( KoLConstants.storage.isEmpty() && KoLConstants.freepulls.isEmpty() && KoLCharacter.getStorageMeat() == 0 )
{
Preferences.setInteger( "lastEmptiedStorage", KoLCharacter.getAscensions() );
}
else if ( Preferences.getInteger( "lastEmptiedStorage" ) == KoLCharacter.getAscensions() )
{
// Storage is not empty, but we erroneously thought it was
Preferences.setInteger( "lastEmptiedStorage", -1 );
}
}
// <b>star hat (1)</b> moved from storage to inventory.
private static final Pattern PULL_ITEM_PATTERN = Pattern.compile( "<b>([^<]*) \\((\\d+)\\)</b> moved from storage to inventory" );
private static final void transferItems( final String responseText, final boolean bulkTransfer )
{
// Transfer items from storage and/or freepulls
Matcher matcher = StorageRequest.PULL_ITEM_PATTERN.matcher( responseText );
int pulls = 0;
ArrayList<AdventureResult> list = bulkTransfer ? new ArrayList<AdventureResult>() : null;
while ( matcher.find() )
{
String name = matcher.group( 1 );
int count = StringUtilities.parseInt( matcher.group( 2 ) );
AdventureResult item = ItemPool.get( name, count );
List source;
if ( KoLConstants.freepulls.contains( item ) )
{
source = KoLConstants.freepulls;
}
else
{
source = KoLConstants.storage;
pulls += count;
}
// Remove from storage
AdventureResult.addResultToList( source, item.getNegation() );
if ( bulkTransfer )
{
list.add( item );
}
else
{
// Add to inventory
ResultProcessor.processResult( item );
}
}
if ( bulkTransfer )
{
AdventureResult[] array = new AdventureResult[ list.size() ];
array = list.toArray( array );
StorageRequest.processBulkItems( array );
}
// If remaining is -1, pulls are unlimited.
int remaining = ConcoctionDatabase.getPullsRemaining();
if ( pulls > 0 && remaining >= pulls )
{
ConcoctionDatabase.setPullsRemaining( remaining - pulls );
}
}
private static final void transferMeat( final String urlString )
{
int meat = TransferItemRequest.transferredMeat( urlString, "amt" );
KoLCharacter.setStorageMeat( KoLCharacter.getStorageMeat() - meat );
ResultProcessor.processMeat( meat );
// If remaining is -1, pulls are unlimited.
int remaining = ConcoctionDatabase.getPullsRemaining();
int pulls = (meat + 999 ) / 1000;
if ( pulls > 0 && remaining >= pulls )
{
ConcoctionDatabase.setPullsRemaining( remaining - pulls );
}
}
public static final boolean registerRequest( final String urlString )
{
if ( !urlString.startsWith( "storage.php" ) )
{
return false;
}
if ( urlString.contains( "action=pullall" ) )
{
RequestLogger.updateSessionLog();
RequestLogger.updateSessionLog( "Emptying storage" );
return true;
}
if ( urlString.contains( "action=tossichor" ) )
{
int ichor = StorageRequest.ichorQuantity( urlString );
if ( ichor > 0 )
{
RequestLogger.updateSessionLog();
RequestLogger.updateSessionLog( "Toss " + ichor + " eldritch ichor into the fissure" );
}
return true;
}
if ( urlString.contains( "action=takemeat" ) )
{
int meat = TransferItemRequest.transferredMeat( urlString, "amt" );
String message = "pull: " + meat + " Meat";
if ( meat > 0 )
{
RequestLogger.updateSessionLog();
RequestLogger.updateSessionLog( message );
}
return true;
}
if ( urlString.contains( "pull" ) )
{
return TransferItemRequest.registerRequest(
"pull", urlString, KoLConstants.storage, 0 );
}
return true;
}
@Override
public boolean allowMementoTransfer()
{
return true;
}
@Override
public boolean allowUntradeableTransfer()
{
return true;
}
@Override
public boolean allowUngiftableTransfer()
{
return true;
}
@Override
public String getStatusMessage()
{
switch ( this.moveType )
{
case REFRESH:
return "Examining Meat and pulls in storage";
case EMPTY_STORAGE:
return "Emptying storage";
case STORAGE_TO_INVENTORY:
return "Pulling items from storage";
case PULL_MEAT_FROM_STORAGE:
return "Pulling meat from storage";
default:
return "Unknown request type";
}
}
/**
* Handle lots of items being received at once, deferring updates to
* the end as much as possible.
*/
private static void processBulkItems( AdventureResult[] items )
{
if ( items.length == 0 )
{
return;
}
if ( RequestLogger.isDebugging() )
{
RequestLogger.updateDebugLog( "Processing bulk items" );
}
KoLmafia.updateDisplay( "Processing bulk items..." );
for ( AdventureResult result : items )
{
AdventureResult.addResultToList( KoLConstants.inventory, result );
EquipmentManager.processResult( result );
}
// Assume that at least one item in the list requires this update
NamedListenerRegistry.fireChange( "(coinmaster)" );
KoLmafia.updateDisplay( "Processing complete." );
}
}