/**
* 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.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.TreeMap;
import java.util.Vector;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.java.dev.spellcast.utilities.LockableListModel;
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.RequestLogger;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.objectpool.IntegerPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.persistence.ItemDatabase;
import net.sourceforge.kolmafia.persistence.MallPriceDatabase;
import net.sourceforge.kolmafia.persistence.NPCStoreDatabase;
import net.sourceforge.kolmafia.request.AutoMallRequest;
import net.sourceforge.kolmafia.request.AutoSellRequest;
import net.sourceforge.kolmafia.request.CoinMasterPurchaseRequest;
import net.sourceforge.kolmafia.request.MallPurchaseRequest;
import net.sourceforge.kolmafia.request.MallSearchRequest;
import net.sourceforge.kolmafia.request.ManageStoreRequest;
import net.sourceforge.kolmafia.request.PurchaseRequest;
import net.sourceforge.kolmafia.swingui.StoreManageFrame;
import net.sourceforge.kolmafia.utilities.AdventureResultArray;
import net.sourceforge.kolmafia.utilities.InputFieldUtilities;
import net.sourceforge.kolmafia.utilities.IntegerArray;
import net.sourceforge.kolmafia.utilities.StringUtilities;
import org.json.JSONException;
import org.json.JSONObject;
public abstract class StoreManager
{
private static final Pattern LOGSPAN_PATTERN = Pattern.compile( "<span class=small>.*?</span>" );
private static final Pattern ADDER_PATTERN =
Pattern.compile( "<tr><td><img src.*?></td><td>(.*?)( *\\((\\d*)\\))?</td><td>([\\d,]+)</td><td>(.*?)</td><td.*?(\\d+)" );
private static final Pattern PRICER_PATTERN =
Pattern.compile( "<tr><td><b>(.*?) .*?<td>([\\d,]+)</td>.*?\"(\\d+)\" name=price\\d+\\[(\\d+).*?value=\"(\\d+)\".*?<td>([\\d,]+)</td>" );
// <tr class="deets" rel="618679857" after="6"><td valign="center"><img src="https://s3.amazonaws.com/images.kingdomofloathing.com/itemimages/cocostraw.gif"></td><td valign="center"><b>slip 'n' slide</b></td><td valign="center" align="center">1,081</td valign="center"><td align="center"><span class="tohide">230</span><input type="text" class="hideit price" rel="230" style="width:80px" name="price[681]" value="230" /></td><td valign="center" align="center"><span class="tohide">∞</span><input type="text" class="hideit lim" style="width:24px" name="limit[681]" value="0" /><input type="submit" value="Save" class="button hideit pricejax" style="font-size: 8pt"/></td><td align="right" valign="center">[<a href="#" class="update">update</a>][<a href="/backoffice.php?pwd=90ef7aca1d45123f7abe567b758c5b89&iid=681&action=prices" class="prices">prices</a>]<span class="tohide">[<a class="take" href="backoffice.php?qty=1&pwd=90ef7aca1d45123f7abe567b758c5b89&action=removeitem&itemid=681">take 1</a>][<a class="take" href="backoffice.php?qty=1081&pwd=90ef7aca1d45123f7abe567b758c5b89&action=removeitem&itemid=681">take ∞</a>]</span><span class="hideit" style="font-size: .9em"> <span class="setp">min price: 230</span><br /><span class="setp">cheapest: 230</span></span></td></tr>
private static Pattern INVENTORY_ROW_PATTERN = Pattern.compile( "<tr class=\"deets\".*?</tr>" );
private static Pattern INVENTORY_PATTERN = Pattern.compile( ".*?>([\\d,]+<).*name=\"price\\[(.*?)\\]\" value=\"(.*?)\".*name=\"limit\\[.*?\\]\" value=\"(.*?)\"" );
// Different formats of inventory table
public static final int ADDER = 1;
public static final int PRICER = 2;
public static final int DEETS = 3;
private static final int RECENT_FIRST = 1;
private static final int OLDEST_FIRST = 2;
private static final int GROUP_BY_NAME = 3;
private static int currentLogSort = StoreManager.RECENT_FIRST;
private static boolean sortItemsByName = false;
private final static long REALISTIC_PRICE_THRESHOLD = 50000000;
private static long potentialEarnings = 0;
private static final LockableListModel<StoreLogEntry> storeLog = new LockableListModel<StoreLogEntry>();
private static final LockableListModel<SoldItem> soldItemList = new LockableListModel<SoldItem>();
private static final LockableListModel<SoldItem> sortedSoldItemList = new LockableListModel<SoldItem>();
private static final IntegerArray mallPrices = new IntegerArray();
private static final LinkedHashMap<Integer, ArrayList<PurchaseRequest>> mallSearches = new LinkedHashMap<Integer, ArrayList<PurchaseRequest>>();
public static boolean soldItemsRetrieved = false;
public static final void clearCache()
{
StoreManager.soldItemsRetrieved = false;
StoreManager.storeLog.clear();
StoreManageFrame.cancelTableEditing();
StoreManager.soldItemList.clear();
StoreManager.sortedSoldItemList.clear();
StoreManager.potentialEarnings = 0;
}
public static long getPotentialEarnings()
{
return StoreManager.potentialEarnings;
}
public static void calculatePotentialEarnings()
{
long earnings = 0;
for ( SoldItem item : StoreManager.soldItemList )
{
int price = item.getPrice();
if ( price < REALISTIC_PRICE_THRESHOLD )
{
earnings += (long)item.getQuantity() * (long)price;
}
}
StoreManager.potentialEarnings = earnings;
StoreManageFrame.updateEarnings( StoreManager.potentialEarnings );
}
/**
* Registers an item inside of the store manager. Note that this includes the price of the item and the limit which
* is used to sell the item.
*/
public static final SoldItem registerItem( final int itemId, final int quantity, final int price, final int limit,
final int lowest )
{
if ( price < REALISTIC_PRICE_THRESHOLD )
{
StoreManager.potentialEarnings += (long) price * (long) quantity;
}
SoldItem newItem = new SoldItem( itemId, quantity, price, limit, lowest );
int itemIndex = StoreManager.soldItemList.indexOf( newItem );
// If the item is brand-new, just return it
if ( itemIndex == -1 )
{
return newItem;
}
// If the item already exists, check it against the one which
// already exists in the list. If there are any changes,
// update.
SoldItem oldItem = StoreManager.soldItemList.get( itemIndex );
if ( oldItem.getQuantity() != quantity ||
oldItem.getPrice() != price ||
oldItem.getLimit() != limit ||
lowest != 0 && oldItem.getLowest() != lowest )
{
return newItem;
}
return oldItem;
}
/**
* Returns the current price of the item with the given item Id. This is useful for auto-adding at the existing
* price.
*/
public static final int getPrice( final int itemId )
{
int currentPrice = 999999999;
for ( int i = 0; i < StoreManager.soldItemList.size(); ++i )
{
if ( StoreManager.soldItemList.get( i ).getItemId() == itemId )
{
currentPrice = StoreManager.soldItemList.get( i ).getPrice();
break;
}
}
return currentPrice;
}
public static final int getLimit( final int itemId )
{
int currentLimit = 0;
for ( int i = 0; i < StoreManager.soldItemList.size(); ++i )
{
if ( StoreManager.soldItemList.get( i ).getItemId() == itemId )
{
currentLimit = StoreManager.soldItemList.get( i ).getLimit();
break;
}
}
return currentLimit;
}
public static final LockableListModel<SoldItem> getSoldItemList()
{
return StoreManager.soldItemList;
}
public static final LockableListModel<SoldItem> getSortedSoldItemList()
{
return StoreManager.sortedSoldItemList;
}
public static final LockableListModel<StoreLogEntry> getStoreLog()
{
return StoreManager.storeLog;
}
public static final void sortStoreLog( final boolean cycleSortType )
{
if ( cycleSortType )
{
switch ( StoreManager.currentLogSort )
{
case RECENT_FIRST:
StoreManager.currentLogSort = StoreManager.OLDEST_FIRST;
break;
case OLDEST_FIRST:
StoreManager.currentLogSort = StoreManager.GROUP_BY_NAME;
break;
case GROUP_BY_NAME:
StoreManager.currentLogSort = StoreManager.RECENT_FIRST;
break;
}
}
// Because StoreLogEntry objects use the current
// internal variable to decide how to sort, a simple
// function call will suffice.
StoreManager.storeLog.sort();
}
public static final void update( String storeText, final int type )
{
// Strip introductory "header" from the string so that we can simplify the matcher.
storeText = storeText.substring( storeText.indexOf( "in Mall:</b></td></tr>" ) + 22 );
StoreManager.potentialEarnings = 0;
ArrayList<SoldItem> newItems = new ArrayList<SoldItem>();
switch ( type )
{
case ADDER:
{
AdventureResult item;
int itemId, price, limit;
// The item matcher here examines each row in the table
// displayed in the standard item-addition page.
Matcher itemMatcher = StoreManager.ADDER_PATTERN.matcher( storeText );
while ( itemMatcher.find() )
{
itemId = StringUtilities.parseInt( itemMatcher.group( 6 ) );
if ( ItemDatabase.getItemName( itemId ) == null )
{
// Do not register new items discovered in your store,
// since the descid is not available
//
// ItemDatabase.registerItem( itemId, itemMatcher.group( 1 ), descId );
continue;
}
int count = itemMatcher.group(2) == null ? 1 : StringUtilities.parseInt( itemMatcher.group(3) );
// Register using item ID, since the name might have changed
item = ItemPool.get( itemId, count );
price = StringUtilities.parseInt( itemMatcher.group( 4 ) );
// In this case, the limit could appear as
// "unlimited", which equates to a limit of 0.
limit = itemMatcher.group( 5 ).startsWith( "<" ) ? 0 : StringUtilities.parseInt( itemMatcher.group( 5 ) );
// Now that all the data has been retrieved,
// register the item that was discovered.
newItems.add( StoreManager.registerItem( item.getItemId(), item.getCount(), price, limit, 0 ) );
}
break;
}
case PRICER:
{
int itemId, quantity, price, limit, lowest;
// The item matcher here examines each row in the table
// displayed in the price management page.
Matcher priceMatcher = StoreManager.PRICER_PATTERN.matcher( storeText );
while ( priceMatcher.find() )
{
itemId = StringUtilities.parseInt( priceMatcher.group( 4 ) );
if ( ItemDatabase.getItemName( itemId ) == null )
{
// Do not register new items discovered in your store,
// since the descid is not available
//
// ItemDatabase.registerItem( itemId, priceMatcher.group( 1 ), descId );
continue;
}
quantity = StringUtilities.parseInt( priceMatcher.group( 2 ) );
price = StringUtilities.parseInt( priceMatcher.group( 3 ) );
limit = StringUtilities.parseInt( priceMatcher.group( 5 ) );
lowest = StringUtilities.parseInt( priceMatcher.group( 6 ) );
// Now that all the data has been retrieved, register
// the item that was discovered.
newItems.add( StoreManager.registerItem( itemId, quantity, price, limit, lowest ) );
}
break;
}
case DEETS:
{
Matcher rowMatcher = StoreManager.INVENTORY_ROW_PATTERN.matcher( storeText );
while ( rowMatcher.find() )
{
Matcher matcher = StoreManager.INVENTORY_PATTERN.matcher( rowMatcher.group( 0 ) );
if ( !matcher.find() )
{
continue;
}
int itemId = StringUtilities.parseInt( matcher.group( 2 ) );
int count = StringUtilities.parseInt( matcher.group( 1 ) );
int price = StringUtilities.parseInt( matcher.group( 3 ) );
int limit = StringUtilities.parseInt( matcher.group( 4 ) );
newItems.add( StoreManager.registerItem( itemId, count, price, limit, 0 ) );
}
break;
}
}
StoreManageFrame.cancelTableEditing();
StoreManager.sortItemsByName = true;
Collections.sort( newItems );
StoreManager.soldItemList.clear();
StoreManager.soldItemList.addAll( newItems );
StoreManager.sortItemsByName = false;
Collections.sort( newItems );
StoreManager.sortedSoldItemList.clear();
StoreManager.sortedSoldItemList.addAll( newItems );
StoreManager.soldItemsRetrieved = true;
// Now, update the title of the store manage
// frame to reflect the new price.
StoreManageFrame.updateEarnings( StoreManager.potentialEarnings );
}
public static final void parseLog( final String logText )
{
StoreManager.storeLog.clear();
Matcher logMatcher = StoreManager.LOGSPAN_PATTERN.matcher( logText );
if ( logMatcher.find() )
{
if ( !logMatcher.group().contains( "<br>" ) )
{
return;
}
ArrayList<StoreLogEntry> currentLog = new ArrayList<StoreLogEntry>();
String[] entries = logMatcher.group().split( "<br>" );
for ( int i = 0; i < entries.length - 1; ++i )
{
String entryString = KoLConstants.ANYTAG_PATTERN.matcher( entries[ i ] ).replaceAll( "" );
StoreLogEntry entry = new StoreLogEntry( entries.length - i - 1, entryString );
currentLog.add( entry );
}
StoreManager.storeLog.addAll( currentLog );
StoreManager.sortStoreLog( false );
}
}
public static class StoreLogEntry
implements Comparable<StoreLogEntry>
{
private final int id;
private final String text;
private final String stringForm;
public StoreLogEntry( final int id, final String text )
{
this.id = id;
String[] pieces = text.split( " " );
this.text = text.substring( pieces[ 0 ].length() + pieces[ 1 ].length() + 2 );
this.stringForm = id + ": " + text;
}
@Override
public String toString()
{
return this.stringForm;
}
public int compareTo( final StoreLogEntry o )
{
if ( o == null )
{
return -1;
}
switch ( StoreManager.currentLogSort )
{
case RECENT_FIRST:
return o.id - this.id;
case OLDEST_FIRST:
return this.id - o.id;
case GROUP_BY_NAME:
return this.text.compareToIgnoreCase( o.text );
default:
return -1;
}
}
}
public static final void flushCache( final int itemId, final int shopId )
{
Iterator<ArrayList<PurchaseRequest>> i1 = StoreManager.mallSearches.values().iterator();
while ( i1.hasNext() )
{
ArrayList<PurchaseRequest> search = i1.next();
// Always remove empty searches
if ( search == null || search.size() == 0 )
{
i1.remove();
continue;
}
if ( itemId != -1 && search.get( 0 ).getItemId() != itemId )
{
continue;
}
Iterator<PurchaseRequest> i2 = search.iterator();
while ( i2.hasNext() )
{
PurchaseRequest purchase = i2.next();
if ( purchase instanceof MallPurchaseRequest &&
shopId == ((MallPurchaseRequest) purchase).getShopId() )
{
i2.remove();
StoreManager.updateMallPrice( ItemPool.get( itemId ), search );
if ( itemId != -1 )
{
return;
}
break;
}
}
}
}
public static final void flushCache( final int itemId )
{
Iterator<ArrayList<PurchaseRequest>> i = StoreManager.mallSearches.values().iterator();
while ( i.hasNext() )
{
ArrayList<PurchaseRequest> search = i.next();
// Always remove empty searches
if ( search == null || search.size() == 0 )
{
i.remove();
continue;
}
int id = search.get( 0 ).getItemId();
if ( itemId == id )
{
i.remove();
StoreManager.updateMallPrice( ItemPool.get( itemId ), search );
return;
}
break;
}
}
public static final void flushCache()
{
long t0, t1;
t1 = System.currentTimeMillis();
t0 = t1 - 15 * 1000;
Iterator<ArrayList<PurchaseRequest>> i = StoreManager.mallSearches.values().iterator();
while ( i.hasNext() )
{
ArrayList<PurchaseRequest> search = i.next();
if ( search == null || search.size() == 0 )
{
i.remove();
continue;
}
long t = search.get( 0 ).getTimestamp();
if ( t < t0 || t > t1 )
{
i.remove();
continue;
}
break;
}
}
/**
* Utility method used to search the mall for a specific item.
*/
private static final ArrayList<PurchaseRequest> getSavedSearch( Integer id, final int needed )
{
// Remove search results that are too old
StoreManager.flushCache();
// See if we have a saved search for this id
ArrayList<PurchaseRequest> results = StoreManager.mallSearches.get( id );
if ( results == null )
{
// Nothing saved
return null;
}
if ( results.size() == 0 )
{
// Nothing found last time we looked
return null;
}
// If we don't care how many are available, any saved search is
// good enough
if ( needed == 0 )
{
return results;
}
// See if the saved search will let you purchase enough of the item
int available = 0;
for ( PurchaseRequest result : results )
{
// If we can't use this request, ignore it
if ( !result.canPurchase() )
{
continue;
}
int count = result.getQuantity();
// If there is an unlimited number of this item
// available (because this is an NPC store), that is
// enough for anybody
if ( count == PurchaseRequest.MAX_QUANTITY )
{
return results;
}
// Accumulate available count
available += count;
// If we have found enough available items, this search
// is good enough
if ( available >= needed )
{
return results;
}
}
// Not enough
return null;
}
public static final ArrayList<PurchaseRequest> searchMall( final AdventureResult item )
{
int itemId = item.getItemId();
int needed = item.getCount();
if ( itemId <= 0 )
{
// This should not happen.
return new ArrayList<PurchaseRequest>();
}
Integer id = IntegerPool.get( itemId );
String name = ItemDatabase.getItemDataName( id );
ArrayList<PurchaseRequest> results = StoreManager.getSavedSearch( id, needed );
if ( results != null )
{
KoLmafia.updateDisplay( "Using cached search results for " + name + "..." );
return results;
}
results = StoreManager.searchMall( "\"" + name + "\"", 0 );
// Flush CoinMasterPurchaseRequests
Iterator<PurchaseRequest> it = results.iterator();
while ( it.hasNext() )
{
if ( it.next() instanceof CoinMasterPurchaseRequest )
{
it.remove();
}
}
if ( KoLmafia.permitsContinue() )
{
StoreManager.mallSearches.put( id, results );
}
return results;
}
public static final ArrayList<PurchaseRequest> searchOnlyMall( final AdventureResult item )
{
// Get a potentially cached list of search request from both PC and NPC stores,
// Coinmaster Requests have already been filtered out
ArrayList<PurchaseRequest> allResults = StoreManager.searchMall( item );
// Filter out NPC stores
ArrayList<PurchaseRequest> results = new ArrayList<PurchaseRequest>();
for ( PurchaseRequest result : allResults )
{
if ( result.isMallStore )
{
results.add( result );
}
}
return results;
}
public static final ArrayList<PurchaseRequest> searchNPCs( final AdventureResult item )
{
ArrayList<PurchaseRequest> results = new ArrayList<PurchaseRequest>();
int itemId = item.getItemId();
if ( itemId <= 0 )
{
// This should not happen.
return results;
}
PurchaseRequest request = NPCStoreDatabase.getPurchaseRequest( itemId );
if ( request != null )
{
results.add( request );
}
return results;
}
/**
* Utility method used to search the mall for a search string
*/
public static final ArrayList<PurchaseRequest> searchMall( final String searchString, final int maximumResults )
{
ArrayList<PurchaseRequest> results = new ArrayList<PurchaseRequest>();
if ( searchString == null )
{
return results;
}
// Format the search string
String formatted = MallSearchRequest.getSearchString( searchString );
// Issue the search request
MallSearchRequest request = new MallSearchRequest( formatted, maximumResults, results, true );
RequestThread.postRequest( request );
// Sort the results by price, so that NPC stores are in the
// appropriate place
Collections.sort( results );
return results;
}
public static final void searchMall( final String searchString, final int maximumResults, final List resultSummary )
{
resultSummary.clear();
if ( searchString == null )
{
return;
}
ArrayList<PurchaseRequest> results = StoreManager.searchMall( searchString, maximumResults );
PurchaseRequest[] resultsArray = results.toArray( new PurchaseRequest[0] );
TreeMap<Integer, Integer> prices = new TreeMap<Integer, Integer>();
for ( int i = 0; i < resultsArray.length; ++i )
{
PurchaseRequest result = resultsArray[ i ];
if ( result instanceof CoinMasterPurchaseRequest )
{
continue;
}
Integer currentPrice = IntegerPool.get( result.getPrice() );
Integer currentQuantity = prices.get( currentPrice );
if ( currentQuantity == null )
{
prices.put( currentPrice, IntegerPool.get( resultsArray[ i ].getLimit() ) );
}
else
{
prices.put( currentPrice, IntegerPool.get( currentQuantity.intValue() + resultsArray[ i ].getLimit() ) );
}
}
Integer[] priceArray = new Integer[ prices.size() ];
prices.keySet().toArray( priceArray );
for ( int i = 0; i < priceArray.length; ++i )
{
resultSummary.add( " " + KoLConstants.COMMA_FORMAT.format( prices.get( priceArray[ i ] ).intValue() ) + " @ " + KoLConstants.COMMA_FORMAT.format( priceArray[ i ].intValue() ) + " meat" );
}
}
public static final void maybeUpdateMallPrice( final AdventureResult item, final ArrayList<PurchaseRequest> results )
{
if ( StoreManager.mallPrices.get( item.getItemId() ) == 0 )
{
StoreManager.updateMallPrice( item, results );
}
}
public static final void updateMallPrice( final AdventureResult item, final ArrayList<PurchaseRequest> results )
{
if ( item.getItemId() < 1 )
{
return;
}
int price = -1;
int qty = 5;
for ( PurchaseRequest req: results )
{
if ( req instanceof CoinMasterPurchaseRequest || !req.canPurchaseIgnoringMeat() )
{
continue;
}
price = req.getPrice();
qty -= req.getLimit();
if ( qty <= 0 )
{
break;
}
}
StoreManager.mallPrices.set( item.getItemId(), price );
if ( price > 0 )
{
MallPriceDatabase.recordPrice( item.getItemId(), price );
}
}
public static final synchronized int getMallPrice( final AdventureResult item )
{
StoreManager.flushCache();
int itemId = item.getItemId();
if ( itemId < 1 ||
( !ItemDatabase.isTradeable( itemId ) && !NPCStoreDatabase.contains( itemId, true ) ) )
{
return 0;
}
if ( StoreManager.mallPrices.get( itemId ) == 0 )
{
ArrayList<PurchaseRequest> results = StoreManager.searchMall( item.getInstance( 5 ) );
StoreManager.updateMallPrice( item, results );
}
return StoreManager.mallPrices.get( itemId );
}
public static int getMallPrice( AdventureResult item, float maxAge )
{
int id = item.getItemId();
int price = MallPriceDatabase.getPrice( id );
if ( price <= 0 || MallPriceDatabase.getAge( id ) > maxAge )
{
price = StoreManager.getMallPrice( item );
}
return price;
}
/**
* Internal immutable class used to hold a single instance of an item sold in a player's store.
*/
public static class SoldItem
extends Vector<Object>
implements Comparable<Object>
{
private final int itemId;
private final String itemName;
private final int quantity;
private final int price;
private final int limit;
private final int lowest;
public SoldItem( final int itemId, final int quantity, final int price, final int limit, final int lowest )
{
this.itemId = itemId;
this.itemName = ItemDatabase.getItemDataName( itemId );
this.quantity = quantity;
this.price = price;
this.limit = limit;
this.lowest = lowest;
super.add( this.itemName );
super.add( IntegerPool.get( price ) );
super.add( IntegerPool.get( lowest ) );
super.add( IntegerPool.get( quantity ) );
super.add( IntegerPool.get( limit ) );
}
public int getItemId()
{
return this.itemId;
}
public String getItemName()
{
return this.itemName;
}
public int getQuantity()
{
return this.quantity;
}
public int getPrice()
{
return this.price;
}
public int getLimit()
{
return this.limit;
}
public int getLowest()
{
return this.lowest;
}
@Override
public synchronized boolean equals( final Object o )
{
return o != null && o instanceof SoldItem && ( (SoldItem) o ).itemId == this.itemId;
}
@Override
public int hashCode()
{
return this.itemId;
}
public int compareTo( final Object o )
{
if ( o == null || !( o instanceof SoldItem ) )
{
return -1;
}
if ( this.price != 999999999 && ( (SoldItem) o ).price == 999999999 )
{
return -1;
}
if ( this.price == 999999999 && ( (SoldItem) o ).price != 999999999 )
{
return 1;
}
if ( this.price == 999999999 && ( (SoldItem) o ).price == 999999999 )
{
return this.itemName.compareToIgnoreCase( ( (SoldItem) o ).itemName );
}
return StoreManager.sortItemsByName ? this.itemName.compareToIgnoreCase( ( (SoldItem) o ).itemName ) : this.price - ( (SoldItem) o ).price;
}
@Override
public synchronized String toString()
{
StringBuilder buffer = new StringBuilder();
buffer.append( ItemDatabase.getItemName( this.itemId ) );
buffer.append( " (" );
buffer.append( KoLConstants.COMMA_FORMAT.format( this.quantity ) );
if ( this.limit < this.quantity )
{
buffer.append( " limit " );
buffer.append( KoLConstants.COMMA_FORMAT.format( this.limit ) );
}
buffer.append( " @ " );
buffer.append( KoLConstants.COMMA_FORMAT.format( this.price ) );
buffer.append( ")" );
return buffer.toString();
}
}
public static int shopAmount( int itemId )
{
SoldItem item = new SoldItem( itemId, 0, 0, 0, 0 );
int index = StoreManager.soldItemList.indexOf( item );
if ( index == -1 )
{
// The item isn't in your store
return 0;
}
return StoreManager.soldItemList.get( index ).getQuantity();
}
public static void priceItemsAtLowestPrice( boolean avoidMinPrice )
{
RequestThread.postRequest( new ManageStoreRequest() );
SoldItem[] sold = new SoldItem[ StoreManager.soldItemList.size() ];
StoreManager.soldItemList.toArray( sold );
int[] itemId = new int[ sold.length ];
int[] prices = new int[ sold.length ];
int[] limits = new int[ sold.length ];
// Now determine the desired prices on items.
for ( int i = 0; i < sold.length; ++i )
{
itemId[ i ] = sold[ i ].getItemId();
limits[ i ] = sold[ i ].getLimit();
int minimumPrice = Math.max( 100, Math.abs( ItemDatabase.getPriceById( sold[ i ].getItemId() ) ) * 2 );
int desiredPrice = Math.max( minimumPrice, sold[ i ].getLowest() - sold[ i ].getLowest() % 100 );
if ( sold[ i ].getPrice() == 999999999 && ( !avoidMinPrice || desiredPrice > minimumPrice ) )
{
prices[ i ] = desiredPrice;
}
else
{
prices[ i ] = sold[ i ].getPrice();
}
}
RequestThread.postRequest( new ManageStoreRequest( itemId, prices, limits ) );
KoLmafia.updateDisplay( "Repricing complete." );
}
public static void endOfRunSale( boolean avoidMinPrice )
{
if ( !KoLCharacter.canInteract() )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "You are not yet out of ronin." );
return;
}
if ( !InputFieldUtilities.confirm( "Are you sure you'd like to host an end-of-run sale?" ) )
{
return;
}
// Only place items in the mall which are not
// sold in NPC stores and can be autosold.
AdventureResult[] items = new AdventureResult[ KoLConstants.inventory.size() ];
KoLConstants.inventory.toArray( items );
AdventureResultArray autosell = new AdventureResultArray();
AdventureResultArray automall = new AdventureResultArray();
for ( int i = 0; i < items.length; ++i )
{
int itemId = items[ i ].getItemId();
if ( itemId == ItemPool.MEAT_PASTE || itemId == ItemPool.MEAT_STACK || itemId == ItemPool.DENSE_STACK )
{
continue;
}
if ( !ItemDatabase.isTradeable( itemId ) )
{
continue;
}
if ( ItemDatabase.getPriceById( itemId ) <= 0 )
{
continue;
}
if ( NPCStoreDatabase.contains( itemId, false ) )
{
autosell.add( items[ i ] );
}
else
{
automall.add( items[ i ] );
}
}
// Now, place all the items in the mall at the
// maximum possible price. This allows KoLmafia
// to determine the minimum price.
if ( autosell.size() > 0 && KoLmafia.permitsContinue() )
{
RequestThread.postRequest( new AutoSellRequest( autosell.toArray() ) );
}
if ( automall.size() > 0 && KoLmafia.permitsContinue() )
{
RequestThread.postRequest( new AutoMallRequest( automall.toArray() ) );
}
// Now, remove all the items that you intended
// to remove from the store due to pricing issues.
if ( KoLmafia.permitsContinue() )
{
priceItemsAtLowestPrice( avoidMinPrice );
}
KoLmafia.updateDisplay( "Undercutting sale complete." );
}
public static void addItems( AdventureResult[] items, int[] prices, int[] limits )
{
for ( int i = 0; i < items.length; ++i )
{
StoreManager.addItem( items[ i ], prices[ i ], limits[ i ] );
}
StoreManager.sortItemsByName = true;
Collections.sort( StoreManager.soldItemList );
StoreManager.sortItemsByName = false;
Collections.sort( StoreManager.sortedSoldItemList );
}
public static void addItem( int itemId, int quantity, int price, int limit )
{
StoreManager.addItem( ItemPool.get( itemId, quantity ), price, limit );
StoreManager.sortItemsByName = true;
Collections.sort( StoreManager.soldItemList );
StoreManager.sortItemsByName = false;
Collections.sort( StoreManager.sortedSoldItemList );
}
private static void addItem( AdventureResult item, int price, int limit )
{
int itemId = item.getItemId();
int quantity = item.getCount();
SoldItem soldItem = new SoldItem( itemId, quantity, price, limit, 0);
int index = StoreManager.soldItemList.indexOf( soldItem );
if ( index < 0 )
{
StoreManager.soldItemList.add( soldItem );
StoreManager.sortedSoldItemList.add( soldItem );
}
else
{
int sortedIndex = StoreManager.sortedSoldItemList.indexOf( soldItem );
soldItem = soldItemList.get( index );
int amount = soldItem.getQuantity() + quantity;
int lowest = soldItem.getLowest();
// The new price and limit override existing price and limit
soldItem = new SoldItem( itemId, amount, price, limit, lowest);
StoreManager.soldItemList.set( index, soldItem );
StoreManager.sortedSoldItemList.set( sortedIndex, soldItem );
}
}
public static void updateItem( int itemId, int quantity, int price, int limit )
{
StoreManager.updateItem( ItemPool.get( itemId, quantity ), price, limit );
StoreManager.sortItemsByName = true;
Collections.sort( StoreManager.soldItemList );
StoreManager.sortItemsByName = false;
Collections.sort( StoreManager.sortedSoldItemList );
}
private static void updateItem( AdventureResult item, int price, int limit )
{
int itemId = item.getItemId();
int quantity = item.getCount();
SoldItem soldItem = new SoldItem( itemId, quantity, price, limit, 0);
int index = StoreManager.soldItemList.indexOf( soldItem );
if ( index < 0 )
{
StoreManager.soldItemList.add( soldItem );
StoreManager.sortedSoldItemList.add( soldItem );
}
else
{
int sortedIndex = StoreManager.sortedSoldItemList.indexOf( soldItem );
StoreManager.soldItemList.set( index, soldItem );
StoreManager.sortedSoldItemList.set( sortedIndex, soldItem );
}
}
public static void removeItem( int itemId, int quantity )
{
SoldItem item = new SoldItem( itemId, 0, 0, 0, 0 );
int index = StoreManager.soldItemList.indexOf( item );
int sortedIndex = StoreManager.sortedSoldItemList.indexOf( item );
if ( index < 0 )
{
// Something went wrong, give up
return;
}
item = soldItemList.get( index );
int amount = item.getQuantity() - quantity;
if ( amount == 0 )
{
StoreManager.soldItemList.remove( index );
StoreManager.sortedSoldItemList.remove( sortedIndex );
return;
}
int price = item.getPrice();
int limit = item.getLimit();
int lowest = item.getLowest();
item = new SoldItem( itemId, amount, price, limit, lowest);
StoreManager.soldItemList.set( index, item );
StoreManager.sortedSoldItemList.set( sortedIndex, item );
}
public static final void updateSomePrices( String storeText )
{
int startIndex = storeText.indexOf( "<!-- U:{" );
if ( startIndex == -1 ) return;
startIndex += 7;
int endIndex = storeText.indexOf( "-->", startIndex );
if ( endIndex == -1 ) return;
storeText = storeText.substring( startIndex, endIndex );
JSONObject json;
try
{
json = new JSONObject( storeText );
String[] itemDescs = JSONObject.getNames( json );
for ( String itemDesc : itemDescs )
{
int itemId = ItemDatabase.getItemIdFromDescription( itemDesc );
JSONObject item = json.getJSONObject( itemDesc );
int newPrice = item.getInt( "price" );
int newLimit = item.getInt( "lim" );
StoreManager.SoldItem soldItem = new StoreManager.SoldItem( itemId, 0, 0, 0, 0 );
int index = StoreManager.soldItemList.indexOf( soldItem );
int sortedIndex = StoreManager.sortedSoldItemList.indexOf( soldItem );
// This should only happen if we have not built these lists.
if ( index == -1 || sortedIndex == -1 )
{
continue;
}
soldItem = soldItemList.get( index );
int quantity = soldItem.getQuantity();
int lowest = Math.min( soldItem.getLowest(), newPrice );
soldItem = new StoreManager.SoldItem( itemId, quantity, newPrice, newLimit, lowest );
StoreManager.soldItemList.set( index, soldItem );
StoreManager.sortedSoldItemList.set( sortedIndex, soldItem );
}
StoreManager.sortItemsByName = true;
Collections.sort( StoreManager.soldItemList );
StoreManager.sortItemsByName = false;
Collections.sort( StoreManager.sortedSoldItemList );
}
catch ( JSONException e )
{
RequestLogger.printLine( "JSON failure while updating prices." );
return;
}
}
}