/*
* AbilityCategory.java
* Copyright (c) 2010 Tom Parker <thpr@users.sourceforge.net>
* Copyright 2006 (C) Aaron Divinsky <boomer70@yahoo.com>
*
* 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
*
* Current Ver: $Revision$
*/
package pcgen.core;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import pcgen.base.formula.Formula;
import pcgen.base.util.WrappedMapSet;
import pcgen.cdom.base.Category;
import pcgen.cdom.base.ChooseInformation;
import pcgen.cdom.base.FormulaFactory;
import pcgen.cdom.base.Loadable;
import pcgen.cdom.enumeration.DisplayLocation;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.Type;
import pcgen.cdom.reference.CDOMAllRef;
import pcgen.cdom.reference.CDOMCategorizedSingleRef;
import pcgen.cdom.reference.CDOMDirectSingleRef;
import pcgen.cdom.reference.CDOMGroupRef;
import pcgen.cdom.reference.CDOMSingleRef;
import pcgen.cdom.reference.CDOMTypeRef;
import pcgen.cdom.reference.CategorizedCreator;
import pcgen.cdom.reference.ManufacturableFactory;
import pcgen.cdom.reference.ReferenceManufacturer;
import pcgen.cdom.reference.UnconstructedValidator;
import pcgen.facade.core.AbilityCategoryFacade;
import pcgen.core.utils.LastGroupSeparator.GroupingMismatchException;
import pcgen.util.Logging;
import pcgen.system.LanguageBundle;
import pcgen.util.enumeration.View;
import pcgen.util.enumeration.Visibility;
/**
* This class stores and manages information about Ability categories.
*
* <p>This is a higher level abstraction than the category specified by the
* ability object itself. The low-level AbilityCategory defaults to the same
* as this category key but this can be changed. For example to specify an
* <tt>AbilityCategory</tt> "Fighter Bonus Feats" you could specify
* the AbilityCategory was "FEAT" and set the ability type to
* "Fighter".
*
* @author boomer70 <boomer70@yahoo.com>
*
*/
public class AbilityCategory implements Category<Ability>, Loadable,
ManufacturableFactory<Ability>, CategorizedCreator<Ability>, AbilityCategoryFacade
{
private URI sourceURI;
private String keyName;
private String displayName;
private String pluralName;
private CDOMSingleRef<AbilityCategory> parentCategory;
private Set<CDOMSingleRef<Ability>> containedAbilities = null;
private DisplayLocation displayLocation;
private boolean isAllAbilityTypes = false;
private Set<Type> types = null;
private Formula poolFormula = FormulaFactory.ZERO;
private Visibility visibility = Visibility.DEFAULT;
private boolean isEditable = true;
private boolean isPoolModifiable = true;
private boolean isPoolFractional = false;
private boolean isInternal = false;
/** A constant used to refer to the "Feat" category. */
public static final AbilityCategory FEAT = new AbilityCategory("FEAT", "in_feat"); //$NON-NLS-1$ //$NON-NLS-2$
public static final AbilityCategory LANGBONUS = new AbilityCategory("*LANGBONUS"); //$NON-NLS-1$
public static final AbilityCategory ANY = new AbilityCategory("ANY"); //$NON-NLS-1$
static
{
FEAT.pluralName = LanguageBundle.getString("in_feats"); //$NON-NLS-1$
FEAT.displayLocation = DisplayLocation.getConstant(LanguageBundle.getString("in_feats")); //$NON-NLS-1$
FEAT.setInternal(true);
LANGBONUS.setPoolFormula(FormulaFactory.getFormulaFor("BONUSLANG"));
LANGBONUS.setInternal(true);
}
/**
* Constructs a new <tt>AbilityCategory</tt> with the specified key.
*
* <p>This method sets the display and plural names to the same value as
* the key name.
*/
public AbilityCategory()
{
//For fooling other things
keyName = "";
//Self until proven otherwise
parentCategory = CDOMDirectSingleRef.getRef(this);
}
/**
* Update this ability category using the values from the supplied
* ability category.
* @param srcCat The category to be copied.
*/
public void copyFields(AbilityCategory srcCat)
{
sourceURI = srcCat.sourceURI;
keyName = srcCat.keyName;
displayName = srcCat.displayName;
pluralName = srcCat.pluralName;
if (srcCat.getParentCategory() == srcCat)
{
parentCategory = CDOMDirectSingleRef.getRef(this);
}
else
{
parentCategory = srcCat.parentCategory;
}
displayLocation = srcCat.displayLocation;
isAllAbilityTypes = srcCat.isAllAbilityTypes;
types = srcCat.types == null ? null : new HashSet<>(srcCat.types);
poolFormula = srcCat.poolFormula;
visibility = srcCat.visibility;
isEditable = srcCat.isEditable;
isPoolModifiable = srcCat.isPoolModifiable;
isPoolFractional = srcCat.isPoolFractional;
isInternal = srcCat.isInternal;
}
/**
* Constructor takes a key name and display name for the category.
*
* @param aKeyName The name to use to reference this category.
* @param aDisplayName The resource key to use for the display name
*/
public AbilityCategory(final String aKeyName, final String aDisplayName)
{
keyName = aKeyName;
setName(aDisplayName);
setPluralName(aDisplayName);
parentCategory = CDOMDirectSingleRef.getRef(this);
displayLocation = DisplayLocation.getConstant(aDisplayName);
}
/**
* Constructor takes a name for the category.
*
* @param aKeyName The name to use to reference this category.
*/
public AbilityCategory(String aKeyName)
{
this(aKeyName, aKeyName);
}
/**
* Sets the parent AbilityCategory this category is part of.
*
* @param category A Reference to an AbilityCategory.
*/
public void setAbilityCategory(CDOMSingleRef<AbilityCategory> category)
{
/*
* Note: This makes an assumption that keyName will not change. We
* should not enable a KEY token for AbilityCategory
*/
if (isInternal)
{
if (!category.getLSTformat(false).equals(this.getKeyName()))
{
throw new IllegalArgumentException(
"Cannot set CATEGORY on an internal AbilityCategory");
}
}
else
{
parentCategory = category;
}
}
/**
* Gets the parent AbilityCategory this category is part of.
*
* @return A reference to the AbilityCategory.
*/
public CDOMSingleRef<AbilityCategory> getAbilityCatRef()
{
return parentCategory;
}
/**
* Adds a new type to the list of types included in this category.
*
* @param type A type string.
*/
public void addAbilityType(final Type type)
{
if (types == null)
{
types = new TreeSet<>();
}
types.add(type);
}
/**
* Gets the <tt>Set</tt> of all the ability types to be included in this
* category.
*
* @return An unmodifiable <tt>Set</tt> of type strings.
*/
public Set<Type> getTypes()
{
if (types == null)
{
return Collections.emptySet();
}
return Collections.unmodifiableSet(types);
}
/**
* Should all ability types be included in this category?
* @return true if all types should be included,
* false if only those listed should be.
*/
public boolean isAllAbilityTypes()
{
return isAllAbilityTypes;
}
/**
* Configure whether all ability types be included in this category?
* @param allAbilityTypes true if all types should be included,
* false if only those listed should be.
*/
public void setAllAbilityTypes(boolean allAbilityTypes)
{
this.isAllAbilityTypes = allAbilityTypes;
}
/**
* @param key the Ability Key to add to the set
*/
public void addAbilityKey(CDOMSingleRef<Ability> key)
{
if ( containedAbilities == null )
{
containedAbilities = new HashSet<>();
}
containedAbilities.add(key);
}
/**
* Gets the formula to use for calculating the base pool size for this
* category of ability.
*
* @return A formula
*/
public Formula getPoolFormula()
{
return poolFormula;
}
/**
* Sets the formula to use to calculate the base pool size for this category
* of ability.
*
* @param formula A valid formula or variable.
*/
public void setPoolFormula(Formula formula)
{
poolFormula = formula;
}
/**
* Sets the internationalized plural name for this category.
*
* @param aName A plural name.
*/
public void setPluralName(final String aName)
{
pluralName = aName;
}
/**
* Returns an internationalized plural version of the category name.
*
* @return The pluralized name
*/
public String getPluralName()
{
String name = pluralName;
if (name == null)
{
name = displayName;
}
if (name.startsWith("in_"))
{
return LanguageBundle.getString(name);
}
else
{
return name;
}
}
public String getRawPluralName()
{
return pluralName;
}
/**
* Returns the location on which the AbilityCategory should be displayed.
*
* @return The display location.
*/
public DisplayLocation getDisplayLocation()
{
if (displayLocation == null)
{
displayLocation = DisplayLocation.getConstant(getPluralName());
}
return displayLocation;
}
/**
* Sets the location where the AbilityCategory should be displayed.
*
* @param location
* The new displayLocation
*/
public void setDisplayLocation(DisplayLocation location)
{
displayLocation = location;
}
/**
* Sets if abilities of this category should be displayed in the UI.
*
* @param visible the visibility for abilities, i.e. hidden, visible, etc.
* @see pcgen.util.enumeration.Visibility
*/
public void setVisible(Visibility visible)
{
visibility = visible;
}
/**
* Checks if this category of ability should be displayed in the UI.
*
* @return <tt>true</tt> if these abilities should be displayed.
*/
public boolean isVisibleTo(View v)
{
return isVisibleTo(null, v);
}
/**
* Checks if this category of ability should be displayed in the
* UI for this PC.
*
* @param pc The character to be tested.
* @return <tt>true</tt> if these abilities should be displayed.
*/
public boolean isVisibleTo(PlayerCharacter pc, View v)
{
if (visibility.equals(Visibility.QUALIFY))
{
/*
* Note that hasAbilityVisibleTo is apparently not how data is
* designed - the problem (in my opinion) being that either an
* undocumented design change was made or a bug was being taken
* advantage of and the data is now dependent upon that bug. This
* reintroduces a wider behavior, but - again in my opinion -
* decreases clarity over the definition of QUALIFIED and whether
* the actual behavior aligns with the dictionary definition of the
* word. - thpr Apr 5 '14
*/
return (pc == null)
|| pc.getTotalAbilityPool(this).floatValue() != 0.0
|| pc.hasAbilityInPool(this);
//|| pc.hasAbilityVisibleTo(this, v);
}
return visibility.isVisibleTo(v);
}
/**
* Sets if abilities in this category should be user-editable
*
* @param yesNo <tt>true</tt> if the user should be able to add and remove
* abilities of this category.
*/
public void setEditable(final boolean yesNo)
{
isEditable = yesNo;
}
/**
* Checks if this category of abilities is user-editable.
*
* @return <tt>true</tt> if these abilities are editable.
*/
public boolean isEditable()
{
return isEditable;
}
/**
* Sets the flag to allow/disallow user editing of the pool.
*
* @param yesNo Set to <tt>true</tt> to allow user editing.
*/
public void setModPool(final boolean yesNo)
{
isPoolModifiable = yesNo;
}
/**
* Checks if this category allows user editing of the pool.
*
* @return <tt>true</tt> to allow user editing.
*/
public boolean allowPoolMod()
{
return isPoolModifiable;
}
/**
* Sets if the pool can use fractional amounts.
*
* @param yesNo <tt>true</tt> to allow fractions.
*/
public void setAllowFractionalPool(final boolean yesNo)
{
isPoolFractional = yesNo;
}
/**
* Checks if the pool should use whole numbers only.
*
* @return <tt>true</tt> if fractional pool amounts are valid.
*/
public boolean allowFractionalPool()
{
return isPoolFractional;
}
// -------------------------------------------
// KeyedObject Support
// -------------------------------------------
/**
* @see pcgen.cdom.base.Category#getDisplayName()
*/
@Override
public String getDisplayName()
{
if (displayName.startsWith("in_"))
{
return LanguageBundle.getString(displayName);
}
else
{
return displayName;
}
}
public String getRawDisplayName()
{
return displayName;
}
/**
* @see pcgen.cdom.base.Category#getKeyName()
*/
@Override
public String getKeyName()
{
return keyName;
}
/**
* @see pcgen.cdom.base.Loadable#setName(java.lang.String)
*/
@Override
public void setName(final String aName)
{
if ("".equals(keyName))
{
keyName = aName;
}
displayName = aName;
}
/**
* Returns the display name for this category.
*
* @see java.lang.Object#toString()
*/
@Override
public String toString()
{
return getDisplayName();
}
/**
* Generates a hash code using the key, category and types.
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode()
{
final int PRIME = 31;
int result = 1;
result = PRIME * result + ((keyName == null) ? 0 : keyName.hashCode());
return result;
}
/**
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
final AbilityCategory other = (AbilityCategory) obj;
if (keyName == null)
{
if (other.keyName != null)
return false;
}
else if (!keyName.equals(other.keyName))
return false;
return true;
}
@Override
public Category<Ability> getParentCategory()
{
return parentCategory.get();
}
/**
* Return the collection of references for abilities that will be directly
* included in the category.
*
* @return the collection of references
*/
public Collection<CDOMSingleRef<Ability>> getAbilityRefs()
{
if (containedAbilities == null)
{
return Collections.emptySet();
}
return Collections.unmodifiableCollection(containedAbilities);
}
public boolean hasDirectReferences()
{
return (containedAbilities != null) && !containedAbilities.isEmpty();
}
public Visibility getVisibility()
{
return visibility;
}
@Override
public URI getSourceURI()
{
return sourceURI;
}
@Override
public void setSourceURI(URI source)
{
sourceURI = source;
}
@Override
public String getLSTformat()
{
return getKeyName();
}
public void setInternal(boolean internal)
{
isInternal = internal;
}
@Override
public boolean isInternal()
{
return isInternal;
}
@Override
public boolean isType(String type)
{
return false;
}
@Override
public CDOMGroupRef<Ability> getAllReference()
{
return new CDOMAllRef<>(Ability.class);
}
@Override
public CDOMGroupRef<Ability> getTypeReference(String... types)
{
return new CDOMTypeRef<>(Ability.class, types);
}
@Override
public CDOMSingleRef<Ability> getReference(String ident)
{
return new CDOMCategorizedSingleRef<>(Ability.class, this,
ident);
}
@Override
public Ability newInstance()
{
Ability a = new Ability();
a.setCDOMCategory(this);
return a;
}
@Override
public boolean isMember(Ability item)
{
if (item == null)
{
return false;
}
Category<Ability> itemCategory = item.getCDOMCategory();
return this.equals(itemCategory)
|| getParentCategory().equals(itemCategory);
}
@Override
public Class<Ability> getReferenceClass()
{
return Ability.class;
}
@Override
public String getReferenceDescription()
{
return "Ability Category " + this.getKeyName();
}
@Override
public boolean resolve(ReferenceManufacturer<Ability> rm, String name,
CDOMSingleRef<Ability> reference, UnconstructedValidator validator)
{
if ((containedAbilities != null)
&& (containedAbilities.contains(reference)))
{
return true;
}
return doResolve(rm, name, reference, validator);
}
private boolean doResolve(ReferenceManufacturer<Ability> rm, String name,
CDOMSingleRef<Ability> reference, UnconstructedValidator validator)
{
boolean returnGood = true;
Ability activeObj = rm.getObject(name);
if (activeObj == null)
{
List<String> choices = new ArrayList<>();
try
{
String reduced = AbilityUtilities.getUndecoratedName(name, choices);
activeObj = rm.getObject(reduced);
}
catch (GroupingMismatchException e)
{
Logging.log(Logging.LST_ERROR, e.getMessage());
}
if (activeObj == null)
{
// Really not constructed...
// Wasn't constructed!
if (name.charAt(0) != '*' && !report(validator, name))
{
Logging.errorPrint("Unconstructed Reference: "
+ getReferenceDescription() + " " + name);
rm.fireUnconstuctedEvent(reference);
returnGood = false;
}
activeObj = rm.buildObject(name);
}
else
{
// Successful on reduced
reference.addResolution(activeObj);
if (choices.size() == 1)
{
reference.setChoice(choices.get(0));
}
else if (choices.size() > 1)
{
Logging.errorPrint("Invalid use of multiple items "
+ "in parenthesis (comma prohibited) in "
+ activeObj + " " + choices.toString());
returnGood = false;
}
}
}
else
{
reference.addResolution(activeObj);
if (reference.requiresTarget()
&& activeObj.getSafe(ObjectKey.MULTIPLE_ALLOWED))
{
ChooseInformation<?> ci = activeObj.get(ObjectKey.CHOOSE_INFO);
// Is MULT:YES.... and not CHOOSE:NOCHOICE
// Null check (unfortunately) required to protect vs. bad data
// No error message though, that is caught by MULT token
if ((ci != null) && !"No Choice".equals(ci.getName()))
{
Logging.errorPrint("Invalid use of MULT:YES Ability "
+ activeObj
+ " where a target [parens] is required");
Logging.errorPrint("PLEASE TAKE NOTE: "
+ "If usage locations are reported, "
+ "not all usages are necessary illegal "
+ "(at least one is)");
rm.fireUnconstuctedEvent(reference);
returnGood = false;
}
}
}
return returnGood;
}
private boolean report(UnconstructedValidator validator, String key)
{
return validator != null
&& validator.allow(getReferenceClass(), this, key);
}
@Override
public boolean populate(ReferenceManufacturer<Ability> parentCrm,
ReferenceManufacturer<Ability> rm, UnconstructedValidator validator)
{
if (parentCrm == null)
{
return true;
}
Collection<Ability> allObjects = parentCrm.getAllObjects();
// Don't add things twice or we'll get dupe messages :)
Set<Ability> added = new WrappedMapSet<>(IdentityHashMap.class);
/*
* Pull in all the base objects... note this skips containsDirectly
* because items haven't been resolved
*/
for (final Ability ability : allObjects)
{
boolean use = isAllAbilityTypes;
if (!use && (types != null))
{
for (Type type : types)
{
if (ability.isType(type.toString()))
{
use = true;
break;
}
}
}
if (use)
{
added.add(ability);
rm.addObject(ability, ability.getKeyName());
}
}
boolean returnGood = true;
if (containedAbilities != null)
{
for (CDOMSingleRef<Ability> ref : containedAbilities)
{
boolean res = doResolve(parentCrm, ref.getLSTformat(false), ref,
validator);
if (res)
{
Ability ability = ref.get();
if (added.add(ability))
{
rm.addObject(ability, ability.getKeyName());
}
}
returnGood &= res;
}
}
return returnGood;
}
@Override
public ManufacturableFactory<Ability> getParent()
{
AbilityCategory parent = parentCategory.get();
if (this.equals(parent))
{
return null;
}
return parent;
}
@Override
public Category<Ability> getCategory()
{
return this;
}
/* (non-Javadoc)
* @see pcgen.core.facade.AbilityCategoryFacade#getName()
*/
@Override
public String getName()
{
return getDisplayName();
}
/* (non-Javadoc)
* @see pcgen.core.facade.AbilityCategoryFacade#getType()
*/
@Override
public String getType()
{
return String.valueOf(getDisplayLocation());
}
}