@Override
 protected void setUp() throws Exception {
   super.setUp();
   knownSpellFacet = FacetLibrary.getFacet(KnownSpellFacet.class);
   wizardSpellList = context.ref.constructNowIfNecessary(ClassSpellList.class, "Wizard");
   fb = context.ref.constructNowIfNecessary(Spell.class, "Fireball");
   TokenRegistration.register(new PreVariableParser());
   classSetUp();
 }
/** A CNAbilitySelectionModel is a TemplateHashModel that wraps a CNAbilitySelection object */
public class CNAbilitySelectionModel implements TemplateHashModel {
  private static final ObjectWrapperFacet WRAPPER_FACET =
      FacetLibrary.getFacet(ObjectWrapperFacet.class);

  /** The underlying CNAbilitySelection for this CNAbilitySelectionModel */
  private final CNAbilitySelection cnas;

  private final CharID id;

  /**
   * Constructs a new CNAbilitySelectionModel with the given CNAbilitySelection as the underlying
   * information.
   *
   * @param cnas The CNAbilitySelection that underlies this CNAbilitySelectionModel
   */
  public CNAbilitySelectionModel(CharID id, CNAbilitySelection cnas) {
    if (id == null) {
      throw new IllegalArgumentException("CharID cannot be null");
    }
    if (cnas == null) {
      throw new IllegalArgumentException("CNAbilitySelection cannot be null");
    }
    this.id = id;
    this.cnas = cnas;
  }

  /**
   * Acts as a hash for producing the contents of this model.
   *
   * <p>Four items are supported: category, nature, ability and selection.
   *
   * @see freemarker.template.TemplateHashModel#get(java.lang.String)
   */
  @Override
  public TemplateModel get(String key) throws TemplateModelException {
    Object towrap;
    if ("category".equals(key)) {
      towrap = cnas.getCNAbility().getAbilityCategory();
    } else if ("nature".equals(key)) {
      towrap = cnas.getCNAbility().getNature();
    } else if ("ability".equals(key)) {
      towrap = cnas.getCNAbility().getAbility();
    } else if ("selection".equals(key)) {
      towrap = cnas.getSelection();
    } else {
      throw new TemplateModelException("CNAbilitySelection did not have output of type " + key);
    }
    return WRAPPER_FACET.wrap(id, towrap);
  }

  /** @see freemarker.template.TemplateHashModel#isEmpty() */
  @Override
  public boolean isEmpty() throws TemplateModelException {
    return false;
  }
}
Beispiel #3
0
 @Override
 protected void setUp() throws Exception {
   super.setUp();
   lscFacet = FacetLibrary.getFacet(ListSkillCostFacet.class);
   sk = context.ref.constructCDOMObject(Skill.class, "MySkill");
   dragon = context.ref.constructCDOMObject(PCClass.class, "Dragon");
   dragon.addToListFor(ListKey.TYPE, Type.MONSTER);
   dragon.put(ObjectKey.IS_MONSTER, Boolean.TRUE);
   TokenRegistration.register(CHOOSE_SKILL_TOKEN);
   ChooserFactory.setDelegate(new MockUIDelegate());
 }
Beispiel #4
0
public class RaceMonCCSkillTest extends AbstractTokenModelTest {

  private static MonccskillToken token = new MonccskillToken();
  private static SkillToken CHOOSE_SKILL_TOKEN = new SkillToken();
  protected RaceInputFacet raceInputFacet = FacetLibrary.getFacet(RaceInputFacet.class);

  private ListSkillCostFacet lscFacet;
  private Skill sk;
  private PCClass dragon;

  @Override
  protected void setUp() throws Exception {
    super.setUp();
    lscFacet = FacetLibrary.getFacet(ListSkillCostFacet.class);
    sk = context.ref.constructCDOMObject(Skill.class, "MySkill");
    dragon = context.ref.constructCDOMObject(PCClass.class, "Dragon");
    dragon.addToListFor(ListKey.TYPE, Type.MONSTER);
    dragon.put(ObjectKey.IS_MONSTER, Boolean.TRUE);
    TokenRegistration.register(CHOOSE_SKILL_TOKEN);
    ChooserFactory.setDelegate(new MockUIDelegate());
  }

  @Test
  public void testDirect() throws PersistenceLayerException {
    Race source = create(Race.class, "Source");
    ParseResult result = token.parseToken(context, source, "MySkill");
    if (result != ParseResult.SUCCESS) {
      result.printMessages();
      fail("Test Setup Failed");
    }
    new ExclusiveToken().parseToken(context, sk, "Yes");
    finishLoad();
    assertEquals(SkillCost.EXCLUSIVE, pc.getSkillCostForClass(sk, dragon));
    raceFacet.directSet(id, source, getAssoc());
    ClassSkillList dragonCSL =
        context.ref.silentlyGetConstructedCDOMObject(ClassSkillList.class, "Dragon");
    assertEquals(SkillCost.EXCLUSIVE, pc.getSkillCostForClass(sk, dragon));
    pc.incrementClassLevel(1, dragon);
    assertEquals(SkillCost.CROSS_CLASS, pc.getSkillCostForClass(sk, dragon));
    assertTrue(lscFacet.contains(id, dragonCSL, SkillCost.CROSS_CLASS, sk));
    raceFacet.remove(id);
    assertFalse(lscFacet.contains(id, dragonCSL, SkillCost.CROSS_CLASS, sk));
  }

  @Override
  public CDOMToken<?> getToken() {
    return token;
  }
}
Beispiel #5
0
public class SolverViewFrame extends JFrame {

  private static final ScopeFacet scopeFacet = FacetLibrary.getFacet(ScopeFacet.class);
  private static final VariableLibraryFacet variableLibraryFacet =
      FacetLibrary.getFacet(VariableLibraryFacet.class);
  private static final SolverManagerFacet solverManagerFacet =
      FacetLibrary.getFacet(SolverManagerFacet.class);
  private static final FormulaSetupFacet formulaSetupFacet =
      FacetLibrary.getFacet(FormulaSetupFacet.class);

  private JComboBoxEx scopeChooser;
  private LegalScope selectedScope;

  private JTextField varName;
  private String varNameText = "                               ";

  private JComboBoxEx objectChooser;
  private VarScoped activeObject;

  private JComboBoxEx identifierChooser;
  private CharID activeIdentifier;

  private JTable viewTable;

  private SolverTableModel tableModel;

  public SolverViewFrame(Frame frame) {
    identifierChooser = new JComboBoxEx();
    for (CharacterFacade pcf : CharacterManager.getCharacters()) {
      String pcname = pcf.getNameRef().get();
      CharID id = pcf.getCharID();
      identifierChooser.addItem(new PCRef(pcname, id));
    }
    identifierChooser.addActionListener(new IdentifierActionListener());

    objectChooser = new JComboBoxEx();
    objectChooser.addActionListener(new ObjectActionListener());

    scopeChooser = new JComboBoxEx();
    scopeChooser.addActionListener(new ScopeActionListener());

    identifierChooser.setSelectedItem(identifierChooser.getItemAt(0));
    scopeChooser.setSelectedItem(scopeChooser.getItemAt(0));

    varName = new JTextField();
    varName.setText(varNameText);
    varName.getDocument().addDocumentListener(new VarNameListener());

    initialize();
    objectChooser.setSelectedItem(objectChooser.getItemAt(0));
  }

  private void update() {
    updateObjects();
    ScopeInstance scope = scopeFacet.get(activeIdentifier, selectedScope, activeObject);
    if (variableLibraryFacet.isLegalVariableName(
        activeIdentifier.getDatasetID(), scope.getLegalScope(), varNameText)) {
      displayInfo(scope);
    } else {
      // TODO Update a status bar
      System.err.println(selectedScope.getName() + " does not have a variable: " + varNameText);
    }
  }

  private void displayInfo(ScopeInstance scope) {
    VariableID<?> varID =
        variableLibraryFacet.getVariableID(activeIdentifier.getDatasetID(), scope, varNameText);
    setSteps(varID);
  }

  private <T> void setSteps(VariableID<T> varID) {
    List<ProcessStep<T>> steps = solverManagerFacet.diagnose(activeIdentifier, varID);
    tableModel.setSteps(steps);
  }

  private class ScopeActionListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
      LegalScopeWrapper wrap = (LegalScopeWrapper) scopeChooser.getSelectedItem();
      selectedScope = wrap.getLegalScope();
      update();
    }
  }

  private class ObjectActionListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
      ObjectNameDisplayer displayer = (ObjectNameDisplayer) objectChooser.getSelectedItem();
      if (displayer == null) {
        activeObject = null;
        tableModel.setSteps(Collections.emptyList());
      } else {
        activeObject = displayer.getObject();
        update();
      }
    }
  }

  private class VarNameListener implements DocumentListener {

    @Override
    public void insertUpdate(DocumentEvent e) {
      varNameText = varName.getText().trim();
      update();
    }

    @Override
    public void removeUpdate(DocumentEvent e) {
      varNameText = varName.getText().trim();
      update();
    }

    @Override
    public void changedUpdate(DocumentEvent e) {
      varNameText = varName.getText().trim();
      update();
    }
  }

  private class IdentifierActionListener implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {
      Object item = identifierChooser.getSelectedItem();
      activeIdentifier = ((PCRef) item).id;
      SplitFormulaSetup formulaSetup = formulaSetupFacet.get(activeIdentifier.getDatasetID());
      for (LegalScope lvs : formulaSetup.getLegalScopeLibrary().getLegalScopes()) {
        scopeChooser.addItem(new LegalScopeWrapper(lvs));
      }
      update();
    }
  }

  private void updateObjects() {
    if (activeIdentifier != null) {
      Collection<VarScoped> objects = scopeFacet.getObjectsWithVariables(activeIdentifier);
      objectChooser.removeAllItems();
      String scopeName = selectedScope.getName();
      for (VarScoped cdo : objects) {
        if (cdo.getLocalScopeName().equals(scopeName)) {
          if (scopeFacet.get(activeIdentifier, selectedScope, cdo) != null) {
            objectChooser.addItem(new ObjectNameDisplayer(cdo));
          }
        }
      }
      if (objectChooser.getItemCount() != 0) {
        objectChooser.setSelectedIndex(0);
      }
    }
  }

  public void initialize() {
    GridBagLayout gridbag = new GridBagLayout();
    GridBagConstraints c = new GridBagConstraints();
    getContentPane().setLayout(gridbag);
    c.fill = GridBagConstraints.HORIZONTAL;
    c.anchor = GridBagConstraints.NORTHWEST;
    c.insets = new Insets(2, 2, 2, 2);

    int col = 0;
    Utility.buildConstraints(c, col, 0, 1, 1, 100, 20);
    JLabel label =
        new JLabel(LanguageBundle.getFormattedString("in_SolverView_Perspective")); // $NON-NLS-1$
    gridbag.setConstraints(label, c);
    getContentPane().add(label);

    Utility.buildConstraints(c, col++, 1, 1, 1, 0, 20);
    gridbag.setConstraints(identifierChooser, c);
    getContentPane().add(identifierChooser);

    Utility.buildConstraints(c, col++, 1, 1, 1, 0, 20);
    gridbag.setConstraints(scopeChooser, c);
    getContentPane().add(scopeChooser);

    Utility.buildConstraints(c, col++, 1, 1, 1, 0, 20);
    gridbag.setConstraints(objectChooser, c);
    getContentPane().add(objectChooser);

    Utility.buildConstraints(c, col++, 1, 1, 1, 0, 20);
    label =
        new JLabel(
            LanguageBundle.getFormattedString("in_SolverView_VarName") // $NON-NLS-1$
            );
    gridbag.setConstraints(label, c);
    getContentPane().add(label);

    Utility.buildConstraints(c, col++, 1, 1, 1, 0, 20);
    gridbag.setConstraints(varName, c);
    getContentPane().add(varName);

    tableModel = new SolverTableModel();
    viewTable = new JTable(tableModel);

    viewTable.getColumnModel().getColumn(0).setPreferredWidth(25);
    viewTable.getColumnModel().getColumn(1).setPreferredWidth(50);
    viewTable.getColumnModel().getColumn(2).setPreferredWidth(25);
    viewTable.getColumnModel().getColumn(3).setPreferredWidth(50);

    Utility.buildConstraints(c, 0, 2, col, 1, 0, 1000);
    JScrollPane pane = new JScrollPane(viewTable);
    viewTable.setFillsViewportHeight(true);
    pane.setPreferredSize(new Dimension(500, 300));
    gridbag.setConstraints(pane, c);
    getContentPane().add(pane);

    setTitle("Core Variable Debug View");
    getContentPane().setSize(500, 400);
    pack();
    Utility.centerFrame(this, true);
  }

  private static class SolverTableModel<T> extends AbstractTableModel {
    private String[] columnNames = {
      "Modification Type", "Modification", "Resulting Value", "Priority", "Source"
    };

    private List<ProcessStep<T>> steps = Collections.emptyList();

    @Override
    public String getColumnName(int column) {
      return columnNames[column];
    }

    @Override
    public int getRowCount() {
      return steps.size();
    }

    @Override
    public int getColumnCount() {
      return columnNames.length;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
      ProcessStep<T> ps = steps.get(rowIndex);
      switch (columnIndex) {
        case 0:
          return ps.getModifier().getIdentification();
        case 1:
          return ps.getModifier().getInstructions();
        case 2:
          return ps.getResult();
        case 3:
          return ps.getModifier().getUserPriority();
        case 4:
          return ps.getSourceInfo();
        default:
          return "";
      }
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
      return false;
    }

    public void setSteps(List<ProcessStep<T>> steps) {
      this.steps = steps;
      fireTableDataChanged();
    }
  }

  private class PCRef {
    public String name;
    public CharID id;

    public PCRef(String pcname, CharID id) {
      this.name = pcname;
      this.id = id;
    }

    @Override
    public String toString() {
      return name;
    }
  }
}
/**
 * UserSpecialAbilityFacet tracks the SpecialAbility objects added to a Player Character by explicit
 * user action (old UI capability).
 *
 * <p>TODO Need to consider whether this sticks around, or how we handle this capability in the new
 * UI (and transition any Player Characters that have a user SpecialAbility.
 *
 * @author Thomas Parker (thpr [at] yahoo.com)
 */
public class UserSpecialAbilityFacet extends AbstractQualifiedListFacet<SpecialAbility> {

  private final PlayerCharacterTrackingFacet trackingFacet =
      FacetLibrary.getFacet(PlayerCharacterTrackingFacet.class);

  /**
   * Returns a non-null copy of the List of resolved SpecialAbility objects for the given source
   * CDOMObject and the Player Character represented by the given CharID. The Player Character must
   * qualify for the Special Ability (if it has prerequisites) in order for the resolved
   * SpecialAbility to be returned by this method. This method returns an empty List if no
   * SpecialAbility objects are in this UserSpecialAbilityFacet for the given source CDOMObject and
   * the Player Character identified by the given CharID.
   *
   * <p>This method is value-semantic in that ownership of the returned List is transferred to the
   * class calling this method. Modification of the returned List will not modify this
   * UserSpecialAbilityFacet and modification of this UserSpecialAbilityFacet will not modify the
   * returned List. Modifications to the returned List will also not modify any future or previous
   * objects returned by this (or other) methods on UserSpecialAbilityFacet. If you wish to modify
   * the information stored in this UserSpecialAbilityFacet, you must use the add*() and remove*()
   * methods of UserSpecialAbilityFacet.
   *
   * @param id The CharID representing the Player Character for which a copy of the resolved items
   *     in this UserSpecialAbilityFacet should be returned
   * @param source The source of the SpecialAbility objects for this UserSpecialAbilityFacet to be
   *     used for the resolution of the SpecialAbility objects in the Player Character
   * @return A non-null List of resolved SpecialAbility objects from this UserSpecialAbilityFacet
   *     for the Player Character represented by the given CharID
   */
  public List<SpecialAbility> getResolved(CharID id, Object source) {
    List<SpecialAbility> returnList = new ArrayList<>();
    SAProcessor proc = new SAProcessor(trackingFacet.getPC(id));
    for (SpecialAbility sa : getQualifiedSet(id, source)) {
      returnList.add(proc.act(sa, source));
    }
    return returnList;
  }

  /**
   * Returns a non-null List of processed SpecialAbility objects for the Player Character
   * represented by the given CharID. The given QualifiedActor will determine the type and contents
   * of the returned List. This method returns an empty List if no objects are in this
   * UserSpecialAbilityFacet for the Player Character identified by the given CharID.
   *
   * <p>This method is value-semantic in that ownership of the returned List is transferred to the
   * class calling this method. Modification of the returned List will not modify this
   * UserSpecialAbilityFacet and modification of this UserSpecialAbilityFacet will not modify the
   * returned List. Modifications to the returned List will also not modify any future or previous
   * objects returned by this (or other) methods on UserSpecialAbilityFacet. If you wish to modify
   * the information stored in this UserSpecialAbilityFacet, you must use the add*() and remove*()
   * methods of UserSpecialAbilityFacet.
   *
   * <p>Note: If a particular item has been granted by more than one source, then the QualifiedActor
   * will only be called for the first source that (successfully grants) the underlying object.
   *
   * @param <T> The type of objects returned by the given QualifiedActor (and thus also the type of
   *     the List returned by this method)
   * @param id The CharID representing the Player Character for which the processed items in this
   *     UserSpecialAbilityFacet should be returned
   * @param qa The QualifiedActor which will act on each of the items in this
   *     UserSpecialAbilityFacet for which the Player Character qualifies.
   * @return A non-null List of objects created by the QualifiedActor from each of the objects in
   *     this UserSpecialAbilityFacet for which the Player Character qualifies.
   */
  public <T> List<T> getAllResolved(CharID id, QualifiedActor<SpecialAbility, T> qa) {
    return actOnQualifiedSet(id, qa);
  }
}
Beispiel #7
0
 public void loadCampaignFacets() {
   FacetLibrary.getFacet(DataSetInitializationFacet.class).initialize(this);
 }
  protected void setUpContext() throws PersistenceLayerException {
    ChooserFactory.pushChooserClassname(RandomChooser.class.getName());
    TokenRegistration.clearTokens();
    TokenRegistration.register(AUTO_LANG_TOKEN);
    TokenRegistration.register(ABILITY_VISIBLE_TOKEN);
    TokenRegistration.register(AUTO_TOKEN);
    TokenRegistration.register(CHOOSE_TOKEN);
    TokenRegistration.register(CHOOSE_LANG_TOKEN);
    TokenRegistration.register(ABILITY_MULT_TOKEN);
    TokenRegistration.register(EQUIP_TYPE_TOKEN);
    TokenRegistration.register(EQUIP_PROFICIENCY_TOKEN);
    TokenRegistration.register(LANGBONUS_PRIM);
    TokenRegistration.register(PC_QUAL);
    TokenRegistration.register(getToken());
    TokenRegistration.register(plugin.bonustokens.Feat.class);

    directAbilityFacet = FacetLibrary.getFacet(DirectAbilityFacet.class);
    activeEqModFacet = FacetLibrary.getFacet(ActiveEqModFacet.class);
    alignmentFacet = FacetLibrary.getFacet(AlignmentFacet.class);
    bioSetFacet = FacetLibrary.getFacet(BioSetFacet.class);
    checkFacet = FacetLibrary.getFacet(CheckFacet.class);
    classFacet = FacetLibrary.getFacet(ClassFacet.class);
    classLevelFacet = FacetLibrary.getFacet(ClassLevelFacet.class);
    companionModFacet = FacetLibrary.getFacet(CompanionModFacet.class);
    deityFacet = FacetLibrary.getFacet(DeityFacet.class);
    domainFacet = FacetLibrary.getFacet(DomainFacet.class);
    expandedCampaignFacet = FacetLibrary.getFacet(ExpandedCampaignFacet.class);
    languageFacet = FacetLibrary.getFacet(LanguageFacet.class);
    raceFacet = FacetLibrary.getFacet(RaceInputFacet.class);
    sizeFacet = FacetLibrary.getFacet(SizeFacet.class);
    skillFacet = FacetLibrary.getFacet(SkillFacet.class);
    statFacet = FacetLibrary.getFacet(StatFacet.class);
    templateInputFacet = FacetLibrary.getFacet(TemplateInputFacet.class);
    templateConsolidationFacet = FacetLibrary.getFacet(TemplateFacet.class);
    weaponProfModelFacet = FacetLibrary.getFacet(WeaponProfModelFacet.class);

    Globals.createEmptyRace();
    Globals.setUseGUI(false);
    Globals.emptyLists();
    GameMode gamemode = SettingsHandler.getGame();
    gamemode.clearLoadContext();

    str = BuildUtilities.createStat("Strength", "STR");
    str.put(VariableKey.getConstant("LOADSCORE"), FormulaFactory.getFormulaFor("STRSCORE"));
    str.put(VariableKey.getConstant("OFFHANDLIGHTBONUS"), FormulaFactory.getFormulaFor(2));
    dex = BuildUtilities.createStat("Dexterity", "DEX");
    PCStat con = BuildUtilities.createStat("Constitution", "CON");
    intel = BuildUtilities.createStat("Intelligence", "INT");
    wis = BuildUtilities.createStat("Wisdom", "WIS");
    cha = BuildUtilities.createStat("Charisma", "CHA");

    AbstractReferenceContext ref = Globals.getContext().getReferenceContext();
    lg = BuildUtilities.createAlignment("Lawful Good", "LG");
    ref.importObject(lg);
    ln = BuildUtilities.createAlignment("Lawful Neutral", "LN");
    ref.importObject(ln);
    le = BuildUtilities.createAlignment("Lawful Evil", "LE");
    ref.importObject(le);
    ng = BuildUtilities.createAlignment("Neutral Good", "NG");
    ref.importObject(ng);
    tn = BuildUtilities.createAlignment("True Neutral", "TN");
    ref.importObject(tn);
    ne = BuildUtilities.createAlignment("Neutral Evil", "NE");
    ref.importObject(ne);
    cg = BuildUtilities.createAlignment("Chaotic Good", "CG");
    ref.importObject(cg);
    cn = BuildUtilities.createAlignment("Chaotic Neutral", "CN");
    ref.importObject(cn);
    ce = BuildUtilities.createAlignment("Chaotic Evil", "CE");
    ref.importObject(ce);
    ref.importObject(BuildUtilities.createAlignment("None", "NONE"));
    ref.importObject(BuildUtilities.createAlignment("Deity's", "Deity"));

    gamemode.setBonusFeatLevels("3|3");

    SettingsHandler.setGame("3.5");

    ref.importObject(str);
    ref.importObject(dex);
    ref.importObject(con);
    ref.importObject(intel);
    ref.importObject(wis);
    ref.importObject(cha);

    fine = BuildUtilities.createSize("Fine");
    diminutive = BuildUtilities.createSize("Diminutive");
    tiny = BuildUtilities.createSize("Tiny");
    small = BuildUtilities.createSize("Small");
    medium = BuildUtilities.createSize("Medium");
    medium.put(ObjectKey.IS_DEFAULT_SIZE, Boolean.TRUE);
    large = BuildUtilities.createSize("Large");
    huge = BuildUtilities.createSize("Huge");
    gargantuan = BuildUtilities.createSize("Gargantuan");
    colossal = BuildUtilities.createSize("Colossal");

    context = Globals.getContext();
    create(Language.class, "Common");
    BuildUtilities.createFact(context, "ClassType", PCClass.class);
    FactDefinition<?, String> fd = BuildUtilities.createFact(context, "SpellType", PCClass.class);
    fd.setSelectable(true);
    context.getReferenceContext().importObject(AbilityCategory.FEAT);
    SourceFileLoader.createLangBonusObject(Globals.getContext());
  }
/**
 * An AbstractQualifiedListFacet is a DataFacet that contains information about QualifyingObjects
 * that are contained in a PlayerCharacter when a PlayerCharacter may have more than one of that
 * type of QualifyingObject (e.g. Language, PCTemplate), the source of that object should be
 * tracked, and the PlayerCharacter can qualify for the object (they have prerequisites)
 *
 * <p>This class is designed to assume that each QualifyingObject may only be contained one time by
 * the PlayerCharacter, even if received from multiple sources. The QualifyingObject will only
 * trigger one DATA_ADDED event (when added by the first source) and if removed by some sources,
 * will only trigger one DATA_REMOVED event (when it is removed by the last remaining source).
 * Sources do not need to be removed in the order in which they are added, and the first source to
 * be added does not possess special status with respect to triggering a DATA_REMOVED event (it will
 * only trigger removal if it was the last source when removed)
 *
 * <p>The sources stored in this AbstractQualifiedListFacet are stored as a List, meaning the list
 * of sources may contain the same source multiple times. If so, each call to remove will only
 * remove that source one time from the list of sources.
 *
 * <p>In general, QualifyingObjects that are stored in an AbstractQualifiedListFacet are those where
 * the Prerequisites are those that are considered requirements. This means that as the Player
 * Character changes, the state of the Prerequisite can change and alter whether the underlying
 * object is granted to the Player Character. For PCGen 5.16, this will mean things like the
 * Prerequisite on the end of an ABILITY token (which are continuously evaluated) not the PRExxx:
 * tokens that appear directly on the line of an Ability in the Ability LST file (those are
 * evaluated only once, when the Ability is first added to the Player Character)
 *
 * <p>null is a valid source but a valid item to be added to the list of objects stored by
 * AbstractQualifiedListFacet.
 *
 * @author Thomas Parker (thpr [at] yahoo.com)
 */
public abstract class AbstractQualifiedListFacet<T extends QualifyingObject>
    extends AbstractDataFacet<CharID, T> {

  private PrerequisiteFacet prereqFacet = FacetLibrary.getFacet(PrerequisiteFacet.class);

  /**
   * Add the given object with the given source to the list of objects stored in this
   * AbstractQualifiedListFacet for the Player Character represented by the given CharID
   *
   * @param id The CharID representing the Player Character for which the given item should be added
   * @param obj The object to be added to the list of objects stored in this
   *     AbstractQualifiedListFacet for the Player Character represented by the given CharID
   * @param source The source for the given object
   */
  public void add(CharID id, T obj, Object source) {
    if (obj == null) {
      throw new IllegalArgumentException("Object to add may not be null");
    }
    Map<T, Set<Object>> map = getConstructingCachedMap(id);
    Set<Object> set = map.get(obj);
    boolean fireNew = (set == null);
    if (fireNew) {
      set = new WrappedMapSet<Object>(IdentityHashMap.class);
      map.put(obj, set);
    }
    set.add(source);
    if (fireNew) {
      fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_ADDED);
    }
  }

  /**
   * Adds all of the objects in the given Collection to the list of objects stored in this
   * AbstractQualifiedListFacet for the Player Character represented by the given CharID. All
   * objects are added as if granted with the given source.
   *
   * @param id The CharID representing the Player Character for which the given items should be
   *     added
   * @param c The Collection of objects to be added to the list of objects stored in this
   *     AbstractQualifiedListFacet for the Player Character represented by the given CharID
   * @param source The source for the objects in the given Collection
   * @throws NullPointerException if the given Collection is null
   */
  public void addAll(CharID id, Collection<? extends T> c, Object source) {
    for (T obj : c) {
      add(id, obj, source);
    }
  }

  /**
   * Removes the given source entry from the list of sources for the given object stored in this
   * AbstractQualifiedListFacet for the Player Character represented by the given CharID. If the
   * given source was the only source for the given object, then the object is removed from the list
   * of objects stored in this AbstractQualifiedListFacet for the Player Character represented by
   * the given CharID and a removal event is fired.
   *
   * @param id The CharID representing the Player Character from which the given item source should
   *     be removed
   * @param obj The object for which the source should be removed
   * @param source The source for the given object to be removed from the list of sources.
   */
  public void remove(CharID id, T obj, Object source) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap != null) {
      processRemoval(id, componentMap, obj, source);
    }
  }

  /**
   * Removes the given source entry from the list of sources for all of the objects in the given
   * Collection for the Player Character represented by the given CharID. If the given source was
   * the only source for any of the objects in the collection, then those objects are removed from
   * the list of objects stored in this AbstractQualifiedListFacet for the Player Character
   * represented by the given CharID and a removal event is fired.
   *
   * @param id The CharID representing the Player Character from which the given items should be
   *     removed
   * @param c The Collection of objects to be removed from the list of objects stored in this
   *     AbstractQualifiedListFacet for the Player Character represented by the given CharID
   * @param source The source for the objects in the given Collection to be removed from the list of
   *     sources.
   * @throws NullPointerException if the given Collection is null
   */
  public void removeAll(CharID id, Collection<T> c, Object source) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap != null) {
      for (T obj : c) {
        processRemoval(id, componentMap, obj, source);
      }
    }
  }

  /**
   * Removes all objects (and all sources for those objects) from the list of objects stored in this
   * AbstractQualifiedListFacet for the Player Character represented by the given CharID
   *
   * <p>This method is value-semantic in that ownership of the returned Map is transferred to the
   * class calling this method. Since this is a remove all function, modification of the returned
   * Map will not modify this AbstractQualifiedListFacet and modification of this
   * AbstractQualifiedListFacet will not modify the returned Map. Modifications to the returned Map
   * will also not modify any future or previous objects returned by this (or other) methods on
   * AbstractQualifiedListFacet. If you wish to modify the information stored in this
   * AbstractQualifiedListFacet, you must use the add*() and remove*() methods of
   * AbstractQualifiedListFacet.
   *
   * @param id The CharID representing the Player Character from which all items should be removed
   * @return A non-null Set of objects removed from the list of objects stored in this
   *     AbstractQualifiedListFacet for the Player Character represented by the given CharID
   */
  public Map<T, Set<Object>> removeAll(CharID id) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap == null) {
      return Collections.emptyMap();
    }
    removeCache(id);
    for (T obj : componentMap.keySet()) {
      fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED);
    }
    return componentMap;
  }

  /**
   * Returns a non-null copy of the Set of objects in this AbstractQualifiedListFacet for the Player
   * Character represented by the given CharID. This method returns an empty set if no objects are
   * in this AbstractQualifiedListFacet for the Player Character identified by the given CharID.
   *
   * <p>This method is value-semantic in that ownership of the returned List is transferred to the
   * class calling this method. Modification of the returned List will not modify this
   * AbstractQualifiedListFacet and modification of this AbstractQualifiedListFacet will not modify
   * the returned List. Modifications to the returned List will also not modify any future or
   * previous objects returned by this (or other) methods on AbstractQualifiedListFacet. If you wish
   * to modify the information stored in this AbstractQualifiedListFacet, you must use the add*()
   * and remove*() methods of AbstractQualifiedListFacet.
   *
   * @param id The CharID representing the Player Character for which the items in this
   *     AbstractQualifiedListFacet should be returned.
   * @return A non-null Set of objects in this AbstractQualifiedListFacet for the Player Character
   *     represented by the given CharID
   */
  public Set<T> getSet(CharID id) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap == null) {
      return Collections.emptySet();
    }
    return Collections.unmodifiableSet(componentMap.keySet());
  }

  /**
   * Returns the count of items in this AbstractQualifiedListFacet for the Player Character
   * represented by the given CharID
   *
   * @param id The CharID representing the Player Character for which the count of items should be
   *     returned
   * @return The count of items in this AbstractQualifiedListFacet for the Player Character
   *     represented by the given CharID
   */
  public int getCount(CharID id) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap == null) {
      return 0;
    }
    return componentMap.size();
  }

  /**
   * Returns true if this AbstractQualifiedListFacet does not contain any items for the Player
   * Character represented by the given CharID
   *
   * @param id The CharId representing the PlayerCharacter to test if any items are contained by
   *     this AbstractsSourcedListFacet
   * @return true if this AbstractQualifiedListFacet does not contain any items for the Player
   *     Character represented by the given CharID; false otherwise (if it does contain items for
   *     the Player Character)
   */
  public boolean isEmpty(CharID id) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    return componentMap == null || componentMap.isEmpty();
  }

  /**
   * Returns true if this AbstractQualifiedListFacet contains the given value in the list of items
   * for the Player Character represented by the given CharID.
   *
   * @param id The CharID representing the Player Character used for testing
   * @param obj The object to test if this AbstractQualifiedListFacet contains that item for the
   *     Player Character represented by the given CharID
   * @return true if this AbstractQualifiedListFacet contains the given value for the Player
   *     Character represented by the given CharID; false otherwise
   */
  public boolean contains(CharID id, T obj) {
    /*
     * TODO obj == null? - log an error?
     *
     * This should share behavior with AbstractListFacet
     */
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    return componentMap != null && componentMap.containsKey(obj);
  }

  /**
   * Returns a Set of sources for this AbstractQualifiedListFacet, the PlayerCharacter represented
   * by the given CharID, and the given object. Will add the given object to the list of items for
   * the PlayerCharacter represented by the given CharID and will return a new, empty Set if no
   * information has been set in this AbstractQualifiedListFacet for the given CharID and given
   * object. Will not return null.
   *
   * <p>Note that this method SHOULD NOT be public. The Set object is owned by
   * AbstractQualifiedListFacet, and since it can be modified, a reference to that object should not
   * be exposed to any object other than AbstractQualifiedListFacet.
   *
   * @param id The CharID for which the Set should be returned
   * @param obj The object for which the Set of sources should be returned
   * @return The Set of sources for the given object and Player Character represented by the given
   *     CharID.
   */
  private Set<Object> getConstructingCachedSetFor(CharID id, T obj) {
    Map<T, Set<Object>> map = getConstructingCachedMap(id);
    Set<Object> set = map.get(obj);
    if (set == null) {
      set = new WrappedMapSet<Object>(IdentityHashMap.class);
      map.put(obj, set);
    }
    return set;
  }

  /**
   * Returns the type-safe Map for this AbstractQualifiedListFacet and the given CharID. May return
   * null if no information has been set in this AbstractQualifiedListFacet for the given CharID.
   *
   * <p>Note that this method SHOULD NOT be public. The Map is owned by AbstractQualifiedListFacet,
   * and since it can be modified, a reference to that object should not be exposed to any object
   * other than AbstractQualifiedListFacet.
   *
   * @param id The CharID for which the Set should be returned
   * @return The Set for the Player Character represented by the given CharID; null if no
   *     information has been set in this AbstractQualifiedListFacet for the Player Character.
   */
  private Map<T, Set<Object>> getCachedMap(CharID id) {
    return (Map<T, Set<Object>>) getCache(id);
  }

  /**
   * Returns a type-safe Map for this AbstractQualifiedListFacet and the given CharID. Will return a
   * new, empty Map if no information has been set in this AbstractQualifiedListFacet for the given
   * CharID. Will not return null.
   *
   * <p>Note that this method SHOULD NOT be public. The Map object is owned by
   * AbstractQualifiedListFacet, and since it can be modified, a reference to that object should not
   * be exposed to any object other than AbstractQualifiedListFacet.
   *
   * @param id The CharID for which the Map should be returned
   * @return The Map for the Player Character represented by the given CharID.
   */
  private Map<T, Set<Object>> getConstructingCachedMap(CharID id) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap == null) {
      componentMap = getComponentMap();
      setCache(id, componentMap);
    }
    return componentMap;
  }

  /**
   * Returns a new (empty) Map for this AbstractQualifiedListFacet. Can be overridden by classes
   * that extend AbstractQualifiedListFacet if a Map other than an IdentityHashMap is desired for
   * storing the information in the AbstractQualifiedListFacet.
   *
   * <p>Note that this method SHOULD NOT be public. The Map object is owned by
   * AbstractQualifiedListFacet, and since it can be modified, a reference to that object should not
   * be exposed to any object other than AbstractQualifiedListFacet.
   *
   * <p>Note that this method should always be the only method used to construct a Map for this
   * AbstractQualifiedListFacet. It is actually preferred to use getConstructingCacheMap(CharID) in
   * order to implicitly call this method.
   *
   * @return A new (empty) Map for use in this AbstractQualifiedListFacet.
   */
  protected Map<T, Set<Object>> getComponentMap() {
    return new IdentityHashMap<T, Set<Object>>();
  }

  /**
   * Copies the contents of the AbstractQualifiedListFacet from one Player Character to another
   * Player Character, based on the given CharIDs representing those Player Characters.
   *
   * <p>This is a method in AbstractQualifiedListFacet in order to avoid exposing the mutable Map
   * object to other classes. This should not be inlined, as the Map is internal information to
   * AbstractQualifiedListFacet and should not be exposed to other classes.
   *
   * <p>Note also the copy is a one-time event and no references are maintained between the Player
   * Characters represented by the given CharIDs (meaning once this copy takes place, any change to
   * the AbstractQualifiedListFacet of one Player Character will only impact the Player Character
   * where the AbstractQualifiedListFacet was changed).
   *
   * @param source The CharID representing the Player Character from which the information should be
   *     copied
   * @param destination The CharID representing the Player Character to which the information should
   *     be copied
   */
  @Override
  public void copyContents(CharID source, CharID destination) {
    Map<T, Set<Object>> sourceMap = getCachedMap(source);
    if (sourceMap != null) {
      for (Map.Entry<T, Set<Object>> me : sourceMap.entrySet()) {
        T obj = me.getKey();
        Set<Object> sourceSet = me.getValue();
        Set<Object> targetSet = getConstructingCachedSetFor(destination, obj);
        targetSet.addAll(sourceSet);
      }
    }
  }

  /**
   * This method implements removal of a source for an object contained by this
   * AbstractQualifiedListFacet. This implements the actual check that determines if the given
   * source was the only source for the given object. If so, then that object is removed from the
   * list of objects stored in this AbstractQualifiedListFacet for the Player Character represented
   * by the given CharID.
   *
   * @param id The CharID representing the Player Character which may have the given item removed.
   * @param componentMap The (private) Map for this AbstractQualifiedListFacet that will as least
   *     have the given source removed from the list for the given object.
   * @param obj The object which may be removed if the given source is the only source for this
   *     object in the Player Character represented by the given CharID
   * @param source The source for the given object to be removed from the list of sources for that
   *     object
   */
  private void processRemoval(CharID id, Map<T, Set<Object>> componentMap, T obj, Object source) {
    if (obj == null) {
      throw new IllegalArgumentException("Object to remove may not be null");
    }
    Set<Object> set = componentMap.get(obj);
    if (set != null) {
      set.remove(source);
      if (set.isEmpty()) {
        componentMap.remove(obj);
        fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED);
      }
    }
  }

  /**
   * Removes all information for the given source from this AbstractQualifiedListFacet for the
   * PlayerCharacter represented by the given CharID.
   *
   * @param id The CharID representing the Player Character for which items from the given source
   *     will be removed
   * @param source The source for the objects to be removed from the list of items stored for the
   *     Player Character identified by the given CharID
   */
  public void removeAll(CharID id, Object source) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap != null) {
      /*
       * This list exists primarily to eliminate the possibility of a
       * concurrent modification exception on a recursive remove
       */
      List<T> removedKeys = new ArrayList<T>();
      for (Iterator<Map.Entry<T, Set<Object>>> it = componentMap.entrySet().iterator();
          it.hasNext(); ) {
        Entry<T, Set<Object>> me = it.next();
        Set<Object> set = me.getValue();
        if (set.remove(source) && set.isEmpty()) {
          T obj = me.getKey();
          it.remove();
          removedKeys.add(obj);
        }
      }
      if (componentMap.isEmpty()) {
        removeCache(id);
      }
      for (T obj : removedKeys) {
        fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED);
      }
    }
  }

  /**
   * Returns a non-null copy of the Set of objects in this AbstractQualifiedListFacet for the Player
   * Character represented by the given CharID and the given source. This method returns an empty
   * set if no objects are in this AbstractQualifiedListFacet for the Player Character identified by
   * the given CharID and source.
   *
   * <p>This method is value-semantic in that ownership of the returned List is transferred to the
   * class calling this method. Modification of the returned List will not modify this
   * AbstractQualifiedListFacet and modification of this AbstractQualifiedListFacet will not modify
   * the returned List. Modifications to the returned List will also not modify any future or
   * previous objects returned by this (or other) methods on AbstractQualifiedListFacet. If you wish
   * to modify the information stored in this AbstractQualifiedListFacet, you must use the add*()
   * and remove*() methods of AbstractQualifiedListFacet.
   *
   * @param id The CharID representing the Player Character for which the items in this
   *     AbstractQualifiedListFacet should be returned.
   * @param owner The source object for which a copy of the List of objects in this
   *     AbstractQualifiedListFacet should be returned.
   * @return A non-null Set of objects in this AbstractQualifiedListFacet for the Player Character
   *     represented by the given CharID
   */
  public List<? extends T> getSet(CharID id, Object owner) {
    List<T> list = new ArrayList<T>();
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap != null) {
      for (Iterator<Map.Entry<T, Set<Object>>> it = componentMap.entrySet().iterator();
          it.hasNext(); ) {
        Entry<T, Set<Object>> me = it.next();
        Set<Object> set = me.getValue();
        if (set.contains(owner)) {
          list.add(me.getKey());
        }
      }
    }
    return list;
  }

  /**
   * Returns a non-null copy of the Set of objects the character qualifies for in this
   * AbstractQualifiedListFacet for the Player Character represented by the given CharID. This
   * method returns an empty set if the Player Character identified by the given CharID qualifies
   * for none of the objects in this AbstractQualifiedListFacet.
   *
   * <p>This method is value-semantic in that ownership of the returned Collection is transferred to
   * the class calling this method. Modification of the returned Collection will not modify this
   * AbstractQualifiedListFacet and modification of this AbstractQualifiedListFacet will not modify
   * the returned Collection. Modifications to the returned Collection will also not modify any
   * future or previous objects returned by this (or other) methods on AbstractQualifiedListFacet.
   * If you wish to modify the information stored in this AbstractQualifiedListFacet, you must use
   * the add*() and remove*() methods of AbstractQualifiedListFacet.
   *
   * @param id The CharID representing the Player Character for which the items in this
   *     AbstractQualifiedListFacet should be returned.
   * @return A non-null Set of objects the Player Character represented by the given CharID
   *     qualifies for in this AbstractQualifiedListFacet
   */
  public Collection<T> getQualifiedSet(CharID id) {
    Set<T> set = new HashSet<T>();
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap != null) {
      for (Map.Entry<T, Set<Object>> me : componentMap.entrySet()) {
        T obj = me.getKey();
        Set<Object> sources = me.getValue();
        for (Object source : sources) {
          if (prereqFacet.qualifies(id, obj, source)) {
            set.add(obj);
            break;
          }
        }
      }
    }
    return set;
  }

  /**
   * Returns a non-null copy of the Set of objects the character qualifies for in this
   * AbstractQualifiedListFacet for the Player Character represented by the given CharID and the
   * given source. This method returns an empty set if the Player Character identified by the given
   * CharID qualifies for none of the objects in this AbstractQualifiedListFacet granted by the
   * given source.
   *
   * <p>This method is value-semantic in that ownership of the returned List is transferred to the
   * class calling this method. Modification of the returned List will not modify this
   * AbstractQualifiedListFacet and modification of this AbstractQualifiedListFacet will not modify
   * the returned List. Modifications to the returned List will also not modify any future or
   * previous objects returned by this (or other) methods on AbstractQualifiedListFacet. If you wish
   * to modify the information stored in this AbstractQualifiedListFacet, you must use the add*()
   * and remove*() methods of AbstractQualifiedListFacet.
   *
   * <p>Generally, use of this method is discouraged in general operational aspects. However, it is
   * recognized that certain output tokens can list certain items by source, and thus this method is
   * required, and it is unreasonable to expect complete elimination of this method or entirely
   * prohibit future use of this method.
   *
   * @param id The CharID representing the Player Character for which the items in this
   *     AbstractQualifiedListFacet should be returned.
   * @param source The source object for which a copy of the List of objects the Player Character
   *     qualifies for should be returned.
   * @return A non-null Set of objects the Player Character represented by the given CharID
   *     qualifies for in this AbstractQualifiedListFacet
   */
  public Collection<T> getQualifiedSet(CharID id, Object source) {
    Set<T> set = new WrappedMapSet<T>(IdentityHashMap.class);
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap != null) {
      for (Map.Entry<T, Set<Object>> me : componentMap.entrySet()) {
        T obj = me.getKey();
        Set<Object> sources = me.getValue();
        if (sources.contains(source)) {
          if (prereqFacet.qualifies(id, obj, source)) {
            set.add(obj);
          }
        }
      }
    }
    return set;
  }

  /**
   * Acts on the Set of objects the character qualifies for in this AbstractQualifiedListFacet for
   * the Player Character represented by the given CharID. The results of each action as provided by
   * the given QualifiedActor are returned in a non-null List.
   *
   * <p>This method returns an empty List if the Player Character identified by the given CharID
   * qualifies for none of the objects in this AbstractQualifiedListFacet.
   *
   * <p>This method is value-semantic in that ownership of the returned List is transferred to the
   * class calling this method. Modification of the returned List will not modify this
   * AbstractQualifiedListFacet and modification of this AbstractQualifiedListFacet will not modify
   * the returned List. Modifications to the returned List will also not modify any future or
   * previous objects returned by this (or other) methods on AbstractQualifiedListFacet. If you wish
   * to modify the information stored in this AbstractQualifiedListFacet, you must use the add*()
   * and remove*() methods of AbstractQualifiedListFacet.
   *
   * <p>Note: If a particular item has been granted by more than one source, then the QualifiedActor
   * will only be called for the first source that (successfully grants) the underlying object.
   *
   * @param id The CharID representing the Player Character for which the items in this
   *     AbstractQualifiedListFacet should be returned.
   * @param qa The QualifiedActor which will act on each of the items in this
   *     AbstractQualifiedListFacet for which the Player Character qualifies.
   * @return A non-null List of objects created by the QualifiedActor from each of the objects in
   *     this AbstractQualifiedListFacet for which the Player Character qualifies.
   */
  public <R> List<R> actOnQualifiedSet(CharID id, QualifiedActor<T, R> qa) {
    List<R> list = new ArrayList<R>();
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap != null) {
      for (Map.Entry<T, Set<Object>> me : componentMap.entrySet()) {
        T obj = me.getKey();
        Set<Object> sources = me.getValue();
        for (Object source : sources) {
          if (prereqFacet.qualifies(id, obj, source)) {
            list.add(qa.act(obj, source));
          }
        }
      }
    }
    return list;
  }

  public Collection<Object> getSources(CharID id, T obj) {
    Map<T, Set<Object>> componentMap = getCachedMap(id);
    if (componentMap != null) {
      Set<Object> sources = componentMap.get(obj);
      if (sources != null) {
        return Collections.unmodifiableSet(sources);
      }
    }
    return Collections.emptySet();
  }
}