/**
* 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;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.sourceforge.kolmafia.objectpool.EffectPool;
import net.sourceforge.kolmafia.objectpool.ItemPool;
import net.sourceforge.kolmafia.persistence.EffectDatabase;
import net.sourceforge.kolmafia.persistence.HolidayDatabase;
import net.sourceforge.kolmafia.persistence.ItemDatabase;
import net.sourceforge.kolmafia.persistence.SkillDatabase;
import net.sourceforge.kolmafia.preferences.Preferences;
import net.sourceforge.kolmafia.request.BasementRequest;
import net.sourceforge.kolmafia.request.FightRequest;
import net.sourceforge.kolmafia.utilities.StringUtilities;
public class Expression
{
private static final Pattern NUM_PATTERN = Pattern.compile( "([+-]?[\\d.]+)(.*)" );
private static final int STACK_SIZE = 128;
protected String name;
protected String text;
private char[] bytecode; // Compiled expression
private ArrayList<Object> literals; // Strings & floats needed by expression
protected AdventureResult effect;
// If non-null, contains concatenated error strings from compiling bytecode
private StringBuilder error = null;
private StringBuilder newError()
{
if ( this.error == null )
{
this.error = new StringBuilder();
}
else
{
this.error.append( KoLConstants.LINE_BREAK );
}
return this.error;
}
public boolean hasErrors()
{
return this.error != null;
}
public String getExpressionErrors()
{
if ( this.error == null )
{
return null;
}
StringBuilder buf = new StringBuilder();
buf.append( "Expression syntax errors for '" );
buf.append( name );
buf.append( "':" );
buf.append( KoLConstants.LINE_BREAK );
buf.append( this.error );
return buf.toString();
}
private static double[] cachedStack;
private synchronized static double[] stackFactory( double[] recycle )
{
if ( recycle != null )
{ // Reuse this stack for the next evaluation.
cachedStack = recycle;
return null;
}
else if ( cachedStack != null )
{ // We have a stack handy; it's yours now.
double[] rv = cachedStack;
cachedStack = null;
return rv;
}
else
{ // We're all out of stacks.
return new double[ STACK_SIZE ];
}
}
public Expression( String text, String name )
{
this.name = name;
this.text = text;
// Let subclass initialize variables needed for
// compilation and evaluation
this.initialize();
// Compile the expression into byte code
this.bytecode = this.validBytecodes().toCharArray();
Arrays.sort( this.bytecode );
String compiled = this.expr() + "r";
//if ( name.length() > 0 && name.equalsIgnoreCase(
// Preferences.getString( "debugEval" ) ) )
//{
// compiled = compiled.replaceAll( ".", "?$0" );
//}
this.bytecode = compiled.toCharArray();
if ( this.text.length() > 0 )
{
StringBuilder buf = this.newError();
buf.append( "Expected end, found " );
buf.append( this.text );
}
this.text = null;
}
public static Expression getInstance( String text, String name )
{
Expression expr = new Expression( text, name );
String errors = expr.getExpressionErrors();
if ( errors != null )
{
KoLmafia.updateDisplay( errors );
}
return expr;
}
protected void initialize()
{
}
public double eval()
{
try
{
return this.evalInternal();
}
catch ( ArrayIndexOutOfBoundsException e )
{
KoLmafia.updateDisplay( "Unreasonably complex expression for " + this.name + ": " + e );
}
catch ( RuntimeException e )
{
KoLmafia.updateDisplay( "Expression evaluation error for " + this.name + ": " + e );
}
catch ( Exception e )
{
KoLmafia.updateDisplay( "Unexpected exception for " + this.name + ": " + e );
}
return 0.0;
}
public double evalInternal()
{
double[] s = stackFactory( null );
int sp = 0;
int pc = 0;
double v = 0.0;
while ( true )
{
char inst = this.bytecode[ pc++ ];
switch ( inst )
{
// case '?': // temporary instrumentation
// KoLmafia.updateDisplay( "\u2326 Eval " + this.name + " from " +
// Thread.currentThread().getName() );
// StringBuffer b = new StringBuffer();
// if ( pc == 1 )
// {
// b.append( "\u2326 Bytecode=" );
// b.append( this.bytecode );
// for ( int i = 1; i < this.bytecode.length; i += 2 )
// {
// b.append( ' ' );
// b.append( Integer.toHexString( this.bytecode[ i ] ) );
// }
// KoLmafia.updateDisplay( b.toString() );
// b.setLength( 0 );
// }
// b.append( "\u2326 PC=" );
// b.append( pc );
// b.append( " Stack=" );
// if ( sp < 0 )
// {
// b.append( sp );
// }
// else
// {
// for ( int i = 0; i < sp && i < INITIAL_STACK; ++i )
// {
// b.append( ' ' );
// b.append( s[ i ] );
// }
// }
// KoLmafia.updateDisplay( b.toString() );
// continue;
case 'r':
v = s[ --sp ];
stackFactory( s ); // recycle this stack
return v;
case '+':
v = s[ --sp ] + s[ --sp ];
break;
case '-':
v = s[ --sp ] - s[ --sp ];
break;
case '*':
v = s[ --sp ] * s[ --sp ];
break;
case '/':
double numerator = s[ --sp ];
double denominator = s[ --sp ];
if ( denominator == 0.0 )
{
throw new ArithmeticException( "Can't divide by zero" );
}
v = numerator / denominator;
break;
case '^':
double base = s[ --sp ];
double expt = s[ --sp ];
v = (double) Math.pow( base, expt );
if ( Double.isNaN( v ) || Double.isInfinite( v ) )
{
throw new ArithmeticException( "Invalid exponentiation: cannot take " + base + " ** " + expt );
}
break;
case 'a':
v = Math.abs( s[ --sp ] );
break;
case 'c':
v = (double) Math.ceil( s[ --sp ] );
break;
case 'f':
v = (double) Math.floor( s[ --sp ] );
break;
case 'm':
v = Math.min( s[ --sp ], s[ --sp ] );
break;
case 'p':
String first = (String) this.literals.get( (int) s[ --sp ] );
String second = null;
int commaIndex = first.indexOf( "," );
if ( commaIndex > -1 )
{
second = first.substring( commaIndex + 1 );
first = first.substring( 0, commaIndex );
}
String prefString = Preferences.getString( first );
if ( second != null )
{
v = prefString.contains( second ) ? 1 : 0;
}
else
{
v =
prefString.contains( "true" ) ? 1 :
prefString.contains( "false" ) ? 0 :
StringUtilities.parseDouble( prefString );
}
break;
case 's':
v = (double) Math.sqrt( s[ --sp ] );
if ( Double.isNaN(v) )
{
throw new ArithmeticException( "Can't take square root of a negative value" );
}
break;
case 'x':
v = Math.max( s[ --sp ], s[ --sp ] );
break;
case '#':
v = ((Double) this.literals.get( (int) s[ --sp ] )).doubleValue();
break;
// Valid with ModifierExpression:
case 'b':
String elem = (String) this.literals.get( (int) s[ --sp ] );
int element =
elem.equalsIgnoreCase( "cold" ) ? Modifiers.COLD_RESISTANCE :
elem.equalsIgnoreCase( "hot" ) ? Modifiers.HOT_RESISTANCE :
elem.equalsIgnoreCase( "sleaze" ) ? Modifiers.SLEAZE_RESISTANCE :
elem.equalsIgnoreCase( "spooky" ) ? Modifiers.SPOOKY_RESISTANCE :
elem.equalsIgnoreCase( "stench" ) ? Modifiers.STENCH_RESISTANCE :
elem.equalsIgnoreCase( "slime" ) ? Modifiers.SLIME_RESISTANCE :
elem.equalsIgnoreCase( "supercold" ) ? Modifiers.SUPERCOLD_RESISTANCE :
-1;
v = KoLCharacter.currentNumericModifier( element );
break;
case 'd':
String skillName = (String) this.literals.get( (int) s[ --sp ] );
if ( StringUtilities.isNumeric( skillName ) )
{
int skillId = StringUtilities.parseInt( skillName );
skillName = SkillDatabase.getSkillName( skillId );
}
v = KoLCharacter.hasSkill( skillName ) ? 1 : 0;
break;
case 'e':
String effectName = (String) this.literals.get( (int) s[ --sp ] );
// If effect name is a number, convert to name
AdventureResult eff = null;
if ( StringUtilities.isNumeric( effectName ) )
{
int effectId = StringUtilities.parseInt( effectName );
eff = EffectPool.get( effectId );
}
else
{
int effectId = EffectDatabase.getEffectId( effectName );
eff = EffectPool.get( effectId );
}
v = eff == null ? 0.0 :
Math.max( 0, eff.getCount( KoLConstants.activeEffects ) );
break;
case 'g':
String itemName = (String) this.literals.get( (int) s[ --sp ] );
int itemId = ItemDatabase.getItemId( itemName );
AdventureResult item = ItemPool.get( itemId );
v = KoLCharacter.hasEquipped( item ) ? 1 : 0;
break;
case 'h':
v = Modifiers.mainhandClass.equalsIgnoreCase( (String) this.literals.get( (int) s[ --sp ] ) ) ? 1 : 0;
break;
case 'j':
v = Modifiers.currentEnvironment.equalsIgnoreCase( (String) this.literals.get( (int) s[ --sp ] ) ) ? 1 : 0;
break;
case 'l':
v = Modifiers.currentLocation.equalsIgnoreCase( (String) this.literals.get( (int) s[ --sp ] ) ) ? 1 : 0;
break;
case 'n':
v = KoLCharacter.getClassName().equals( (String) this.literals.get( (int) s[ --sp ] ) ) ? 1 : 0;
break;
case 'w':
v = Modifiers.currentFamiliar.equalsIgnoreCase( (String) this.literals.get( (int) s[ --sp ] ) ) ? 1 : 0;
break;
case 'z':
v = Modifiers.currentZone.equalsIgnoreCase( (String) this.literals.get( (int) s[ --sp ] ) ) ? 1 : 0;
break;
case 'v':
Calendar date = Calendar.getInstance( TimeZone.getTimeZone( "GMT-0700" ) );
String event = (String) this.literals.get( (int) s[ --sp ] );
if ( event.equals( "Crimbo2015" ) )
{
// Event ends just after rollover on 3rd January 2016
GregorianCalendar eventEnd = new GregorianCalendar( 2016, Calendar.JANUARY, 3, 20, 30 );
eventEnd.setTimeZone( TimeZone.getTimeZone( "GMT-0700" ) );
v = date.before( eventEnd ) ? 1 : 0;
}
else if ( event.equals( "December" ) )
{
int month = date.get( Calendar.MONTH );
v = ( month == Calendar.DECEMBER ) ? 1 : 0;
}
break;
case '\u0092':
v = KoLCharacter.getPath().equals( (String) this.literals.get( (int) s[ --sp ] ) ) ? 1 : 0;
break;
case '\u0093':
Modifiers mods = KoLCharacter.getCurrentModifiers();
String modName = (String) this.literals.get( (int) s[ --sp ] );
v = mods.getExtra( modName );
break;
case '\u0094':
v = KoLCharacter.canInteract() ? 1 : 0;
break;
case 'A':
v = KoLCharacter.getAscensions();
break;
case 'B':
v = HolidayDatabase.getBloodEffect();
break;
case 'C':
v = KoLCharacter.getMinstrelLevel();
break;
case 'D':
v = KoLCharacter.getInebriety();
break;
case 'E':
{
int size = KoLConstants.activeEffects.size();
AdventureResult[] effectsArray = new AdventureResult[ size ];
KoLConstants.activeEffects.toArray( effectsArray );
v = 0;
for ( int i = 0; i < size; i++ )
{
AdventureResult effect = effectsArray[ i ];
int duration = effect.getCount();
if ( duration != Integer.MAX_VALUE )
{
v++;
}
}
break;
}
case 'F':
v = KoLCharacter.getFullness();
break;
case 'G':
v = HolidayDatabase.getGrimaciteEffect() / 10.0;
break;
case 'H':
v = Modifiers.hoboPower;
break;
case 'I':
v = KoLCharacter.getDiscoMomentum();
break;
case 'J':
v = HolidayDatabase.getHoliday().contains( "Festival of Jarlsberg" ) ? 1.0 : 0.0;
break;
case 'K':
v = Modifiers.smithsness;
break;
case 'L':
v = KoLCharacter.getLevel();
break;
case 'M':
v = HolidayDatabase.getMoonlight();
break;
case 'N':
v = KoLCharacter.getAudience();
break;
case 'P':
v = KoLCharacter.currentPastaThrall.getLevel();
break;
case 'R':
v = KoLCharacter.getReagentPotionDuration();
break;
case 'S':
v = KoLCharacter.getSpleenUse();
break;
case 'T':
v = this.effect == null ? 0.0 :
Math.max( 1, this.effect.getCount( KoLConstants.activeEffects ) );
break;
case 'U':
v = KoLCharacter.getTelescopeUpgrades();
break;
case 'W':
v = Modifiers.currentWeight;
break;
case 'X':
v = KoLCharacter.getGender();
break;
case 'Y':
v = KoLCharacter.getFury();
break;
// Valid with MonsterExpression:
case '\u0080':
v = KoLCharacter.getAdjustedMuscle();
break;
case '\u0081':
v = KoLCharacter.getAdjustedMysticality();
break;
case '\u0082':
v = KoLCharacter.getAdjustedMoxie();
break;
case '\u0083':
v = KoLCharacter.getMonsterLevelAdjustment();
break;
case '\u0084':
v = KoLCharacter.getMindControlLevel();
break;
case '\u0085':
v = KoLCharacter.getMaximumHP();
break;
case '\u0086':
v = BasementRequest.getBasementLevel();
break;
case '\u0087':
v = FightRequest.dreadKisses( "Woods" );
break;
case '\u0088':
v = FightRequest.dreadKisses( "Village" );
break;
case '\u0089':
v = FightRequest.dreadKisses( "Castle" );
break;
case '\u0090':
v = KoLCharacter.getAdjustedHighestStat();
break;
// Valid with RestoreExpression:
case '\u0091':
v = KoLCharacter.getMaximumMP();
break;
default:
if ( inst > '\u00FF' )
{
v = inst - 0x8000;
break;
}
throw new RuntimeException( "Evaluator bytecode invalid at " +
(pc - 1) + ": " + String.valueOf( this.bytecode ) );
}
s[ sp++ ] = v;
}
}
protected String validBytecodes()
{ // Allowed operations in the A-Z range.
return "";
}
private void expect( String token )
{
if ( this.text.startsWith( token ) )
{
this.text = this.text.substring( token.length() );
return;
}
StringBuilder buf = this.newError();
buf.append( "Expected " );
buf.append( token );
buf.append( ", found " );
buf.append( this.text );
}
protected String until( String token )
{
int pos = this.text.indexOf( token );
if ( pos == -1 )
{
StringBuilder buf = this.newError();
buf.append( "Expected " );
buf.append( token );
buf.append( ", found " );
buf.append( this.text );
return "";
}
String rv = this.text.substring( 0, pos );
this.text = this.text.substring( pos + token.length() );
return rv;
}
protected boolean optional( String token )
{
if ( this.text.startsWith( token ) )
{
this.text = this.text.substring( token.length() );
return true;
}
return false;
}
private char optional( String token1, String token2 )
{
if ( this.text.startsWith( token1 ) )
{
this.text = this.text.substring( token1.length() );
return token1.charAt( 0 );
}
if ( this.text.startsWith( token2 ) )
{
this.text = this.text.substring( token2.length() );
return token2.charAt( 0 );
}
return '\0';
}
protected String literal( Object value, char op )
{
if ( this.literals == null )
{
this.literals = new ArrayList<Object>();
}
this.literals.add( value == null ? "" : value );
return String.valueOf( (char)( this.literals.size() - 1 + 0x8000 ) ) + op;
}
private String expr()
{
String rv = this.term();
while ( true )
{
switch ( this.optional( "+", "-" ) )
{
case '+':
rv = this.term() + rv + "+";
break;
case '-':
rv = this.term() + rv + "-";
break;
default:
return rv;
}
}
}
private String term()
{
String rv = this.factor();
while ( true )
{
switch ( this.optional( "*", "/" ) )
{
case '*':
rv = this.factor() + rv + "*";
break;
case '/':
rv = this.factor() + rv + "/";
break;
default:
return rv;
}
}
}
private String factor()
{
String rv = this.value();
while ( this.optional( "^", "**" ) != '\0' )
{
rv = this.value() + rv + "^";
}
return rv;
}
private String value()
{
String rv;
if ( this.optional( "(" ) )
{
rv = this.expr();
this.expect( ")" );
return rv;
}
if ( this.optional( "ceil(" ) )
{
rv = this.expr();
this.expect( ")" );
return rv + "c";
}
if ( this.optional( "floor(" ) )
{
rv = this.expr();
this.expect( ")" );
return rv + "f";
}
if ( this.optional( "sqrt(" ) )
{
rv = this.expr();
this.expect( ")" );
return rv + "s";
}
if ( this.optional( "min(" ) )
{
rv = this.expr();
this.expect( "," );
rv = rv + this.expr() + "m";
this.expect( ")" );
return rv;
}
if ( this.optional( "max(" ) )
{
rv = this.expr();
this.expect( "," );
rv = rv + this.expr() + "x";
this.expect( ")" );
return rv;
}
if ( this.optional( "abs(" ) )
{
rv = this.expr();
this.expect( ")" );
return rv + "a";
}
if ( this.optional( "pref(" ) )
{
return this.literal( this.until( ")" ), 'p' );
}
rv = this.function();
if ( rv != null )
{
return rv;
}
if ( this.text.length() == 0 )
{
StringBuilder buf = this.newError();
buf.append( "Unexpected end of expr" );
return "\u8000";
}
rv = this.text.substring( 0, 1 );
if ( rv.charAt( 0 ) >= 'A' && rv.charAt( 0 ) <= 'Z' )
{
this.text = this.text.substring( 1 );
if ( Arrays.binarySearch( this.bytecode, rv.charAt( 0 ) ) < 0 )
{
StringBuilder buf = this.newError();
buf.append( "'" );
buf.append( rv );
buf.append( "' is not valid in this context" );
return "\u8000";
}
return rv;
}
Matcher m = NUM_PATTERN.matcher( this.text );
if ( m.matches() )
{
double v = Double.parseDouble( m.group( 1 ) );
this.text = m.group( 2 );
if ( v % 1.0 == 0.0 && v >= -0x7F00 && v < 0x8000 )
{
return String.valueOf( (char)((int)v + 0x8000) );
}
else
{
return this.literal( new Double( v ), '#' );
}
}
if ( this.optional( "-" ) )
{
return this.value() + "\u8000-";
}
StringBuilder buf = this.newError();
buf.append( "Can't understand " );
buf.append( this.text );
this.text = "";
return "\u8000";
}
protected String function()
{
return null;
}
}