/* * CompanionListLst.java * Copyright 2006 (C) Aaron Divinsky <boomer70@yahoo.com> * Copyright 2008 (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.HashSet; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeSet; import pcgen.base.util.TripleKeyMapToList; import pcgen.cdom.base.CDOMList; import pcgen.cdom.base.CDOMObject; import pcgen.cdom.base.CDOMReference; import pcgen.cdom.base.Constants; import pcgen.cdom.base.Ungranted; import pcgen.cdom.enumeration.ListKey; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.enumeration.RaceSubType; import pcgen.cdom.enumeration.RaceType; import pcgen.cdom.list.CompanionList; import pcgen.cdom.reference.CDOMSingleRef; import pcgen.cdom.reference.ListMatchingReference; import pcgen.cdom.reference.ObjectMatchingReference; import pcgen.cdom.reference.ReferenceUtilities; import pcgen.core.FollowerOption; import pcgen.core.Race; import pcgen.core.prereq.Prerequisite; import pcgen.persistence.lst.LstUtils; import pcgen.rules.context.Changes; import pcgen.rules.context.LoadContext; import pcgen.rules.persistence.token.AbstractTokenWithSeparator; import pcgen.rules.persistence.token.CDOMPrimaryToken; import pcgen.rules.persistence.token.ComplexParseResult; import pcgen.rules.persistence.token.ParseResult; /** * This class implments the parsing for the COMPANIONLIST token. * <p> * <b>Tag Name</b>: {@code COMPANIONLIST}:x|y,y|z * <p> * <b>Variables Used (x)</b>: <i>Text</i> (The type of companion list to add * to).<br> * <b>Variables Used (y)</b>: <i>Text</i> (A race of companion to allow to the * character).<br> * <b>Variables Used (y)</b>: {@code RACETYPE}=<i>Text</i> (all races * with the specified {@code RACETYPE} are available as this type of * companion). <br> * <b>Variables Used (y)</b>: {@code ANY} (Any race can be a companion * of this type).<br> * <b>Variables Used (z)</b>: {@code FOLLOWERADJUSTMENT}=<i>Number</i> * (Adjustment to the follower level variable). * <p> * <b>What it does:</b> * <ul> * <li>Adds a specific race or races to the list of available companions for * the specified companion type.</li> * <li>PRExxx tags can be added at the end of COMPANIONLIST tags, PRExxx tags * are checked against the master.</li> * <li>If the master does not meet the prereqs the companion will be displayed * in the list but will be listed in red and cannot be added as a companion. * </li> * </ul> * <p> * <b>Examples:</b><br> * {@code COMPANIONLIST:Familiar|Bat,Cat,Hawk,Lizard,Owl,Rat,Raven, * Snake (Tiny/Viper),Toad,Weasel}<br> * Would build the list of standard familiars available to a Sorcerer or Wizard. * <p> * {@code COMPANIONLIST:Pet|RACETYPE=Animal}<br> * Would build a list of all animals to available as a Pet. * <p> * {@code COMPANIONLIST:Familiar|Quasit|PREFEAT:1,Special Familiar| * PREALIGN:CE}<br> * A Quasit can be chosen as a Familiar but only if the master is evil and has * the Special Familiar feat. * <p> * {@code COMPANIONLIST:Animal Companion|Ape|FOLLOWERADJUSTMENT:-3} * <br> * An Ape companion to a 4th level Druid gains the benefits normally granted to * a companion of a 1st level Druid. * * @author divaa01 * */ public class CompanionListLst extends AbstractTokenWithSeparator<CDOMObject> implements CDOMPrimaryToken<CDOMObject> { private static final String COMPANIONLIST = "COMPANIONLIST"; //$NON-NLS-1$ private static final String FOLLOWERADJUSTMENT = "FOLLOWERADJUSTMENT"; //$NON-NLS-1$ /** * Returns the name of the token this class can process. * * @return Token name * @see pcgen.persistence.lst.LstToken#getTokenName() */ @Override public String getTokenName() { return COMPANIONLIST; } @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, LstUtils.PIPE); String companionType = tok.nextToken(); if (!tok.hasMoreTokens()) { return new ParseResult.Fail(getTokenName() + " requires more than just a Type: " + value, context); } String list = tok.nextToken(); ParseResult pr = checkForIllegalSeparator(',', list); if (!pr.passed()) { return pr; } StringTokenizer subTok = new StringTokenizer(list, LstUtils.COMMA); Set<CDOMReference<Race>> races = new HashSet<>(); boolean foundAny = false; while (subTok.hasMoreTokens()) { String tokString = subTok.nextToken(); if (Constants.LST_ANY.equalsIgnoreCase(tokString)) { foundAny = true; races.add(context.getReferenceContext().getCDOMAllReference(Race.class)); } else if (tokString.startsWith("RACETYPE=")) { String raceType = tokString.substring(9); if (raceType.isEmpty()) { return new ParseResult.Fail(getTokenName() + " Error: RaceType was not specified.", context); } races.add(new ObjectMatchingReference<>(tokString, Race.class, context.getReferenceContext().getCDOMAllReference(Race.class), ObjectKey.RACETYPE, RaceType.getConstant(raceType))); } else if (tokString.startsWith("RACESUBTYPE=")) { String raceSubType = tokString.substring(12); if (raceSubType.isEmpty()) { return new ParseResult.Fail(getTokenName() + " Error: RaceSubType was not specified.", context); } races.add(new ListMatchingReference<>(tokString, Race.class, context.getReferenceContext().getCDOMAllReference(Race.class), ListKey.RACESUBTYPE, RaceSubType.getConstant(raceSubType))); } else if (looksLikeAPrerequisite(tokString)) { return new ParseResult.Fail(getTokenName() + " Error: " + tokString + " found where companion race expected.", context); } else { races.add(context.getReferenceContext().getCDOMReference(Race.class, tokString)); } } if (foundAny && races.size() > 1) { return new ParseResult.Fail("Non-sensical Race List includes Any and specific races: " + value, context); } if (!tok.hasMoreTokens()) { // No other args, so we're done finish(context, obj, companionType, races, null, null); return ParseResult.SUCCESS; } // The remainder of the elements are optional. Integer followerAdjustment = null; String optArg = tok.nextToken(); while (true) { if (optArg.startsWith(FOLLOWERADJUSTMENT)) { if (followerAdjustment != null) { return new ParseResult.Fail(getTokenName() + " Error: Multiple " + FOLLOWERADJUSTMENT + " tags specified.", context); } int faStringLength = FOLLOWERADJUSTMENT.length(); if (optArg.length() <= faStringLength + 1) { return new ParseResult.Fail("Empty FOLLOWERADJUSTMENT value in " + getTokenName() + " is prohibited", context); } String adj = optArg.substring(faStringLength + 1); try { followerAdjustment = Integer.valueOf(adj); } catch (NumberFormatException nfe) { ComplexParseResult cpr = new ComplexParseResult(); cpr.addErrorMessage("Expecting a number for FOLLOWERADJUSTMENT: " + adj); cpr.addErrorMessage(" was parsing Token " + getTokenName()); return cpr; } } else if (looksLikeAPrerequisite(optArg)) { break; } else { return new ParseResult.Fail( getTokenName() + ": Unknown argument (was expecting FOLLOWERADJUSTMENT: or PRExxx): " + optArg, context); } if (!tok.hasMoreTokens()) { // No prereqs, so we're done finish(context, obj, companionType, races, followerAdjustment, null); return ParseResult.SUCCESS; } optArg = tok.nextToken(); } List<Prerequisite> prereqs = new ArrayList<>(); while (true) { Prerequisite prereq = getPrerequisite(optArg); if (prereq == null) { return new ParseResult.Fail(" (Did you put items after the " + "PRExxx tags in " + getTokenName() + ":?)", context); } prereqs.add(prereq); if (!tok.hasMoreTokens()) { break; } optArg = tok.nextToken(); } finish(context, obj, companionType, races, followerAdjustment, prereqs); return ParseResult.SUCCESS; } private void finish(LoadContext context, CDOMObject obj, String companionType, Set<CDOMReference<Race>> races, Integer followerAdjustment, List<Prerequisite> prereqs) { context.getReferenceContext().constructIfNecessary(CompanionList.class, companionType); CDOMSingleRef<CompanionList> ref = context.getReferenceContext().getCDOMReference( CompanionList.class, companionType); for (CDOMReference<Race> race : races) { final FollowerOption option = new FollowerOption(race, ref); if (prereqs != null && !prereqs.isEmpty()) { option.addAllPrerequisites(prereqs); } if (followerAdjustment != null && followerAdjustment != 0) { option.setAdjustment(followerAdjustment.intValue()); } context.getObjectContext().addToList(obj, ListKey.COMPANIONLIST, option); } } @Override public String[] unparse(LoadContext context, CDOMObject obj) { Changes<FollowerOption> changes = context.getObjectContext() .getListChanges(obj, ListKey.COMPANIONLIST); Collection<FollowerOption> removedItems = changes.getRemoved(); if (removedItems != null && !removedItems.isEmpty() || changes.includesGlobalClear()) { context .addWriteMessage(getTokenName() + " does not support .CLEAR"); return null; } Collection<FollowerOption> added = changes.getAdded(); if (added == null || added.isEmpty()) { // Zero indicates no Token (and no global clear, so nothing to do) return null; } TripleKeyMapToList<Set<Prerequisite>, CDOMReference<? extends CDOMList<?>>, Integer, CDOMReference<Race>> m = new TripleKeyMapToList<>(); for (FollowerOption fo : added) { m.addToListFor(new HashSet<>(fo.getPrerequisiteList()), fo.getListRef(), fo.getAdjustment(), fo.getRaceRef()); } Set<String> set = new TreeSet<>(); StringBuilder sb = new StringBuilder(); for (Set<Prerequisite> prereqs : m.getKeySet()) { String prereqString = null; if (prereqs != null && !prereqs.isEmpty()) { prereqString = getPrerequisiteString(context, prereqs); } for (CDOMReference<? extends CDOMList<?>> cl : m .getSecondaryKeySet(prereqs)) { for (Integer fa : m.getTertiaryKeySet(prereqs, cl)) { sb.setLength(0); sb.append(cl.getLSTformat(false)); sb.append(Constants.PIPE); Set<CDOMReference<Race>> raceSet = new TreeSet<>( ReferenceUtilities.REFERENCE_SORTER); raceSet.addAll(m.getListFor(prereqs, cl, fa)); sb.append(ReferenceUtilities.joinLstFormat(raceSet, Constants.COMMA, true)); if (fa != null && fa != 0) { sb.append(Constants.PIPE); sb.append("FOLLOWERADJUSTMENT:"); sb.append(fa); } if (prereqString != null) { sb.append(Constants.PIPE); sb.append(prereqString); } set.add(sb.toString()); } } } return set.toArray(new String[set.size()]); } @Override public Class<CDOMObject> getTokenClass() { return CDOMObject.class; } }