/*
* ExportHandler.java
* Copyright 2002 (C) Thomas Behr <ravenlock@gmx.de>
*
* 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 March 07, 2002, 8:30 PM
*
* Current Ver: $Revision$
*
*/
package pcgen.io;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.io.StringWriter;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.Version;
import pcgen.cdom.base.CDOMObject;
import pcgen.cdom.base.Constants;
import pcgen.cdom.enumeration.ListKey;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.PCStringKey;
import pcgen.core.AbilityCategory;
import pcgen.core.Equipment;
import pcgen.core.GameMode;
import pcgen.core.Globals;
import pcgen.core.PCClass;
import pcgen.core.PCTemplate;
import pcgen.core.PObject;
import pcgen.core.PlayerCharacter;
import pcgen.core.SettingsHandler;
import pcgen.core.Skill;
import pcgen.core.character.CharacterSpell;
import pcgen.core.character.Follower;
import pcgen.core.display.CharacterDisplay;
import pcgen.core.display.SkillDisplay;
import pcgen.core.utils.CoreUtility;
import pcgen.io.exporttoken.AbilityListToken;
import pcgen.io.exporttoken.AbilityToken;
import pcgen.io.exporttoken.BonusToken;
import pcgen.io.exporttoken.EqToken;
import pcgen.io.exporttoken.EqTypeToken;
import pcgen.io.exporttoken.GameModeToken;
import pcgen.io.exporttoken.MovementToken;
import pcgen.io.exporttoken.SkillToken;
import pcgen.io.exporttoken.SkillpointsToken;
import pcgen.io.exporttoken.StatToken;
import pcgen.io.exporttoken.Token;
import pcgen.io.exporttoken.TotalToken;
import pcgen.io.exporttoken.WeaponToken;
import pcgen.io.exporttoken.WeaponhToken;
import pcgen.io.freemarker.EquipSetLoopDirective;
import pcgen.io.freemarker.LoopDirective;
import pcgen.io.freemarker.PCBooleanFunction;
import pcgen.io.freemarker.PCHasVarFunction;
import pcgen.io.freemarker.PCStringDirective;
import pcgen.io.freemarker.PCVarFunction;
import pcgen.output.publish.OutputDB;
import pcgen.system.PluginLoader;
import pcgen.util.Delta;
import pcgen.util.Logging;
import pcgen.util.enumeration.View;
/**
* This class deals with exporting a PC to various types of output sheets
* including XML, HTML, PDF and Text.
*
* Very basically it takes a PC (or PCs) and replaces tokens in a character
* sheet template with the appropriate values from the PC (PCs). Much of the
* code in here deals with replacing tokens and dealing with the FOR and IIF
* constructs that can be found in the character sheet templates.
*
* @author Thomas Behr
*/
public final class ExportHandler
{
/** A constant stating that we are using JEP parsing */
private static final Float JEP_TRUE = new Float(1.0);
/** A map of output tokens to export */
private static Map<String, Token> tokenMap = new HashMap<>();
/**
* A variable to hold the state of whether or not the output token map to
* be exported is populated or not.
*/
private static boolean tokenMapPopulated;
/**
* ExportEngine describes a possible templating engine to be used to
* process a character and a template to produce the character output.
*/
private enum ExportEngine { PCGEN, FREEMARKER}
// Processing state variables
/** TODO What is this used for? */
private boolean existsOnly;
/** A state variable to indicate whether there are more items to process */
private boolean noMoreItems;
/** A state variable to indicate whether the OS author controls whitespace */
private boolean manualWhitespace;
/** The template file to use for exporting (effectively the sheet to use) */
private File templateFile;
/**
* These maps hold the loop variables and parameters of FOR constructs that
* will be replaced by their actual values when evaluated.
*/
private final Map<Object, Object> loopVariables = new HashMap<>();
private final Map<Object, Object> loopParameters = new HashMap<>();
/** The delimiter used by embedded DFOR/FOR loops */
private String csheetTag2 = "\\";
/** A state variable to indicate whether we skip processing the math */
private boolean skipMath;
/**
* A state variable to indicate whether we should write out what we are currently
* processing, would be set to false for example if we were filtering some output
*
* defaults to true.
*/
private boolean canWrite = true;
/** TODO What is this used for? */
private boolean checkBefore;
/** TODO What is this used for? */
private boolean inLabel;
/** The templating engine we will be using for this export. */
private ExportEngine exportEngine;
/**
* Constructor. Populates the token map (a list of possible output tokens) and
* sets the character sheet template we are using.
*
* @param templateFile the template to use while exporting.
*/
public ExportHandler(File templateFile)
{
populateTokenMap();
setTemplateFile(templateFile);
decideExportEngine();
}
/**
* Replace the token, but deliberately skip the math
*
* @param aPC The PC being exported
* @param aString the string which will have its tokens replaced
* @param output the object that represents the sheet we are exporting
*/
public void replaceTokenSkipMath(PlayerCharacter aPC, String aString,
BufferedWriter output)
{
final boolean oldSkipMath = skipMath;
skipMath = true;
replaceToken(aString, output, aPC);
skipMath = oldSkipMath;
}
/**
* Exports the contents of the given PlayerCharacter to a Writer
* according to the handler's template
*
* <br>author: Thomas Behr 12-04-02
*
* @param aPC the PlayerCharacter to write
* @param out the Writer to be written to
* @throws ExportException If the export fails.
*/
public void write(PlayerCharacter aPC, BufferedWriter out) throws ExportException
{
if (templateFile == null)
{
throw new IllegalStateException("Template file must not be null");
}
if (exportEngine == ExportEngine.FREEMARKER)
{
FileAccess.setCurrentOutputFilter(templateFile.getName().substring(
0, templateFile.getName().length() - 4));
exportCharacterUsingFreemarker(aPC, out);
return;
}
// Set an output filter based on the type of template in use.
FileAccess.setCurrentOutputFilter(templateFile.getName());
BufferedReader br = null;
FileInputStream fis = null;
InputStreamReader isr = null;
try
{
fis = new FileInputStream(templateFile);
isr = new InputStreamReader(fis, "UTF-8");
br = new BufferedReader(isr);
// A Buffer to hold the result of the preparation
StringBuilder template = prepareTemplate(br);
// Create a tokenizer based on EOL characters
// 03-Nov-2008 Karianna, changed to use line separator instead of /r/n
final StringTokenizer tokenizer =
new StringTokenizer(template.toString(),
Constants.LINE_SEPARATOR, false);
// Get FOR loops and IIF statements
final FORNode root = parseFORsAndIIFs(tokenizer);
// TODO Not sure what these lines are for
loopVariables.put(null, "0");
existsOnly = false;
// Ensure that there 'are more items to process'
noMoreItems = false;
// Now actually process the FOR loops in the template
// and then clear the loop variables
loopFOR(root, 0, 0, 1, out, aPC);
loopVariables.clear();
}
catch (IOException exc)
{
Logging.errorPrint("Error in ExportHandler::write", exc);
}
finally
{
// Close off the reader
if (br != null)
{
try
{
br.close();
}
catch (IOException e)
{
Logging
.errorPrint(
"Error closing off the character sheet template in ExportHandler::write",
e);
}
}
if (out != null)
{
try
{
out.flush();
}
catch (IOException e)
{
Logging.errorPrint(
"Error flushing the output in ExportHandler::write", e);
}
}
}
// TODO Not sure
csheetTag2 = "\\";
}
/**
* Produce an output file for a character using a FreeMarker template.
*
* @param aPC The character being output.
* @param outputWriter The destination for the output.
* @throws ExportException If the export fails.
*/
private void exportCharacterUsingFreemarker(PlayerCharacter aPC, BufferedWriter outputWriter) throws ExportException
{
Configuration cfg = new Configuration();
try
{
// Set Directory for templates
cfg.setDirectoryForTemplateLoading(templateFile.getParentFile());
cfg.setIncompatibleImprovements(new Version("2.3.20"));
// load template
Template template = cfg.getTemplate(templateFile.getName());
// Configure our custom directives and functions.
cfg.setSharedVariable("pcstring", new PCStringDirective(aPC, this));
cfg.setSharedVariable("pcvar", new PCVarFunction(aPC));
cfg.setSharedVariable("pcboolean", new PCBooleanFunction(aPC, this));
cfg.setSharedVariable("pchasvar", new PCHasVarFunction(aPC, this));
cfg.setSharedVariable("loop", new LoopDirective());
cfg.setSharedVariable("equipsetloop", new EquipSetLoopDirective(aPC));
GameMode gamemode = SettingsHandler.getGame();
// data-model
Map<String, Object> pc = OutputDB.buildDataModel(aPC.getCharID());
Map<String, Object> mode = OutputDB.buildModeDataModel(gamemode);
Map<String, Object> input = new HashMap<>();
input.put("pcgen", OutputDB.getGlobal());
input.put("pc", ObjectWrapper.DEFAULT_WRAPPER.wrap(pc));
input.put("gamemode", mode);
input.put("gamemodename", gamemode.getName());
// Process the template
template.process(input, outputWriter);
}
catch (IOException | TemplateException exc)
{
String message = "Error exporting character using template " + templateFile;
Logging.errorPrint(message, exc);
throw new ExportException(exc, message + " : " + exc.getLocalizedMessage());
}
finally
{
if (outputWriter != null)
{
try
{
outputWriter.flush();
}
catch (Exception e2)
{
}
}
}
}
/**
* A helper method to prepare the template for exporting
*
* Read lines from the character sheet template and store them in a buffer
* with empty lines replaced by a space character and || replaced by | |
*
* @param br The BufferedReader containing the template
* @throws IOException
*/
private static StringBuilder prepareTemplate(BufferedReader br) throws IOException
{
// A pattern to replace || with | | to stop StringTokenizer from merging them
Pattern pat = Pattern.compile(Pattern.quote("||"));
String rep = Matcher.quoteReplacement("| |");
// Hold the results of the preparation
StringBuilder inputLine = new StringBuilder();
String aString = br.readLine();
while (aString != null)
{
// Karianna 29/11/2008 - No Longer replace blank lines with spaces,
// doesn't seem to be needed
// If the line is blank then append a space character
//if (aString.length() == 0)
//{
//inputLine.append(' ');
//}
//else
//{
// Adjacent separators get merged by StringTokenizer,
// so we break them up here, e.g. Change || to | |
Matcher mat = pat.matcher(aString);
inputLine.append(mat.replaceAll(rep));
//}
inputLine.append(Constants.LINE_SEPARATOR);
aString = br.readLine();
}
return inputLine;
}
/**
* Exports a PlayerCharacter-Party to a Writer
* according to the handler's template
*
* <br>author: Thomas Behr 13-11-02
*
* @param PCs the Collection of PlayerCharacter instances which compromises the Party to write
* @param out the Writer to be written to
*/
public void write(Collection<PlayerCharacter> PCs, BufferedWriter out)
{
write(PCs.toArray(new PlayerCharacter[PCs.size()]), out);
}
/**
* Sets the template to use for export<br>
* Use this method to reset this handler, if it should be used
* to export to different/multiple templates
*
* @param templateFile the template to use while exporting.
*/
private void setTemplateFile(File templateFile)
{
this.templateFile = templateFile;
}
/**
* Returns the current templateFile being used
* @return templateFile
*/
public File getTemplateFile()
{
return templateFile;
}
/**
* Determine which templating engine should be used for the template file.
*/
private void decideExportEngine()
{
exportEngine = ExportEngine.PCGEN;
if (templateFile != null && templateFile.getName().toLowerCase().endsWith(".ftl"))
{
exportEngine = ExportEngine.FREEMARKER;
}
}
/**
* Get variable value from the variable string passed in, this might be
* an old style variable string (COUNT[EQ and STRLEN) or a new style
* (JEP formula)
*
* @param varString Variable string that we want to calculate value from
* @param aPC The PC that holds the data that we need to get the info from
* @return The result
*/
private int getVarValue(String varString, PlayerCharacter aPC)
{
String vString = varString;
// While COUNT[EQ tokens exist, build up a string
vString = processCountEquipmentTokens(vString, aPC);
// While STRLEN[ tokens exist, build up a string
vString = processStringLengthTokens(vString, aPC);
// If it is the new JEP style variable then deal with that
String valueString;
if (varString.startsWith("${") && varString.endsWith("}"))
{
String jepString = varString.substring(2, varString.length() - 1);
valueString = jepString.replace(';', ',');
}
else
{
valueString = vString;
}
Float floatValue = aPC.getVariableValue(valueString, "");
return floatValue.intValue();
}
/**
* Helper method for getting the variable value out of a variable string
*
* @param vString The variable String
* @param aPC The PC to get the token from
* @return the altered variable string
*/
private String processCountEquipmentTokens(String vString,
PlayerCharacter aPC)
{
int countIndex = vString.indexOf("COUNT[EQ");
while (countIndex >= 0)
{
char chC = vString.charAt(countIndex + 8);
// If the character after COUNT[EQ is . or [1-9]
if ((chC == '.') || ((chC >= '0') && (chC <= '9')))
{
final int i = vString.indexOf(']', countIndex + 8);
if (i >= 0)
{
String aString = vString.substring(countIndex + 6, i);
// Either deal with an EQTYPE or a straight EQ token
EqToken token = null;
if (aString.indexOf("EQTYPE") > -1)
{
token = new EqTypeToken();
}
else
{
token = new EqToken();
}
String baString = token.getToken(aString, aPC, this);
vString =
vString.substring(0, countIndex) + baString
+ vString.substring(i + 1);
}
}
countIndex = vString.indexOf("COUNT[EQ", countIndex + 1);
}
return vString;
}
/**
* Helper method for getting the variable value out of a variable string,
* deals with STRLEN tokens
*
* @param vString The variable string to get the values out of
* @param aPC The PC to get the token value out of
* @return The altered variable string
*/
private String processStringLengthTokens(String vString, PlayerCharacter aPC)
{
int strlenIndex = vString.indexOf("STRLEN[", 0);
while (strlenIndex >= 0)
{
final int i = vString.indexOf(']', strlenIndex + 7);
if (i >= 0)
{
String aString = vString.substring(strlenIndex + 7, i);
StringWriter sWriter = new StringWriter();
BufferedWriter aWriter = new BufferedWriter(sWriter);
replaceToken(aString, aWriter, aPC);
sWriter.flush();
try
{
aWriter.flush();
}
catch (IOException e)
{
Logging
.errorPrint(
"Error flushing outputstream in ExportHandler::getVarValue",
e);
}
String result = sWriter.toString();
vString =
vString.substring(0, strlenIndex) + result.length()
+ vString.substring(i + 1);
}
strlenIndex = vString.indexOf("STRLEN[", strlenIndex + 1);
}
return vString;
}
/**
* Add to the token map, called mainly by the plugin loader
*
* @param newToken the token to add
*/
public static void addToTokenMap(Token newToken)
{
Token test = tokenMap.put(newToken.getTokenName(), newToken);
if (test != null)
{
Logging
.errorPrint("More than one Output Token has the same Token Name: '"
+ newToken.getTokenName() + "'");
}
}
public static PluginLoader getPluginLoader()
{
return new PluginLoader(){
@Override
public void loadPlugin(Class<?> clazz) throws Exception
{
Token pl = (Token) clazz.newInstance();
addToTokenMap(pl);
}
@Override
public Class[] getPluginClasses()
{
return new Class[]{Token.class};
}
};
}
private static class VariableComparator implements Comparator<Object>, Serializable
{
@Override
public int compare(Object o1, Object o2)
{
final String s1 = (o1 == null ? "" : o1.toString());
final String s2 = (o2 == null ? "" : o2.toString());
if (s1.length() > s2.length())
{
return -1;
}
else if (s1.length() < s2.length())
{
return 1;
}
else
{
return s1.compareTo(s2);
}
}
}
private String replaceVariables(String expr, Map<Object,Object> variables)
{
List<Object> keys = new ArrayList<>(variables.keySet());
keys.sort(new VariableComparator());
for (final Object anObject : variables.keySet())
{
if (anObject != null)
{
final String fString = anObject.toString();
final String rString = variables.get(fString).toString();
expr = expr.replaceAll(Pattern.quote(fString), rString);
}
}
return expr;
}
/**
* Helper method to evaluate an expression, used by OIF and IIF tokens
*
* @param expr Expression to evaluate
* @param aPC PC containing values to help evaluate the expression
* @return true if the expression was evaluated successfully, else false
*/
private boolean evaluateExpression(final String expr,
final PlayerCharacter aPC)
{
// Deal with the AND case
if (expr.indexOf(".AND.") > 0)
{
final String part1 = expr.substring(0, expr.indexOf(".AND."));
final String part2 = expr.substring(expr.indexOf(".AND.") + 5);
return (evaluateExpression(part1, aPC) && evaluateExpression(part2,
aPC));
}
// Deal with the OR case
if (expr.indexOf(".OR.") > 0)
{
final String part1 = expr.substring(0, expr.indexOf(".OR."));
final String part2 = expr.substring(expr.indexOf(".OR.") + 4);
return (evaluateExpression(part1, aPC) || evaluateExpression(part2,
aPC));
}
/*
* Deal with objects held in the loopVariables and loopParameters
* sets, e.g. replace the key place holder with the actual value
*/
String expr1 = expr;
expr1 = replaceVariables(expr1, loopParameters);
expr1 = replaceVariables(expr1, loopVariables);
// Deal with HASVAR:
if (expr1.startsWith("HASVAR:"))
{
expr1 = expr1.substring(7).trim();
return (aPC.getVariableValue(expr1, "").intValue() > 0);
}
// Deal with HASFEAT:
if (expr1.startsWith("HASFEAT:"))
{
expr1 = expr1.substring(8).trim();
return (aPC.hasAbilityKeyed(AbilityCategory.FEAT, expr1));
}
// Deal with HASSA:
if (expr1.startsWith("HASSA:"))
{
expr1 = expr1.substring(6).trim();
return (aPC.hasSpecialAbility(expr1));
}
// Deal with HASEQUIP:
if (expr1.startsWith("HASEQUIP:"))
{
expr1 = expr1.substring(9).trim();
return (aPC.getEquipmentNamed(expr1) != null);
}
if (expr1.startsWith("SPELLCASTER:"))
{
return processSpellcasterExpression(expr1, aPC);
}
// Deal with EVEN:
if (expr1.startsWith("EVEN:"))
{
int i = 0;
try
{
i = Integer.parseInt(expr1.substring(5).trim());
}
catch (NumberFormatException exc)
{
Logging.errorPrint("EVEN:" + i);
return true;
}
return ((i % 2) == 0);
}
// Deal with UNTRAINED (skills)
if (expr1.endsWith("UNTRAINED") && !expr1.startsWith("SKILLSIT."))
{
final StringTokenizer aTok = new StringTokenizer(expr1, ".");
final String fString = aTok.nextToken();
Skill aSkill = null;
if (fString.length() > 5)
{
final int i = Integer.parseInt(fString.substring(5));
final List<Skill> pcSkills = SkillDisplay.getSkillListInOutputOrder(aPC);
if (i <= (pcSkills.size() - 1))
{
aSkill = pcSkills.get(i);
}
}
if (aSkill == null)
{
return false;
}
else if (aSkill.getSafe(ObjectKey.USE_UNTRAINED))
{
return true;
}
return false;
}
// Deal with JEP formula
final Float res =
aPC.getVariableProcessor().getJepOnlyVariableValue(null, expr1,
"", 0);
if (res != null)
{
return res.equals(JEP_TRUE);
}
/*
* Deal with anything else
*
* Before returning a default false, let's see if this is a valid token, like this:
*
* |IIF(WEAPON%weap.CATEGORY:Ranged)|
* something 1
* |ELSE|
* something 2
* |ENDIF|
*
* It can theoretically be used with any valid token, doing an equal compare
* (integer or string equalities are valid)
*
* Can now contain a token on the right side as well, so two tokens can be
* compared to each other. Comparison is case-insensitive.
*/
final StringTokenizer aTok = new StringTokenizer(expr1, ":");
final String leftToken;
final String rightToken;
final int tokenCount = aTok.countTokens();
if (tokenCount == 1)
{
leftToken = expr1;
rightToken = "TRUE";
}
else if (tokenCount != 2)
{
Logging
.errorPrint("evaluateExpression: Incorrect syntax (missing parameter)");
return false;
}
else
{
leftToken = aTok.nextToken();
rightToken = aTok.nextToken();
}
final StringWriter sLeftWriter = new StringWriter();
final BufferedWriter leftWriter = new BufferedWriter(sLeftWriter);
replaceToken(leftToken, leftWriter, aPC);
sLeftWriter.flush();
final StringWriter sRightWriter = new StringWriter();
final BufferedWriter rightWriter = new BufferedWriter(sRightWriter);
replaceToken(rightToken, rightWriter, aPC);
sRightWriter.flush();
// Try to flush the output writer
try
{
leftWriter.flush();
rightWriter.flush();
}
catch (IOException ignore)
{
if (Logging.isDebugMode())
{
Logging.debugPrint(
"Could not flush output buffer in evaluateExpression",
ignore);
}
}
String leftString = sLeftWriter.toString();
if (leftToken.startsWith("VAR."))
{
leftString = aPC.getVariableValue(leftToken.substring(4), "").toString();
}
String rightString = sRightWriter.toString();
if (rightToken.startsWith("VAR."))
{
rightString = aPC.getVariableValue(rightToken.substring(4), "").toString();
}
try
{
// integer values
final int left = Integer.parseInt(leftString);
final int right= Integer.parseInt(rightString);
if (left == right)
{
return true;
}
return false;
}
catch (NumberFormatException e)
{
// String values
// if right string starts with =, test exact match, otherwise test substring match
if (rightString.startsWith("="))
{
return leftString.equals(rightString.substring(1));
}
return 0 <= leftString.toUpperCase().indexOf(rightString.toUpperCase());
}
}
/**
* Deal with SPELLCASTER.
*
* Could look like one of the following:
*
* Arcane
* Chaos
* Divine
* EleMage
* Psionic
* Wizard
* Prepare
* !Prepare
* 0=Wizard (%classNum=className)
* 0=Divine (%classNum=spell_type)
* 0=Prepare (%classNum=preparation_type)
*
* @param expr1 Expression to evaluate
* @param aPC PC containing values to help evaluate the expression
* @return true if the expression was evaluated successfully, else false
*/
private static boolean processSpellcasterExpression(String expr1,
PlayerCharacter aPC)
{
final String fString = expr1.substring(12).trim();
// If the SPELLCASTER expression has an '=' sign
if (fString.indexOf('=') != -1)
{
final StringTokenizer aTok =
new StringTokenizer(fString, "=", false);
final int i = Integer.parseInt(aTok.nextToken());
final String cs = aTok.nextToken();
final List<PCClass> cList = aPC.getClassList();
if (i >= cList.size())
{
return false;
}
final PCClass aClass = cList.get(i);
if (cs.equalsIgnoreCase(aClass.getSpellType()))
{
return true;
}
if (cs.equalsIgnoreCase(aClass.getKeyName()))
{
return true;
}
if ("!Prepare".equalsIgnoreCase(cs)
&& aClass.getSafe(ObjectKey.MEMORIZE_SPELLS))
{
return true;
}
if ("Prepare".equalsIgnoreCase(cs)
&& (!aClass.getSafe(ObjectKey.MEMORIZE_SPELLS)))
{
return true;
}
}
else
{
for (final PCClass pcClass : aPC.getClassSet())
{
if (fString.equalsIgnoreCase(pcClass.getSpellType()))
{
return true;
}
if (fString.equalsIgnoreCase(pcClass.getKeyName()))
{
return true;
}
if ("!Prepare".equalsIgnoreCase(fString)
&& pcClass.getSafe(ObjectKey.MEMORIZE_SPELLS))
{
return true;
}
if ("Prepare".equalsIgnoreCase(fString)
&& (!pcClass.getSafe(ObjectKey.MEMORIZE_SPELLS)))
{
return true;
}
}
}
Logging
.errorPrint("Should have exited before this in ExportHandler::processSpellcasterExpression");
return false;
}
/**
* Helper method to evaluate a IIF token
*
* @param node The IIFNode to evaluate
* @param output The output to write to (character sheet template)
* @param aPC The PC we are outputting
*/
private void evaluateIIF(final IIFNode node, final BufferedWriter output,
final PlayerCharacter aPC)
{
// Comma is a delimiter for a higher-level parser, so
// we'll use a semicolon and replace it with a comma for
// expressions like:
// |IIF(VAR.IF(var("COUNT[SKILLTYPE=Strength]")>0;1;0):1)|
final String aString = node.expr().replaceAll(Pattern.quote(";"), ",");
// If we can evaluate the expression then evaluate its children
if (evaluateExpression(aString, aPC))
{
evaluateIIFChildren(node.trueChildren(), output, aPC);
}
else
{
evaluateIIFChildren(node.falseChildren(), output, aPC);
}
}
/**
* Helper method to evaluate the results of a IIF child node
*
* @param children The list of children for the IIF node
* @param output The output to write to (filling in the character sheet template)
* @param aPC THe PC to output
*/
private void evaluateIIFChildren(final List<?> children,
final BufferedWriter output,
final PlayerCharacter aPC)
{
for (Object aChild : children)
{
if (aChild instanceof FORNode)
{
// If the child is a FORNode then put it in the loopVariables map as
// a key with a corresponding value of 0
final FORNode nextFor = (FORNode) aChild;
loopVariables.put(nextFor.var(), 0);
existsOnly = nextFor.exists();
String minString = nextFor.min();
String maxString = nextFor.max();
String stepString = nextFor.step();
// Go through the list of objects in the loopVariables and loopParameters
// sets and set the values in place of keys for min, max and step
minString = replaceVariables(minString, loopParameters);
minString = replaceVariables(minString, loopVariables);
maxString = replaceVariables(maxString, loopParameters);
maxString = replaceVariables(maxString, loopVariables);
stepString = replaceVariables(stepString, loopParameters);
stepString = replaceVariables(stepString, loopVariables);
int minValue = getVarValue(minString, aPC);
int maxValue = getVarValue(maxString, aPC);
int stepValue = getVarValue(stepString, aPC);
String var = nextFor.var();
loopParameters.put(var + "!MIN", minValue);
loopParameters.put(var + "!MAX", maxValue);
loopParameters.put(var + "!STEP", stepValue);
loopFOR(nextFor, minValue, maxValue, stepValue, output, aPC);
loopParameters.remove(var + "!MIN");
loopParameters.remove(var + "!MAX");
loopParameters.remove(var + "!STEP");
existsOnly = nextFor.exists();
loopVariables.remove(nextFor.var());
}
// If child is an IIFNode, then evaluate that
else if (aChild instanceof IIFNode)
{
evaluateIIF((IIFNode) aChild, output, aPC);
}
// Else it's something to be processed
else
{
String lineString = (String) aChild;
lineString = replaceVariables(lineString, loopParameters);
lineString = replaceVariables(lineString, loopVariables);
replaceLine(lineString, output, aPC);
// Each time we replace a line that is part of an IIF statement
// we output a newline if we are allowed to write and the
// whitespace is not controlled by the OS author
if (canWrite && !manualWhitespace)
{
FileAccess.newLine(output);
}
}
}
}
/**
* Loop through a set of output as required by a FOR loop.
*
* @param node The node being processed
* @param start The starting value of the loop
* @param end The ending value of the loop
* @param step The amount by which the counter should be changed each iteration.
* @param output The writer output is to be sent to.
* @param aPC The character being processed.
*/
private void loopFOR(final FORNode node, final int start, final int end,
final int step, final BufferedWriter output,
final PlayerCharacter aPC)
{
for (int x = start; ((step < 0) ? x >= end : x <= end); x += step)
{
if (processLoop(node, output, aPC, x))
{
break;
}
}
}
/**
* Process an iteration of a FOR loop.
*
* @param node The node being processed
* @param output The writer output is to be sent to.
* @param aPC The character being processed.
* @param index The current value of the loop index
* @return true if the loop should be stopped.
*/
private boolean processLoop(FORNode node, BufferedWriter output,
PlayerCharacter aPC, int index)
{
loopVariables.put(node.var(), index);
int numberOfChildrenNodes = node.children().size();
for (int y = 0; y < numberOfChildrenNodes; ++y)
{
if (node.children().get(y) instanceof FORNode)
{
FORNode nextFor = (FORNode) node.children().get(y);
loopVariables.put(nextFor.var(), 0);
existsOnly = nextFor.exists();
String minString = nextFor.min();
String maxString = nextFor.max();
String stepString = nextFor.step();
minString = replaceVariables(minString, loopParameters);
minString = replaceVariables(minString, loopVariables);
maxString = replaceVariables(maxString, loopParameters);
maxString = replaceVariables(maxString, loopVariables);
stepString = replaceVariables(stepString, loopParameters);
stepString = replaceVariables(stepString, loopVariables);
final int varMin = getVarValue(minString, aPC);
final int varMax = getVarValue(maxString, aPC);
final int varStep = getVarValue(stepString, aPC);
String var = nextFor.var();
loopParameters.put(var + "!MIN", varMin);
loopParameters.put(var + "!MAX", varMax);
loopParameters.put(var + "!STEP", varMax);
loopFOR(nextFor, varMin, varMax, varStep, output, aPC);
loopParameters.remove(var + "!MIN");
loopParameters.remove(var + "!MAX");
loopParameters.remove(var + "!STEP");
existsOnly = node.exists();
loopVariables.remove(nextFor.var());
}
else if (node.children().get(y) instanceof IIFNode)
{
evaluateIIF((IIFNode) node.children().get(y), output, aPC);
}
else
{
String lineString = (String) node.children().get(y);
lineString = replaceVariables(lineString, loopParameters);
lineString = replaceVariables(lineString, loopVariables);
noMoreItems = false;
replaceLine(lineString, output, aPC);
// If the output sheet author has no control
// over the whitespace then print a newline.
if (canWrite && !manualWhitespace)
{
FileAccess.newLine(output);
}
// break out of loop if no more items
if (existsOnly && noMoreItems)
{
return true;
}
}
}
return false;
}
/**
* Math Mode - Most of the code logic was copied from PlayerCharacter.getVariableValue
* included a treatment for math with attack routines (for example +6/+1 - 2 = +4/-1)
*
* @param aString The string to be converted
* @param aPC the PC being exported
* @return String
*/
private String mathMode(String aString, PlayerCharacter aPC)
{
String str = aString;
// Deal with Knowledge () type tokens
str = processBracketedTokens(str, aPC);
// Replace all square brackets with curved ones
str = str.replaceAll(Pattern.quote("["), "(");
str = str.replaceAll(Pattern.quote("]"), ")");
// A list of mathematical delimiters
final String delimiter = "+-/*";
String valString = "";
final int ADDITION_MODE = 0;
final int SUBTRACTION_MODE = 1;
final int MULTIPLICATION_MODE = 2;
final int DIVISION_MODE = 3;
// Mode is addition mode by default
int mode = ADDITION_MODE;
int nextMode = 0;
final int REGULAR_MODE = 0;
final int INTVAL_MODE = 1;
final int SIGN_MODE = 2;
final int NO_ZERO_MODE = 3;
int endMode = REGULAR_MODE;
boolean attackRoutine = false;
String attackData = "";
Float total = new Float(0.0);
for (int i = 0; i < str.length(); ++i)
{
valString += str.substring(i, i + 1);
if ((i == (str.length() - 1))
|| ((delimiter.lastIndexOf(str.charAt(i)) > -1) && (i > 0) && (str
.charAt(i - 1) != '.')))
{
if (delimiter.lastIndexOf(str.charAt(i)) > -1)
{
valString = valString.substring(0, valString.length() - 1);
}
if (i < str.length())
{
// Deal with .TRUNC
if (valString.endsWith(".TRUNC"))
{
if (attackRoutine)
{
Logging
.errorPrint("Math Mode Error: Not allowed to use .TRUNC in Attack Mode.");
}
else
{
valString =
String.valueOf(Float.valueOf(
mathMode(valString.substring(0,
valString.length() - 6), aPC))
.intValue());
}
}
// Deal with .INTVAL
if (valString.endsWith(".INTVAL"))
{
if (attackRoutine)
{
Logging
.errorPrint("Math Mode Error: Using .INTVAL in Attack Mode.");
}
else
{
valString =
mathMode(valString.substring(0, valString
.length() - 7), aPC);
}
endMode = INTVAL_MODE;
}
// Deal with .SIGN
if (valString.endsWith(".SIGN"))
{
valString =
mathMode(valString.substring(0, valString
.length() - 5), aPC);
endMode = SIGN_MODE;
}
// Deal with .NOZERO
if (valString.endsWith(".NOZERO"))
{
valString =
mathMode(valString.substring(0, valString
.length() - 7), aPC);
endMode = NO_ZERO_MODE;
}
// Set the next mode based on the mathematical sign
if ((!str.isEmpty()) && (str.charAt(i) == '+'))
{
nextMode = ADDITION_MODE;
}
else if ((!str.isEmpty()) && (str.charAt(i) == '-'))
{
nextMode = SUBTRACTION_MODE;
}
else if ((!str.isEmpty()) && (str.charAt(i) == '*'))
{
nextMode = MULTIPLICATION_MODE;
}
else if ((!str.isEmpty()) && (str.charAt(i) == '/'))
{
nextMode = DIVISION_MODE;
}
StringWriter sWriter = new StringWriter();
BufferedWriter aWriter = new BufferedWriter(sWriter);
replaceTokenSkipMath(aPC, valString, aWriter);
sWriter.flush();
try
{
aWriter.flush();
}
catch (IOException e)
{
Logging.errorPrint(
"Failed to flush oputput in MathMode.", e);
}
final String bString = sWriter.toString();
try
{
// Float values
DecimalFormatSymbols decimalFormatSymbols = new DecimalFormatSymbols(Locale.US);
DecimalFormat decimalFormat = new DecimalFormat("#,##0.##", decimalFormatSymbols);
valString = String.valueOf(decimalFormat.parse(bString));
}
catch (ParseException e)
{
// String values
valString = bString;
}
if ((!attackRoutine)
&& Pattern.matches("^([-+]\\d+/)*[-+]\\d+$", valString))
{
attackRoutine = true;
attackData = valString;
valString = "";
}
}
try
{
if (!valString.isEmpty())
{
if (attackRoutine)
{
StringTokenizer bTok =
new StringTokenizer(attackData, "/");
if (bTok.countTokens() > 0)
{
String newAttackData = "";
while (bTok.hasMoreTokens())
{
final String bString = bTok.nextToken();
float bf = Float.parseFloat(bString);
float vf = Float.parseFloat(valString);
switch (mode)
{
case ADDITION_MODE:
float addf = bf + vf;
newAttackData +=
("/+" + Integer
.toString((int) addf));
break;
case SUBTRACTION_MODE:
float subf = bf - vf;
newAttackData +=
("/+" + Integer
.toString((int) subf));
break;
case MULTIPLICATION_MODE:
float multf = bf * vf;
newAttackData +=
("/+" + Integer
.toString((int) multf));
break;
case DIVISION_MODE:
float divf = bf / vf;
newAttackData +=
("/+" + Integer
.toString((int) divf));
break;
default:
Logging
.errorPrint("In mathMode the mode "
+ mode + " is unsupported.");
break;
}
}
attackData =
newAttackData.substring(1).replaceAll(
Pattern.quote("+-"), "-");
}
}
else
{
switch (mode)
{
case ADDITION_MODE:
total =
new Float(total.doubleValue()
+ Double.parseDouble(valString));
break;
case SUBTRACTION_MODE:
total =
(float) (total.doubleValue()
- Double.parseDouble(valString));
break;
case MULTIPLICATION_MODE:
total =
(float) (total.doubleValue()
* Double.parseDouble(valString));
break;
case DIVISION_MODE:
total =
(float) (total.doubleValue()
/ Double.parseDouble(valString));
break;
default:
Logging.errorPrint("In mathMode the mode "
+ mode + " is unsupported.");
break;
}
}
}
}
catch (NumberFormatException exc)
{
StringWriter sWriter = new StringWriter();
BufferedWriter aWriter = new BufferedWriter(sWriter);
replaceTokenSkipMath(aPC, str, aWriter);
sWriter.flush();
try
{
aWriter.flush();
}
catch (IOException e)
{
Logging
.errorPrint("Math Mode Error: Could not flush output.");
}
return sWriter.toString();
}
mode = nextMode;
// Set the nextMode back to the default
nextMode = ADDITION_MODE;
valString = "";
}
}
if (attackRoutine)
{
return attackData;
}
if (endMode == INTVAL_MODE)
{
return Integer.toString(total.intValue());
}
if (endMode == SIGN_MODE)
{
return Delta.toString(total.intValue());
}
if (endMode == NO_ZERO_MODE)
{
final int totalIntValue = total.intValue();
if (totalIntValue == 0)
{
return "";
}
return Delta.toString(totalIntValue);
}
return total.toString();
}
/**
* Helper method to process the math for Knowledge (xx) types of tokens
*
* @param str String to process
* @param aPC PC we are exporting
* @return Processed string
*/
private String processBracketedTokens(String str, PlayerCharacter aPC)
{
while (str.lastIndexOf('(') != -1)
{
int x = CoreUtility.innerMostStringStart(str);
int y = CoreUtility.innerMostStringEnd(str);
// If the end is before the start we have a problem
if (y < x)
{
// This was breaking some homebrew sheets. [Felipe - 13-may-03]
Logging
.debugPrint("End is before start for string processing. We are skipping the processing of this item.");
break;
}
String bString = str.substring(x + 1, y);
// This will treat Knowledge (xx) kind of token
if ((x > 0) && (str.charAt(x - 1) == ' ')
&& ((str.charAt(y + 1) == '.') || (y == (str.length() - 1))))
{
str =
str.substring(0, x) + "[" + bString + "]"
+ str.substring(y + 1);
}
else
{
str =
str.substring(0, x) + mathMode(bString, aPC)
+ str.substring(y + 1);
}
}
return str;
}
/**
* Helper class to output normal text
*
* @param nonToken
* @param output
*/
private void outputNonToken(String nonToken, java.io.Writer output)
{
// Do nothing if something shouldn't be output.
if (canWrite && !nonToken.isEmpty())
{
String finalToken = null;
// If we have manual white space then remove an tab characters
if (manualWhitespace)
{
finalToken = nonToken.replaceAll("[ \\t]", "");
}
else
{
finalToken = nonToken;
}
FileAccess.write(output, finalToken);
}
}
/**
* Parse the tokens for |FOR and |IIF sections and plain text sections
*
* @param tokens
* @return a FORNode object
*/
private FORNode parseFORsAndIIFs(StringTokenizer tokens)
{
// A FORNode that will hold a 'tree' of all of the FOR and IIF sections found
final FORNode root = new FORNode(null, "0", "0", "1", false);
while (tokens.hasMoreTokens())
{
final String line = tokens.nextToken();
// If we detect a |FOR then add it as a child, if it has its own children
// then add those as well
if (line.startsWith("|FOR"))
{
StringTokenizer newFor = new StringTokenizer(line, ",");
if (newFor.countTokens() > 1)
{
newFor.nextToken();
if (newFor.nextToken().startsWith("%"))
{
root.addChild(parseFORs(line, tokens));
}
else
{
root.addChild(line);
}
}
else
{
root.addChild(line);
}
}
// If |IIF( is found and there is no ',' character on that line
// then add it as a child
else if (line.startsWith("|IIF(") && (line.lastIndexOf(',') == -1))
{
String expr = line.substring(5, line.lastIndexOf(')'));
root.addChild(parseIIFs(expr, tokens));
}
// Else it's plain text so then just add it
else
{
root.addChild(line);
}
}
return root;
}
/**
* Helper method to parse |FOR tokens (pre-processing for a template)
*
* @param forLine
* @param tokens
* @return A FORNode of the parsed tokens
*/
private FORNode parseFORs(String forLine, StringTokenizer tokens)
{
final List<String> forVars = getParameters(forLine);
final String var = forVars.get(1);
final String min = forVars.get(2);
final String max = forVars.get(3);
final String step = forVars.get(4);
final String eTest = forVars.get(5);
boolean exists = false;
if (((!eTest.isEmpty()) && (eTest.charAt(0) == '1'))
|| ((!eTest.isEmpty()) && (eTest.charAt(0) == '2')))
{
exists = true;
}
final FORNode node = new FORNode(var, min, max, step, exists);
while (tokens.hasMoreTokens())
{
final String line = tokens.nextToken();
if (line.startsWith("|FOR"))
{
StringTokenizer newFor = new StringTokenizer(line, ",");
newFor.nextToken();
if (newFor.nextToken().startsWith("%"))
{
node.addChild(parseFORs(line, tokens));
}
else
{
node.addChild(line);
}
}
else if (line.startsWith("|IIF(") && (line.lastIndexOf(',') == -1))
{
String expr = line.substring(5, line.lastIndexOf(')'));
node.addChild(parseIIFs(expr, tokens));
}
else if (line.startsWith("|ENDFOR|"))
{
return node;
}
else
{
node.addChild(line);
}
}
return node;
}
/**
* Retrieve the parameters of a comma separated command such as a
* FOR token. Commas inside brackets are ignored, thus allowing JEP
* functions with multiple parameters to be included in FOR loops.
*
* @param forToken The token to be broken up.
* @return The token parameters.
*/
public static List<String> getParameters(String forToken)
{
String splitStr[] = forToken.split(",");
List<String> result = new ArrayList<>();
StringBuilder buf = new StringBuilder();
boolean inFormula = false;
for (String string : splitStr)
{
if (string.indexOf("(") >= 0
&& (string.indexOf(")") < string.indexOf("(")))
{
inFormula = true;
buf.append(string);
}
else if (inFormula && string.indexOf(")") >= 0)
{
inFormula = false;
buf.append(",");
buf.append(string);
result.add(buf.toString());
buf = new StringBuilder();
}
else if (inFormula)
{
buf.append(",");
buf.append(string);
}
else
{
result.add(string);
}
}
return result;
}
/**
* Helper method to parse the IIF tokens, includes dealing with a
* |FOR child, |IIF child, ELSE, END IF and plain text
*
* @param expr
* @param tokens
* @return IIFNode representing the parsed tokens
*/
private IIFNode parseIIFs(String expr, StringTokenizer tokens)
{
final IIFNode node = new IIFNode(expr);
// Flag to indicate whether we are adding the
// true case (e.g. The IF) or the false case
// (e.g. The ELSE)
boolean trueCase = true;
while (tokens.hasMoreTokens())
{
final String line = tokens.nextToken();
// It's a |FOR child
if (line.startsWith("|FOR"))
{
StringTokenizer newFor = new StringTokenizer(line, ",");
newFor.nextToken();
// It's the first type of |FOR, e.g. With a variable name,
// see PCGen docs for |FOR token
if (newFor.nextToken().startsWith("%"))
{
if (trueCase)
{
node.addTrueChild(parseFORs(line, tokens));
}
else
{
node.addFalseChild(parseFORs(line, tokens));
}
}
else
{
if (trueCase)
{
node.addTrueChild(line);
}
else
{
node.addFalseChild(line);
}
}
}
// It's a child IIF, make a recursive call
else if (line.startsWith("|IIF(") && (line.lastIndexOf(',') == -1))
{
String newExpr = line.substring(5, line.lastIndexOf(')'));
if (trueCase)
{
node.addTrueChild(parseIIFs(newExpr, tokens));
}
else
{
node.addFalseChild(parseIIFs(newExpr, tokens));
}
}
// Set the flag so that the false case is added next
else if (line.startsWith("|ELSE|"))
{
trueCase = false;
}
// We're done, so exit
else if (line.startsWith("|ENDIF|"))
{
return node;
}
else
{
if (trueCase)
{
node.addTrueChild(line);
}
else
{
node.addFalseChild(line);
}
}
}
return node;
}
/**
* Populate the token map (if not already done so), e.g. Add all
* of the types of Output Tokens to the map
*/
private static void populateTokenMap()
{
if (!tokenMapPopulated)
{
addToTokenMap(new AbilityToken());
addToTokenMap(new AbilityListToken());
addToTokenMap(new BonusToken());
addToTokenMap(new EqToken());
addToTokenMap(new EqTypeToken());
addToTokenMap(new GameModeToken());
addToTokenMap(new MovementToken());
addToTokenMap(new SkillToken());
addToTokenMap(new SkillpointsToken());
addToTokenMap(new StatToken());
addToTokenMap(new TotalToken());
addToTokenMap(new WeaponToken());
addToTokenMap(new WeaponhToken());
tokenMapPopulated = true;
}
}
/**
* This method performs some work on a given character sheet template line,
* namely replacing tokens, dealing with Malformed lines and simply outputting
* plain text.
*
* @param aLine The line to do the work on
* @param output The output buffer that is effectively the character sheet template
* @param aPC The PC that we are outputting
*/
private void replaceLine(String aLine, BufferedWriter output,
PlayerCharacter aPC)
{
// Find the last index of the | character
int lastIndex = aLine.lastIndexOf('|');
// If there are no pipes and it's a non empty string, just output the fixed text
if (lastIndex < 0 && !aLine.isEmpty())
{
outputNonToken(aLine, output);
}
/*
* When the line starts with a pipe and that pipe is the only
* one on the line, this operation ignores the line. This is
* because the token is malformed. Malformed because it should be
* between pipes.
*/
if (lastIndex >= 1)
{
final StringTokenizer aTok = new StringTokenizer(aLine, "|", false);
boolean inPipe = false;
if (aLine.charAt(0) == '|')
{
inPipe = true;
}
boolean lastIsPipe = false;
if (aLine.charAt(aLine.length() - 1) == '|')
{
lastIsPipe = true;
}
while (aTok.hasMoreTokens())
{
String tok = aTok.nextToken();
if (inPipe)
{
if (aTok.hasMoreTokens() || lastIsPipe)
{
replaceToken(tok, output, aPC);
}
/*
* No else condition because we should be between
* pipes at this point i.e. this should be a token but
* it appears to be malformed. Malformed because there
* are no more tokens and the last character of the string
* is not a pipe
*/
}
else
{
outputNonToken(tok, output);
}
// Reverse the inPipe state, causing the next token to
// take the other decision path
if (aTok.hasMoreTokens())
{
inPipe = !inPipe;
}
}
}
}
/**
* Replace the token with the value it represents
*
* @param aString The string containing the token to be replaced
* @param output The object that will capture the output
* @param aPC The PC currently being exported
* @return value
*/
public int replaceToken(String aString, BufferedWriter output,
PlayerCharacter aPC)
{
try
{
// If it is plain text then there's no replacement necessary
if (isPlainText(aString))
{
return 0;
}
// If it is purely a filter everything (not a filter on a specific token)
// then there is nothing to replace so return 0
if ("%".equals(aString))
{
inLabel = false;
canWrite = true;
return 0;
}
// If the line starts with ${ and ends with } then write the JEP variable
// and return the length of the line (minus any whitespace)
if (aString.startsWith("${") && aString.endsWith("}"))
{
String jepString = aString.substring(2, aString.length() - 1);
String variableValue =
aPC.getVariableValue(jepString, "").toString();
FileAccess.write(output, variableValue);
return aString.trim().length();
}
// TODO Why?
FileAccess.maxLength(-1);
// Start the |%blah| token section, e.g. Deal with filtering tokens (e.g. If it doesn't meet a criteria then don't write)
// If the string is a non empty filter and does not have a '<' or a '>' in it then replace the token
if (isFilterToken(aString))
{
return dealWithFilteredTokens(aString, aPC);
}
String tokenString = aString;
// now check for max length tokens
// e.g: |SUB10.ARMOR.AC|
if (isValidSubToken(tokenString))
{
tokenString = replaceSubToken(tokenString);
}
// Now check for the rest of the tokens
populateTokenMap();
StringTokenizer tok = new StringTokenizer(tokenString, ".,", false);
String firstToken = tok.nextToken();
// Get the remaining token/test string
// TODO Understand this
String testString = tokenString;
if (testString.indexOf(',') > -1)
{
testString = testString.substring(0, testString.indexOf(','));
}
if (testString.indexOf('~') > -1)
{
testString = testString.substring(0, testString.indexOf('~'));
}
int len = 1;
// Deal with FOR/DFOR token
if (isForOrDForToken(tokenString))
{
processLoopToken(tokenString, output, aPC);
return 0;
}
// Deal with OIF token
else if (tokenString.startsWith("OIF("))
{
replaceTokenOIF(tokenString, output, aPC);
}
// Deal with mathematical tokenLeave
else if (containsMathematicalToken(testString) && (!skipMath))
{
FileAccess.maxLength(-1);
FileAccess.write(output, mathMode(tokenString, aPC));
return 0;
}
// Deal with CSHEETTAG2.
else if (tokenString.startsWith("CSHEETTAG2."))
{
csheetTag2 = tokenString.substring(11, 12);
FileAccess.maxLength(-1);
return 0;
}
// Else if the token is in the list of valid output tokens
else if (tokenMap.get(firstToken) != null)
{
Token token = tokenMap.get(firstToken);
if (token.isEncoded())
{
FileAccess.encodeWrite(output, token.getToken(tokenString,
aPC, this));
}
else
{
FileAccess.write(output, token.getToken(tokenString, aPC,
this));
}
}
// Default case
else
{
len = tokenString.trim().length();
if (manualWhitespace)
{
tokenString = tokenString.replaceAll("[ \\t]", "");
if (len > 0)
{
FileAccess.write(output, tokenString);
}
}
else
{
FileAccess.write(output, tokenString);
}
}
FileAccess.maxLength(-1);
return len;
}
catch (Exception exc)
{
Logging.errorPrint("Error replacing " + aString, exc);
return 0;
}
}
/**
* Helper method to determine if a line of text needs replacing or not
*
* @param aString
* @return true If it is plain text (e.g. Does not need replacing)
*/
private boolean isPlainText(String aString)
{
// If we 'cannot write' and the string is non-empty, non-filter token then
// there is nothing to replace so return 0
if (!canWrite && (!aString.isEmpty()) && (aString.charAt(0) != '%'))
{
return true;
}
if (aString.isEmpty())
{
return true;
}
return false;
}
/**
* Helper method to determine if a token is a filter token or not
*
* @param aString token to evaluate
* @return true if it is a filter token
*/
private boolean isFilterToken(String aString)
{
if ((!aString.isEmpty()) && (aString.charAt(0) == '%')
&& (aString.length() > 1) && (aString.lastIndexOf('<') == -1)
&& (aString.lastIndexOf('>') == -1))
{
return true;
}
return false;
}
/**
* Helper method, determines if a token is a valid SUB token
*
* @param tokenString token to evaluate
* @return true if it is a valid SUB token
*/
private boolean isValidSubToken(String tokenString)
{
if (tokenString.indexOf("SUB") == 0 && (tokenString.indexOf(".") > 3))
{
return true;
}
return false;
}
/**
* Helper method to detect if a token is a DFOR or FOR token
*
* @param tokenString token to check
* @return true if it is a DFOR or FOR token
*/
boolean isForOrDForToken(String tokenString)
{
if (tokenString.startsWith("FOR.") || tokenString.startsWith("DFOR."))
{
return true;
}
return false;
}
/**
* Helper method to determine if a string contains a mathematical token
*
* @param testString String to test
* @return true if it
*/
private boolean containsMathematicalToken(String testString)
{
if ((testString.indexOf('+') >= 0) || (testString.indexOf('-') >= 0)
|| (testString.indexOf(".INTVAL") >= 0)
|| (testString.indexOf(".SIGN") >= 0)
|| (testString.indexOf(".NOZERO") >= 0)
|| (testString.indexOf(".TRUNC") >= 0)
|| (testString.indexOf('*') >= 0) || (testString.indexOf('/') >= 0))
{
return true;
}
return false;
}
/**
* Helper method, deals with replacing the SUB token
*
* @param tokenString the SUB token
* @return The altered SUB token
*/
private String replaceSubToken(String tokenString)
{
int iEnd = tokenString.indexOf(".");
int maxLength;
try
{
maxLength = Integer.parseInt(tokenString.substring(3, iEnd));
}
catch (NumberFormatException ex)
{
// Hmm, no number?
Logging.errorPrint("Number format error: " + tokenString);
maxLength = -1;
}
if (maxLength > 0)
{
tokenString = tokenString.substring(iEnd + 1);
FileAccess.maxLength(maxLength);
}
return tokenString;
}
/**
* Helper method that deals with Processing the FOR./DFOR. tokens as a
* DFOR loop
*
* @param tokenString the token to loop over
* @param output The writer we write to
* @param aPC The PC we are exporting
*/
private void processLoopToken(String tokenString, BufferedWriter output,
PlayerCharacter aPC)
{
FileAccess.maxLength(-1);
existsOnly = false;
noMoreItems = false;
checkBefore = false;
replaceTokenForDfor(tokenString, output, aPC);
existsOnly = false;
noMoreItems = false;
}
/**
* Helper method for replaceToken, deals with the filter tokens e.g. %DOMAIN, basically
* returns 0 if we should not be writing something out, e.g. It's filtered out
*
* @param aString
* @param aPC
* @return 0 If we should not be writing something out
*/
private int dealWithFilteredTokens(String aString, PlayerCharacter aPC)
{
// Start by stating that we are allowed to write
canWrite = true;
// Get the merge strategy for equipment for later use
int merge = getEquipmentMergingStrategy(aString);
// Filter out on GAMEMODE
if (aString.substring(1).startsWith("GAMEMODE:"))
{
if (aString.substring(10)
.endsWith(GameModeToken.getGameModeToken()))
{
canWrite = false;
}
return 0;
}
// Filter out REGION
CharacterDisplay display = aPC.getDisplay();
if ("REGION".equals(aString.substring(1)))
{
if (display.getRegionString().equals(Constants.NONE))
{
canWrite = false;
}
return 0;
}
// Filter out NOTES
if ("NOTES".equals(aString.substring(1)))
{
if (aPC.getDisplay().getNotesCount() <= 0)
{
canWrite = false;
}
return 0;
}
// Filter out SKILLPOINTS
if ("SKILLPOINTS".equals(aString.substring(1)))
{
if (SkillpointsToken.getUnusedSkillPoints(aPC) == 0)
{
canWrite = false;
}
return 0;
}
// Filter out TEMPLATE
if (aString.substring(1).startsWith("TEMPLATE"))
{
// New token syntax |%TEMPLATE.x| instead of |%TEMPLATEx|
final StringTokenizer aTok =
new StringTokenizer(aString.substring(1), ".");
final List<PCTemplate> tList = new ArrayList<>(aPC.getTemplateSet());
String fString = aTok.nextToken();
final int index;
if (aTok.hasMoreTokens())
{
index = Integer.parseInt(aTok.nextToken());
}
else
{
// When removing old syntax, remove the else and leave the if
if ("TEMPLATE".equals(fString))
{
if (tList.isEmpty())
{
canWrite = false;
}
return 0;
}
Logging
.errorPrint("Old syntax %TEMPLATEx will be replaced for %TEMPLATE.x");
index = Integer.parseInt(aString.substring(9));
}
if (index >= tList.size())
{
canWrite = false;
return 0;
}
final PCTemplate template = tList.get(index);
if (!template.getSafe(ObjectKey.VISIBILITY).isVisibleTo(View.VISIBLE_EXPORT))
{
canWrite = false;
}
return 0;
}
// Filter out FOLLOWER
if ("FOLLOWER".equals(aString.substring(1)))
{
if (!aPC.hasFollowers())
{
canWrite = false;
}
return 0;
}
// Filter out FOLLOWEROF
if ("FOLLOWEROF".equals(aString.substring(1)))
{
if (aPC.getMasterPC() == null)
{
canWrite = false;
}
return 0;
}
// Filter out FOLLOWERTYPE.
if (aString.substring(1).startsWith("FOLLOWERTYPE."))
{
List<Follower> aList = new ArrayList<>();
for (Follower follower : aPC.getFollowerList())
{
// only allow followers that currently loaded
// Otherwise the stats a zero
for (PlayerCharacter pc : Globals.getPCList())
{
if (pc.getFileName().equals(follower.getFileName()))
{
aList.add(follower);
}
}
}
StringTokenizer aTok = new StringTokenizer(aString, ".");
aTok.nextToken(); // FOLLOWERTYPE
String typeString = aTok.nextToken();
for (int i = aList.size() - 1; i >= 0; --i)
{
final Follower fol = aList.get(i);
if (!fol.getType().getKeyName().equalsIgnoreCase(typeString))
{
aList.remove(i);
}
}
if (aList.isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out PROHIBITEDLIST
if ("PROHIBITEDLIST".equals(aString.substring(1)))
{
for (PCClass pcClass : aPC.getClassSet())
{
if (aPC.getLevel(pcClass) > 0)
{
if (pcClass.containsListFor(ListKey.PROHIBITED_SPELLS)
|| aPC.containsProhibitedSchools(pcClass))
{
return 0;
}
}
}
canWrite = false;
return 0;
}
// Filter out CATCHPHRASE
if ("CATCHPHRASE".equals(aString.substring(1)))
{
String catchPhrase = display.getCatchPhrase();
if (catchPhrase.equals(Constants.NONE))
{
canWrite = false;
}
else if (catchPhrase.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out LOCATION
if ("LOCATION".equals(aString.substring(1)))
{
String location = display.getLocation();
if (location.equals(Constants.NONE))
{
canWrite = false;
}
else if (location.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out RESIDENCE
if ("RESIDENCE".equals(aString.substring(1)))
{
String residence = aPC.getSafeStringFor(PCStringKey.RESIDENCE);
if (residence.equals(Constants.NONE))
{
canWrite = false;
}
else if (residence.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out PHOBIAS
if ("PHOBIAS".equals(aString.substring(1)))
{
String phobias = display.getSafeStringFor(PCStringKey.PHOBIAS);;
if (phobias.equals(Constants.NONE))
{
canWrite = false;
}
else if (phobias.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out INTERESTS
if ("INTERESTS".equals(aString.substring(1)))
{
String interests = display.getInterests();
if (interests.equals(Constants.NONE))
{
canWrite = false;
}
else if (interests.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out SPEECHTENDENCY
if ("SPEECHTENDENCY".equals(aString.substring(1)))
{
String speechTendency = display.getSpeechTendency();
if (speechTendency.equals(Constants.NONE))
{
canWrite = false;
}
else if (speechTendency.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out PERSONALITY1
if ("PERSONALITY1".equals(aString.substring(1)))
{
String trait1 = display.getTrait1();
if (trait1.equals(Constants.NONE))
{
canWrite = false;
}
else if (trait1.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out PERSONALITY2
if ("PERSONALITY2".equals(aString.substring(1)))
{
String trait2 = display.getTrait2();
if (trait2.equals(Constants.NONE))
{
canWrite = false;
}
else if (trait2.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out MISC.FUNDS
if ("MISC.FUNDS".equals(aString.substring(1)))
{
if (aPC.getSafeStringFor(PCStringKey.ASSETS).equals(Constants.NONE))
{
canWrite = false;
}
else if ((aPC.getSafeStringFor(PCStringKey.ASSETS)).trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out MISC.COMPANIONS and COMPANIONS
if ("COMPANIONS".equals(aString.substring(1))
|| "MISC.COMPANIONS".equals(aString.substring(1)))
{
if (aPC.getSafeStringFor(PCStringKey.COMPANIONS).equals(Constants.NONE))
{
canWrite = false;
}
else if (aPC.getSafeStringFor(PCStringKey.COMPANIONS).trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out MISC.MAGIC
if ("MISC.MAGIC".equals(aString.substring(1)))
{
if (aPC.getSafeStringFor(PCStringKey.MAGIC).equals(Constants.NONE))
{
canWrite = false;
}
else if (aPC.getSafeStringFor(PCStringKey.MAGIC).trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out DESC
if ("DESC".equals(aString.substring(1)))
{
String description = display.getSafeStringFor(PCStringKey.DESCRIPTION);
if (description.equals(Constants.NONE))
{
canWrite = false;
}
else if (description.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out BIO
if ("BIO".equals(aString.substring(1)))
{
String bio = display.getBio();
if (bio.equals(Constants.NONE))
{
canWrite = false;
}
else if (bio.trim().isEmpty())
{
canWrite = false;
}
return 0;
}
// Filter out SUBREGION
if ("SUBREGION".equals(aString.substring(1)))
{
if (display.getSubRegion().equals(Constants.NONE))
{
canWrite = false;
}
return 0;
}
// Filter out TEMPBONUS.
if (aString.substring(1).startsWith("TEMPBONUS."))
{
StringTokenizer aTok =
new StringTokenizer(aString.substring(1), ".");
aTok.nextToken(); // discard first one
int index = -1;
if (aTok.hasMoreTokens())
{
index = Integer.parseInt(aTok.nextToken());
}
if (index > aPC.getNamedTempBonusList().size())
{
canWrite = false;
return 0;
}
if (aPC.getUseTempMods())
{
canWrite = true;
return 1;
}
}
// Filter out ARMOR.ITEM
if (aString.substring(1).startsWith("ARMOR.ITEM"))
{
// New token syntax |%ARMOR.ITEM.x| instead of |%ARMOR.ITEMx|
final StringTokenizer aTok =
new StringTokenizer(aString.substring(1), ".");
aTok.nextToken(); // ARMOR
String fString = aTok.nextToken();
final Collection<Equipment> aArrayList = new ArrayList<>();
for (Equipment eq : aPC.getEquipmentListInOutputOrder())
{
if (eq.altersAC(aPC)
&& (!eq.isArmor() && !eq.isShield()))
{
aArrayList.add(eq);
}
}
// When removing old syntax, remove the else and leave the if
final int count;
if (aTok.hasMoreTokens())
{
count = Integer.parseInt(aTok.nextToken());
}
else
{
Logging
.errorPrint("Old syntax %ARMOR.ITEMx will be replaced for %ARMOR.ITEM.x");
count =
Integer.parseInt(fString
.substring(fString.length() - 1));
}
if (count > aArrayList.size())
{
canWrite = false;
}
return 0;
}
// Filter out ARMOR.SHIELD
if (aString.substring(1).startsWith("ARMOR.SHIELD"))
{
// New token syntax |%ARMOR.SHIELD.x| instead of |%ARMOR.SHIELDx|
final StringTokenizer aTok =
new StringTokenizer(aString.substring(1), ".");
aTok.nextToken(); // ARMOR
String fString = aTok.nextToken();
final int count;
final List<Equipment> aArrayList =
aPC.getEquipmentOfTypeInOutputOrder("SHIELD", 3);
// When removing old syntax, remove the else and leave the if
if (aTok.hasMoreTokens())
{
count = Integer.parseInt(aTok.nextToken());
}
else
{
Logging
.errorPrint("Old syntax %ARMOR.SHIELDx will be replaced for %ARMOR.SHIELD.x");
count =
Integer.parseInt(fString
.substring(fString.length() - 1));
}
if (count > aArrayList.size())
{
canWrite = false;
}
return 0;
}
// Filter out ARMOR
if (aString.substring(1).startsWith("ARMOR"))
{
// New token syntax |%ARMOR.x| instead of |%ARMORx|
final StringTokenizer aTok =
new StringTokenizer(aString.substring(1), ".");
String fString = aTok.nextToken();
List<Equipment> aArrayList =
aPC.getEquipmentOfTypeInOutputOrder("ARMOR", 3);
//Get list of shields. Remove any from list of armor
//Since shields are included in the armor list they will appear twice and they really shouldn't be in the list of armor
List<Equipment> shieldList =
aPC.getEquipmentOfTypeInOutputOrder("SHIELD", 3);
int z = 0;
while (z < shieldList.size())
{
aArrayList.remove(shieldList.get(z));
z++;
}
// When removing old syntax, remove the else and leave the if
final int count;
if (aTok.hasMoreTokens())
{
count = Integer.parseInt(aTok.nextToken());
}
else
{
Logging
.errorPrint("Old syntax %ARMORx will be replaced for %ARMOR.x");
count =
Integer.parseInt(fString
.substring(fString.length() - 1));
}
if (count > aArrayList.size())
{
canWrite = false;
}
return 0;
}
// Filter out WEAPONPROF
if ("WEAPONPROF".equals(aString.substring(1)))
{
if (!SettingsHandler.getWeaponProfPrintout())
{
canWrite = false;
}
return 0;
}
// Filter out WEAPON
if (aString.substring(1).startsWith("WEAPON"))
{
// New token syntax |%WEAPON.x| instead of |%WEAPONx|
final StringTokenizer aTok =
new StringTokenizer(aString.substring(1), ".");
String fString = aTok.nextToken();
final List<Equipment> aArrayList = aPC.getExpandedWeapons(merge);
int count;
// When removing old syntax, remove the else and leave the if
if (aTok.hasMoreTokens())
{
count = Integer.parseInt(aTok.nextToken());
}
else
{
Logging
.errorPrint("Old syntax %WEAPONx will be replaced for %WEAPON.x");
count =
Integer.parseInt(fString
.substring(fString.length() - 1));
}
if (count >= aArrayList.size())
{
canWrite = false;
}
return 0;
}
// Filter out DOMAIN
if (aString.substring(1).startsWith("DOMAIN"))
{
// New token syntax |%DOMAIN.x| instead of |%DOMAINx|
final StringTokenizer aTok =
new StringTokenizer(aString.substring(1), ".");
String fString = aTok.nextToken();
final int index;
// When removing old syntax, remove the else and leave the if
if (aTok.hasMoreTokens())
{
index = Integer.parseInt(aTok.nextToken());
}
else
{
Logging
.errorPrint("Old syntax %DOMAINx will be replaced for %DOMAIN.x");
index = Integer.parseInt(fString.substring(6));
}
canWrite = (index <= display.getDomainCount());
return 0;
}
// Filter out SPELLLISTBOOK
if (aString.substring(1).startsWith("SPELLLISTBOOK"))
{
if (SettingsHandler.getPrintSpellsWithPC())
{
// New token syntax |%SPELLLISTBOOK.x| instead of |%SPELLLISTBOOKx|
// To remove old syntax, replace i with 15
int i;
if (aString.charAt(14) == '.')
{
i = 15;
}
else
{
i = 14;
}
return replaceTokenSpellListBook(aString.substring(i), aPC);
}
canWrite = false;
return 0;
}
// Filter out VAR.
if (aString.substring(1).startsWith("VAR."))
{
replaceTokenVar(aString, aPC);
return 0;
}
// Filter out COUNT[
if (aString.substring(1).startsWith("COUNT["))
{
if (getVarValue(aString.substring(1), aPC) > 0)
{
canWrite = true;
return 1;
}
canWrite = false;
return 0;
}
// finally, check for classes
final StringTokenizer aTok =
new StringTokenizer(aString.substring(1), ",", false);
boolean found = false;
while (aTok.hasMoreTokens())
{
String cString = aTok.nextToken();
StringTokenizer bTok = new StringTokenizer(cString, "=", false);
String bString = bTok.nextToken();
int i = 0;
if (bTok.hasMoreTokens())
{
i = Integer.parseInt(bTok.nextToken());
}
PCClass aClass = aPC.getClassKeyed(bString);
found = aClass != null;
if (aClass == null)
{
canWrite = false;
}
else
{
canWrite = (aPC.getLevel(aClass) >= i);
}
// Filter out SPELLLISTCLASS
if (bString.startsWith("SPELLLISTCLASS"))
{
// New token syntax |%SPELLLISTCLASS.x| instead of |%SPELLLISTCLASSx|
// To remove old syntax, keep the if and remove the else
if (bString.charAt(14) == '.')
{
bString = bString.substring(15);
}
else
{
bString = bString.substring(14);
}
found = true;
CDOMObject aObject =
aPC.getSpellClassAtIndex(Integer.parseInt(bString));
canWrite = (aObject != null);
}
}
if (found)
{
inLabel = true;
return 0;
}
canWrite = false;
inLabel = true;
Logging
.debugPrint("Return 0 (don't write/no replacement) for an undetermined filter token.");
return 0;
}
/**
* Helper method to get the equipment merging strategy
*
* @param aString
* @return merging strategy constant
*/
private int getEquipmentMergingStrategy(String aString)
{
// Set how we are merging equipment, default is to merge all
int merge = Constants.MERGE_ALL;
if (aString.indexOf("MERGENONE") > 0)
{
merge = Constants.MERGE_NONE;
}
if (aString.indexOf("MERGELOC") > 0)
{
merge = Constants.MERGE_LOCATION;
}
return merge;
}
/**
* Helper method to deal with DFOR token, e.g.
*
* DFOR.0,(COUNT[SKILLS]+1)/2,1,COUNT[SKILLS],(COUNT[SKILLS]+1)/2,<td>\SKILL%\</td>
* <td>\SKILL%.TOTAL\</td><td>\SKILL%.RANK\</td>
* <td>\SKILL%.ABILITY\</td><td>\SKILL%.MOD\,<tr align="center">,</tr>,0
*
* Produces a 2 column row table of all skills.
*
* @param aString String to process
* @param output Output we are writing to
* @param aPC PC we are exporting
*/
private void replaceTokenForDfor(String aString, BufferedWriter output,
PlayerCharacter aPC)
{
StringTokenizer aTok;
// Split after DFOR. or DFOR by the ',' delimiter
if (aString.startsWith("DFOR."))
{
aTok = new StringTokenizer(aString.substring(5), ",", false);
}
else
{
aTok = new StringTokenizer(aString.substring(4), ",", false);
}
int cMin = 0;
int cMax = 100;
int cStep = 1;
int cStepLine = 1;
int cStepLineMax = 0;
String cString = "";
String cStartLineString = "";
String cEndLineString = "";
boolean isDFor = false;
int i = 0;
// While there are more tokens
while (aTok.hasMoreTokens())
{
String tokA = aTok.nextToken();
switch (i)
{
case 0:
cMin = getVarValue(tokA, aPC);
break;
case 1:
cMax = getVarValue(tokA, aPC);
break;
case 2:
cStep = getVarValue(tokA, aPC);
if (aString.startsWith("DFOR."))
{
isDFor = true;
cStepLineMax = getVarValue(aTok.nextToken(), aPC);
cStepLine = getVarValue(aTok.nextToken(), aPC);
}
break;
case 3:
cString = tokA;
break;
case 4:
cStartLineString = tokA;
break;
case 5:
cEndLineString = tokA;
break;
case 6:
existsOnly = (!"0".equals(tokA));
if ("2".equals(tokA))
{
checkBefore = true;
}
break;
default:
Logging
.errorPrint("ExportHandler.replaceTokenForDfor can't handle token number "
+ i
+ " this probably means you've passed in too many parameters.");
break;
}
i++;
}
if ("COMMA".equals(cStartLineString))
{
cStartLineString = ",";
}
if ("COMMA".equals(cEndLineString))
{
cEndLineString = ",";
}
if ("NONE".equals(cStartLineString))
{
cStartLineString = "";
}
if ("NONE".equals(cEndLineString))
{
cEndLineString = "";
}
if ("CRLF".equals(cStartLineString))
{
cStartLineString = Constants.LINE_SEPARATOR;
}
if ("CRLF".equals(cEndLineString))
{
cEndLineString = Constants.LINE_SEPARATOR;
}
int iStart = cMin;
int x = 0;
while (iStart < cMax)
{
if (x == 0)
{
FileAccess.write(output, cStartLineString);
}
x++;
int iNow = iStart;
if (!isDFor)
{
cStepLineMax = iNow + cStep;
}
if ((cStepLineMax > cMax) && !isDFor)
{
cStepLineMax = cMax;
}
while ((iNow < cStepLineMax) || (isDFor && (iNow < cMax)))
{
boolean insideToken = false;
if (cString.startsWith(csheetTag2))
{
insideToken = true;
}
aTok = new StringTokenizer(cString, csheetTag2, false);
int j = 0;
while (aTok.hasMoreTokens())
{
String eString = aTok.nextToken();
String gString = "";
String hString = eString;
int index = 0;
while (hString.indexOf('%', index) > 0)
{
index = hString.indexOf('%', index);
if (index == -1)
{
break;
}
if ((index < (hString.length() - 1))
&& (hString.charAt(index + 1) != '.'))
{
index++;
continue;
}
String fString = hString.substring(0, index);
if ((index + 1) < eString.length())
{
gString = hString.substring(index + 1);
}
hString = fString + Integer.toString(iNow) + gString;
}
if ("%0".equals(eString) || "%1".equals(eString))
{
final int cInt =
iNow + Integer.parseInt(eString.substring(1));
FileAccess.write(output, Integer.toString(cInt));
}
else
{
if (insideToken)
{
replaceToken(hString, output, aPC);
}
else
{
boolean oldSkipMath = skipMath;
skipMath = true;
replaceToken(hString, output, aPC);
skipMath = oldSkipMath;
}
}
if (checkBefore && noMoreItems)
{
iNow = cMax;
iStart = cMax;
if (j == 0)
{
existsOnly = false;
}
break;
}
++j;
insideToken = !insideToken;
}
iNow += cStepLine;
if (cStepLine == 0)
{
break;
}
}
if ((cStepLine > 0) || ((cStepLine == 0) && (x == cStep))
|| (existsOnly == noMoreItems))
{
FileAccess.write(output, cEndLineString);
x = 0;
if (existsOnly && noMoreItems)
{
return;
}
}
iStart += cStep;
}
}
/**
* Helper method to parse OIF token, e.g.
*
* OIF(expr,truepart,falsepart)
* OIF(HASFEAT:Armor Prof (Light), <b>Yes</b>, <b>No</b>)
*
* If the character has the Light Armor proficiency, then returns "Yes".
* Otherwise it returns "No".
*
* @param aString String to parse
* @param output output to write to
* @param aPC PC we are exporting
*/
private void replaceTokenOIF(String aString, java.io.Writer output,
PlayerCharacter aPC)
{
int iParenCount = 0;
final String[] tokenizedString = new String[3];
int iParamCount = 0;
int iStart = 4;
for (int i = iStart; i < aString.length(); ++i)
{
if (iParamCount == 3)
{
break;
}
switch (aString.charAt(i))
{
case '(':
iParenCount += 1;
break;
case ')':
iParenCount -= 1;
if (iParenCount == -1)
{
if (iParamCount == 2)
{
tokenizedString[iParamCount] =
aString.substring(iStart, i).trim();
iParamCount++;
iStart = i + 1;
}
else
{
Logging.errorPrint("OIF: not enough parameters ("
+ Integer.toString(iParamCount) + ')');
for (int j = 0; j < iParamCount; ++j)
{
Logging.errorPrint(" " + Integer.toString(j)
+ ':' + tokenizedString[j]);
}
}
}
break;
case ',':
if (iParenCount == 0)
{
if (iParamCount < 2)
{
tokenizedString[iParamCount] =
aString.substring(iStart, i).trim();
iStart = i + 1;
}
else
{
Logging.errorPrint("OIF: too many parameters");
}
iParamCount += 1;
}
break;
default:
break;
}
}
String remainder = "";
// Actually evaluate the expression
if (iParamCount != 3)
{
Logging.errorPrint("OIF: invalid parameter count: " + iParamCount);
}
else
{
remainder = aString.substring(iStart);
int i = 0;
if (evaluateExpression(tokenizedString[0], aPC))
{
i = 1;
}
else
{
i = 2;
}
// Write out the true or false case
FileAccess.write(output, tokenizedString[i]);
}
if (!remainder.isEmpty())
{
Logging.errorPrint("OIF: extra characters on line: " + remainder);
FileAccess.write(output, remainder);
}
}
/**
* Helper method to deal with the SpellListBook token
*
* @param aString
* @param aPC
* @return 0
*/
private int replaceTokenSpellListBook(String aString, PlayerCharacter aPC)
{
int sbookNum = 0;
final StringTokenizer aTok = new StringTokenizer(aString, ".");
final int classNum = Integer.parseInt(aTok.nextToken());
final int levelNum = Integer.parseInt(aTok.nextToken());
// Get the spell book number
if (aTok.hasMoreTokens())
{
sbookNum = Integer.parseInt(aTok.nextToken());
}
// Set the spell book name
String bookName = Globals.getDefaultSpellBook();
if (sbookNum > 0)
{
bookName = aPC.getDisplay().getSpellBookNames().get(sbookNum);
}
canWrite = false;
final PObject aObject = aPC.getSpellClassAtIndex(classNum);
if (aObject != null)
{
final List<CharacterSpell> aList =
aPC.getCharacterSpells(aObject, null, bookName, levelNum);
canWrite = !aList.isEmpty();
}
return 0;
}
/**
* Helper method for replacing token variables
*
* @param aString String to process
* @param aPC PC we are exporting
*/
private void replaceTokenVar(String aString, PlayerCharacter aPC)
{
final StringTokenizer aTok =
new StringTokenizer(aString.substring(5), ".", false);
final String varName = aTok.nextToken();
String bString = "EQ";
boolean intVal = false;
boolean maxVal = true;
if (aTok.hasMoreTokens())
{
bString = aTok.nextToken();
}
while ("INTVAL".equals(bString) || "MINVAL".equals(bString))
{
if ("INTVAL".equals(bString))
{
intVal = true;
}
else if ("MINVAL".equals(bString))
{
maxVal = false;
}
if (aTok.hasMoreTokens())
{
bString = aTok.nextToken();
}
else
{
Logging.errorPrint("Missing comparison type in VAR filter " + aString + " assuming NEQ");
bString = "NEQ";
}
}
String value = "0";
if (aTok.hasMoreTokens())
{
value = aTok.nextToken();
}
Float varval = aPC.getVariable(varName, maxVal);
if (intVal)
{
varval = (float) Math.floor(varval);
}
final Float valval = aPC.getVariableValue(value, "");
if ("GTEQ".equals(bString))
{
canWrite = varval.doubleValue() >= valval.doubleValue();
}
else if ("GT".equals(bString))
{
canWrite = varval.doubleValue() > valval.doubleValue();
}
else if ("LTEQ".equals(bString))
{
canWrite = varval.doubleValue() <= valval.doubleValue();
}
else if ("LT".equals(bString))
{
canWrite = varval.doubleValue() < valval.doubleValue();
}
else if ("NEQ".equals(bString))
{
canWrite =
!CoreUtility.doublesEqual(varval.doubleValue(), valval
.doubleValue());
}
else
{
Logging.errorPrint("Unknown comparison type: " + bString
+ " in VAR filter " + aString + " assuming NEQ");
canWrite =
!CoreUtility.doublesEqual(varval.doubleValue(), valval
.doubleValue());
}
}
/**
* Exports a PlayerCharacter-Party to a Writer
* according to the handler's template
*
* <br>author: Thomas Behr 13-11-02
*
* @param PCs the PlayerCharacter[] which compromises the Party to write
* @param out the Writer to be written to
*/
private void write(PlayerCharacter[] PCs, BufferedWriter out)
{
// Set an output filter based on the type of template in use.
FileAccess.setCurrentOutputFilter(templateFile.getName());
BufferedReader br = null;
try
{
br =
new BufferedReader(new InputStreamReader(
new FileInputStream(templateFile), "UTF-8"));
boolean betweenPipes = false;
StringBuilder textBetweenPipes = new StringBuilder();
// Starts with pipe pattern
Pattern pat1 = Pattern.compile("^\\Q|");
// Ends with pipe pattern
Pattern pat2 = Pattern.compile("\\Q|\\E$");
String aLine = br.readLine();
while (aLine != null)
{
int lastPipeIndex = aLine.lastIndexOf('|');
// If not inside a TAG and there is no | character on this line
if (!betweenPipes && lastPipeIndex == -1)
{
// If output sheet author controls new lines
// then replace tabs with empty space.
if (manualWhitespace)
{
aLine = aLine.replaceAll("[ \\t]", "");
FileAccess.write(out, aLine);
}
else
{
FileAccess.write(out, aLine);
FileAccess.newLine(out);
}
}
// Else if we are Inside a tag but we are not at the finish of
// the tag e.g.
//
// |
// x
// |
//
// Or we are at the start of a tag that wraps onto the next line e.g.
//
// |x
//
// Collect this text (without the pipe)
// to be passed for replacement later.
else if (betweenPipes && lastPipeIndex == -1 || !betweenPipes
&& lastPipeIndex == 0)
{
textBetweenPipes.append(aLine.substring(lastPipeIndex + 1));
betweenPipes = true;
}
// See if we are between pipes or not
else
{
Matcher mat1 = pat1.matcher(textBetweenPipes);
Matcher mat2 = pat2.matcher(textBetweenPipes);
boolean startsWithPipe = mat1.find();
boolean endsWithPipe = mat2.find();
// not currently in a pipe enclosed section, but first
// char starts one.
if (!betweenPipes && startsWithPipe)
{
betweenPipes = true;
}
betweenPipes =
processPipedLine(PCs, aLine, textBetweenPipes, out,
betweenPipes);
if (betweenPipes && endsWithPipe)
{
betweenPipes = false;
}
}
aLine = br.readLine();
}
}
catch (IOException exc)
{
Logging.errorPrint("Error in ExportHandler::write", exc);
}
finally
{
if (br != null)
{
try
{
br.close();
}
catch (IOException ignore)
{
if (Logging.isDebugMode())
{
Logging.debugPrint(
"Couldn't close file in ExportHandler::write",
ignore);
}
}
}
}
}
/**
* Helper method to process a line that begins with a | (and may end with a |)
*
* @param PCs List of PCs to output
* @param aLine Line to parse
* @param buf
* @param out character sheet we are building up
* @param between Whether we are processing a line between pipes
* @return true if we processed successfully
*/
private boolean processPipedLine(PlayerCharacter[] PCs, String aLine,
StringBuilder buf, BufferedWriter out, boolean between)
{
final StringTokenizer aTok = new StringTokenizer(aLine, "|", false);
boolean noPipes = false;
if (aTok.countTokens() == 1)
{
noPipes = true;
}
boolean betweenPipes = between;
while (aTok.hasMoreTokens())
{
String tok = aTok.nextToken();
// If we're not between pipes then just write to the output
// removing tab characters if asked to do so
if (!betweenPipes)
{
if (manualWhitespace)
{
tok = tok.replaceAll("[ \\t]", "");
}
FileAccess.write(out, tok);
}
// Guaranteed to be between pipes here
else if (!noPipes && !aTok.hasMoreTokens())
{
buf.append(tok);
}
else
{
buf.append(tok);
String aString = buf.toString();
// We have finished dealing with section
// between the pipe characters so clear out the
// StringBuilder
int l = buf.length();
buf.delete(0, l);
if (aString.startsWith("FOR."))
{
doPartyForToken(PCs, out, aString);
}
else
{
Matcher mat = Pattern.compile("^(\\d+)").matcher(aString);
int charNum =
mat.matches() ? Integer.parseInt(mat.group()) : -1;
// This seems bizarre since we haven't stripped the
// integer from the front of this string which means
// that it will not be recognised as a token and will
// just be written to the output verbatim
if ((charNum >= 0)
&& (charNum < Globals.getPCList().size()))
{
PlayerCharacter currPC = PCs[charNum];
replaceToken(aString, out, currPC);
}
else if (aString.startsWith("EXPORT"))
{
// We can safely do EXPORT tags with no PC
replaceToken(aString, out, null);
}
}
}
if (aTok.hasMoreTokens() || noPipes)
{
betweenPipes = !betweenPipes;
}
}
return betweenPipes;
}
/**
* Deal with the FOR. token, but at a party level
*
* @param PCs The PCs to export
* @param out The Output we are writing to
* @param tokenString The token string to process
*/
private void doPartyForToken(PlayerCharacter[] PCs, BufferedWriter out,
String tokenString)
{
PartyForParser forParser = new PartyForParser(tokenString, PCs.length);
int x = 0;
for (int i = forParser.min(); i < forParser.max(); i++)
{
if (x == 0)
{
FileAccess.write(out, forParser.startOfLine());
}
PlayerCharacter currPC = (0 <= i && i < PCs.length) ? PCs[i] : null;
String[] tokens = forParser.tokenString().split("\\\\\\\\");
for (String tok : tokens)
{
if (tok.startsWith("%."))
{
if (currPC != null)
{
replaceToken(tok.substring(2), out, currPC);
}
}
else
{
FileAccess.write(out, tok);
}
}
// Note: This was changed from == to && since I can't see how
// == could possibly be correct behaviour. If we were not
// just printing characters that exist the loop would
// terminate after printing one character.
boolean breakloop = (forParser.existsOnly() && (currPC == null));
++x;
if (x == forParser.step() || breakloop)
{
x = 0;
FileAccess.write(out, forParser.endOfLine());
if (breakloop)
{
break;
}
}
}
}
/*
* ##########################################################################
* various getters and setters
* ##########################################################################
*/
/**
* @param canWrite The canWrite flag to set.
*/
public void setCanWrite(boolean canWrite)
{
this.canWrite = canWrite;
}
/**
* @return Returns the checkBefore flag.
*/
public boolean getCheckBefore()
{
return checkBefore;
}
/**
* @return Returns the inLabel flag.
*/
public boolean getInLabel()
{
return inLabel;
}
/**
* @return Returns the existsOnly flag.
*/
public boolean getExistsOnly()
{
return existsOnly;
}
/**
* @param noMoreItems The noMoreItems flag to set.
*/
public void setNoMoreItems(boolean noMoreItems)
{
this.noMoreItems = noMoreItems;
}
/**
* @return Returns the manualWhitespace flag.
*/
public boolean isManualWhitespace()
{
return manualWhitespace;
}
/**
* @param manualWhitespace Set the manualWhitespace flag.
*/
public void setManualWhitespace(boolean manualWhitespace)
{
this.manualWhitespace = manualWhitespace;
}
/**
* Get the token string
*
* @param aPC the PC being exported
* @param aString The token string to convert
* @return token string
*/
public static String getTokenString(final PlayerCharacter aPC,
final String aString)
{
final StringTokenizer tok = new StringTokenizer(aString, ".,", false);
final String firstToken = tok.nextToken();
// Make sure the token list has been populated
populateTokenMap();
final Token token = tokenMap.get(firstToken);
if (token != null)
{
return token.getToken(aString, aPC, null);
}
return "";
}
/*
* ######################################################
* inner classes
* ######################################################
*/
/**
* {@code PStringTokenizer}
*
* @author Bryan McRoberts <merton_monk@users.sourceforge.net>
*/
private static final class PStringTokenizer
{
private String _andThat = "";
private String _delimiter = "";
private String _forThisString = "";
private String _ignoreBetweenThis = "";
PStringTokenizer(String forThisString, String delimiter,
String ignoreBetweenThis, String andThat)
{
_forThisString = forThisString;
_delimiter = delimiter;
_ignoreBetweenThis = ignoreBetweenThis;
_andThat = andThat;
}
/**
* Return true if we have more tokens
* @return true if we have
*/
public boolean hasMoreTokens()
{
return (!_forThisString.isEmpty());
}
/**
* Return the next token
* @return next token
*/
public String nextToken()
{
String aString;
if (_forThisString.lastIndexOf(_delimiter) == -1)
{
aString = _forThisString;
_forThisString = "";
}
else
{
int i;
final StringBuilder b = new StringBuilder();
int ignores = 0;
for (i = 0; i < _forThisString.length(); i++)
{
if (_forThisString.substring(i).startsWith(_delimiter)
&& (ignores == 0))
{
break;
}
if (_forThisString.substring(i).startsWith(
_ignoreBetweenThis)
&& (ignores == 0))
{
ignores = 1;
}
else if (_forThisString.substring(i).startsWith(_andThat))
{
ignores = 0;
}
b.append(_forThisString.substring(i, i + 1));
}
aString = b.toString();
_forThisString = _forThisString.substring(i + 1);
}
return aString;
}
}
private static final class PartyForParser
{
final PStringTokenizer pTok;
private final Integer cMin;
private final Integer cMax;
private final Integer cStep;
private final String tokenString;
private final String stringForStartOfLine;
private final String stringForEndOfLine;
private final boolean existsOnly;
private PartyForParser(String aString, final Integer numOfPCs)
{
pTok =
new PStringTokenizer(aString.substring(4), ",", "\\\\",
"\\\\");
cMin = pTok.hasMoreTokens() ? Delta.decode(pTok.nextToken()) : 0;
Integer max =
pTok.hasMoreTokens() ? Delta.decode(pTok.nextToken()) : 100;
cStep = pTok.hasMoreTokens() ? Delta.decode(pTok.nextToken()) : 1;
tokenString = pTok.hasMoreTokens() ? pTok.nextToken() : "";
stringForStartOfLine = pTok.hasMoreTokens() ? pTok.nextToken() : "";
stringForEndOfLine = pTok.hasMoreTokens() ? pTok.nextToken() : "";
existsOnly =
pTok.hasMoreTokens() && !("0".equals(pTok.nextToken()));
cMax = (max >= numOfPCs) && existsOnly ? numOfPCs : max;
if (pTok.hasMoreTokens())
{
StringBuilder sBuf = new StringBuilder();
sBuf.append("In Party.print there is an unhandled case in a ");
sBuf.append("switch (the value is ").append(pTok.nextToken());
sBuf.append(".");
String log = sBuf.toString();
Logging.errorPrint(log);
}
}
public Integer min()
{
return cMin;
}
public Integer max()
{
return cMax;
}
public Integer step()
{
return cStep;
}
private String tokenString()
{
return tokenString;
}
private String startOfLine()
{
return stringForStartOfLine;
}
private String endOfLine()
{
return stringForEndOfLine;
}
private boolean existsOnly()
{
return existsOnly;
}
}
}