/** * 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.util.HashSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import net.sourceforge.kolmafia.AdventureResult; import net.sourceforge.kolmafia.KoLCharacter; import net.sourceforge.kolmafia.KoLConstants; import net.sourceforge.kolmafia.KoLmafia; import net.sourceforge.kolmafia.MonsterData; import net.sourceforge.kolmafia.RequestLogger; import net.sourceforge.kolmafia.moods.MPRestoreItemList; import net.sourceforge.kolmafia.moods.MPRestoreItemList.MPRestoreItem; 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.FightRequest; import net.sourceforge.kolmafia.session.EquipmentManager; import net.sourceforge.kolmafia.textui.DataTypes; import net.sourceforge.kolmafia.textui.Interpreter; import net.sourceforge.kolmafia.textui.parsetree.Value; import net.sourceforge.kolmafia.utilities.StringUtilities; import net.sourceforge.kolmafia.webui.DiscoCombatHelper; public class Macrofier { private static String macroOverride = null; private static Interpreter macroInterpreter = null; private static final Pattern ALLCALLS_PATTERN = Pattern.compile( "call (\\w+)" ); private static final Pattern ALLSUBS_PATTERN = Pattern.compile( "sub (\\w+)([\\s;\\n]+endsub)?" ); public static void resetMacroOverride() { Macrofier.macroOverride = null; Macrofier.macroInterpreter = null; } public static void setMacroOverride( String macroOverride, Interpreter interpreter ) { if ( macroOverride == null || macroOverride.length() == 0 ) { Macrofier.macroOverride = null; Macrofier.macroInterpreter = null; } else if ( macroOverride.indexOf( ';' ) != -1 ) { Macrofier.macroOverride = macroOverride; Macrofier.macroInterpreter = null; } else { Macrofier.macroOverride = macroOverride; Macrofier.macroInterpreter = interpreter; } } private static boolean isSimpleAction( String action ) { if ( action.startsWith( "consult" ) ) { return false; } if ( action.startsWith( "delevel" ) ) { return false; } if ( action.startsWith( "twiddle" ) ) { return false; } return true; } public static String macrofyRoundZero() { // Don't try to macrofy the first round, since we don't know // what monster it is, and "special action" could include // olfaction. boolean debug = Preferences.getBoolean( "macroDebug" ); if ( Macrofier.macroInterpreter == null && Macrofier.macroOverride != null && Macrofier.macroOverride.length() > 0 ) { if ( debug ) { RequestLogger.printLine( "Using macroOverride" ); } return Macrofier.macroOverride; } return null; } public static String macrofy() { boolean debug = Preferences.getBoolean( "macroDebug" ); // If there's an override, always use it if ( Macrofier.macroInterpreter == null && Macrofier.macroOverride != null && Macrofier.macroOverride.length() > 0 ) { if ( debug ) { RequestLogger.printLine( "Using macroOverride" ); } return Macrofier.macroOverride; } // Begin monster-specific macrofication. MonsterData monster = MonsterStatusTracker.getLastMonster(); String monsterName = ( monster != null ) ? monster.getName() : ""; if ( Macrofier.macroInterpreter != null ) { Object[] parameters = new Object[ 3 ]; parameters[ 0 ] = new Integer( FightRequest.getRoundIndex() ); parameters[ 1 ] = monster; parameters[ 2 ] = FightRequest.lastResponseText; // Execute a single function in the scope of the // currently executing file. Do not re-execute // top-level code in that file. Value returnValue = Macrofier.macroInterpreter.execute( macroOverride, parameters, false ); if ( returnValue == null || returnValue.getType().equals( DataTypes.TYPE_VOID ) ) { String message = "Macro override \"" + macroOverride + "\" returned void."; RequestLogger.printLine( message ); RequestLogger.updateSessionLog( message ); } else { String result = returnValue.toString(); if ( result.length() > 0 ) { if ( result.startsWith( "\"" ) && result.charAt( result.length() - 1 ) == '\"') { StringBuffer macro = new StringBuffer(); macro.append( "#macro action\n" ); macro.append( result.substring( 1, result.length() - 1 ) ); macro.append( '\n' ); if ( debug ) { RequestLogger.printLine( "Generated macro:" ); Macrofier.indentify( macro.toString(), false ); RequestLogger.printLine( "" ); } return macro.toString(); } return result; } } } StringBuffer macro = new StringBuffer(); if ( monsterName.equals( "rampaging adding machine" ) && !KoLConstants.activeEffects.contains( FightRequest.BIRDFORM ) && !FightRequest.waitingForSpecial ) { if ( debug ) { RequestLogger.printLine( "(unable to macrofy vs. RAM)" ); } return null; } if ( monsterName.equals( "hulking construct" ) ) { // use ATTACK & WALL punchcards macro.append( "if hascombatitem 3146 && hascombatitem 3155\n" ); if ( KoLCharacter.hasSkill( "Ambidextrous Funkslinging" ) ) { macro.append( " use 3146,3155\n" ); } else { macro.append( " use 3146; use 3155\n" ); } macro.append( "endif\nrunaway; repeat\n" ); if ( debug ) { RequestLogger.printLine( "Generated macro:" ); Macrofier.indentify( macro.toString(), false ); RequestLogger.printLine( "" ); } return macro.toString(); } float thresh = Preferences.getFloat( "autoAbortThreshold" ); if ( thresh > 0.0f ) { macro.append( "abort hppercentbelow " ); macro.append( (int) ( thresh * 100.0f ) ); macro.append( '\n' ); } Macrofier.macroCommon( macro ); macro.append( "#mafiaheader\n" ); // Load up the "global prefix", if there is one if ( CombatActionManager.hasGlobalPrefix() ) { for ( int i = 0; i < 10000; ++i ) { String action = CombatActionManager.getCombatAction( "global prefix", i, true ); if ( !Macrofier.isSimpleAction( action ) ) { if ( debug ) { RequestLogger.printLine( "(unable to macrofy global prefix due to action: " + action + ")" ); } return null; } Macrofier.macroAction( macro, action, 0 ); if ( CombatActionManager.atEndOfStrategy() ) { break; // continue with actual CCS section } } } int macrolen = FightRequest.getMacroPrefixLength(); int start = macrolen > 0 ? macrolen : 0; for ( int i = start; i < 10000; ++i ) { String action = CombatActionManager.getCombatAction( monsterName, i, true ); if ( !Macrofier.isSimpleAction( action ) ) { if ( debug ) { RequestLogger.printLine( "stopping macrofication due to action: " + action ); } if ( i == start ) { return null; } FightRequest.setMacroPrefixLength( i ); break; } int finalRound = 0; if ( CombatActionManager.atEndOfStrategy() ) { macro.append( "mark mafiafinal\n" ); finalRound = macro.length(); } Macrofier.macroAction( macro, action, finalRound ); if ( finalRound != 0 ) { if ( finalRound == macro.length() ) { // last line of CCS generated no action! macro.append( "call mafiaround; attack\n" ); } macro.append( "goto mafiafinal" ); FightRequest.setMacroPrefixLength( 0 ); break; } } if ( debug ) { RequestLogger.printLine( "Generated macro:" ); Macrofier.indentify( macro.toString(), false ); RequestLogger.printLine( "" ); } HashSet<String> allCalls = new HashSet<String>(); Matcher m = Macrofier.ALLCALLS_PATTERN.matcher( macro ); while ( m.find() ) { allCalls.add( m.group( 1 ) ); } m = Macrofier.ALLSUBS_PATTERN.matcher( macro.toString() ); while ( m.find() ) { String label = m.group( 1 ); if ( m.group( 2 ) != null || !allCalls.contains( label ) ) { // this sub is useless! Matcher del = Pattern.compile( "call " + label + "\\b|sub " + label + "\\b.*?endsub", Pattern.DOTALL ).matcher( macro.toString() ); macro.setLength( 0 ); while ( del.find() ) { del.appendReplacement( macro, "" ); } del.appendTail( macro ); } } if ( debug ) { RequestLogger.updateDebugLog( "Optimized macro:" ); Macrofier.indentify( macro.toString(), true ); } return macro.toString(); } protected static void macroAction( StringBuffer macro, String action, final int finalRound ) { if ( action.length() == 0 || action.equals( "skip" ) || action.startsWith( "note " ) ) { return; } if ( CombatActionManager.isMacroAction( action ) ) { if ( action.startsWith( "\"" ) ) { action = action.substring( 1 ); } if ( action.charAt( action.length() - 1 ) == '\"' ) { action = action.substring( 0, action.length() - 1 ); } macro.append( action ); macro.append( '\n' ); return; } action = CombatActionManager.getShortCombatOptionName( action ); if ( action.equals( "skip" ) ) { return; } if ( action.equals( "special" ) ) { if ( FightRequest.waitingForSpecial ) { // only allow once per combat FightRequest.waitingForSpecial = false; String specialAction = FightRequest.getSpecialAction(); if ( specialAction != null ) { if ( specialAction.startsWith( "skill" ) ) { Macrofier.macroSkill( macro, StringUtilities.parseInt( specialAction.substring( 5 ) ) ); } else { macro.append( "call mafiaround; use " + specialAction + "\n" ); // TODO } } } } else if ( action.equals( "abort" ) ) { if ( finalRound != 0 ) { macro.append( "abort \"KoLmafia CCS abort\"\n" ); } else { macro.append( "abort \"Click Script button again to continue\"\n" ); macro.append( "#mafiarestart\n" ); } } else if ( action.equals( "abort after" ) ) { KoLmafia.abortAfter( "Aborted by CCS request" ); } else if ( action.equals( "runaway" ) ) { macro.append( "runaway\n" ); } else if ( action.startsWith( "runaway" ) ) { int runaway = StringUtilities.parseInt( action.substring( 7 ) ); if ( FightRequest.freeRunawayChance() >= runaway ) { macro.append( "runaway\n" ); } } else if ( action.startsWith( "attack" ) ) { macro.append( "call mafiaround; attack\n" ); } else if ( action.equals( "steal" ) ) { if ( MonsterStatusTracker.shouldSteal() ) { macro.append( "pickpocket\n" ); } } else if ( action.equals( "jiggle" ) ) { if ( EquipmentManager.usingChefstaff() ) { macro.append( "call mafiaround; jiggle\n" ); } } else if ( action.startsWith( "combo " ) ) { int[] combo = DiscoCombatHelper.getCombo( action.substring( 6 ) ); if ( combo != null ) { String name = action.substring( 6 ); String raveSteal = DiscoCombatHelper.COMBOS[ DiscoCombatHelper.RAVE_STEAL ] [ 0 ]; if ( DiscoCombatHelper.disambiguateCombo( name ).equals( raveSteal ) && !DiscoCombatHelper.canRaveSteal() ) { // There the limit on the number of Rave Steals has been reached, // no point in executing the combo. } else { Macrofier.macroCombo( macro, combo ); } } } else if ( action.startsWith( "skill" ) ) { int skillId = StringUtilities.parseInt( action.substring( 5 ) ); String skillName = SkillDatabase.getSkillName( skillId ); if ( skillName.equals( "Transcendent Olfaction" ) ) { // You can't sniff if you are already on the trail. // You can't sniff in Bad Moon, even though the skill // shows up on the char sheet, unless you've recalled // your skills. if ( ( KoLCharacter.inBadMoon() && !KoLCharacter.skillsRecalled() ) || KoLConstants.activeEffects.contains( EffectPool.get( EffectPool.ON_THE_TRAIL ) ) ) { // ignore } else { // must insert On The Trail check in generated macro // too, in case more than one olfact is attempted. macro.append( "if !haseffect 331\n" ); Macrofier.macroSkill( macro, skillId ); macro.append( "endif\n" ); } } else if ( skillName.equals( "CLEESH" ) ) { // Macrofied combat will continue with the same CCS after // a CLEESH, unlike round-by-round combat which switches // sections. Make sure there's something to finish off // the amphibian. Macrofier.macroSkill( macro, skillId ); if ( finalRound != 0 ) { macro.append( "attack; repeat\n" ); } } else { Macrofier.macroSkill( macro, skillId ); } } else if ( !KoLConstants.activeEffects.contains( FightRequest.BIRDFORM ) ) { // Must be an item use // Can't use items in Birdform int comma = action.indexOf( "," ); int item1 = StringUtilities.parseInt( comma != -1 ? action.substring( 0, comma ).trim() : action ); int item2 = comma != -1 ? StringUtilities.parseInt( action.substring( comma + 1 ).trim() ) : -1; macro.append( "call mafiaround; use " ); macro.append( String.valueOf( item1 ) ); if ( item2 != -1 ) { macro.append( "," ); macro.append( String.valueOf( item2 ) ); } macro.append( "\n" ); } else { // Trying to use an item in Birdform. Ignore it. } } public static void indentify( String macro, boolean debug ) { String indent = ""; String element = debug ? "\t" : "\u00A0\u00A0\u00A0\u00A0"; String[] pieces = macro.split( "\n" ); for ( int i = 0; i < pieces.length; ++i ) { String line = pieces[ i ].trim(); if ( line.startsWith( "end" ) && indent.length() > 0 ) { indent = indent.substring( element.length() ); } if ( debug ) { RequestLogger.updateDebugLog( indent + line ); } else { RequestLogger.printLine( indent + line ); } if ( line.startsWith( "if " ) || line.startsWith( "while " ) || line.startsWith( "sub " ) ) { indent = indent + element; } } } public static void macroCommon( StringBuffer macro ) { macro.append( "sub mafiaround\n" ); Macrofier.macroUseAntidote( macro ); macro.append( "endsub#mafiaround\n" ); macro.append( "sub mafiamp\n" ); Macrofier.macroManaRestore( macro ); macro.append( "endsub#mafiamp\n" ); } public static void macroSkill( StringBuffer macro, int skillId ) { int cost = SkillDatabase.getMPConsumptionById( skillId ); if ( cost > KoLCharacter.getMaximumMP() ) { return; // no point in even trying } if ( cost > 0 && Preferences.getBoolean( "autoManaRestore" ) ) { macro.append( "while mpbelow " ); macro.append( cost ); macro.append( "\ncall mafiamp\nendwhile\n" ); } macro.append( "if hasskill " ); macro.append( skillId ); macro.append( "\ncall mafiaround; skill " ); macro.append( skillId ); macro.append( "\nendif\n" ); } public static void macroCombo( StringBuffer macro, int[] combo ) { int cost = 0; for ( int i = 0; i < combo.length; ++i ) { cost += SkillDatabase.getMPConsumptionById( combo[ i ] ); } if ( cost > KoLCharacter.getMaximumMP() ) { return; // no point in even trying } boolean restore = Preferences.getBoolean( "autoManaRestore" ); if ( restore ) { macro.append( "while mpbelow " ); macro.append( cost ); macro.append( "\ncall mafiamp\nendwhile\n" ); } else { macro.append( "if !mpbelow " ); macro.append( cost ); macro.append( "\n" ); } macro.append( "call mafiaround; " ); for ( int i = 0; i < combo.length; ++i ) { macro.append( "skill " ); macro.append( combo[ i ] ); macro.append( "; " ); } macro.append( "\n" ); if ( !restore ) { macro.append( "endif\n" ); } } public static final void macroUseAntidote( StringBuffer macro ) { if ( !KoLConstants.inventory.contains( FightRequest.ANTIDOTE ) ) { return; } if ( KoLConstants.activeEffects.contains( FightRequest.BIRDFORM ) ) { return; // can't use items! } int minLevel = Preferences.getInteger( "autoAntidote" ); int poison = MonsterStatusTracker.getPoisonLevel(); if ( poison > minLevel || minLevel == 0 ) { return; // no poison expected that the user wants to remove } macro.append( "if hascombatitem " ); macro.append( ItemPool.ANTIDOTE ); macro.append( " && (" ); boolean first = true; for ( int i = minLevel; i > 0; --i ) { if ( poison != 0 && i != poison ) { // only check for the monster's known poison attack continue; } if ( !first ) { macro.append( " || " ); } first = false; macro.append( "haseffect " ); macro.append( EffectDatabase.POISON_ID[ i ] ); } macro.append( ")\n use " ); macro.append( ItemPool.ANTIDOTE ); macro.append( "\nendif\n" ); } public static void macroManaRestore( StringBuffer macro ) { if ( KoLConstants.activeEffects.contains( FightRequest.BIRDFORM ) ) { macro.append( "abort \"Cannot use combat items while in Birdform!\"\n" ); return; } int cumulative = 0; for ( int i = 0; i < MPRestoreItemList.CONFIGURES.length; ++i ) { MPRestoreItem restorer = MPRestoreItemList.CONFIGURES[ i ]; if ( restorer.isCombatUsable() ) { AdventureResult restoreItem = restorer.getItem(); if ( restoreItem == null ) { continue; } int count = restoreItem.getCount( KoLConstants.inventory ); if ( count <= 0 ) { continue; } String itemId = String.valueOf( restoreItem.getItemId() ); cumulative += count; if ( cumulative >= 30 ) { // Assume this item will be sufficient for all requests macro.append( "call mafiaround; use " ); macro.append( itemId ); macro.append( "\nmark mafiampexit\n" ); return; } macro.append( "if hascombatitem " ); macro.append( itemId ); macro.append( "\ncall mafiaround; use " ); macro.append( itemId ); macro.append( "\ngoto mafiampexit\nendif\n" ); } } macro.append( "abort \"No MP restoratives!\"\n" ); macro.append( "mark mafiampexit\n" ); } }