/** * 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.combat; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.PrintStream; import java.util.HashSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.java.dev.spellcast.utilities.DataUtilities; import net.java.dev.spellcast.utilities.LockableListModel; 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.StaticEntity; import net.sourceforge.kolmafia.objectpool.ItemPool; import net.sourceforge.kolmafia.persistence.ItemDatabase; import net.sourceforge.kolmafia.persistence.SkillDatabase; import net.sourceforge.kolmafia.preferences.Preferences; import net.sourceforge.kolmafia.session.EquipmentManager; import net.sourceforge.kolmafia.session.InventoryManager; import net.sourceforge.kolmafia.utilities.FileUtilities; import net.sourceforge.kolmafia.utilities.LogStream; import net.sourceforge.kolmafia.utilities.StringUtilities; import net.sourceforge.kolmafia.webui.DiscoCombatHelper; public abstract class CombatActionManager { public static final Pattern TRY_TO_RUN_AWAY_PATTERN = Pattern.compile( "run away if (\\d+)% chance of being free" ); private static final LockableListModel<String> availableLookups = new LockableListModel<String>(); private static final CustomCombatLookup strategyLookup = new CustomCombatLookup(); public static final void updateFromPreferences() { CombatActionManager.loadStrategyLookup( CombatActionManager.getStrategyLookupName() ); } public static final LockableListModel<String> getAvailableLookups() { String[] list = DataUtilities.list( KoLConstants.CCS_LOCATION ); for ( int i = 0; i < list.length; ++i ) { if ( list[ i ].endsWith( ".ccs" ) ) { String name = list[ i ].substring( 0, list[ i ].length() - 4 ); if ( !CombatActionManager.availableLookups.contains( name ) ) { CombatActionManager.availableLookups.add( name ); } } } if ( !CombatActionManager.availableLookups.contains( "default" ) ) { CombatActionManager.availableLookups.add( "default" ); } return CombatActionManager.availableLookups; } public static final File getStrategyLookupFile() { return CombatActionManager.getStrategyLookupFile( CombatActionManager.getStrategyLookupName() ); } public static final File getStrategyLookupFile( String name ) { if ( !name.endsWith( ".ccs" ) ) { name = name + ".ccs"; } return new File( KoLConstants.CCS_LOCATION, name ); } public static void loadStrategyLookup( String name ) { if ( name == null || name.equals( "" ) ) { name = "default"; } if ( name.endsWith( ".ccs" ) ) { name = name.substring( 0, name.length() - 4 ); } if ( !CombatActionManager.availableLookups.contains( name ) ) { CombatActionManager.availableLookups.add( name ); } CombatActionManager.strategyLookup.removeAllChildren(); CombatActionManager.strategyLookup.addEncounterKey( "default" ); File file = getStrategyLookupFile( name ); if ( !file.exists() ) { PrintStream ostream = LogStream.openStream( file, true ); ostream.println( "[ default ]" ); ostream.println( "special action" ); ostream.println( "attack with weapon" ); ostream.close(); } try { BufferedReader reader = FileUtilities.getReader( file ); CombatActionManager.strategyLookup.load( reader ); reader.close(); } catch ( IOException e1 ) { // This should not happen. Therefore, print // a stack trace for debug purposes. StaticEntity.printStackTrace( e1 ); } Preferences.setString( "customCombatScript", name ); CombatActionManager.availableLookups.setSelectedItem( name ); } public static final String getStrategyLookupName() { String script = Preferences.getString( "customCombatScript" ); if ( script == null || script.length() == 0 ) { return "default"; } return script; } public static final void saveStrategyLookup( String name ) { PrintStream writer = LogStream.openStream( getStrategyLookupFile( name ), true ); CombatActionManager.strategyLookup.store( writer ); writer.close(); } public static final CustomCombatLookup getStrategyLookup() { return CombatActionManager.strategyLookup; } public static final void copyStrategyLookup( String name ) { if ( name == null || name.equals( "" ) ) { return; } File source = getStrategyLookupFile(); File destination = getStrategyLookupFile( name ); FileUtilities.copyFile( source, destination ); } public static final String encounterKey( final String line ) { return CombatActionManager.encounterKey( line, true ); } public static final String encounterKey( String line, final boolean changeCase ) { line = StringUtilities.globalStringReplace( line.trim(), " ", " " ); // Preserve <i></i> tags line = StringUtilities.getEntityEncode( line ); line = StringUtilities.globalStringReplace( line, "<i>", "<i>" ); line = StringUtilities.globalStringReplace( line, "</i>", "</i>" ); String key = line.toLowerCase(); if ( key.startsWith( "a " ) ) { key = key.substring( 2 ); line = line.substring( 2 ); } else if ( key.startsWith( "an " ) ) { key = key.substring( 3 ); line = line.substring( 3 ); } else if ( key.startsWith( "the " ) ) { // It really is "The Man" or "The Big Wisniewski" } else if ( key.startsWith( "some " ) ) { key = key.substring( 5 ); line = line.substring( 5 ); } return changeCase ? key : line; } public static final void setDefaultAction( final String actionList ) { CombatActionManager.strategyLookup.clearEncounterKey( "default" ); String[] rounds = actionList.split( "\\s*;\\s*" ); for ( int i = 0; i < rounds.length; ++i ) { CombatActionManager.strategyLookup.addEncounterAction( "default", i + 1, "", rounds[ i ], false ); } } public static final boolean hasGlobalPrefix() { return CombatActionManager.strategyLookup.getStrategy( "global prefix" ) != null; } public static final String getBestEncounterKey( final String encounter ) { return CombatActionManager.strategyLookup.getBestEncounterKey( encounter ); } private static boolean atEndOfStrategy; public static final boolean atEndOfStrategy() { return CombatActionManager.atEndOfStrategy; } public static final String getCombatAction( final String encounter, final int roundIndex, boolean allowMacro ) { CombatActionManager.atEndOfStrategy = false; if ( roundIndex < 0 || roundIndex >= 100 ) { // prevent hang if the combat is somehow not progressing at all CombatActionManager.atEndOfStrategy = true; return "abort"; } if ( !encounter.equals( "global prefix" ) ) { String action = Preferences.getString( "battleAction" ); // Custom combat doesn't have a simple action. if ( !action.startsWith( "custom" ) ) { // Use the round index to decide what action to return. switch ( roundIndex ) { case 0: return Preferences.getBoolean( "autoSteal" ) ? "try to steal an item" : "skip"; case 1: return Preferences.getBoolean( "autoSteal" ) && KoLCharacter.hasEquipped( ItemPool.get( ItemPool.NEW_WAVE_BLING, 1 ) ) ? "try to steal an item" : "skip"; case 2: String classStun = CombatActionManager.getStun(); return Preferences.getBoolean( "autoEntangle" ) && !( KoLCharacter.inClasscore2() && KoLCharacter.getMonsterLevelAdjustment() > 75 ) && !classStun.equals( "none" ) ? classStun : "skip"; case 3: return "special action"; default: CombatActionManager.atEndOfStrategy = true; return action; } } } String encounterKey = CombatActionManager.getBestEncounterKey( encounter ); CustomCombatStrategy strategy = CombatActionManager.strategyLookup.getStrategy( encounterKey ); int actionCount = strategy.getActionCount( strategyLookup, new HashSet() ); if ( roundIndex + 1 >= actionCount ) { CombatActionManager.atEndOfStrategy = true; } return strategy.getAction( CombatActionManager.strategyLookup, roundIndex, allowMacro ); } public static final boolean isMacroAction( String action ) { return action.startsWith( "scrollwhendone" ) || action.startsWith( "mark " ) || action.startsWith( "goto " ) || action.startsWith( "if " ) || action.startsWith( "endif" ) || action.startsWith( "while " ) || action.startsWith( "endwhile" ) || action.startsWith( "sub " ) || action.startsWith( "endsub" ) || action.startsWith( "call " ) || action.startsWith( "#" ) || action.startsWith( "\"" ); } private static final String getStun() { String classStun = KoLCharacter.getClassStun(); // Sometimes classStun isn't available or doesn't stun, don't return it in those cases if ( ( classStun.equals( "Club Foot" ) && KoLCharacter.getFury() == 0 ) || ( classStun.equals( "Shell Up" ) && KoLCharacter.getBlessingType() != KoLCharacter.STORM_BLESSING ) || ( classStun.equals( "Soul Bubble" ) && KoLCharacter.getSoulsauce() < 5 ) || ( classStun.equals( "Accordion Bash" ) && !EquipmentManager.wieldingAccordion() ) ) { classStun = Preferences.getBoolean( "considerShadowNoodles" ) ? "Shadow Noodles" : "none"; } return classStun; } public static final String getLongCombatOptionName( String action ) { if ( action == null ) { return "attack with weapon"; } action = action.trim(); if ( action.startsWith( "attack" ) || action.length() == 0 ) { return "attack with weapon"; } if ( isMacroAction( action ) ) { return action; } if ( action.contains( "pick" ) || ( action.contains( "steal" ) && !action.contains( "stealth" ) && !action.contains( "combo" ) && !action.contains( "accordion" ) ) ) { return "try to steal an item"; } if ( action.equals( "default" ) ) { return "default"; } if ( action.startsWith( "section" ) ) { return action; } if ( action.startsWith( "jiggle" ) ) { return "jiggle chefstaff"; } if ( action.startsWith( "special" ) ) { return "special action"; } if ( action.equals( "skip" ) ) { return "skip"; } if ( action.equals( "stun" ) ) { return "stun"; } if ( action.startsWith( "note" ) ) { return action; } if ( action.startsWith( "abort" ) ) { if ( action.indexOf( "after" ) != -1 ) { return "abort after this combat"; } return "abort"; } if ( action.startsWith( "consult" ) ) { return action; } if ( action.startsWith( "custom" ) ) { return "custom combat script"; } if ( action.startsWith( "delevel" ) ) { return "delevel and plink"; } if ( action.startsWith( "twiddle" ) ) { return "twiddle your thumbs"; } if ( action.indexOf( "run" ) != -1 && action.indexOf( "away" ) != -1 ) { Matcher runAwayMatcher = CombatActionManager.TRY_TO_RUN_AWAY_PATTERN.matcher( action ); int runaway = 0; if ( runAwayMatcher.find() ) { runaway = StringUtilities.parseInt( runAwayMatcher.group( 1 ) ); } if ( runaway <= 0 ) { return "try to run away"; } return "run away if " + runaway + "% chance of being free"; } if ( action.startsWith( "combo " ) ) { String combo = DiscoCombatHelper.disambiguateCombo( action.substring( 6 ) ); if ( combo == null ) { return "note unknown " + action; } return "combo " + combo; } if ( action.startsWith( "item" ) || action.startsWith( "use " ) ) { String item = CombatActionManager.getLongItemAction( action.substring( 4 ).trim() ); return item.startsWith( "attack" ) ? item : "item " + item; } if ( action.startsWith( "skill" ) ) { String potentialSkill = SkillDatabase.getCombatSkillName( action.substring( 5 ).trim() ); if ( potentialSkill != null ) { return "skill " + potentialSkill.toLowerCase(); } else { return "note unknown/ambiguous " + action; } } // Well, it's either a standard skill, or it's an item, // or it's something you need to lookup in the tables. String potentialSkill = SkillDatabase.getCombatSkillName( action ); if ( potentialSkill != null ) { return "skill " + potentialSkill.toLowerCase(); } String item = CombatActionManager.getLongItemAction( action ); return item.startsWith( "attack" ) ? item : "item " + item; } private static final String getLongItemAction( final String action ) { int commaIndex = action.indexOf( "," ); if ( commaIndex != -1 ) { String firstName = action.substring( 0, commaIndex ).trim(); String secondName = action.substring( commaIndex + 1 ).trim(); String first = CombatActionManager.getLongItemAction( firstName ); // Invalid item name if ( first.startsWith( "attack" ) ) { return CombatActionManager.getLongItemAction( secondName ); } if ( secondName.equals( "" ) || secondName.equals( "none" ) ) { return firstName + "," + "none"; } String second = CombatActionManager.getLongItemAction( action.substring( commaIndex + 1 ).trim() ); // Invalid item name if ( second.startsWith( "attack" ) ) { return first; } return first + "," + second; } if ( action.startsWith( "item" ) ) { return CombatActionManager.getLongItemAction( action.substring( 4 ).trim() ); } int itemId = CombatActionManager.getCombatItem( action ); if ( itemId <= 0 ) { return "attack with weapon"; } return ItemDatabase.getItemName( itemId ); } public static final String getShortCombatOptionName( String action ) { if ( action == null ) { return "attack"; } if ( action.startsWith( "consult" ) ) { return action; } if ( action.equals( "default" ) ) { return "default"; } action = action.trim(); if ( isMacroAction( action ) ) { return action; } boolean isSkillNumber = true; for ( int i = 0; i < action.length() && isSkillNumber; ++i ) { isSkillNumber = Character.isDigit( action.charAt( i ) ); } if ( isSkillNumber ) { return action; } if ( action.startsWith( "attack" ) || action.length() == 0 ) { return "attack"; } if ( action.startsWith( "abort" ) ) { if ( action.indexOf( "after" ) != -1 ) { return "abort after"; } return "abort"; } if ( action.contains( "pick" ) || ( action.contains( "steal" ) && !action.contains( "stealth" ) && !action.contains( "combo" ) && !action.contains( "accordion" ) ) ) { return "steal"; } if ( action.startsWith( "jiggle" ) ) { return "jiggle"; } if ( action.startsWith( "special" ) ) { return "special"; } if ( action.equals( "skip" ) || action.startsWith( "note" ) ) { return "skip"; } if ( action.startsWith( "consult" ) ) { return action; } if ( action.startsWith( "custom" ) ) { return "custom"; } if ( action.startsWith( "delevel" ) ) { return "delevel"; } if ( action.startsWith( "twiddle" ) ) { return "twiddle"; } if ( action.indexOf( "run" ) != -1 && action.indexOf( "away" ) != -1 ) { Matcher runAwayMatcher = CombatActionManager.TRY_TO_RUN_AWAY_PATTERN.matcher( action ); int runaway = runAwayMatcher.find() ? StringUtilities.parseInt( runAwayMatcher.group( 1 ) ) : 0; return runaway <= 0 ? "runaway" : ( "runaway" + runaway ); } if ( action.startsWith( "combo " ) ) { String name = action.substring( 6 ); String combo = DiscoCombatHelper.disambiguateCombo( name ); if ( combo == null ) { KoLmafia.updateDisplay( MafiaState.ABORT, "Invalid combo '" + name + "' requested" ); Macrofier.resetMacroOverride(); return "skip"; } return "combo " + combo; } if ( action.startsWith( "item" ) ) { return CombatActionManager.getShortItemAction( action.substring( 4 ).trim() ); } if ( action.equals( "stun" ) ) { String name = CombatActionManager.getStun(); return name == null || name.equals( "none" ) ? "skip" : "skill" + SkillDatabase.getSkillId( name ); } if ( action.startsWith( "skill" ) ) { String name = SkillDatabase.getCombatSkillName( action.substring( 5 ).trim() ); return name == null ? "attack" : "skill" + SkillDatabase.getSkillId( name ); } String potentialSkill = SkillDatabase.getCombatSkillName( action ); if ( potentialSkill != null ) { return "skill" + SkillDatabase.getSkillId( potentialSkill ); } return CombatActionManager.getShortItemAction( action ); } private static final String getShortItemAction( final String action ) { int commaIndex = action.indexOf( "," ); if ( commaIndex != -1 ) { String firstName = action.substring( 0, commaIndex ).trim(); String secondName = action.substring( commaIndex + 1 ).trim(); String first = CombatActionManager.getShortItemAction( firstName ); // Invalid item name if ( first.startsWith( "attack" ) ) { return CombatActionManager.getShortItemAction( secondName ); } if ( secondName.equals( "none" ) ) { // Asking for no Funkslinging return first + ",-1"; } String second = CombatActionManager.getShortItemAction( secondName ); // Invalid item name if ( second.startsWith( "attack" ) ) { return first; } return first + "," + second; } if ( action.startsWith( "item" ) ) { return CombatActionManager.getShortItemAction( action.substring( 4 ) ); } int itemId = CombatActionManager.getCombatItem( action ); if ( itemId <= 0 ) { return "attack"; } if ( itemId == ItemPool.DICTIONARY && InventoryManager.getCount( ItemPool.DICTIONARY ) < 1 ) { itemId = ItemPool.FACSIMILE_DICTIONARY; } if ( itemId == ItemPool.FACSIMILE_DICTIONARY && InventoryManager.getCount( ItemPool.FACSIMILE_DICTIONARY ) < 1 ) { itemId = ItemPool.DICTIONARY; } return String.valueOf( itemId ); } public static final int getCombatItem( String action ) { if ( action.equals( "none" ) ) { return -1; } List matchingNames = ItemDatabase.getMatchingNames( action ); int count = matchingNames.size(); for ( int i = 0; i < count; ++i ) { String name = (String) matchingNames.get( i ); int id = ItemDatabase.getItemId( name ); if ( ItemDatabase.getAttribute( id, ItemDatabase.ATTR_COMBAT | ItemDatabase.ATTR_COMBAT_REUSABLE ) ) { return id; } } return -1; } }