/* * VariableProcessor.java * Copyright 2004 (C) Chris Ward <frugal@purplewombat.co.uk> * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Created on 13-Dec-2004 */ package pcgen.core; import java.io.BufferedWriter; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.StringTokenizer; import pcgen.core.character.CachedVariable; import pcgen.core.character.CharacterSpell; import pcgen.core.utils.CoreUtility; import pcgen.io.ExportHandler; import pcgen.util.Logging; import pcgen.util.PJEP; import pcgen.util.PjepPool; /** * {@code VariableProcessor} is the base class for PCGen variable * processors. These are classes that convert a formula or variable * into a value and are used extensively both in defintions of objects * and for output to output sheets. * * * @author Chris Ward <frugal@purplewombat.co.uk> */ public abstract class VariableProcessor { /** A simple mathematical operation. */ private enum MATH_OP { PLUS, MINUS, MULTIPLY, DIVIDE}; /** The current indenting to be used for debug output of jep evaluations. */ protected String jepIndent = ""; protected PlayerCharacter pc; private int cachePaused; private int serial; private Map<String, CachedVariable<String>> sVariableCache = new HashMap<>(); private Map<String, CachedVariable<Float>> fVariableCache = new HashMap<>(); protected Float convertToFloat(String element, String foo) { Float d = null; try { d = new Float(foo); } catch (NumberFormatException nfe) { // What we got back was not a number } Float retVal = null; if (d != null && !d.isNaN()) { retVal = d; if (Logging.isDebugMode()) { Logging.debugPrint(new StringBuilder().append(jepIndent) .append("export variable for: '").append(element) .append("' = ").append(d).toString()); } } return retVal; } /** * {@code CachableResult} encapsulates a result returned from JEP processing * allowing us to retrieve both the result and its cachability. */ private static class CachableResult { final Float result; final boolean cachable; CachableResult(Float result, boolean cachable) { this.result = result; this.cachable = cachable; } } /** * Create a new Variable Processor instance. * @param pc The character the processor is for. */ public VariableProcessor(PlayerCharacter pc) { this.pc = pc; } /** * Evaluates a variable for this character. * e.g: getVariableValue("3+CHA","CLASS:Cleric") for Turn Undead * * @param aSpell This is specifically to compute bonuses to CASTERLEVEL * for a specific spell. * @param varString The variable to be evaluated * @param src The source within which the variable is evaluated * @param spellLevelTemp The temporary spell level * @return The value of the variable */ public Float getVariableValue( final CharacterSpell aSpell, String varString, String src, int spellLevelTemp) { Float result = getJepOnlyVariableValue( aSpell, varString, src, spellLevelTemp); if (null == result) { result = processBrokenParser( aSpell, varString, src, spellLevelTemp); String cacheString = makeCacheString(aSpell == null ? null : aSpell, varString, src, spellLevelTemp); addCachedVariable(cacheString, result); } return result; } /** * Evaluates a JEP variable for this character. * e.g: getJepOnlyVariableValue("3+CHA","CLASS:Cleric") for Turn Undead * * @param aSpell This is specifically to compute bonuses to CASTERLEVEL for a specific spell. * @param varString The variable to be evaluated * @param src The source within which the variable is evaluated * @param spellLevelTemp The temporary spell level * @return The value of the variable, or null if the formula is not JEP */ public Float getJepOnlyVariableValue( final CharacterSpell aSpell, String varString, String src, int spellLevelTemp) { // First try to just parse it as a number. try { return new Float(varString); } catch (NumberFormatException e) { // Nothing to handle here, we're attempting to see if varString was a // number, If we got here it wasn't } String cacheString = makeCacheString(aSpell == null ? null : aSpell, varString, src, spellLevelTemp); Float total = getCachedVariable(cacheString); if (total != null) { return total; } CachableResult cRes = processJepFormula(aSpell, varString, src); if (cRes != null) { if (cRes.cachable) { addCachedVariable(cacheString, cRes.result); } return cRes.result; } return null; } private String makeCacheString(CharacterSpell aSpell, String varString, String src, int spellLevelTemp) { StringBuilder cS = new StringBuilder(varString).append("#").append(src); if (aSpell != null) { if (aSpell.getSpell() != null) { cS.append(aSpell.getSpell().getKeyName()); } cS.append(aSpell.getFixedCasterLevel()); } if (spellLevelTemp > 0) { cS.append(spellLevelTemp); } return cS.toString(); } /** * Evaluate the variable using the old non-JEP variable parser. Use of this * parser is being phased out. * * @param aSpell This is specifically to compute bonuses to CASTERLEVEL for a specific spell. * @param aString The variable to be evaluated * @param src The source within which the variable is evaluated * @param spellLevelTemp The temporary spell level * @return The value of the variable */ private Float processBrokenParser( final CharacterSpell aSpell, String aString, String src, int spellLevelTemp) { Float total = new Float(0.0); aString = aString.toUpperCase(); src = src.toUpperCase(); while (aString.lastIndexOf('(') >= 0) { final int x = CoreUtility.innerMostStringStart(aString); final int y = CoreUtility.innerMostStringEnd(aString); if (y < x) { Logging.errorPrint("Missing closing parenthesis: " + aString); return total; } final String bString = aString.substring(x + 1, y); aString = aString.substring(0, x) + getVariableValue(aSpell, bString, src, spellLevelTemp) + aString.substring(y + 1); } final String delimiter = "+-/*"; String valString = ""; MATH_OP mode = MATH_OP.PLUS; MATH_OP nextMode = MATH_OP.PLUS; if (aString.startsWith(".IF.")) { final StringTokenizer aTok = new StringTokenizer(aString.substring(4), ".", true); String bString = ""; Float val1 = null; // first value Float val2 = null; // other value in comparison Float valt = null; // value if comparison is true final Float valf; // value if comparison is false int comp = 0; while (aTok.hasMoreTokens()) { final String cString = aTok.nextToken(); if ("GT".equals(cString) || "GTEQ".equals(cString) || "EQ".equals(cString) || "LTEQ".equals(cString) || "LT".equals(cString)) { val1 = getVariableValue(aSpell, bString.substring(0, bString.length() - 1), src, spellLevelTemp); // truncat final . character aTok.nextToken(); // discard next . character bString = ""; if ("LT".equals(cString)) { comp = 1; } else if ("LTEQ".equals(cString)) { comp = 2; } else if ("EQ".equals(cString)) { comp = 3; } else if ("GT".equals(cString)) { comp = 4; } else if ("GTEQ".equals(cString)) { comp = 5; } } else if ("THEN".equals(cString)) { val2 = getVariableValue(aSpell, bString.substring(0, bString.length() - 1), src, spellLevelTemp); // truncat final . character aTok.nextToken(); // discard next . character bString = ""; } else if ("ELSE".equals(cString)) { valt = getVariableValue(aSpell, bString.substring(0, bString.length() - 1), src, spellLevelTemp); // truncat final . character aTok.nextToken(); // discard next . character bString = ""; } else { bString += cString; } } if ((val1 != null) && (val2 != null) && (valt != null)) { valf = getVariableValue(aSpell, bString, src, spellLevelTemp); total = valt; switch (comp) { case 1: // LT if (val1.doubleValue() >= val2.doubleValue()) { total = valf; } break; case 2: // LTEQ if (val1.doubleValue() > val2.doubleValue()) { total = valf; } break; case 3: // EQ if (!CoreUtility.doublesEqual(val1.doubleValue(), val2.doubleValue())) { total = valf; } break; case 4: // GT if (val1.doubleValue() <= val2.doubleValue()) { total = valf; } break; case 5: // GTEQ if (val1.doubleValue() < val2.doubleValue()) { total = valf; } break; default: Logging.errorPrint("ERROR - badly formed statement:" + aString + ":" + val1.toString() + ":" + val2.toString() + ":" + comp); return new Float(0.0); } return total; } } for (int i = 0; i < aString.length(); ++i) { valString += aString.substring(i, i + 1); if ( // end of string (i == (aString.length() - 1)) || // have found one of +, -, *, / (delimiter.lastIndexOf(aString.charAt(i)) > -1) ) { if ((valString.length() == 1) && (delimiter.lastIndexOf(aString.charAt(i)) > -1)) { continue; } if (delimiter.lastIndexOf(aString.charAt(i)) > -1) { valString = valString.substring(0, valString.length() - 1); } final Float tmp= lookupVariable(valString, src, aSpell); if (tmp != null) { valString = tmp.toString(); } if (i < aString.length()) { if (!aString.isEmpty() && aString.charAt(i) == '+') { nextMode = MATH_OP.PLUS; } else if (!aString.isEmpty() && aString.charAt(i) == '-') { nextMode = MATH_OP.MINUS; } else if (!aString.isEmpty() && aString.charAt(i) == '*') { nextMode = MATH_OP.MULTIPLY; } else if (!aString.isEmpty() && aString.charAt(i) == '/') { nextMode = MATH_OP.DIVIDE; } } if (!valString.isEmpty()) { float valFloat = 0.0f; try { valFloat = Float.parseFloat(valString); } catch (NumberFormatException exc) { // Don't care, as it's just zero //Logging.debugPrint("Will use default for total: " + total, exc); } switch (mode) { case PLUS: total += valFloat; break; case MINUS: total -= valFloat; break; case MULTIPLY: total *= valFloat; break; case DIVIDE: total /= valFloat; break; default: Logging.errorPrint("In PlayerCharacter.getVariableValue the mode " + mode + " is unsupported."); break; } } mode = nextMode; nextMode = MATH_OP.PLUS; valString = ""; } } return total; } /** * Evaluate the forumla using the JEP parser. This will always be tried before * using the old non-JEP parser and null will be returned if the forumla is not * a recognised JEP formula. * * @param spell This is specifically to compute bonuses to CASTERLEVEL for a specific spell. * @param formula The formula to be evaluated * @param src The source within which the variable is evaluated * @return The value of the variable encapsulated in a CachableResult */ private CachableResult processJepFormula(final CharacterSpell spell, final String formula, final String src) { final String DEBUG_FORMULA_PREFIX = "CLASSLEVEL"; if (Logging.isLoggable(Logging.DEBUG) && formula.startsWith(DEBUG_FORMULA_PREFIX)) { Logging.debugPrint(jepIndent + "getJepVariable: " + formula); } jepIndent += " "; PJEP parser = null; try { parser = PjepPool.getInstance().aquire(this, src); parser.parseExpression(formula); if (parser.hasError()) { if (Logging.isLoggable(Logging.DEBUG) && formula.startsWith(DEBUG_FORMULA_PREFIX)) { Logging.debugPrint(jepIndent + "not a JEP expression: " + formula); } return null; } for (Iterator<String> iter = parser.getSymbolTable().keySet().iterator(); iter.hasNext();) { final String element = iter.next(); if ( "e".equals(element) || "FALSE".equals(element) || "pi".equals(element) || "TRUE".equals(element)) { continue; } Float d = lookupVariable(element, src, spell); if (d != null) { parser.addVariable(element, d.doubleValue()); } else { // we could not get a value for all of the variables, so it must not have been a JEP function // after all... return null; } } final Object result = parser.getValueAsObject(); if (result != null) { if (Logging.isLoggable(Logging.DEBUG) && formula.startsWith(DEBUG_FORMULA_PREFIX)) { Logging.debugPrint(jepIndent + "Result '" + formula + "' = " + result); } try { return new CachableResult(new Float(result.toString()), parser.isResultCachable()); } catch (NumberFormatException nfe) { if (Logging.isLoggable(Logging.DEBUG) && formula.startsWith(DEBUG_FORMULA_PREFIX)) { Logging.debugPrint(jepIndent + "Result '" + formula + "' = " + result + " was not a number..."); } return null; } } if (parser.hasError()) { Logging.errorPrint("Failed to process formala " + formula + " due to error: " + parser.getErrorInfo()); } if (Logging.isLoggable(Logging.DEBUG) && formula.startsWith(DEBUG_FORMULA_PREFIX)) { Logging.debugPrint(jepIndent + "Result '" + formula + "' was null..."); } return null; } finally { if (jepIndent != null && jepIndent.length() >= 4) { jepIndent = jepIndent.substring(4); } PjepPool.getInstance().release(parser); } } abstract Float getInternalVariable( final CharacterSpell aSpell, String valString, final String src); /** * Get a value for the term as evaluated in the context of the PC that * owns this VariableEvaluator (getPc()) the term itself and the source * of the term e.g. RACE:Halfling. If the term is CASTERLEVEL the * Spell parameter is also used, if not it is ignored and may be null. * * @param term * The string to be evaluated * @param src * The source of the term * @param spell * A spell which is only used if the term is related to CASTERLEVEL * * @return a Float value for this term */ public Float lookupVariable(String term, String src, CharacterSpell spell) { Float retVal = null; if (pc.hasVariable(term)) { final Float value = pc.getVariable(term, true); if (Logging.isDebugMode()) { Logging.debugPrint(new StringBuilder().append(jepIndent) .append("variable for: '").append(term).append("' = ") .append(value).toString()); } retVal = new Float(value.doubleValue()); } if (retVal == null) { retVal = getInternalVariable(spell, term, src); } if (retVal == null) { final String evReturn = getExportVariable(term); if (evReturn != null) { retVal = convertToFloat(term, evReturn); } } return retVal; } /** * Attempt to retrieve a cached value of a variable. * * @param lookup The name of the variable to be checked. * @return The value of the variable */ public Float getCachedVariable(final String lookup) { if (isCachePaused()) { return null; } final CachedVariable<Float> cached = fVariableCache.get(lookup); if (cached != null) { if (cached.getSerial()>=getSerial()) { return cached.getValue(); } fVariableCache.remove(lookup); } return null; } /** * Add a new variable to the cache. * * @param lookup The name of the variable to be added. * @param value The value of the variable */ public void addCachedVariable(final String lookup, final Float value) { if (isCachePaused()) { return; } final CachedVariable<Float> cached = new CachedVariable<>(); cached.setSerial( getSerial() ); cached.setValue(value); // if (lookup.equals("floor(SCORE/2)-5#STAT:CHA")) // { // Logging.errorPrint("At " + cached.getSerial() + " caching " + lookup + " of " + value); // } fVariableCache.put(lookup, cached); } /** * Restart caching of variable values. Used after caching has * been paused by a call to pauseCache. */ public void restartCache() { serial = cachePaused; cachePaused = 0; } /** * Pause caching of variable values. Normally used when making temporary * changes to a character. */ public void pauseCache() { cachePaused = serial; } /** * Identify if the cache is current paused or not. * @return True if the cache is currently paused, false otherwise. */ public boolean isCachePaused() { return cachePaused>0; } /** * Retrieve the current cache serial. This value identifies the currency * of the cache and can be compared against the serial of entries in the * cache to detemrine if they have expired. * * @return The current cache serial. */ public int getSerial() { return serial; } /** * Set the current cache serial. This value identifies the currency * of the cache and is generally set to match the PC's serial value. * @param serial The new serial value to set. */ public void setSerial(int serial) { this.serial = serial; } /** * Retrieve a value from the cache. This method will not return * expired values, but instead removes them from the cache if * they are found. * * @param lookup The name of the variable (or the formula) to retrieve. * @return String The value of the variable, or null if a current value is not present in the cache. */ String getCachedString(final String lookup) { if (isCachePaused()) { return null; } final CachedVariable<String> cached = sVariableCache.get(lookup); if (cached != null) { if (cached.getSerial()>=getSerial()) { return cached.getValue(); } sVariableCache.remove(lookup); } return null; } /** * Add a value to the cache. If the cache is paused, the value will * not be added. * * @param lookup The name of the variable (or the formula) to cache. * @param value The value of the variable or formula. */ public void addCachedString(final String lookup, final String value) { if (isCachePaused()) { return; } final CachedVariable<String> cached = new CachedVariable<>(); cached.setSerial( getSerial() ); cached.setValue(value); sVariableCache.put(lookup, cached); } /** * Returns a float value representing a variable used by the * export process, for example, any token that is used in an outputsheet. * * @param valString The name of the token to process. i.e. "LOCK.CON" * @return The evaluated value of valString as a String. */ public String getExportVariable(String valString) { final StringWriter sWriter = new StringWriter(); final BufferedWriter aWriter = new BufferedWriter(sWriter); final ExportHandler aExport = new ExportHandler(new File("")); aExport.replaceTokenSkipMath(pc, valString, aWriter); sWriter.flush(); try { aWriter.flush(); } catch (IOException e) { Logging.errorPrint("Couldn't flush the StringWriter used in PlayerCharacter.getVariableValue.", e); } final String bString = sWriter.toString(); String result; try { // Float values result = String.valueOf(Float.parseFloat(bString)); } catch (NumberFormatException e) { // String values result = bString; } return result; } /** * Retrieve the PlayerCharacter object that this VariableProcessor * instance serves. * * @return The PlayerCharacter instance. */ public PlayerCharacter getPc() { return pc; } }