/**
* 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.moods;
import java.io.File;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import net.sourceforge.kolmafia.AdventureResult;
import net.sourceforge.kolmafia.BuffBotHome;
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.KoLmafiaASH;
import net.sourceforge.kolmafia.KoLmafiaCLI;
import net.sourceforge.kolmafia.RequestLogger;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.SpecialOutfit;
import net.sourceforge.kolmafia.StaticEntity;
import net.sourceforge.kolmafia.moods.HPRestoreItemList.HPRestoreItem;
import net.sourceforge.kolmafia.moods.MPRestoreItemList.MPRestoreItem;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.CharPaneRequest;
import net.sourceforge.kolmafia.request.FightRequest;
import net.sourceforge.kolmafia.request.UseItemRequest;
import net.sourceforge.kolmafia.session.ChoiceManager;
import net.sourceforge.kolmafia.session.LightsOutManager;
import net.sourceforge.kolmafia.textui.Interpreter;
import net.sourceforge.kolmafia.textui.parsetree.Value;
public class RecoveryManager
{
private static boolean recoveryActive;
public static boolean isRecoveryActive()
{
return RecoveryManager.recoveryActive;
}
public static void setRecoveryActive( final boolean recoveryActive )
{
RecoveryManager.recoveryActive = recoveryActive;
}
public static boolean isRecoveryPossible()
{
return !RecoveryManager.isRecoveryActive() &&
FightRequest.currentRound == 0 &&
!FightRequest.inMultiFight && !FightRequest.choiceFollowsFight &&
( !ChoiceManager.handlingChoice || ChoiceManager.canWalkAway() ) &&
!CharPaneRequest.inValhalla() && KoLCharacter.getLimitmode() == null;
}
public static boolean runThresholdChecks()
{
float autoStopValue = Preferences.getFloat( "autoAbortThreshold" );
if ( autoStopValue >= 0.0f )
{
autoStopValue *= KoLCharacter.getMaximumHP();
if ( KoLCharacter.getCurrentHP() <= autoStopValue )
{
KoLmafia.updateDisplay(
MafiaState.ABORT, "Health fell below " + (int) autoStopValue + ". Auto-abort triggered." );
return false;
}
}
return true;
}
public static void runBetweenBattleChecks( final boolean isFullCheck )
{
RecoveryManager.runBetweenBattleChecks( isFullCheck, isFullCheck, true, isFullCheck );
}
public static void runBetweenBattleChecks( final boolean isScriptCheck, final boolean isMoodCheck,
final boolean isHealthCheck, final boolean isManaCheck )
{
// Do not run between battle checks if you are in the middle
// of your checks or if you have aborted.
if ( !RecoveryManager.isRecoveryPossible() || KoLmafia.refusesContinue() )
{
return;
}
// First, run the between battle script defined by the
// user, which may obviate the built in behavior.
RecoveryManager.recoveryActive = true;
if ( isScriptCheck )
{
KoLmafia.executeScript( Preferences.getString( "betweenBattleScript" ) );
}
// Now, run the built-in behavior to take care of
// any loose ends.
SpecialOutfit.createImplicitCheckpoint();
if ( isMoodCheck )
{
MoodManager.execute();
}
SpecialOutfit.restoreImplicitCheckpoint();
SpecialOutfit.createImplicitCheckpoint();
if ( isHealthCheck )
{
RecoveryManager.recoverHP();
}
if ( isMoodCheck )
{
ManaBurnManager.burnExtraMana( false );
}
if ( isManaCheck )
{
RecoveryManager.recoverMP();
}
SpecialOutfit.restoreImplicitCheckpoint();
if ( KoLmafia.permitsContinue() && KoLCharacter.getCurrentHP() == 0 && !FightRequest.edFightInProgress() )
{
KoLmafia.updateDisplay( MafiaState.ABORT, "Insufficient health to continue (auto-abort triggered)." );
}
if ( KoLmafia.permitsContinue() && KoLmafia.currentIterationString.length() > 0 )
{
RequestLogger.printLine();
KoLmafia.updateDisplay( KoLmafia.currentIterationString );
KoLmafia.currentIterationString = "";
}
LightsOutManager.checkCounter();
FightRequest.haveFought(); // reset flag
RecoveryManager.recoveryActive = false;
}
/**
* Utility. The method called in between battles. This method checks to see if the character's HP has dropped below
* the tolerance value, and recovers if it has (if the user has specified this in their settings).
*/
public static boolean recoverHP()
{
return RecoveryManager.recoverHP( 0 );
}
public static boolean recoverHP( final int recover )
{
if ( KoLmafia.refusesContinue() )
{
return false;
}
try
{
if ( Preferences.getBoolean( "removeMalignantEffects" ) )
{
MoodManager.removeMalignantEffects();
}
HPRestoreItemList.updateHealthRestored();
if ( RecoveryManager.invokeRecoveryScript( "HP", recover ) )
{
return true;
}
return RecoveryManager.recover(
recover, "hpAutoRecovery", "getCurrentHP", "getMaximumHP", HPRestoreItemList.CONFIGURES );
}
catch ( Exception e )
{
// This should not happen. Therefore, print
// a stack trace for debug purposes.
StaticEntity.printStackTrace( e );
return false;
}
}
/**
* Utility. The method called in between commands. This method checks to see if the character's MP has dropped below
* the tolerance value, and recovers if it has (if the user has specified this in their settings).
*/
public static boolean recoverMP()
{
return RecoveryManager.recoverMP( 0 );
}
/**
* Utility. The method which restores the character's current mana points above the given value.
*/
public static boolean recoverMP( final int mpNeeded )
{
if ( KoLmafia.refusesContinue() )
{
return false;
}
try
{
MPRestoreItemList.updateManaRestored();
if ( RecoveryManager.invokeRecoveryScript( "MP", mpNeeded ) )
{
return true;
}
return RecoveryManager.recover(
mpNeeded, "mpAutoRecovery", "getCurrentMP", "getMaximumMP", MPRestoreItemList.CONFIGURES );
}
catch ( Exception e )
{
// This should not happen. Therefore, print
// a stack trace for debug purposes.
StaticEntity.printStackTrace( e );
return false;
}
}
/**
* Utility. The method which ensures that the amount needed exists, and if not, calls the appropriate scripts to do
* so.
*/
private static boolean recover( float desired, final String settingName, final String currentName,
final String maximumName, final Object[] techniques )
throws Exception
{
// First, check for beaten up, if the person has tongue as an
// auto-heal option. This takes precedence over all other checks.
String restoreSetting = Preferences.getString( settingName + "Items" ).trim().toLowerCase();
// Next, check against the restore needed to see if
// any restoration needs to take place.
Object[] empty = new Object[ 0 ];
Method currentMethod, maximumMethod;
currentMethod = KoLCharacter.class.getMethod( currentName, new Class[ 0 ] );
maximumMethod = KoLCharacter.class.getMethod( maximumName, new Class[ 0 ] );
float setting = Preferences.getFloat( settingName );
if ( setting < 0.0f && desired == 0 )
{
return true;
}
int current = ( (Number) currentMethod.invoke( null, empty ) ).intValue();
// If you've already reached the desired value, don't
// bother restoring.
if ( desired != 0 && current >= desired )
{
return true;
}
int maximum = ( (Number) maximumMethod.invoke( null, empty ) ).intValue();
int needed = (int) Math.min( maximum, Math.max( desired, setting * maximum + 1.0f ) );
// Next, check against the restore target to see how
// far you need to go.
setting = Preferences.getFloat( settingName + "Target" );
desired = Math.min( maximum, Math.max( desired, setting * maximum ) );
if ( BuffBotHome.isBuffBotActive() || desired > maximum )
{
desired = maximum;
}
// Special handling of the Hidden Temple. Here, as
// long as your health is above zero, you're okay.
boolean isNonCombatHealthRestore =
settingName.startsWith( "hp" ) && KoLmafia.isAdventuring() && KoLmafia.currentAdventure.isNonCombatsOnly();
if ( isNonCombatHealthRestore )
{
needed = 1;
desired = 1;
}
if ( current >= needed )
{
return true;
}
// If it gets this far, then you should attempt to recover
// using the selected items. This involves a few extra
// reflection methods.
String currentTechniqueName;
// Determine all applicable items and skills for the restoration.
// This is a little bit memory intensive, but it allows for a lot
// more flexibility.
ArrayList possibleItems = new ArrayList();
ArrayList possibleSkills = new ArrayList();
for ( int i = 0; i < techniques.length; ++i )
{
currentTechniqueName = techniques[ i ].toString().toLowerCase();
if ( restoreSetting.indexOf( currentTechniqueName ) == -1 )
{
continue;
}
if ( techniques[ i ] instanceof HPRestoreItem )
{
HPRestoreItem item = (HPRestoreItem) techniques[ i ];
if ( item.isSkill() )
{
possibleSkills.add( item );
}
else if ( item.usableInCurrentPath() )
{
possibleItems.add( item );
}
}
if ( techniques[ i ] instanceof MPRestoreItem )
{
MPRestoreItem item = (MPRestoreItem) techniques[ i ];
if ( item.isSkill() )
{
possibleSkills.add( item );
}
else if ( item.usableInCurrentPath() )
{
possibleItems.add( item );
}
}
}
HPRestoreItemList.setPurchaseBasedSort( false );
MPRestoreItemList.setPurchaseBasedSort( false );
// First, use any available skills.
int last = -1;
if ( !possibleSkills.isEmpty() )
{
current = ( (Number) currentMethod.invoke( null, empty ) ).intValue();
while ( last != current && current < needed )
{
int indexToTry = 0;
Collections.sort( possibleSkills );
do
{
last = current;
currentTechniqueName = possibleSkills.get( indexToTry ).toString().toLowerCase();
RecoveryManager.recoverOnce(
possibleSkills.get( indexToTry ), currentTechniqueName, (int) desired, false );
current = ( (Number) currentMethod.invoke( null, empty ) ).intValue();
maximum = ( (Number) maximumMethod.invoke( null, empty ) ).intValue();
desired = Math.min( maximum, desired );
needed = Math.min( maximum, needed );
if ( last >= current )
{
++indexToTry;
}
}
while ( indexToTry < possibleSkills.size() && current < needed );
}
if ( KoLmafia.refusesContinue() )
{
return false;
}
}
// Iterate through every restore item which is already available
// in the player's inventory.
Collections.sort( possibleItems );
for ( int i = 0; i < possibleItems.size() && current < needed; ++i )
{
do
{
last = current;
currentTechniqueName = possibleItems.get( i ).toString().toLowerCase();
RecoveryManager.recoverOnce( possibleItems.get( i ), currentTechniqueName, (int) desired, false );
current = ( (Number) currentMethod.invoke( null, empty ) ).intValue();
maximum = ( (Number) maximumMethod.invoke( null, empty ) ).intValue();
desired = Math.min( maximum, desired );
needed = Math.min( maximum, needed );
}
while ( last != current && current < needed );
}
if ( KoLmafia.refusesContinue() )
{
return false;
}
// If we get here, we still need healing. For areas that are
// all noncombats, then you can heal using only unguent.
if ( isNonCombatHealthRestore && KoLCharacter.getAvailableMeat() >= 30 )
{
RequestThread.postRequest( UseItemRequest.getInstance( ItemPool.PUNGENT_UNGUENT ) );
return true;
}
// If things are still not restored, try looking for items you
// don't have but can purchase.
if ( !possibleItems.isEmpty() )
{
HPRestoreItemList.setPurchaseBasedSort( true );
MPRestoreItemList.setPurchaseBasedSort( true );
current = ( (Number) currentMethod.invoke( null, empty ) ).intValue();
last = -1;
while ( last != current && current < needed )
{
int indexToTry = 0;
Collections.sort( possibleItems );
do
{
last = current;
currentTechniqueName = possibleItems.get( indexToTry ).toString().toLowerCase();
RecoveryManager.recoverOnce(
possibleItems.get( indexToTry ), currentTechniqueName, (int) desired, true );
current = ( (Number) currentMethod.invoke( null, empty ) ).intValue();
maximum = ( (Number) maximumMethod.invoke( null, empty ) ).intValue();
desired = Math.min( maximum, desired );
if ( last >= current )
{
++indexToTry;
}
}
while ( indexToTry < possibleItems.size() && current < needed );
}
HPRestoreItemList.setPurchaseBasedSort( false );
MPRestoreItemList.setPurchaseBasedSort( false );
}
else if ( current < needed )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "You ran out of restores." );
return false;
}
// Fall-through check, just in case you've reached the
// desired value.
if ( KoLmafia.refusesContinue() )
{
return false;
}
if ( current < needed )
{
KoLmafia.updateDisplay( MafiaState.ERROR, "Autorecovery failed." );
return false;
}
return true;
}
/**
* Utility. The method which uses the given recovery technique (not specified in a script) in order to restore.
*/
private static void recoverOnce( final Object technique, final String techniqueName, final int needed,
final boolean purchase )
{
// If the technique is an item, and the item is not readily
// available, then don't bother with this item -- however, if
// it is the only item present, then rethink it.
if ( technique instanceof HPRestoreItem )
{
( (HPRestoreItem) technique ).recoverHP( needed, purchase );
}
if ( technique instanceof MPRestoreItem )
{
( (MPRestoreItem) technique ).recoverMP( needed, purchase );
}
}
/**
* Returns the total number of mana restores currently available to the player.
*/
public static int getRestoreCount()
{
int restoreCount = 0;
String mpRestoreSetting = Preferences.getString( "mpAutoRecoveryItems" );
for ( int i = 0; i < MPRestoreItemList.CONFIGURES.length; ++i )
{
if ( mpRestoreSetting.indexOf( MPRestoreItemList.CONFIGURES[ i ].toString().toLowerCase() ) != -1 )
{
AdventureResult item = MPRestoreItemList.CONFIGURES[ i ].getItem();
if ( item != null )
{
restoreCount += item.getCount( KoLConstants.inventory );
}
}
}
return restoreCount;
}
private static boolean invokeRecoveryScript( final String type, final int needed )
{
String scriptName = Preferences.getString( "recoveryScript" );
if ( scriptName.length() == 0 )
{
return false;
}
List<File> scriptFiles = KoLmafiaCLI.findScriptFile( scriptName );
Interpreter interpreter = KoLmafiaASH.getInterpreter( scriptFiles );
if ( interpreter != null )
{
File scriptFile = scriptFiles.get( 0 );
KoLmafiaASH.logScriptExecution( "Starting recovery script: ", scriptFile.getName(), interpreter );
Value v = interpreter.execute( "main", new String[]
{
type,
String.valueOf( needed )
} );
KoLmafiaASH.logScriptExecution( "Finished recovery script: ", scriptFile.getName(), interpreter );
return v != null && v.intValue() != 0;
}
return false;
}
}