/**
* 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.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import net.java.dev.spellcast.utilities.LockableListModel;
import net.java.dev.spellcast.utilities.SortedListModel;
import net.sourceforge.kolmafia.AdventureResult;
import net.sourceforge.kolmafia.KoLCharacter;
import net.sourceforge.kolmafia.KoLConstants;
import net.sourceforge.kolmafia.KoLmafia;
import net.sourceforge.kolmafia.KoLmafiaCLI;
import net.sourceforge.kolmafia.RequestThread;
import net.sourceforge.kolmafia.StaticEntity;
import net.sourceforge.kolmafia.objectpool.EffectPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.persistence.EffectDatabase;
import net.sourceforge.kolmafia.persistence.SkillDatabase;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.UneffectRequest;
import net.sourceforge.kolmafia.request.UseSkillRequest;
import net.sourceforge.kolmafia.session.EquipmentManager;
import net.sourceforge.kolmafia.session.InventoryManager;
import net.sourceforge.kolmafia.session.Limitmode;
import net.sourceforge.kolmafia.utilities.FileUtilities;
import net.sourceforge.kolmafia.utilities.LogStream;
import net.sourceforge.kolmafia.utilities.StringUtilities;
public abstract class MoodManager
{
private static final AdventureResult[] AUTO_CLEAR =
{
EffectPool.get( EffectPool.BEATEN_UP ),
EffectPool.get( EffectPool.TETANUS ),
EffectPool.get( EffectPool.AMNESIA ),
EffectPool.get( EffectPool.CUNCTATITIS ),
EffectPool.get( EffectPool.HARDLY_POISONED ),
EffectPool.get( EffectPool.MAJORLY_POISONED ),
EffectPool.get( EffectPool.A_LITTLE_BIT_POISONED ),
EffectPool.get( EffectPool.SOMEWHAT_POISONED ),
EffectPool.get( EffectPool.REALLY_QUITE_POISONED ),
};
public static final AdventureResult TURTLING_ROD = ItemPool.get( ItemPool.TURTLING_ROD, 1 );
public static final AdventureResult EAU_DE_TORTUE = EffectPool.get( EffectPool.EAU_DE_TORTUE );
private static Mood currentMood = null;
private static final SortedListModel<Mood> availableMoods = new SortedListModel<Mood>();
private static final SortedListModel<MoodTrigger> displayList = new SortedListModel<MoodTrigger>();
static boolean isExecuting = false;
public static final File getFile()
{
return new File( KoLConstants.SETTINGS_LOCATION, KoLCharacter.baseUserName() + "_moods.txt" );
}
public static final boolean isExecuting()
{
return MoodManager.isExecuting;
}
public static final void updateFromPreferences()
{
MoodTrigger.clearKnownSources();
MoodManager.availableMoods.clear();
MoodManager.currentMood = null;
MoodManager.displayList.clear();
String currentMood = Preferences.getString( "currentMood" );
MoodManager.loadSettings();
MoodManager.setMood( currentMood );
MoodManager.saveSettings();
}
public static final LockableListModel<Mood> getAvailableMoods()
{
return MoodManager.availableMoods;
}
/**
* Sets the current mood to be executed to the given mood. Also ensures that all defaults are loaded for the given
* mood if no data exists.
*/
public static final void setMood( String newMoodName )
{
if ( newMoodName == null || newMoodName.trim().equals( "" ) )
{
newMoodName = "default";
}
if ( newMoodName.equals( "clear" ) || newMoodName.equals( "autofill" ) || newMoodName.startsWith( "exec" ) || newMoodName.startsWith( "repeat" ) )
{
return;
}
Preferences.setString( "currentMood", newMoodName );
MoodManager.currentMood = null;
Mood newMood = new Mood( newMoodName );
for ( Mood mood : MoodManager.availableMoods )
{
if ( mood.equals( newMood) )
{
MoodManager.currentMood = mood;
if ( newMoodName.indexOf( " extends " ) != -1 || newMoodName.indexOf( "," ) != -1 )
{
MoodManager.currentMood.setParentNames( newMood.getParentNames() );
}
break;
}
}
if ( MoodManager.currentMood == null )
{
MoodManager.currentMood = newMood;
MoodManager.availableMoods.remove( MoodManager.currentMood );
MoodManager.availableMoods.add( MoodManager.currentMood );
}
MoodManager.displayList.clear();
MoodManager.displayList.addAll( MoodManager.currentMood.getTriggers() );
MoodManager.availableMoods.setSelectedItem( MoodManager.currentMood );
}
/**
* Retrieves the model associated with the given mood.
*/
public static final LockableListModel<MoodTrigger> getTriggers()
{
return MoodManager.displayList;
}
public static final List<MoodTrigger> getTriggers( String moodName )
{
if ( moodName == null || moodName.length() == 0 )
{
return Collections.EMPTY_LIST;
}
for ( Mood mood : MoodManager.availableMoods)
{
if ( mood.getName().equals( moodName ) )
{
return mood.getTriggers();
}
}
return Collections.EMPTY_LIST;
}
/**
* Adds a trigger to the temporary mood settings.
*/
public static final MoodTrigger addTrigger( final String type, final String name, final String action )
{
MoodTrigger trigger = MoodTrigger.constructNode( type + " " + name + " => " + action );
if ( MoodManager.currentMood.addTrigger( trigger ) )
{
MoodManager.displayList.remove( trigger );
MoodManager.displayList.add( trigger );
}
return trigger;
}
/**
* Removes all the current displayList.
*/
public static final void removeTriggers( final Object[] triggers )
{
for ( int i = 0; i < triggers.length; ++i )
{
MoodTrigger trigger = (MoodTrigger) triggers[ i ];
if ( MoodManager.currentMood.removeTrigger( trigger ) )
{
MoodManager.displayList.remove( trigger );
}
}
}
public static final void removeTriggers( final Collection<MoodTrigger> triggers )
{
for ( MoodTrigger trigger : triggers )
{
if ( MoodManager.currentMood.removeTrigger( trigger ) )
{
MoodManager.displayList.remove( trigger );
}
}
}
public static final void minimalSet()
{
String currentMood = Preferences.getString( "currentMood" );
if ( currentMood.equals( "apathetic" ) )
{
return;
}
// If there's any effects the player currently has and there
// is a known way to re-acquire it (internally known, anyway),
// make sure to add those as well.
AdventureResult[] effects = new AdventureResult[ KoLConstants.activeEffects.size() ];
KoLConstants.activeEffects.toArray( effects );
for ( int i = 0; i < effects.length; ++i )
{
String action = MoodManager.getDefaultAction( "lose_effect", effects[ i ].getName() );
if ( action != null && !action.equals( "" ) )
{
MoodManager.addTrigger( "lose_effect", effects[ i ].getName(), action );
}
}
}
/**
* Fills up the trigger list automatically.
*/
private static final String [] hardcoreThiefBuffs = new String[]
{
"Fat Leon's Phat Loot Lyric",
"The Moxious Madrigal",
"Aloysius' Antiphon of Aptitude",
"The Sonata of Sneakiness",
"The Psalm of Pointiness",
"Ur-Kel's Aria of Annoyance"
};
private static final String [] softcoreThiefBuffs = new String[]
{
"Fat Leon's Phat Loot Lyric",
"Aloysius' Antiphon of Aptitude",
"Ur-Kel's Aria of Annoyance",
"The Sonata of Sneakiness",
"Jackasses' Symphony of Destruction",
"Cletus's Canticle of Celerity"
};
private static final String [] rankedBorisSongs = new String[]
{
"Song of Fortune",
"Song of Accompaniment",
// Can't actually pick the following, since it is in the same
// skill tree as the preceding Songs
"Song of Solitude",
"Song of Cockiness",
};
public static final void maximalSet()
{
String currentMood = Preferences.getString( "currentMood" );
if ( currentMood.equals( "apathetic" ) )
{
return;
}
UseSkillRequest[] skills = new UseSkillRequest[ KoLConstants.availableSkills.size() ];
KoLConstants.availableSkills.toArray( skills );
ArrayList<String> thiefSkills = new ArrayList<String>();
ArrayList<String> borisSongs = new ArrayList<String>();
for ( int i = 0; i < skills.length; ++i )
{
int skillId = skills[ i ].getSkillId();
if ( skillId < 1000 )
{
continue;
}
// Combat rate increasers are not handled by mood
// autofill, since KoLmafia has a preference for
// non-combats in the area below.
// Musk of the Moose, Carlweather's Cantata of Confrontation,
// Song of Battle
if ( skillId == 1019 || skillId == 6016 || skillId == 11019 )
{
continue;
}
// Skip skills that aren't mood appropriate because they add effects
// outside of battle.
// Canticle of Carboloading, The Ode to Booze,
// Inigo's Incantation of Inspiration, Song of the Glorious Lunch
if ( skillId == 3024 || skillId == 6014 || skillId == 6028 || skillId == 11023 )
{
continue;
}
String skillName = skills[ i ].getSkillName();
if ( SkillDatabase.isAccordionThiefSong( skillId ) )
{
thiefSkills.add( skillName );
continue;
}
if ( skillId >= 11000 && skillId < 12000 )
{
if ( SkillDatabase.isSong( skillId ) )
{
borisSongs.add( skillName );
continue;
}
}
String effectName = UneffectRequest.skillToEffect( skillName );
int effectId = EffectDatabase.getEffectId( effectName );
if ( EffectDatabase.contains( effectId ) )
{
String action = MoodManager.getDefaultAction( "lose_effect", effectName );
MoodManager.addTrigger( "lose_effect", effectName, action );
}
}
// If we know Boris Songs, pick one
if ( !borisSongs.isEmpty() )
{
MoodManager.pickSkills( borisSongs, 1, MoodManager.rankedBorisSongs );
}
// If we know Accordion Thief Songs, pick some
if ( !thiefSkills.isEmpty() )
{
String[] rankedBuffs =
KoLCharacter.isHardcore() ?
MoodManager.hardcoreThiefBuffs :
MoodManager.softcoreThiefBuffs;
MoodManager.pickSkills( thiefSkills, UseSkillRequest.songLimit(), rankedBuffs );
}
// Now add in all the buffs from the minimal buff set, as those
// are included here.
MoodManager.minimalSet();
}
private static final void pickSkills( final List<String> skills, final int limit, final String [] rankedBuffs )
{
if ( skills.isEmpty() )
{
return;
}
int skillCount = skills.size();
// If we know fewer skills than our capacity, add them all
if ( skillCount <= limit )
{
String[] skillNames = new String[ skillCount ];
skills.toArray( skillNames );
for ( int i = 0; i < skillNames.length; ++i )
{
String effectName = UneffectRequest.skillToEffect( skillNames[ i ] );
MoodManager.addTrigger( "lose_effect", effectName, "cast " + skillNames[ i ] );
}
return;
}
// Otherwise, pick from the ranked list of "useful" skills
int foundSkillCount = 0;
for ( int i = 0; i < rankedBuffs.length && foundSkillCount < limit; ++i )
{
if ( KoLCharacter.hasSkill( rankedBuffs[ i ] ) )
{
String effectName = UneffectRequest.skillToEffect( rankedBuffs[ i ] );
MoodManager.addTrigger( "lose_effect",effectName, "cast " + rankedBuffs[ i ] );
++foundSkillCount;
}
}
}
/**
* Deletes the current mood and sets the current mood to apathetic.
*/
public static final void deleteCurrentMood()
{
MoodManager.displayList.clear();
Mood current = MoodManager.currentMood;
if ( current.getName().equals( "default" ) )
{
MoodManager.removeTriggers( current.getTriggers() );
return;
}
MoodManager.availableMoods.remove( current );
MoodManager.setMood( "apathetic" );
}
/**
* Duplicates the current trigger list into a new list
*/
public static final void copyTriggers( final String newMoodName )
{
// Copy displayList from current list, then
// create and switch to new list
Mood newMood = new Mood( newMoodName );
newMood.copyFrom( MoodManager.currentMood );
MoodManager.availableMoods.add( newMood );
MoodManager.setMood( newMoodName );
}
/**
* Executes all the mood displayList for the current mood.
*/
public static final void execute()
{
MoodManager.execute( -1 );
}
public static final boolean effectInMood( final AdventureResult effect )
{
return MoodManager.currentMood.isTrigger( effect );
}
public static final void execute( final int multiplicity )
{
if ( KoLmafia.refusesContinue() )
{
return;
}
if ( !MoodManager.willExecute( multiplicity ) )
{
return;
}
// If in limitmode, eg. Spelunky, do not run moods
if ( KoLCharacter.getLimitmode() != null )
{
return;
}
MoodManager.isExecuting = true;
MoodTrigger current = null;
AdventureResult[] effects = new AdventureResult[ KoLConstants.activeEffects.size() ];
KoLConstants.activeEffects.toArray( effects );
// If you have too many accordion thief buffs to execute
// your displayList, then shrug off your extra buffs, but
// only if the user allows for this.
// First we determine which buffs are already affecting the
// character in question.
ArrayList<AdventureResult> thiefBuffs = new ArrayList<AdventureResult>();
for ( int i = 0; i < effects.length; ++i )
{
String skillName = UneffectRequest.effectToSkill( effects[ i ].getName() );
if ( SkillDatabase.contains( skillName ) )
{
int skillId = SkillDatabase.getSkillId( skillName );
if ( SkillDatabase.isAccordionThiefSong( skillId ) )
{
thiefBuffs.add( effects[ i ] );
}
}
}
// Then, we determine the triggers which are thief skills, and
// thereby would be cast at this time.
ArrayList<AdventureResult> thiefKeep = new ArrayList<AdventureResult>();
ArrayList<AdventureResult> thiefNeed = new ArrayList<AdventureResult>();
List<MoodTrigger> triggers = MoodManager.currentMood.getTriggers();
for ( MoodTrigger trigger : triggers )
{
if ( trigger.isThiefTrigger() )
{
AdventureResult effect = trigger.getEffect();
if ( thiefBuffs.remove( effect ) )
{ // Already have this one
thiefKeep.add( effect );
}
else
{ // New or completely expired buff - we may
// need to shrug a buff to make room for it.
thiefNeed.add( effect );
}
}
}
int buffsToRemove = thiefNeed.isEmpty() ? 0 :
thiefBuffs.size() + thiefKeep.size() + thiefNeed.size() - UseSkillRequest.songLimit();
for ( int i = 0; i < buffsToRemove && i < thiefBuffs.size(); ++i )
{
KoLmafiaCLI.DEFAULT_SHELL.executeLine( "uneffect " + thiefBuffs.get( i ).getName() );
}
// Now that everything is prepared, go ahead and execute
// the displayList which have been set. First, start out
// with any skill casting.
for ( MoodTrigger trigger : triggers )
{
if ( KoLmafia.refusesContinue() )
{
break;
}
if ( trigger.isSkill() )
{
trigger.execute( multiplicity );
}
}
for ( MoodTrigger trigger : triggers )
{
if ( !trigger.isSkill() )
{
trigger.execute( multiplicity );
}
}
MoodManager.isExecuting = false;
}
public static final boolean willExecute( final int multiplicity )
{
if ( !MoodManager.currentMood.isExecutable() )
{
return false;
}
boolean willExecute = false;
List<MoodTrigger> triggers = MoodManager.currentMood.getTriggers();
for ( MoodTrigger trigger : triggers )
{
willExecute |= trigger.shouldExecute( multiplicity );
}
return willExecute;
}
public static final List<AdventureResult> getMissingEffects()
{
List<MoodTrigger> triggers = MoodManager.currentMood.getTriggers();
if ( triggers.isEmpty() )
{
return Collections.EMPTY_LIST;
}
ArrayList<AdventureResult> missing = new ArrayList<AdventureResult>();
for ( MoodTrigger trigger : triggers )
{
if ( trigger.getType().equals( "lose_effect" ) && !trigger.matches() )
{
missing.add( trigger.getEffect() );
}
}
// Special case: if the character has a turtling rod equipped,
// assume the Eau de Tortue is a possibility
if ( KoLCharacter.hasEquipped( MoodManager.TURTLING_ROD, EquipmentManager.OFFHAND ) &&
!KoLConstants.activeEffects.contains( MoodManager.EAU_DE_TORTUE ) )
{
missing.add( MoodManager.EAU_DE_TORTUE );
}
return missing;
}
public static final void removeMalignantEffects()
{
for ( int i = 0; i < MoodManager.AUTO_CLEAR.length && KoLmafia.permitsContinue(); ++i )
{
AdventureResult effect = MoodManager.AUTO_CLEAR[ i ];
if ( KoLConstants.activeEffects.contains( effect ) )
{
RequestThread.postRequest( new UneffectRequest( effect ) );
}
}
}
public static final int getMaintenanceCost()
{
List<MoodTrigger> triggers = MoodManager.currentMood.getTriggers();
if ( triggers.isEmpty() )
{
return 0;
}
int runningTally = 0;
// Iterate over the entire list of applicable triggers,
// locate the ones which involve spellcasting, and add
// the MP cost for maintenance to the running tally.
for ( MoodTrigger trigger : triggers )
{
if ( !trigger.getType().equals( "lose_effect" ) || !trigger.shouldExecute( -1 ) )
{
continue;
}
String action = trigger.getAction();
if ( !action.startsWith( "cast" ) && !action.startsWith( "buff" ) )
{
continue;
}
int spaceIndex = action.indexOf( " " );
if ( spaceIndex == -1 )
{
continue;
}
action = action.substring( spaceIndex + 1 );
int multiplier = 1;
if ( Character.isDigit( action.charAt( 0 ) ) )
{
spaceIndex = action.indexOf( " " );
multiplier = StringUtilities.parseInt( action.substring( 0, spaceIndex ) );
action = action.substring( spaceIndex + 1 );
}
String skillName = SkillDatabase.getSkillName( action );
if ( skillName != null )
{
runningTally +=
SkillDatabase.getMPConsumptionById( SkillDatabase.getSkillId( skillName ) ) * multiplier;
}
}
// Running tally calculated, return the amount of
// MP required to sustain this mood.
return runningTally;
}
/**
* Stores the settings maintained in this <code>MoodManager</code> object to disk for later retrieval.
*/
public static final void saveSettings()
{
PrintStream writer = LogStream.openStream( getFile(), true );
for ( Mood mood : MoodManager.availableMoods )
{
writer.println( mood.toSettingString() );
}
writer.close();
}
/**
* Loads the settings located in the given file into this object. Note that all settings are overridden; if the
* given file does not exist, the current global settings will also be rewritten into the appropriate file.
*/
public static final void loadSettings()
{
MoodManager.availableMoods.clear();
Mood mood = new Mood( "apathetic" );
MoodManager.availableMoods.add( mood );
mood = new Mood( "default" );
MoodManager.availableMoods.add( mood );
try
{
// First guarantee that a settings file exists with
// the appropriate Properties data.
BufferedReader reader = FileUtilities.getReader( getFile() );
String line;
while ( ( line = reader.readLine() ) != null )
{
line = line.trim();
if ( line.length() == 0 )
{
continue;
}
if ( !line.startsWith( "[" ) )
{
mood.addTrigger( MoodTrigger.constructNode( line ) );
continue;
}
int closeBracketIndex = line.indexOf( "]" );
if ( closeBracketIndex == -1 )
{
continue;
}
String moodName = line.substring( 1, closeBracketIndex );
mood = new Mood( moodName );
MoodManager.availableMoods.remove( mood );
MoodManager.availableMoods.add( mood );
}
reader.close();
reader = null;
MoodManager.setMood( Preferences.getString( "currentMood" ) );
}
catch ( IOException e )
{
// This should not happen. Therefore, print
// a stack trace for debug purposes.
StaticEntity.printStackTrace( e );
}
}
public static final String getDefaultAction( final String type, final String name )
{
if ( type == null || name == null )
{
return "";
}
// We can look at the displayList list to see if it matches
// your current mood. That way, the "default action" is
// considered whatever your current mood says it is.
String action = "";
List<MoodTrigger> triggers = ( MoodManager.currentMood == null ) ? Collections.EMPTY_LIST : MoodManager.currentMood.getTriggers();
for ( MoodTrigger trigger : triggers )
{
if ( trigger.getType().equals( type ) && trigger.getName().equals( name ) )
{
action = trigger.getAction();
break;
}
}
if ( type.equals( "unconditional" ) )
{
return action;
}
else if ( type.equals( "lose_effect" ) )
{
if ( action.equals( "" ) )
{
int effectId = EffectDatabase.getEffectId( name );
action = EffectDatabase.getDefaultAction( effectId );
if ( action == null )
{
action = MoodTrigger.getKnownSources( name );
}
}
return action;
}
else
{
if ( action.equals( "" ) )
{
int effectId = EffectDatabase.getEffectId( name );
if ( UneffectRequest.isRemovable( effectId ) )
{
action = "uneffect " + name;
}
}
return action;
}
}
public static final boolean currentlyExecutable( final AdventureResult effect, final String action )
{
// It's always OK to boost a stackable effect.
// Otherwise, it's only OK if it's not active.
return !MoodManager.unstackableAction( action ) || !KoLConstants.activeEffects.contains( effect );
}
public static final boolean unstackableAction( final String action )
{
return
action.indexOf( "absinthe" ) != -1 ||
action.indexOf( "astral mushroom" ) != -1 ||
action.indexOf( "oasis" ) != -1 ||
action.indexOf( "turtle pheromones" ) != -1 ||
action.indexOf( "gong" ) != -1;
}
public static final boolean canMasterTrivia()
{
if ( InventoryManager.canUseMall() )
{
return true;
}
return ( InventoryManager.getAccessibleCount( ItemPool.WHAT_CARD ) > 0
&& InventoryManager.getAccessibleCount( ItemPool.WHEN_CARD ) > 0
&& InventoryManager.getAccessibleCount( ItemPool.WHERE_CARD ) > 0
&& InventoryManager.getAccessibleCount( ItemPool.WHO_CARD ) > 0 );
}
}