/*
* AbilityToken.java
* Copyright 2006 (C) James Dempsey
*
* 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 20/11/2006
*
* $Id: $
*/
package pcgen.io.exporttoken;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeSet;
import pcgen.base.lang.StringUtil;
import pcgen.base.lang.UnreachableError;
import pcgen.base.util.GenericMapToList;
import pcgen.base.util.HashMapToList;
import pcgen.base.util.MapToList;
import pcgen.cdom.base.Constants;
import pcgen.cdom.content.CNAbility;
import pcgen.cdom.enumeration.AspectName;
import pcgen.cdom.enumeration.MapKey;
import pcgen.cdom.enumeration.Nature;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.SourceFormat;
import pcgen.cdom.helper.Aspect;
import pcgen.core.Ability;
import pcgen.core.AbilityCategory;
import pcgen.core.BenefitFormatting;
import pcgen.core.Globals;
import pcgen.core.PlayerCharacter;
import pcgen.core.SettingsHandler;
import pcgen.core.analysis.QualifiedName;
import pcgen.io.ExportHandler;
import pcgen.util.Logging;
import pcgen.util.enumeration.View;
/**
* {@code AbilityToken} handles the output of ability information.
*
* The format is ABILITY.u.v.w.x.y.z where:
* <ul>
* <li>u is the AbilityCategory (FEAT, FIGHTER etc, or ALL) - Mandatory</li>
* <li>v is the visibility (DEFAULT, ALL, VISIBLE, HIDDEN) - Optional</li>
* <li>w is the ability type filtering via strings - Optional</li>
* <li>x is the ability's position in the list of abilities, 0-based index -
* Optional</li>
* <li>y is the ability type filtering via AbilityType - default is ALL).</li>
* <li>z is what is to be output DESC, TYPE, SOURCE, default is name, or
* TYPE=<type> - type filter</li>
* </ul>
*
* @author James Dempsey <jdempsey@users.sourceforge.net>
*/
public class AbilityToken extends Token
{
/** Token Name */
public static final String TOKENNAME = "ABILITY";
/** The list of abilities to get the ability from */
private MapToList<Ability, CNAbility> abilityList = new HashMapToList<>();
/** The current visibility filtering to apply */
private View view = View.VISIBLE_EXPORT;
/** The cached PC */
private PlayerCharacter cachedPC = null;
/** The cached PC serial (serial holds whether a PC has been changed) */
private int cachedPcSerial = 0;
/** The last token in the list of abilities */
private String lastToken = null;
/** The last ability category in the list of abilities */
private AbilityCategory lastCategory = null;
/**
* Get the TOKENNAME
*
* @return TOKENNAME
*/
@Override
public String getTokenName()
{
return TOKENNAME;
}
/**
* @see pcgen.io.exporttoken.Token#getToken(java.lang.String,
* pcgen.core.PlayerCharacter, pcgen.io.ExportHandler)
*/
@Override
public String getToken(String tokenSource, PlayerCharacter pc,
ExportHandler eh)
{
// Skip the ABILITY token itself
final StringTokenizer aTok = new StringTokenizer(tokenSource, ".");
final String tokenString = aTok.nextToken();
// Get the Ability Category from the Gamemode given the key
final String categoryString = aTok.nextToken();
final AbilityCategory aCategory = "ANY".equals(categoryString) ?
AbilityCategory.ANY :
SettingsHandler.getGame().getAbilityCategory(categoryString);
// Get the ABILITY token for the category
return getTokenForCategory(tokenSource, pc, eh, aTok, tokenString,
aCategory);
}
/**
* Produce the ABILITY token output for a specific ability category.
*
* @param tokenSource
* The token being processed.
* @param pc
* The character being processed.
* @param eh
* The export handler in use for the export.
* @param aTok
* The tokenised request, already past the category.
* @param tokenString
* The output token requested
* @param aCategory
* The ability category being output.
* @return The token value.
*/
protected String getTokenForCategory(String tokenSource,
PlayerCharacter pc, ExportHandler eh, final StringTokenizer aTok,
final String tokenString, final AbilityCategory aCategory)
{
boolean cacheAbilityProcessingData =
(cachedPC != pc || !aCategory.equals(lastCategory)
|| cachedPcSerial != pc.getSerial() || !tokenString
.equals(lastToken));
// As this method can effectively be called by an OS FOR token, there
// is a performance saving in caching some of the one-off processing data
if (cacheAbilityProcessingData)
{
// Overridden by subclasses to return the right list.
abilityList = getAbilityList(pc, aCategory);
cachedPC = pc;
lastCategory = aCategory;
cachedPcSerial = pc.getSerial();
lastToken = tokenString;
}
// Ability Types Filter List
List<String> types = new ArrayList<>();
// Negated Ability Types Filter List (excludes from types)
List<String> negate = new ArrayList<>();
// Ability Type
String abilityType = null;
// Ability Types Filter List
String key = null;
// Ability Aspect Filter
String aspect = null;
/*
* abilityIndex holds the number of the ability we want, is decremented
* as we iterate through the list. It is only decremented if the current
* ability matches the desired ability
*/
int abilityIndex = -1;
/*
* Grab the next token which will either be be:
* visibility (v), type (w) or index (x), stop processing
* once you hit the index token
*/
while (aTok.hasMoreTokens())
{
final String bString = aTok.nextToken();
try
{
// Get the mandatory ability index
abilityIndex = Integer.parseInt(bString);
break;
}
// The optional visibility (v) or type (w) has been provided, so deal with those
catch (NumberFormatException exc)
{
switch (bString)
{
case "VISIBLE":
view = View.VISIBLE_EXPORT;
continue;
case "HIDDEN":
view = View.HIDDEN_EXPORT;
continue;
case "ALL":
view = View.ALL;
continue;
default:
abilityType = bString;
break;
}
}
}
/*
* Grab the next token which will either be be:
* TYPE (y) or property (z), stop processing
* once you hit the last token
*/
while (aTok.hasMoreTokens())
{
final String typeStr = aTok.nextToken();
int typeInd = typeStr.indexOf("TYPE=");
int extypeInd = typeStr.indexOf("EXCLUDETYPE=");
// If it's TYPE and it actually has a value attached then process it
if (typeInd != -1 && extypeInd == -1 && typeStr.length() > 5)
{
// It's a type to be excluded from the filter list
if (typeStr.startsWith("!"))
{
Logging.deprecationPrint("The use of !TYPE with ABILITY output tokens is deprecated. Please use EXCLUDETYPE.");
negate.add(typeStr.substring(typeInd + 5));
}
else
{
StringTokenizer incTok = new StringTokenizer(typeStr.substring(typeInd + 5), Constants.SEMICOLON);
while (incTok.hasMoreTokens())
{
types.add(incTok.nextToken());
}
}
}
// If it's EXCLUDETYPE and it actually has a value attached then process it
if (extypeInd != -1 && typeStr.length() > 12)
{
// exclude TYPEs from comma-separated list
StringTokenizer exTok = new StringTokenizer(typeStr.substring(extypeInd + 12), Constants.SEMICOLON);
while (exTok.hasMoreTokens())
{
negate.add(exTok.nextToken());
}
}
int keyInd = typeStr.indexOf("KEY=");
// If it's KEY and it actually has a value attached then process it
if (keyInd != -1 && typeStr.length() > 4)
{
key = typeStr.substring(keyInd + 4);
}
int aspectInd = typeStr.indexOf("ASPECT=");
// If it's ASPECT and it actually has a value attached then process it
if (aspectInd != -1 && typeStr.length() > 7)
{
aspect = typeStr.substring(aspectInd + 7);
}
}
// Ability List
MapToList<Ability, CNAbility> aList = null;
// Build the list of abilities that we should display
if (key == null)
{
aList = AbilityToken.buildAbilityList(types, negate, abilityType,
view, aspect, abilityList);
}
else
{
aList = AbilityToken.buildAbilityList(key, view, abilityList);
}
// Build the return string to give to the OutputSheet
String retString =
getRetString(tokenSource, pc, eh, abilityIndex, aList);
return retString;
}
/**
* Build up the list of abilities of interest based on the type and visibility selection.
*
* @param types
* The list of types which it must match at least one of.
* @param negate
* The list of types it must not match any of.
* @param abilityType
* The type definition it must match.
* @param aspect
* The aspect which it must match.
* @return List of abilities based on the type, visibility, and aspect selection.
*/
static MapToList<Ability, CNAbility> buildAbilityList(List<String> types,
List<String> negate, String abilityType, View view,
String aspect, MapToList<Ability, CNAbility> listOfAbilities)
{
List<Ability> aList = new ArrayList<>();
aList.addAll(listOfAbilities.getKeySet());
// Sort the ability list passed in
Globals.sortPObjectListByName(aList);
boolean matchTypeDef = false;
boolean matchVisibilityDef = false;
boolean matchAspectDef = false;
// List to build up
List<Ability> bList = new ArrayList<>();
// For each ability figure out whether it should be displayed depending
// on its visibility filtering and its ability type filtering
for (Ability aAbility : aList)
{
matchTypeDef = abilityMatchesType(abilityType, aAbility, types, negate);
matchVisibilityDef = abilityVisibleTo(view, aAbility);
matchAspectDef = abilityMatchesAspect(aspect, aAbility);
if (matchTypeDef && matchVisibilityDef && matchAspectDef)
{
bList.add(aAbility);
}
}
try
{
MapToList<Ability, CNAbility> mtl =
new GenericMapToList<>(
LinkedHashMap.class);
for (Ability a : bList)
{
mtl.addAllToListFor(a, listOfAbilities.getListFor(a));
}
return mtl;
}
catch (InstantiationException | IllegalAccessException e)
{
throw new UnreachableError(e);
}
}
/**
* Build up the list of abilities of interest based on the key and visibility selection.
*
* @param key
* The key of the wanted ability.
* @return List of abilities based on the type and visibility selection.
*/
static MapToList<Ability, CNAbility> buildAbilityList(String key, View view,
MapToList<Ability, CNAbility> listOfAbilities)
{
List<Ability> aList = new ArrayList<>();
aList.addAll(listOfAbilities.getKeySet());
// Sort the ability list passed in
Globals.sortPObjectListByName(aList);
boolean matchKeyDef = false;
boolean matchVisibilityDef = false;
// List to build up
List<Ability> bList = new ArrayList<>();
// For each ability figure out whether it should be displayed depending
// on its visibility filtering and its ability type filtering
for (Ability aAbility : aList)
{
matchKeyDef = aAbility.getKeyName().equalsIgnoreCase(key);
matchVisibilityDef = abilityVisibleTo(view, aAbility);
if (matchKeyDef && matchVisibilityDef)
{
bList.add(aAbility);
}
}
try
{
MapToList<Ability, CNAbility> mtl =
new GenericMapToList<>(
LinkedHashMap.class);
for (Ability a : bList)
{
mtl.addAllToListFor(a, listOfAbilities.getListFor(a));
}
return mtl;
}
catch (InstantiationException | IllegalAccessException e)
{
throw new UnreachableError(e);
}
}
/**
* Helper method, returns true if the ability has one of the ability types that
* we are matching on.
*
* @param abilityType The ability Type to test
* @param aAbility The ability
* @param types The list of types we're trying to match on
* @param negate The exclusion list of types
* @return True if it matches one of the types else false
*/
static boolean abilityMatchesType(String abilityType,
Ability aAbility, List<String> types, List<String> negate)
{
boolean matchTypeDef = false;
// If the ability type is an actual properly registered type or its null
// then match the type definition
if (abilityType != null)
{
if (aAbility.isType(abilityType))
{
matchTypeDef = true;
}
}
else
{
matchTypeDef = true;
}
boolean istype = false;
boolean isnttype = true;
// If the types contains at least one of the types we've asked for
if (!types.isEmpty())
{
for (String typeStr : types)
{
istype |= aAbility.isType(typeStr);
}
}
else
{
istype = true;
}
// It isn't all the types we've said
for (String typeStr : negate)
{
isnttype &= !aAbility.isType(typeStr);
}
matchTypeDef = matchTypeDef && istype && isnttype;
return matchTypeDef;
}
/**
* Helper method, returns true if the ability meets the visibility requirements.
*
* @param visibility The ability Type to test
* @param aAbility The ability
* @return true if it meets the visibility requirements
*/
static boolean abilityVisibleTo(View v, Ability aAbility)
{
return aAbility.getSafe(ObjectKey.VISIBILITY).isVisibleTo(v);
}
/**
* Helper method, returns true if the ability has the aspect we are matching on.
*
* @param aspect The aspecte we're trying to match on
* @param aAbility The ability
* @return True if it matches the aspect else false
*/
static boolean abilityMatchesAspect(String aspect, Ability aAbility)
{
return (aspect == null) ||
(aAbility.get(MapKey.ASPECT, AspectName.getConstant(aspect)) != null);
}
/**
* Calculate the token value (return string) for the ability token.
*
* @param tokenSource
* The text of the export token.
* @param pc
* The character being exported.
* @param eh
* The export handler.
* @param abilityIndex
* The location of the ability in the list.
* @param aList
* The list of abilities to get the ability from.
* @return The token value.
*/
private String getRetString(String tokenSource, PlayerCharacter pc,
ExportHandler eh, int abilityIndex, MapToList<Ability, CNAbility> aMapToList)
{
String retString = "";
Ability aAbility;
List<Ability> aList = new ArrayList<>(aMapToList.getKeySet());
// If the ability index given is within a valid range
if (abilityIndex >= 0 && abilityIndex < aList.size())
{
aAbility = aList.get(abilityIndex);
List<CNAbility> abilities = aMapToList.getListFor(aAbility);
if (abilities.isEmpty())
{
return "";
}
// If it is the last item and there's a valid export handler and ??? TODO
// Then tell the ExportHandler that there is no more processing needed
if (abilityIndex == aList.size() - 1 && eh != null
&& eh.getExistsOnly())
{
eh.setNoMoreItems(true);
}
if (tokenSource.endsWith(".DESC"))
{
retString = pc.getDescription(abilities);
}
else if (tokenSource.endsWith(".BENEFIT"))
{
retString = BenefitFormatting.getBenefits(pc, abilities);
}
else if (tokenSource.endsWith(".TYPE"))
{
retString = aAbility.getType().toUpperCase();
}
else if (tokenSource.endsWith(".ASSOCIATED"))
{
List<String> assocs = new ArrayList<>();
for (CNAbility cna : abilities)
{
assocs.addAll(pc.getAssociationExportList(cna));
}
Collections.sort(assocs);
retString = StringUtil.join(assocs, ",");
}
else if (tokenSource.contains(".ASSOCIATED."))
{
final String key =
tokenSource
.substring(tokenSource.indexOf(".ASSOCIATED.") + 12);
retString = getAssociationString(pc, abilities, key);
}
else if (tokenSource.endsWith(".ASSOCIATEDCOUNT"))
{
int count = 0;
for (CNAbility cna : abilities)
{
count += pc.getDetailedAssociationCount(cna);
}
retString = Integer.toString(count);
}
else if (tokenSource.endsWith(".SOURCE"))
{
retString =
SourceFormat.getFormattedString(aAbility, Globals
.getSourceDisplay(), true);
}
else if (tokenSource.endsWith(".SOURCESHORT"))
{
retString =
SourceFormat.formatShort(aAbility, 8);
}
else if (tokenSource.endsWith(".ASPECT"))
{
retString = getAspectString(pc, abilities);
}
else if (tokenSource.contains(".ASPECT."))
{
final String key =
tokenSource
.substring(tokenSource.indexOf(".ASPECT.") + 8);
retString = getAspectString(pc, abilities, key);
}
else if (tokenSource.endsWith(".ASPECTCOUNT"))
{
retString =
Integer.toString(aAbility
.getSafeSizeOfMapFor(MapKey.ASPECT));
}
else if (tokenSource.contains(".HASASPECT."))
{
final String key =
tokenSource.substring(tokenSource
.indexOf(".HASASPECT.") + 11);
retString =
getHasAspectString(pc, aAbility,
AspectName.getConstant(key));
}
else if (tokenSource.contains(".NAME"))
{
retString = aAbility.getDisplayName();
}
else if (tokenSource.contains(".KEY"))
{
retString = aAbility.getKeyName();
}
else
{
retString = QualifiedName.qualifiedName(pc, abilities);
}
}
// If the ability index is not in a valid range then tell the
// ExportHandler that there are no more items to process
else if (eh != null && eh.getExistsOnly())
{
eh.setNoMoreItems(true);
}
return retString;
}
/**
* @return The nature of the abilities being listed.
*/
protected Nature getTargetNature()
{
return Nature.NORMAL;
}
private String getAssociationString(PlayerCharacter pc,
List<CNAbility> abilities, String key)
{
int index = Integer.parseInt(key);
if (index < 0)
{
return Constants.EMPTY_STRING;
}
List<String> assocs = new ArrayList<>();
for (CNAbility cna : abilities)
{
assocs.addAll(pc.getAssociationExportList(cna));
}
Collections.sort(assocs);
int count = assocs.size();
if (index < count)
{
return assocs.get(index);
}
//index was too large
return Constants.EMPTY_STRING;
}
/**
* Gets the aspect string.
*
* @param pc
* The character being exported.
* @param ability
* The ability
*
* @return the aspect string
*/
private String getAspectString(PlayerCharacter pc, List<CNAbility> abilities)
{
if (abilities.isEmpty())
{
return "";
}
Ability sampleAbilityObject = abilities.get(0).getAbility();
Set<AspectName> aspectKeys = sampleAbilityObject.getKeysFor(MapKey.ASPECT);
SortedSet<AspectName> sortedKeys = new TreeSet<>(aspectKeys);
StringBuilder buff = new StringBuilder();
for (AspectName key : sortedKeys)
{
if (buff.length() > 0)
{
buff.append(", ");
}
buff.append(Aspect.printAspect(pc, key, abilities));
}
return buff.toString();
}
/**
* Gets the aspect string for an aspect identified by position or name.
*
* @param pc
* The character being exported.
* @param ability
* The ability being queried.
* @param key
* The key (number or name) of the aspect to retrieve
*
* @return the aspect string
*/
private String getAspectString(PlayerCharacter pc,
List<CNAbility> abilities, String key)
{
if (key == null)
{
return "";
}
if (abilities.isEmpty())
{
return "";
}
Ability sampleAbilityObject = abilities.get(0).getAbility();
try
{
int index = Integer.parseInt(key);
if ((index >= 0) && (index < sampleAbilityObject.getSafeSizeOfMapFor(MapKey.ASPECT)))
{
Set<AspectName> aspectKeys = sampleAbilityObject.getKeysFor(MapKey.ASPECT);
List<AspectName> sortedKeys =
new ArrayList<>(aspectKeys);
Collections.sort(sortedKeys);
AspectName aspectName = sortedKeys.get(index);
return Aspect.printAspect(pc, aspectName, abilities);
}
else
{
return "";
}
}
catch (NumberFormatException e)
{
// Ignore exception - expect this as we can get a String at this point
AspectName aspectName = AspectName.getConstant(key);
return Aspect.printAspectValue(pc, aspectName, abilities);
}
}
/**
* Gets the boolean (Y/N) string for the presence of the named aspect.
*
* @param ability
* The ability being queried.
* @param key
* The key (name only) of the aspect to check
*
* @return Y if the aspect is present, N if not.
*/
private String getHasAspectString(PlayerCharacter pc, Ability ability, AspectName key)
{
List<Aspect> aspects = ability.get(MapKey.ASPECT, key);
Aspect aspect = Aspect.lastPassingAspect(aspects, pc, ability);
if (aspect == null)
{
return "N";
}
return "Y";
}
/**
* Returns the correct list of abilities for the character. This method is
* overridden in subclasses if they need to change the list of abilities
* looked at.
*
* @param pc
* The character who's abilities we are retrieving.
* @param aCategory
* The category of ability being reported.
* @return List of abilities.
*/
protected MapToList<Ability, CNAbility> getAbilityList(PlayerCharacter pc,
final AbilityCategory aCategory)
{
final MapToList<Ability, CNAbility> listOfAbilities = new HashMapToList<>();
Collection<AbilityCategory> allCats =
SettingsHandler.getGame().getAllAbilityCategories();
for (AbilityCategory aCat : allCats)
{
if (AbilityCategory.ANY.equals(aCategory) || aCat.getParentCategory().equals(aCategory))
{
for (CNAbility cna : pc.getPoolAbilities(aCat, Nature.NORMAL))
{
listOfAbilities.addToListFor(cna.getAbility(), cna);
}
}
}
return listOfAbilities;
}
/**
* @return the visibility
*/
protected View getView()
{
return view;
}
/**
* @param v the view to set
*/
protected void setView(View v)
{
this.view = v;
}
}