/**
* Copyright (c) 2005-2017, KoLmafia development team
* http://kolmafia.sourceforge.net/
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* [1] Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* [2] Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in
* the documentation and/or other materials provided with the
* distribution.
* [3] Neither the name "KoLmafia" nor the names of its contributors may
* be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
package net.sourceforge.kolmafia.session;
import java.io.BufferedReader;
import java.io.File;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.java.dev.spellcast.utilities.LockableListModel;
import net.java.dev.spellcast.utilities.SortedListModel;
import net.sourceforge.kolmafia.AdventureResult;
import net.sourceforge.kolmafia.BuffBotHome;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.KoLMailMessage;
import net.sourceforge.kolmafia.KoLmafia;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.StaticEntity;
import net.sourceforge.kolmafia.moods.MPRestoreItemList;
import net.sourceforge.kolmafia.moods.RecoveryManager;
import net.sourceforge.kolmafia.objectpool.IntegerPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.objectpool.SkillPool;
import net.sourceforge.kolmafia.persistence.NPCStoreDatabase;
import net.sourceforge.kolmafia.persistence.SkillDatabase;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.MailboxRequest;
import net.sourceforge.kolmafia.request.SendMailRequest;
import net.sourceforge.kolmafia.request.UseSkillRequest;
import net.sourceforge.kolmafia.utilities.FileUtilities;
import net.sourceforge.kolmafia.utilities.LogStream;
import net.sourceforge.kolmafia.utilities.PauseObject;
import net.sourceforge.kolmafia.utilities.StringUtilities;
public abstract class BuffBotManager
{
public static final int SAVEBOX = 0;
public static final int DISPOSE = 1;
private static final int REFUND_THRESHOLD = 4;
private static int initialRestores = 0;
private static boolean isInitializing = false;
private static final ArrayList<KoLMailMessage> saveList = new ArrayList<KoLMailMessage>();
private static final ArrayList<KoLMailMessage> deleteList = new ArrayList<KoLMailMessage>();
private static final ArrayList<SendMailRequest> sendList = new ArrayList<SendMailRequest>();
private static int messageDisposalSetting;
private static String refundMessage;
private static String thanksMessage;
private static List<String> whiteList = new ArrayList<String>();
private static final Map<Integer, Offering> buffCostMap = new TreeMap<Integer, Offering>();
private static final SortedListModel<Offering> buffCostTable = new SortedListModel<Offering>();
public static final Pattern MEAT_PATTERN =
Pattern.compile( "<img src=[^>]*?(?:images.kingdomofloathing.com|/images)/itemimages/meat.gif\" height=30 width=30 alt=\"Meat\">You gain ([\\d,]+) Meat" );
public static final Pattern GIFT1_PATTERN =
Pattern.compile( "<a class=nounder style='color: blue' href='showplayer.php\\?who=(\\d+)' target=mainpane>" );
public static final Pattern GIFT2_PATTERN = Pattern.compile( ">>([^<]+)" );
/**
* Resets the buffbot's internal variables and reloads the appropriate variables from memory.
*/
public static final void loadSettings()
{
BuffBotManager.isInitializing = true;
MailManager.clearMailboxes();
BuffBotManager.buffCostMap.clear();
BuffBotManager.buffCostTable.clear();
BuffBotManager.saveList.clear();
BuffBotManager.deleteList.clear();
BuffBotManager.sendList.clear();
String[] currentBuff;
BufferedReader reader =
FileUtilities.getReader( new File( KoLConstants.BUFFBOT_LOCATION, KoLCharacter.baseUserName() + ".txt" ) );
if ( reader == null )
{
BuffBotManager.isInitializing = false;
BuffBotManager.saveBuffs();
return;
}
// It's possible the person is starting from an older release
// of KoLmafia. If that's the case, reload the data from the
// properties file, clear it out, and continue.
while ( ( currentBuff = FileUtilities.readData( reader ) ) != null )
{
if ( currentBuff.length < 3 )
{
continue;
}
BuffBotManager.addBuff(
SkillDatabase.getSkillName( StringUtilities.parseInt( currentBuff[ 0 ] ) ),
StringUtilities.parseInt( currentBuff[ 1 ] ), StringUtilities.parseInt( currentBuff[ 2 ] ) );
}
try
{
reader.close();
}
catch ( Exception e )
{
StaticEntity.printStackTrace( e );
}
BuffBotManager.isInitializing = false;
}
/**
* Returns the table of costs for each buff managed by this buffbot.
*/
public static final LockableListModel<Offering> getBuffCostTable()
{
return BuffBotManager.buffCostTable;
}
/**
* An internal method which adds a buff to the list of available buffs. This also registers the buff inside of the
* list of available buffs.
*/
public static final void addBuff( final String skillName, final int price, final int castCount )
{
if ( price <= 0 || castCount <= 0 )
{
return;
}
Integer newPrice = IntegerPool.get( price );
// Because the new concept allows multiple buffs
// to have the same price, store things in a list.
Offering castList = (Offering) BuffBotManager.buffCostMap.get( newPrice );
// If this price has never existing before, go
// ahead and add a new list to the data structure.
if ( castList == null )
{
castList = new Offering( skillName, price, castCount );
BuffBotManager.buffCostMap.put( newPrice, castList );
BuffBotManager.buffCostTable.add( castList );
}
else
{
int skillId = SkillDatabase.getSkillId( skillName );
int duration = Math.max( 5, SkillDatabase.getEffectDuration( skillId ) );
castList.addBuff( skillName, castCount * duration );
castList.updateFreeState();
int index = BuffBotManager.buffCostTable.indexOf( castList );
BuffBotManager.buffCostTable.fireContentsChanged( BuffBotManager.buffCostTable, index, index );
}
BuffBotManager.saveBuffs();
}
/**
* An internal method which removes the list of selected buffs from the current mappings.
*/
public static final void removeBuffs( final Object[] buffs )
{
Offering toRemove;
boolean removedOne = false;
for ( int i = 0; i < buffs.length; ++i )
{
if ( !BuffBotManager.buffCostTable.contains( buffs[ i ] ) )
{
continue;
}
removedOne = true;
toRemove = (Offering) buffs[ i ];
BuffBotManager.buffCostTable.remove( toRemove );
BuffBotManager.buffCostMap.remove( IntegerPool.get( toRemove.getPrice() ) );
}
if ( removedOne )
{
BuffBotManager.saveBuffs();
}
}
/**
* An internal method which saves the list of buffs into the user-specific settings file.
*/
private static final void saveBuffs()
{
if ( BuffBotManager.isInitializing )
{
return;
}
FileUtilities.downloadFile( "http://kolmafia.sourceforge.net/buffbot.xsl", new File(
KoLConstants.BUFFBOT_LOCATION, "buffbot.xsl" ) );
File datafile = new File( KoLConstants.BUFFBOT_LOCATION, KoLCharacter.baseUserName() + ".txt" );
File xmlfile = new File( KoLConstants.BUFFBOT_LOCATION, KoLCharacter.baseUserName() + ".xml" );
PrintStream settings = LogStream.openStream( datafile, true, "ISO-8859-1" );
PrintStream document = LogStream.openStream( xmlfile, true, "ISO-8859-1" );
document.println( "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>" );
document.println( "<?xml-stylesheet type=\"text/xsl\" href=\"buffbot.xsl\"?>" );
document.println();
document.println( "<botdata>" );
document.println( "<name>" + KoLCharacter.getUserName() + "</name>" );
document.println( "<playerid>" + KoLCharacter.getUserId() + "</playerid>" );
document.println( "<free-list>" );
Offering currentCast;
for ( int i = 0; i < BuffBotManager.buffCostTable.size(); ++i )
{
// First, append the buff to the setting string, then
// print the buff to the XML tree.
currentCast = (Offering) BuffBotManager.buffCostTable.get( i );
settings.println( currentCast.toSettingString() );
for ( int j = 0; j < currentCast.buffs.length; ++j )
{
document.println( "\t<buffdata>" );
document.println( "\t\t<name>" + currentCast.buffs[ j ] + "</name>" );
document.println( "\t\t<skillid>" + SkillDatabase.getSkillId( currentCast.buffs[ j ] ) + "</skillid>" );
document.println( "\t\t<price>" + KoLConstants.COMMA_FORMAT.format( currentCast.price ) + "</price>" );
document.println( "\t\t<turns>" + KoLConstants.COMMA_FORMAT.format( currentCast.turns[ j ] ) + "</turns>" );
document.println( "\t\t<philanthropic>" + currentCast.free + "</philanthropic>" );
document.println( "\t</buffdata>" );
}
}
document.println( "</free-list>" );
document.println( "<normal-list></normal-list>" );
document.println( "</botdata>" );
document.close();
}
/**
* This is the main BuffBot method. It loops until the user cancels, or an exception (such as not enough MP to
* continue). On each pass, it gets all messages from the mailbox, then iterates on the mailbox.
*/
public static final void runBuffBot( final int iterations )
{
BuffBotHome.loadSettings();
// Make sure that the buffbot is wearing the best
// equipment they have available.
UseSkillRequest.optimizeEquipment( 6003 );
BuffBotHome.setBuffBotActive( true );
BuffBotHome.timeStampedLogEntry( BuffBotHome.NOCOLOR, "Buffbot started." );
BuffBotManager.messageDisposalSetting =
StringUtilities.parseInt( Preferences.getString( "buffBotMessageDisposal" ) );
BuffBotManager.whiteList = ClanManager.getWhiteList();
BuffBotManager.refundMessage = Preferences.getString( "invalidBuffMessage" );
BuffBotManager.thanksMessage = Preferences.getString( "thanksMessage" );
BuffBotManager.initialRestores = Math.max( RecoveryManager.getRestoreCount(), 100 );
String restoreItems = Preferences.getString( "mpAutoRecoveryItems" );
PauseObject pauser = new PauseObject();
boolean usingAdventures = restoreItems.indexOf( "rest" ) != -1;
// The outer loop goes until user cancels, or
// for however many iterations are needed.
for ( int i = iterations; BuffBotHome.isBuffBotActive(); --i )
{
// If you run out of adventures and/or restores, then
// check to see if you need to abort.
if ( RecoveryManager.getRestoreCount() == 0 )
{
if ( !usingAdventures || KoLCharacter.getAdventuresLeft() == 0 )
{
if ( NPCStoreDatabase.contains( ItemPool.MMJ ) )
{
AdventureResult restores =
ItemPool.get( ItemPool.MMJ, BuffBotManager.initialRestores );
BuffBotHome.setBuffBotActive( InventoryManager.retrieveItem( restores ) );
}
else
{
AdventureResult restores = ItemPool.get( ItemPool.PHONICS_DOWN, BuffBotManager.initialRestores );
BuffBotHome.setBuffBotActive( InventoryManager.retrieveItem( restores ) );
}
}
}
// If no abort happened due to lack of restores, then you
// can proceed with the next iteration.
BuffBotManager.runOnce();
BuffBotHome.timeStampedLogEntry( BuffBotHome.NOCOLOR, "Message processing complete. Buffbot is sleeping." );
if ( BuffBotManager.initialRestores > 0 )
{
BuffBotHome.timeStampedLogEntry(
BuffBotHome.NOCOLOR, "(" + RecoveryManager.getRestoreCount() + " mana restores remaining)" );
}
else if ( usingAdventures )
{
BuffBotHome.timeStampedLogEntry(
BuffBotHome.NOCOLOR, "(" + KoLCharacter.getAdventuresLeft() + " adventures remaining)" );
}
if ( BuffBotHome.isBuffBotActive() )
{
BuffBotHome.setBuffBotActive( i > 1 );
}
if ( !BuffBotHome.isBuffBotActive() )
{
break;
}
// Sleep for a while and then try again (don't go
// away for more than 1 second at a time to avoid
// automatic re-enabling problems).
for ( int j = 0; j < 60; ++j )
{
pauser.pause( 1000 );
}
}
// After the buffbot is finished running, make sure
// to reset the continue state.
BuffBotHome.timeStampedLogEntry( BuffBotHome.NOCOLOR, "Buffbot stopped." );
BuffBotHome.setBuffBotActive( false );
}
public static final void runOnce()
{
MailManager.getMessages( "Inbox" ).clear();
RequestThread.postRequest( new MailboxRequest( "Inbox" ) );
while ( !BuffBotManager.deleteList.isEmpty() || !BuffBotManager.saveList.isEmpty() )
{
while ( !BuffBotManager.deleteList.isEmpty() )
{
Object[] messages = BuffBotManager.deleteList.toArray();
BuffBotManager.deleteList.clear();
MailManager.deleteMessages( "Inbox", messages );
}
if ( !BuffBotManager.saveList.isEmpty() )
{
Object[] messages = BuffBotManager.saveList.toArray();
BuffBotManager.saveList.clear();
MailManager.saveMessages( "Inbox", messages );
}
}
}
/**
* Queues the message to be sent later. Note that only one message can ever be queued. This ensures that thank-you
* messages do not result in refunds.
*/
private static final void queueOutgoingMessage( final String recipient, final String message,
final AdventureResult result )
{
if ( BuffBotManager.sendList.isEmpty() )
{
BuffBotManager.sendList.add( new SendMailRequest( recipient, message, result ) );
}
}
/**
* Queues an incoming message to be processed. This ensures that the message only appears on one list.
*/
public static final void queueIncomingMessage( final KoLMailMessage message, final boolean delete )
{
if ( !BuffBotManager.saveList.contains( message ) && !BuffBotManager.deleteList.contains( message ) )
{
if ( delete )
{
BuffBotManager.deleteList.add( message );
}
else
{
BuffBotManager.saveList.add( message );
}
}
}
/**
* Overrides/hides the message handling method in <code>MailManager</code>. Because this is a static final
* entity, this doesn't really matter, but it is convenient to have, from a style perspective.
*/
public static final KoLMailMessage addMessage( final String boxname, final String message )
{
KoLMailMessage success = MailManager.addMessage( boxname, message );
if ( success == null || !BuffBotHome.isBuffBotActive() || !boxname.equals( "Inbox" ) )
{
return success;
}
try
{
BuffBotManager.processMessage( success );
KoLmafia.forceContinue();
}
catch ( Exception e )
{
// This should not happen. Therefore, print
// a stack trace for debug purposes.
StaticEntity.printStackTrace( e );
return success;
}
// Abort the buffbot only when you run out of MP
// restores -- otherwise, it's always okay to
// continue using the buffbot.
if ( !BuffBotManager.sendList.isEmpty() )
{
SendMailRequest sending = (SendMailRequest) BuffBotManager.sendList.get( 0 );
BuffBotHome.update(
BuffBotHome.NOCOLOR,
"Sending queued message to " + ContactManager.getPlayerName( sending.getRecipient() ) + "..." );
RequestThread.postRequest( sending );
BuffBotManager.sendList.clear();
}
return success;
}
/**
* Returns whether or not the given username exists on the current white list for restricted buffs.
*/
private static final boolean onWhiteList( final String userName )
{
return Collections.binarySearch( BuffBotManager.whiteList, userName.toLowerCase() ) > -1;
}
/**
* Sends a refund for the given amount to the given user with the appropriate reason attached.
*/
private static final void sendRefund( final String recipient, final String reason, final int amount )
{
if ( BuffBotManager.sendList.isEmpty() )
{
BuffBotManager.queueOutgoingMessage( recipient, reason, new AdventureResult( AdventureResult.MEAT, amount ) );
BuffBotHome.update( BuffBotHome.NONBUFFCOLOR, "Queued refund message for [" + recipient + "]" );
}
}
/**
* Checks to see if there's an attached donation by seeing if there's an image tag, with width of 30. Valentine
* images have width of 100 so we don't mark those as a false positive.
*
* @return <code>true</code> if there is a donation
*/
private static final boolean containsDonation( final KoLMailMessage message )
{
return message.getMessageHTML().indexOf( "You acquire" ) != -1;
}
/**
* Sends a thank you message to the given user, with the given message HTML quoted.
*/
private static final void sendThankYou( final String recipient, final String messageHTML )
{
if ( BuffBotManager.sendList.isEmpty() && !BuffBotManager.thanksMessage.equals( "" ) )
{
String reason =
BuffBotManager.thanksMessage + KoLConstants.LINE_BREAK + KoLConstants.LINE_BREAK + ">" + messageHTML.replaceAll(
"<.*?>", " " ).replaceAll( "[ ]+", " " );
BuffBotManager.queueOutgoingMessage( recipient, reason, new AdventureResult( AdventureResult.MEAT, 0 ) );
BuffBotHome.update( BuffBotHome.NONBUFFCOLOR, "Queued thank you for [" + recipient + "]" );
}
}
private static final Offering extractRequest( final KoLMailMessage message, final int meatSent )
{
Offering castList = (Offering) BuffBotManager.buffCostMap.get( IntegerPool.get( meatSent ) );
// If what is sent does not match anything in the buff table,
// handle it. Once it gets beyond this point, it is known to
// be a valid buff request.
if ( castList != null )
{
BuffBotManager.queueIncomingMessage( message, true );
return castList;
}
if ( meatSent >= 100000 )
{
// If the amount of meat sent is extremely large,
// and no buff matches that value, assume that it's
// a donation and send a thank you note.
BuffBotManager.sendThankYou( message.getSenderName(), message.getMessageHTML() );
BuffBotManager.queueIncomingMessage( message, false );
return null;
}
if ( meatSent != 0 )
{
// If the cast list is empty, and the meat sent was
// not a donation, then the user is not receiving
// any buffs. Therefore, reset the variable.
BuffBotManager.queueIncomingMessage( message, true );
BuffBotHome.update(
BuffBotHome.NONBUFFCOLOR,
"Invalid amount (" + meatSent + " meat) received from " + message.getSenderName() );
BuffBotManager.sendRefund(
message.getSenderName(),
KoLConstants.COMMA_FORMAT.format( meatSent ) + " meat is not a valid buff price. " + BuffBotManager.refundMessage,
meatSent );
return null;
}
// If it gets this far, then it's an empty message.
// Based on the user's settings, do something with
// the message (save, delete, etc.)
switch ( BuffBotManager.messageDisposalSetting )
{
case SAVEBOX:
String messageText = message.getMessageHTML().replaceAll( "<.*?>", "" );
boolean willDelete = messageText.length() < 10;
BuffBotManager.queueIncomingMessage( message, willDelete );
if ( willDelete )
{
BuffBotHome.update(
BuffBotHome.NONBUFFCOLOR, "Deleting non-buff message from [" + message.getSenderName() + "]" );
}
else
{
BuffBotHome.update(
BuffBotHome.NONBUFFCOLOR, "Saving non-buff message from [" + message.getSenderName() + "]" );
}
return null;
case DISPOSE:
BuffBotManager.queueIncomingMessage( message, true );
BuffBotHome.update(
BuffBotHome.NONBUFFCOLOR, "Deleting non-buff message from [" + message.getSenderName() + "]" );
return null;
default:
BuffBotHome.update(
BuffBotHome.NONBUFFCOLOR, "Ignoring non-buff message from [" + message.getSenderName() + "]" );
return null;
}
}
/**
* Utility method which processes the message that was received. This parses out any applicable buffs and sends any
* applicable thank you messages.
*/
private static final void processMessage( final KoLMailMessage message )
throws Exception
{
// Now that you're guaranteed to be above the threshold,
// go ahead and process the message.
BuffBotHome.update( BuffBotHome.NONBUFFCOLOR, "Received message from [" + message.getSenderName() + "]" );
if ( BuffBotManager.containsDonation( message ) )
{
BuffBotManager.sendThankYou( message.getSenderName(), message.getMessageHTML() );
BuffBotManager.queueIncomingMessage( message, false );
return;
}
Matcher meatMatcher = BuffBotManager.MEAT_PATTERN.matcher( message.getMessageHTML() );
int meatSent = meatMatcher.find() ? StringUtilities.parseInt( meatMatcher.group( 1 ) ) : 0;
Offering castList = BuffBotManager.extractRequest( message, meatSent );
if ( castList == null )
{
return;
}
// Ensure that every buff is handled. In the event
// that a buff was partially completed, pretend that
// all buffs were completed.
String requestor = message.getSenderName();
String recipient = requestor;
// If it's clear that the person is receiving a refund because they are
// restricted due to failure, ignore this request.
if ( !BuffBotHome.isPermitted( requestor ) )
{
return;
}
boolean isGiftBuff = false;
Matcher giftMatcher = BuffBotManager.GIFT1_PATTERN.matcher( message.getMessageHTML() );
if ( giftMatcher.find() )
{
isGiftBuff = true;
recipient = giftMatcher.group( 1 );
}
else
{
giftMatcher = BuffBotManager.GIFT2_PATTERN.matcher( message.getMessageHTML() );
if ( giftMatcher.find() )
{
isGiftBuff = true;
recipient = giftMatcher.group( 1 ).trim();
}
}
if ( isGiftBuff && castList.free )
{
return;
}
if ( BuffBotManager.executeBuff( castList, recipient, meatSent ) )
{
return;
}
int failureCount = BuffBotHome.getInstanceCount( 0, requestor ) + 1;
BuffBotHome.addToRecipientList( 0, requestor );
if ( UseSkillRequest.lastUpdate.startsWith( "Selected target cannot receive" ) )
{
BuffBotHome.denyFutureBuffs( requestor );
}
// Record the inability to buff inside of a separate
// file which stores how many refunds were sent that day.
if ( failureCount == BuffBotManager.REFUND_THRESHOLD + 1 )
{
BuffBotManager.sendRefund(
requestor,
"This message is to provide notification that you have already sent " + BuffBotManager.REFUND_THRESHOLD + " " + "buff requests which resulted in a refund to your account. In order to preserve the integrity of this buffbot, from now until the next rollover begins, " + "all requests for once-per-day buffs and all buffs which which might result in a refund will instead be treated as donations.",
0 );
}
else if ( failureCount < BuffBotManager.REFUND_THRESHOLD )
{
if ( !BuffBotHome.isPermitted( requestor ) )
{
BuffBotManager.sendRefund(
requestor,
"It has been determined that at some point during an attempt to buff you in the last 24 hours, you could not receive buffs. " + "This could be either due to engaging in combat, having too many AT songs in your head, or ascending before receiving your buff. As a result of this failure, " + "all of your requests are being refunded rather than processed in order to maintain throughput. Apologies for the inconvenience.",
meatSent );
}
else
{
BuffBotManager.sendRefund(
requestor,
"This buffbot was unable to process your request. " + UseSkillRequest.lastUpdate + " Please try again later." + KoLConstants.LINE_BREAK + KoLConstants.LINE_BREAK + BuffBotManager.refundMessage,
meatSent );
}
}
}
private static final boolean executeBuff( final Offering buff, final String recipient, final int meatSent )
{
// If it's not a philanthropic buff, process the buff as
// normal (no need to slow down to verify).
if ( !buff.free )
{
return buff.castOnTarget( recipient );
}
// If it's not a philanthropic buff request, then go ahead
// and check to see that it's okay to send it.
switch ( Preferences.getInteger( "buffBotPhilanthropyType" ) )
{
case 0:
BuffBotHome.update( BuffBotHome.NONBUFFCOLOR, "Philanthropic buff request from " + recipient );
BuffBotHome.update( BuffBotHome.ERRORCOLOR, " ---> Could not cast #" + meatSent + " on " + recipient );
UseSkillRequest.lastUpdate = "Philanthropic buffs temporarily disabled.";
return false;
case 1:
int instanceCount = BuffBotHome.getInstanceCount( meatSent, recipient );
if ( instanceCount == 0 || buff.casts.length == 1 && buff.getLowestBuffId() == SkillPool.ODE_TO_BOOZE && instanceCount == 1 )
{
break;
}
// This is a philanthropic buff and the user has already
// requested it the maximum number of times alotted. The
// user will not be buffed.
BuffBotHome.update( BuffBotHome.NONBUFFCOLOR, "Philanthropy limit exceeded for " + recipient );
BuffBotHome.update( BuffBotHome.ERRORCOLOR, " ---> Could not cast #" + meatSent + " on " + recipient );
UseSkillRequest.lastUpdate = "Philanthropy limit exceeded.";
return false;
case 2:
if ( BuffBotManager.onWhiteList( recipient ) )
{
break;
}
// This is a restricted buff for a non-allowed user.
// The user will not be buffed.
BuffBotHome.update( BuffBotHome.NONBUFFCOLOR, "Received white list request from un-whitelisted player" );
BuffBotHome.update( BuffBotHome.ERRORCOLOR, " ---> Could not cast #" + meatSent + " on " + recipient );
UseSkillRequest.lastUpdate = "Philanthropic buffs temporarily disabled.";
return false;
}
// Under all other circumstances, you go ahead and
// process the buff request.
BuffBotHome.addToRecipientList( meatSent, recipient );
return buff.castOnTarget( recipient );
}
public static class Offering
implements Comparable<Offering>
{
private final String botName;
private final int price;
private boolean free;
private int lowestBuffId;
public String[] buffs;
public int[] casts;
public int[] turns;
private boolean changed;
private boolean useFullStrings;
private final StringBuffer settingString;
private final StringBuffer stringForm;
public Offering( final String buffName, final int price, final int casts )
{
this( buffName, KoLCharacter.getUserName(), price, 0, false );
int buffId = SkillDatabase.getSkillId( buffName );
this.casts[ 0 ] = casts;
this.turns[ 0 ] = casts * Math.max( 5, SkillDatabase.getEffectDuration( buffId ) );
this.updateFreeState();
this.useFullStrings = true;
this.changed = true;
}
public void updateFreeState()
{
int totalCost = 0;
for ( int i = 0; i < this.casts.length; ++i )
{
totalCost +=
this.casts[ i ] * SkillDatabase.getMPConsumptionById( SkillDatabase.getSkillId( this.buffs[ i ] ) );
}
this.free = this.price <= totalCost * 95 / MPRestoreItemList.getManaRestored( "magical mystery juice" );
}
public Offering( final String buffName, final String botName, final int price, final int turns,
final boolean free )
{
this.buffs = new String[] { buffName };
this.casts = new int[] { turns / 10 };
this.turns = new int[] { turns };
this.lowestBuffId = SkillDatabase.getSkillId( buffName );
this.botName = botName;
this.price = price;
this.free = free;
this.settingString = new StringBuffer();
this.stringForm = new StringBuffer();
this.useFullStrings = false;
this.changed = true;
}
public String getBotName()
{
return this.botName;
}
public int getPrice()
{
return this.price;
}
public int[] getTurns()
{
return this.turns;
}
public int getLowestBuffId()
{
return this.lowestBuffId;
}
@Override
public String toString()
{
if ( this.changed )
{
this.constructStringForm();
this.changed = false;
}
return this.stringForm.toString();
}
public String toSettingString()
{
if ( this.changed )
{
this.constructStringForm();
this.changed = false;
}
return this.settingString.toString();
}
public void addBuff( final String buffName, final int turns )
{
String[] tempNames = new String[ this.buffs.length + 1 ];
int[] tempCasts = new int[ this.casts.length + 1 ];
int[] tempTurns = new int[ this.turns.length + 1 ];
System.arraycopy( this.buffs, 0, tempNames, 0, this.buffs.length );
System.arraycopy( this.casts, 0, tempCasts, 0, this.buffs.length );
System.arraycopy( this.turns, 0, tempTurns, 0, this.buffs.length );
this.buffs = tempNames;
this.casts = tempCasts;
this.turns = tempTurns;
int skillId = SkillDatabase.getSkillId( buffName );
int duration = Math.max( 5, SkillDatabase.getEffectDuration( skillId ) );
this.buffs[ this.buffs.length - 1 ] = buffName;
this.casts[ this.casts.length - 1 ] = turns / duration;
this.turns[ this.turns.length - 1 ] = turns;
if ( skillId < this.lowestBuffId )
{
this.lowestBuffId = skillId;
}
this.changed = true;
}
public boolean castOnTarget( final String target )
{
for ( int i = 0; i < this.buffs.length; ++i )
{
BuffBotHome.recordBuff( target, this.buffs[ i ], this.casts[ i ], this.price );
// Figure out how much MP the buff will take, and then identify
// the number of casts per request that this character can handle.
BuffBotHome.update(
BuffBotHome.BUFFCOLOR,
"Casting " + this.buffs[ i ] + ", " + this.casts[ i ] + " times on " + target + " for " + this.price + " meat... " );
RequestThread.postRequest( UseSkillRequest.getInstance( this.buffs[ i ], target, this.casts[ i ] ) );
if ( UseSkillRequest.lastUpdate.equals( "" ) )
{
BuffBotHome.update(
BuffBotHome.BUFFCOLOR, " ---> Successfully cast " + this.buffs[ i ] + " on " + target );
}
else
{
BuffBotHome.update(
BuffBotHome.ERRORCOLOR, " ---> Could not cast " + this.buffs[ i ] + " on " + target );
return i != 0;
}
}
return true;
}
private void constructStringForm()
{
this.stringForm.setLength( 0 );
this.stringForm.append( KoLConstants.COMMA_FORMAT.format( this.price ) );
this.stringForm.append( " meat for " );
if ( this.turns.length == 1 )
{
this.stringForm.append( KoLConstants.COMMA_FORMAT.format( this.turns[ 0 ] ) );
this.stringForm.append( " turns" );
if ( this.useFullStrings )
{
this.stringForm.append( " of " );
this.stringForm.append( this.buffs[ 0 ] );
if ( this.free )
{
this.stringForm.append( " (once per day)" );
}
}
}
else
{
this.stringForm.insert( 0, "<html>" );
this.stringForm.append( "a" );
if ( this.useFullStrings && this.free )
{
this.stringForm.append( " Once-Per-Day" );
}
this.stringForm.append( " Buff Pack which includes:" );
for ( int i = 0; i < this.buffs.length; ++i )
{
this.stringForm.append( "<br> - " );
this.stringForm.append( KoLConstants.COMMA_FORMAT.format( this.turns[ i ] ) );
this.stringForm.append( " turns of " );
this.stringForm.append( this.buffs[ i ] );
}
this.stringForm.append( "</html>" );
}
this.settingString.setLength( 0 );
for ( int i = 0; i < this.buffs.length; ++i )
{
this.settingString.append( SkillDatabase.getSkillId( this.buffs[ i ] ) );
this.settingString.append( '\t' );
this.settingString.append( this.price );
this.settingString.append( '\t' );
this.settingString.append( this.casts[ i ] );
this.settingString.append( KoLConstants.LINE_BREAK );
}
}
@Override
public boolean equals( final Object o )
{
if ( o == null || !( o instanceof Offering ) )
{
return false;
}
Offering off = (Offering) o;
return this.botName.equalsIgnoreCase( off.botName ) && this.price == off.price && this.turns == off.turns && this.free == off.free;
}
@Override
public int hashCode()
{
int hash = 0;
hash = this.botName != null ? this.botName.hashCode() : 0;
hash = 31 * hash + this.price;
hash = 31 * hash + (this.free ? 1 : 0);
hash = 31 * hash + Arrays.hashCode( this.turns );
return hash;
}
public SendMailRequest toRequest()
{
return new SendMailRequest( this.botName, KoLConstants.DEFAULT_KMAIL, new AdventureResult(
AdventureResult.MEAT, this.price ) );
}
public int compareTo( final Offering o )
{
if ( o == null || !( o instanceof Offering ) )
{
return -1;
}
Offering off = (Offering) o;
// First, buffpacks should come before standard offerings
if ( ( this.turns.length == 1 || off.turns.length == 1 ) && this.turns.length != off.turns.length )
{
return off.turns.length - this.turns.length;
}
// If a buffpack, compare price
if ( this.turns.length > 1 && off.turns.length > 1 )
{
return this.price - off.price;
}
// Compare the Id of the lowest Id buffs
if ( this.lowestBuffId != off.lowestBuffId )
{
return this.lowestBuffId - off.lowestBuffId;
}
// Next compare turns
if ( this.turns[ 0 ] != off.turns[ 0 ] )
{
return this.turns[ 0 ] - off.turns[ 0 ];
}
// Next compare price
if ( this.price != off.price )
{
return this.price - off.price;
}
// Then, compare the names of the bots
return this.botName.compareToIgnoreCase( off.botName );
}
}
}