/*
* AbilityLst.java
* Copyright 2006 (C) Aaron Divinsky <boomer70@yahoo.com>
* Copyright 2008-12 (C) Thomas Parker <thpr@users.sourceforge.net>
*
* 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 plugin.lsttokens;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import pcgen.base.util.MapToList;
import pcgen.base.util.TripleKeyMapToList;
import pcgen.cdom.base.AssociatedPrereqObject;
import pcgen.cdom.base.CDOMList;
import pcgen.cdom.base.CDOMObject;
import pcgen.cdom.base.CDOMReference;
import pcgen.cdom.base.ChooseSelectionActor;
import pcgen.cdom.base.Constants;
import pcgen.cdom.base.PrereqObject;
import pcgen.cdom.base.Ungranted;
import pcgen.cdom.enumeration.AssociationKey;
import pcgen.cdom.enumeration.ListKey;
import pcgen.cdom.enumeration.Nature;
import pcgen.cdom.helper.AbilitySelector;
import pcgen.cdom.helper.AbilityTargetSelector;
import pcgen.cdom.list.AbilityList;
import pcgen.cdom.reference.CDOMDirectSingleRef;
import pcgen.cdom.reference.CDOMSingleRef;
import pcgen.cdom.reference.ReferenceManufacturer;
import pcgen.cdom.reference.ReferenceUtilities;
import pcgen.core.Ability;
import pcgen.core.AbilityCategory;
import pcgen.core.AbilityUtilities;
import pcgen.core.prereq.Prerequisite;
import pcgen.persistence.PersistenceLayerException;
import pcgen.rules.context.AssociatedChanges;
import pcgen.rules.context.Changes;
import pcgen.rules.context.LoadContext;
import pcgen.rules.persistence.TokenUtilities;
import pcgen.rules.persistence.token.AbstractTokenWithSeparator;
import pcgen.rules.persistence.token.CDOMPrimaryToken;
import pcgen.rules.persistence.token.DeferredToken;
import pcgen.rules.persistence.token.ParseResult;
/**
* Implements the ABILITY: global LST token.
*
* <p>
* <b>Tag Name</b>: {@code ABILITY}:x|y|z|z<br>
* <b>Variables Used (x)</b>: Ability Category (The Ability Category this ability will be added to).<br>
* <b>Variables Used (y)</b>: Ability Nature (The nature of the added ability:
* <tt>NORMAL</tt>, <tt>AUTOMATIC</tt>, or <tt>VIRTUAL</tt>)<br>
* <b>Variables Used (z)</b>: Ability Key or TYPE(The Ability to add. Can have
* choices specified in "()")<br>
* <b>Prereqs Allowed</b>: Yes <br>
* <p>
* <b>What it does:</b><br>
* <ul>
* <li>Adds an Ability to a character.</li>
* <li>The Ability is added to the Ability Category specied and that category's
* pool will be charged if the Nature is <tt>NORMAL</tt></li>
* <li>This tag will <b>not</b> cause a chooser to appear so all required
* choices must be specified in the tag</li>
* <li>Choices can be specified by including them in parenthesis after the
* ability key name (whitespace is ignored).</li>
* <li>A <tt>CATEGORY</tt> tag can be added to the ability key to specify that
* the innate ability category specified be searched for a matching ability.</li>
* <li>If no <tt>CATEGORY</tt> is specified the standard list for the ability
* category will be used to find a matching ability.</li>
* <li>This tag is a replacement for the following tags: <tt>FEAT</tt>,
* <tt>VFEAT</tt>, and <tt>FEATAUTO</tt>.
* </ul>
* <b>Where it is used:</b><br>
* Global tag can be used anywhere.
* <p>
* <b>Examples:</b><br>
* {@code ABILITY:FEAT|AUTOMATIC|TYPE=Metamagic}<br>
* Adds a Metamagic feat as an Auto feat.
* <p>
*
* {@code ABILITY:CLASSFEATURE|VIRTUAL|CATEGORY=FEAT:Stunning Fist}<br>
* Adds the Stunning Fist feat as a virtual class feature.
* <p>
*
* @author boomer70 <boomer70@yahoo.com>
*
*
*/
public class AbilityLst extends AbstractTokenWithSeparator<CDOMObject>
implements CDOMPrimaryToken<CDOMObject>, DeferredToken<CDOMObject>
{
private static final Class<Ability> ABILITY_CLASS = Ability.class;
private static final Class<AbilityCategory> ABILITY_CATEGORY_CLASS = AbilityCategory.class;
@Override
public String getTokenName()
{
return "ABILITY";
}
@Override
protected char separator()
{
return '|';
}
@Override
protected ParseResult parseTokenWithSeparator(LoadContext context,
CDOMObject obj, String value)
{
if (obj instanceof Ungranted)
{
return new ParseResult.Fail("Cannot use " + getTokenName()
+ " on an Ungranted object type: "
+ obj.getClass().getSimpleName(), context);
}
StringTokenizer tok = new StringTokenizer(value, Constants.PIPE);
String cat = tok.nextToken();
CDOMSingleRef<AbilityCategory> acRef = context.getReferenceContext()
.getCDOMReference(ABILITY_CATEGORY_CLASS, cat);
if (!tok.hasMoreTokens())
{
return new ParseResult.Fail(getTokenName() + " must have a Nature, "
+ "Format is: CATEGORY|NATURE|AbilityName: " + value, context);
}
final String natureKey = tok.nextToken();
Nature nature;
try
{
nature = Nature.valueOf(natureKey);
}
catch (IllegalArgumentException iae)
{
return new ParseResult.Fail(getTokenName()
+ " refers to invalid Ability Nature: " + natureKey, context);
}
if (Nature.ANY.equals(nature))
{
return new ParseResult.Fail(getTokenName()
+ " refers to ANY Ability Nature, cannot be used in "
+ getTokenName() + ": " + value, context);
}
if (!tok.hasMoreTokens())
{
return new ParseResult.Fail(getTokenName()
+ " must have abilities, Format is: "
+ "CATEGORY|NATURE|AbilityName: " + value, context);
}
String token = tok.nextToken();
if (looksLikeAPrerequisite(token))
{
return new ParseResult.Fail("Cannot have only PRExxx subtoken in "
+ getTokenName() + ": " + value, context);
}
String lkString = "GA_CA_" + cat + "_" + natureKey;
ListKey glk = ListKey.getKeyFor(ChooseSelectionActor.class, lkString);
ListKey<ChooseSelectionActor<?>> lk = glk;
ArrayList<PrereqObject> edgeList = new ArrayList<>();
CDOMReference<AbilityList> abilList =
AbilityList.getAbilityListReference(acRef, nature);
boolean first = true;
boolean removed = false;
ReferenceManufacturer<Ability> rm = context.getReferenceContext().getManufacturer(
ABILITY_CLASS, ABILITY_CATEGORY_CLASS, cat);
if (rm == null)
{
return new ParseResult.Fail(
"Could not get Reference Manufacturer for Category: " + cat,
context);
}
boolean prereqsAllowed = true;
while (true)
{
if (Constants.LST_DOT_CLEAR.equals(token))
{
if (!first)
{
return new ParseResult.Fail(" Non-sensical " + getTokenName()
+ ": .CLEAR was not the first list item: " + value, context);
}
context.getListContext().removeAllFromList(getTokenName(), obj,
abilList);
context.getObjectContext().removeFromList(obj, ListKey.GA_CAKEYS, lk);
context.getObjectContext().removeList(obj, lk);
removed = true;
}
else if (token.startsWith(Constants.LST_DOT_CLEAR_DOT))
{
String clearText = token.substring(7);
CDOMReference<Ability> ref = TokenUtilities.getTypeOrPrimitive(rm, clearText);
if (ref == null)
{
return ParseResult.INTERNAL_ERROR;
}
AssociatedPrereqObject assoc = context.getListContext()
.removeFromList(getTokenName(), obj, abilList, ref);
assoc.setAssociation(AssociationKey.NATURE, nature);
assoc.setAssociation(AssociationKey.CATEGORY, acRef);
removed = true;
}
else if (Constants.LST_PERCENT_LIST.equals(token))
{
prereqsAllowed = false;
AbilitySelector as =
new AbilitySelector(getTokenName(), acRef, nature);
context.getObjectContext().addToList(obj,
ListKey.NEW_CHOOSE_ACTOR, as);
}
else
{
CDOMReference<Ability> ability = TokenUtilities.getTypeOrPrimitive(rm, token);
if (ability == null)
{
return ParseResult.INTERNAL_ERROR;
}
ability.setRequiresTarget(true);
boolean loadList = true;
List<String> choices = null;
if (token.indexOf('(') != -1)
{
choices = new ArrayList<>();
AbilityUtilities.getUndecoratedName(token, choices);
if (choices.size() == 1)
{
if (Constants.LST_PERCENT_LIST.equals(choices.get(0))
&& (ability instanceof CDOMSingleRef))
{
CDOMSingleRef<Ability> ref = (CDOMSingleRef<Ability>) ability;
AbilityTargetSelector ats = new AbilityTargetSelector(
getTokenName(), acRef, ref, nature);
context.getObjectContext().addToList(obj, ListKey.GA_CAKEYS,
lk);
context.getObjectContext().addToList(obj, lk, ats);
edgeList.add(ats);
loadList = false;
}
}
}
if (loadList)
{
AssociatedPrereqObject assoc = context.getListContext()
.addToList(getTokenName(), obj, abilList, ability);
assoc.setAssociation(AssociationKey.NATURE, nature);
assoc.setAssociation(AssociationKey.CATEGORY, acRef);
if (choices != null)
{
assoc.setAssociation(AssociationKey.ASSOC_CHOICES, choices);
}
edgeList.add(assoc);
}
}
if (!tok.hasMoreTokens())
{
// No prereqs, so we're done
return ParseResult.SUCCESS;
}
first = false;
token = tok.nextToken();
if (looksLikeAPrerequisite(token))
{
break;
}
}
if (removed || !prereqsAllowed)
{
return new ParseResult.Fail(
"Cannot use PREREQs when using .CLEAR, .CLEAR., or %LIST in "
+ getTokenName(), context);
}
while (true)
{
Prerequisite prereq = getPrerequisite(token);
if (prereq == null)
{
return new ParseResult.Fail(" (Did you put feats after the "
+ "PRExxx tags in " + getTokenName() + ":?)", context);
}
for (PrereqObject edge : edgeList)
{
edge.addPrerequisite(prereq);
}
if (!tok.hasMoreTokens())
{
break;
}
token = tok.nextToken();
}
return ParseResult.SUCCESS;
}
@Override
public String[] unparse(LoadContext context, CDOMObject obj)
{
Collection<CDOMReference<? extends CDOMList<?>>> changedLists =
context.getListContext()
.getChangedLists(obj, AbilityList.class);
Changes<ListKey<ChooseSelectionActor<?>>> actors = context.getObjectContext()
.getListChanges(obj, ListKey.GA_CAKEYS);
Set<String> returnSet = new TreeSet<>();
TripleKeyMapToList<Nature, CDOMSingleRef<AbilityCategory>, List<Prerequisite>, CDOMReference<Ability>> m =
new TripleKeyMapToList<>();
TripleKeyMapToList<Nature, CDOMSingleRef<AbilityCategory>, List<Prerequisite>, CDOMReference<Ability>> clear =
new TripleKeyMapToList<>();
Changes<ChooseSelectionActor<?>> listChanges =
context.getObjectContext().getListChanges(obj,
ListKey.NEW_CHOOSE_ACTOR);
Collection<ChooseSelectionActor<?>> listAdded = listChanges.getAdded();
if (listAdded != null && !listAdded.isEmpty())
{
for (ChooseSelectionActor<?> csa : listAdded)
{
if (csa.getSource().equals(getTokenName()))
{
try
{
AbilitySelector as = (AbilitySelector) csa;
StringBuilder sb = new StringBuilder();
sb.append(as.getAbilityCategory().getLSTformat(false)).append(Constants.PIPE);
sb.append(as.getNature()).append(Constants.PIPE);
sb.append(as.getLstFormat());
returnSet.add(sb.toString());
}
catch (PersistenceLayerException e)
{
context.addWriteMessage(getTokenName()
+ " encountered error: " + e.getMessage());
return null;
}
}
}
}
for (CDOMReference ref : changedLists)
{
AssociatedChanges<CDOMReference<Ability>> changes =
context.getListContext().getChangesInList(getTokenName(),
obj, ref);
if (changes.includesGlobalClear())
{
CDOMDirectSingleRef<AbilityList> dr = (CDOMDirectSingleRef<AbilityList>) ref;
AbilityList al = dr.get();
StringBuilder sb = new StringBuilder();
sb.append(al.getCategory().getLSTformat(false)).append(Constants.PIPE);
sb.append(al.getNature()).append(Constants.PIPE);
sb.append(Constants.LST_DOT_CLEAR);
returnSet.add(sb.toString());
}
MapToList<CDOMReference<Ability>, AssociatedPrereqObject> mtl =
changes.getAddedAssociations();
if (mtl != null)
{
for (CDOMReference<Ability> ab : mtl.getKeySet())
{
for (AssociatedPrereqObject assoc : mtl.getListFor(ab))
{
Nature nature =
assoc.getAssociation(AssociationKey.NATURE);
CDOMSingleRef<AbilityCategory> cat =
assoc.getAssociation(AssociationKey.CATEGORY);
m.addToListFor(nature, cat,
assoc.getPrerequisiteList(), ab);
}
}
}
mtl = changes.getRemovedAssociations();
if (mtl != null)
{
for (CDOMReference<Ability> ab : mtl.getKeySet())
{
for (AssociatedPrereqObject assoc : mtl.getListFor(ab))
{
Nature nature = assoc
.getAssociation(AssociationKey.NATURE);
CDOMSingleRef<AbilityCategory> cat = assoc
.getAssociation(AssociationKey.CATEGORY);
clear.addToListFor(nature, cat, assoc
.getPrerequisiteList(), ab);
}
}
}
}
for (Nature nature : m.getKeySet())
{
for (CDOMSingleRef<AbilityCategory> category : m.getSecondaryKeySet(nature))
{
for (List<Prerequisite> prereqs : m.getTertiaryKeySet(nature,
category))
{
StringBuilder sb = new StringBuilder();
sb.append(category.getLSTformat(false)).append(Constants.PIPE);
sb.append(nature);
List<CDOMReference<Ability>> clearList = clear
.removeListFor(nature, category, prereqs);
if (clearList != null && !clearList.isEmpty())
{
sb.append(Constants.PIPE);
sb.append(Constants.LST_DOT_CLEAR_DOT);
sb.append(ReferenceUtilities.joinLstFormat(clearList,
Constants.PIPE + Constants.LST_DOT_CLEAR_DOT));
}
sb.append(Constants.PIPE);
sb.append(ReferenceUtilities.joinLstFormat(m.getListFor(
nature, category, prereqs), Constants.PIPE));
if (prereqs != null && !prereqs.isEmpty())
{
sb.append(Constants.PIPE);
sb.append(getPrerequisiteString(context, prereqs));
}
returnSet.add(sb.toString());
}
}
}
for (Nature nature : clear.getKeySet())
{
for (CDOMSingleRef<AbilityCategory> category : clear.getSecondaryKeySet(nature))
{
for (List<Prerequisite> prereqs : clear.getTertiaryKeySet(
nature, category))
{
StringBuilder sb = new StringBuilder();
sb.append(category.getLSTformat(false)).append(Constants.PIPE);
sb.append(nature).append(Constants.PIPE).append(
Constants.LST_DOT_CLEAR_DOT);
sb.append(ReferenceUtilities.joinLstFormat(clear
.getListFor(nature, category, prereqs),
Constants.PIPE + Constants.LST_DOT_CLEAR_DOT));
if (prereqs != null && !prereqs.isEmpty())
{
sb.append(Constants.PIPE);
sb.append(getPrerequisiteString(context, prereqs));
}
returnSet.add(sb.toString());
}
}
}
Collection<ListKey<ChooseSelectionActor<?>>> addedActors = actors.getAdded();
if (addedActors != null)
{
for (ListKey<ChooseSelectionActor<?>> lk : addedActors)
{
Changes<ChooseSelectionActor<?>> cras =
context.getObjectContext().getListChanges(obj, lk);
for (ChooseSelectionActor<?> cra : cras.getAdded())
{
if (getTokenName().equals(cra.getSource()))
{
try
{
AbilityTargetSelector ats =
(AbilityTargetSelector) cra;
StringBuilder sb = new StringBuilder();
sb.append(ats.getAbilityCategory().getLSTformat(false))
.append(Constants.PIPE);
sb.append(ats.getNature()).append(Constants.PIPE)
.append(cra.getLstFormat());
List<Prerequisite> prereqs = ats.getPrerequisiteList();
if (prereqs != null && !prereqs.isEmpty())
{
sb.append(Constants.PIPE);
sb.append(getPrerequisiteString(context, prereqs));
}
returnSet.add(sb.toString());
}
catch (PersistenceLayerException e)
{
context.addWriteMessage(getTokenName()
+ " encountered error: " + e.getMessage());
return null;
}
}
}
}
}
if (returnSet.isEmpty())
{
return null;
}
return returnSet.toArray(new String[returnSet.size()]);
}
@Override
public Class<CDOMObject> getTokenClass()
{
return CDOMObject.class;
}
/*
* This is a DeferredToken because attempting to extract "self" out of the
* "generic" (widely shared) CHOOSE_ACTOR list is extremely difficult since
* the item added is not this token but a derivative object whose reference
* is not saved by this token. Therefore a unique list is used to store the
* CHOOSE_ACTORs generated by this token and they are added into the
* "global" list when load is complete - thpr Dec 15, 2012
*/
@Override
public boolean process(LoadContext context, CDOMObject cdo)
{
List<ListKey<ChooseSelectionActor<?>>> lkList =
cdo.getListFor(ListKey.GA_CAKEYS);
if (lkList != null)
{
for (ListKey<ChooseSelectionActor<?>> lk : lkList)
{
cdo.addAllToListFor(ListKey.NEW_CHOOSE_ACTOR, cdo.getListFor(lk));
}
}
return true;
}
@Override
public Class<CDOMObject> getDeferredTokenClass()
{
return CDOMObject.class;
}
}