/* * Copyright (c) Thomas Parker, 2009. * * This program 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 program 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., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ package pcgen.cdom.facet.model; import java.util.Collections; import java.util.EventListener; import java.util.EventObject; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Set; import javax.swing.event.EventListenerList; import pcgen.cdom.base.SetFacet; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.enumeration.IntegerKey; import pcgen.cdom.enumeration.ObjectKey; import pcgen.cdom.facet.base.AbstractDataFacet; import pcgen.cdom.facet.event.DataFacetChangeEvent; import pcgen.cdom.inst.PCClassLevel; import pcgen.core.PCClass; import pcgen.output.publish.OutputDB; /** * ClassFacet is a Facet that tracks the PCClass objects possessed by a Player * Character. * * @author Thomas Parker (thpr [at] yahoo.com) */ public class ClassFacet extends AbstractDataFacet<CharID, PCClass> implements SetFacet<CharID, PCClass> { private final ClassLevelChangeSupport support = new ClassLevelChangeSupport(); /** * Add the given PCClass to the list of PCClass objects stored in this * ClassFacet for the Player Character represented by the given CharID. * * @param id * The CharID representing the Player Character for which the * given PCClass should be added * @param obj * The PCClass to be added to the list of PCClass objects stored * in this AbstractListFacet for the Player Character represented * by the given CharID */ public void addClass(CharID id, PCClass obj) { if (obj == null) { throw new IllegalArgumentException("PCClass to add may not be null"); } if (getConstructingClassInfo(id).addClass(obj)) { fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_ADDED); } } /** * Sets the PCClassLevel object associated with the given PCClass for the Player * Character represented by the given CharID. Returns true if the set is successful. * The set will be successful if the given PCClass is possessed by the given * PlayerCharacter; false otherwise. * * The (numeric) class level for which the given PCClassLevel should be applied is * determined by the level value set in the PCClassLevel. * * @param id * The CharID representing the Player Character for which the given * PCClassLevel should be set * @param pcc * The PCClass object for which the PCClassLevel object is set as the * PCClass * @param pcl * The PCClassLevel object to be associated with the given PCClass and * Player Character represented by the given CharID * @return true if the set is successful; false otherwise. * @throws CloneNotSupportedException * if the class level cannot be thrown */ public boolean setClassLevel(CharID id, PCClass pcc, PCClassLevel pcl) throws CloneNotSupportedException { if (pcc == null) { throw new IllegalArgumentException( "Class cannot be null in setClassLevel"); } if (pcl == null) { throw new IllegalArgumentException( "Class Level cannot be null in setClassLevel"); } ClassInfo info = getClassInfo(id); if (info == null) { return false; } PCClassLevel old = info.getClassLevel(pcc, pcl.get(IntegerKey.LEVEL)); boolean returnVal = info.setClassLevel(pcc, pcl); support.fireClassLevelObjectChangeEvent(id, pcc, old, pcl); return returnVal; } /** * Returns the PCClassLevel object associated with the Player Character * represented by the given CharID, the given PCClass, and the given * (numeric) class level. * * @param id * The CharID representing the Player Character for which the * associated PCClassLevel will be returned * @param obj * The PCClass object for which the PCClassLevel object should be * returned * @param level * The (numeric) class level for which the PCClassLevel object * should be returned * @return The PCClassLevel object associated with the Player Character * represented by the given CharID, the given PCClass, and the given * (numeric) class level. */ public PCClassLevel getClassLevel(CharID id, PCClass obj, int level) { ClassInfo info = getClassInfo(id); if (info == null) { return null; } return info.getClassLevel(obj, level); } /** * Remove the given PCClass from the list of PCClass objects stored in this * ClassFacet for the Player Character represented by the given CharID. * * @param id * The CharID representing the Player Character from which the * given PCClass should be removed * @param obj * The PCClass to be removed from the list of PCClass objects * stored in this AbstractListFacet for the Player Character * represented by the given CharID */ public void removeClass(CharID id, PCClass obj) { if (obj == null) { throw new IllegalArgumentException("PCClass to add may not be null"); } ClassInfo info = getClassInfo(id); if (info != null) { if (info.containsClass(obj)) { setLevel(id, obj, 0); info.removeClass(obj); fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED); } if (info.isEmpty()) { removeCache(id); } } } /** * Removes all PCClass objects from the list of PCClass objects stored in * this ClassFacet for the Player Character represented by the given CharID. * * @param id * The CharID representing the Player Character from which all * PCClass objects should be removed */ public ClassInfo removeAllClasses(CharID id) { ClassInfo info = (ClassInfo) removeCache(id); if (info != null) { for (PCClass obj : info.getClassSet()) { fireDataFacetChangeEvent(id, obj, DataFacetChangeEvent.DATA_REMOVED); int oldLevel = info.getLevel(obj); support.fireClassLevelChangeEvent(id, obj, oldLevel, 0); } } return info; } /** * Replaces the given old PCClass stored in this ClassFacet with the given * new PCClass for the Player Character represented by the given CharID. * * @param id * The CharID representing the Player Character from which the * given old PCClass should be replaced * @param oldClass * The old PCClass to be removed from the list of PCClass objects * stored in this ClassFacet for the Player Character represented * by the given CharID * @param newClass * The new PCClass to replace the old PCClass stored in this * ClassFacet for the Player Character represented by the given * CharID */ public void replaceClass(CharID id, PCClass oldClass, PCClass newClass) { ClassInfo info = getClassInfo(id); if (info != null) { info.replace(oldClass, newClass); } } /** * Returns a non-null copy of the Set of PCClass objects in this ClassFacet * for the Player Character represented by the given CharID. This method * returns an empty Set if no objects are in this ClassFacet for the Player * Character identified by the given CharID. * * 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 ClassFacet and modification of this * ClassFacet 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 ClassFacet. If you wish to modify * the information stored in this ClassFacet, you must use the add*() and * remove*() methods of ClassFacet. * * @param id * The CharID representing the Player Character for which the * items in this ClassFacet should be returned. * @return A non-null Set of PCClass objects in this ClassFacet for the * Player Character represented by the given CharID */ @Override public Set<PCClass> getSet(CharID id) { ClassInfo info = getClassInfo(id); if (info == null) { return Collections.emptySet(); } return info.getClassSet(); } /** * Returns the count of PCClass objects in this ClassFacet for the Player * Character represented by the given CharID. * * @param id * The CharID representing the Player Character for which the * count of PCClass objects should be returned * @return The count of PCClass objects in this ClassFacet for the Player * Character represented by the given CharID */ @Override public int getCount(CharID id) { ClassInfo info = getClassInfo(id); if (info == null) { return 0; } return info.classCount(); } /** * Returns true if this ClassFacet does not contain any PCClass objects for * the Player Character represented by the given CharID. * * @param id * The CharId representing the PlayerCharacter to test if any * PCClass objects are contained by this AbstractListFacet * @return true if this ClassFacet does not contain any PCClass objects 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) { ClassInfo info = getClassInfo(id); return info == null || info.isEmpty(); } /** * Returns true if this ClassFacet contains the given PCClass in the list of * PCClass objects for the Player Character represented by the given CharID. * * @param id * The CharID representing the Player Character used for testing * @param obj * The PCClass to test if this ClassFacet contains that PCClass * for the Player Character represented by the given CharID * @return true if this AbstractListFacet contains the given PCClass for the * Player Character represented by the given CharID; false otherwise */ public boolean contains(CharID id, PCClass obj) { ClassInfo info = getClassInfo(id); return info != null && info.containsClass(obj); } /** * Sets the level for the given PCClass and the Player Character identified * by the given CharID to the given value. * * @param id * The CharID identifying the Player Character for which a level * value is being set * @param pcc * The PCClass identifying which class level is being set * @param level * The level of the PCClass for the Player Character identified * by the given CharID */ public void setLevel(CharID id, PCClass pcc, int level) { int oldLevel = getConstructingClassInfo(id).setLevel(pcc, level); support.fireClassLevelChangeEvent(id, pcc, oldLevel, level); } /** * Returns the current (numerical) level for the given PCClass in the Player * Character identified by the given CharID. * * @param id * The CharID identifying the Player Character for which the * level of the given PCClass should be returned * @param pcc * The PCClass for which the level of the Player Character should * be returned * @return The numeric value of the level for the given PCClass in the * Player Character identified by the given CharID */ public int getLevel(CharID id, PCClass pcc) { ClassInfo info = getClassInfo(id); return (info == null) ? 0 : info.getLevel(pcc); } /** * Returns the ClassInfo for this ClassFacet and the given CharID. May * return null if no information has been set in this ClassFacet for the * given CharID. * * Note that this method SHOULD NOT be public. The ClassInfo is owned by * ClassFacet, and since it can be modified, a reference to that object * should not be exposed to any object other than ClassFacet. * * @param id * The CharID for which the ClassInfo should be returned * @return The ClassInfo for the Player Character represented by the given * CharID; null if no information has been set in this ClassFacet * for the Player Character. */ private ClassInfo getClassInfo(CharID id) { return (ClassInfo) getCache(id); } /** * Returns a ClassInfo for this ClassFacet and the given CharID. Will return * a new, empty ClassInfo if no information has been set in this ClassFacet * for the given CharID. Will not return null. * * Note that this method SHOULD NOT be public. The ClassInfo object is owned * by ClassFacet, and since it can be modified, a reference to that object * should not be exposed to any object other than ClassFacet. * * @param id * The CharID for which the ClassInfo should be returned * @return The ClassInfo for the Player Character represented by the given * CharID. */ private ClassInfo getConstructingClassInfo(CharID id) { ClassInfo info = getClassInfo(id); if (info == null) { info = new ClassInfo(); setCache(id, info); } return info; } /** * ClassInfo is the Class used by ClassFacet to store information in the * global character cache. This stores both the PCClassLevel objects active * for a PCClass and Player Character, as well as the levels of the * PCClasses for a Player Character. * * @author Thomas Parker (thpr [at] yahoo.com) */ public static class ClassInfo { /** * Map that stores the PCClassLevel objects active for a Player * Character. */ private Map<PCClass, Map<Integer, PCClassLevel>> map = new LinkedHashMap<>(); /** * Map that stores the numeric level values for the PCClasses in a * Player Character. */ private Map<PCClass, Integer> levelmap = new HashMap<>(); public ClassInfo() { //Default constructor for an empty ClassInfo } public ClassInfo(ClassInfo info) { for (Map.Entry<PCClass, Map<Integer, PCClassLevel>> me : info.map .entrySet()) { map.put(me.getKey(), new HashMap<>(me .getValue())); } levelmap.putAll(info.levelmap); } public Integer setLevel(PCClass pcc, int level) { if (pcc == null) { throw new IllegalArgumentException( "Class for setLevel must not be null"); } if (level < 0) { throw new IllegalArgumentException("Level for " + pcc.getDisplayName() + " must be > 0"); } if (level != 0 && !map.containsKey(pcc)) { throw new IllegalArgumentException( "Cannot set level for PCClass " + pcc.getKeyName() + " which is not added"); } Integer oldlvl = levelmap.put(pcc, level); return (oldlvl == null) ? 0 : oldlvl; } public int getLevel(PCClass pcc) { Integer level = levelmap.get(pcc); return (level == null) ? 0 : level; } public void replace(PCClass oldClass, PCClass newClass) { Map<PCClass, Map<Integer, PCClassLevel>> oldMap = map; map = new LinkedHashMap<>(); for (Map.Entry<PCClass, Map<Integer, PCClassLevel>> me : oldMap .entrySet()) { PCClass currentClass = me.getKey(); if (oldClass.equals(currentClass)) { addClass(newClass); } else { map.put(currentClass, me.getValue()); } } } public boolean addClass(PCClass pcc) { if (map.containsKey(pcc)) { return false; } Map<Integer, PCClassLevel> levelMap = new HashMap<>(); map.put(pcc, levelMap); /* * DO NOT initialize levelMap here - see CODE-208 */ return true; } public boolean setClassLevel(PCClass pcc, PCClassLevel pcl) throws CloneNotSupportedException { Map<Integer, PCClassLevel> localMap = map.get(pcc); if (localMap == null) { return false; } pcl.ownBonuses(pcc); pcl.put(ObjectKey.PARENT, pcc); localMap.put(pcl.get(IntegerKey.LEVEL), pcl); return true; } public PCClassLevel getClassLevel(PCClass pcc, int level) { if (pcc == null) { throw new IllegalArgumentException( "Class in getClassLevel cannot be null"); } if (level < 0) { throw new IllegalArgumentException( "Level cannot be negative in getClassLevel"); } //map.get(pcc) doesn't seem to find the monster class //Map<Integer, PCClassLevel> localMap = map.get(pcc); Map<Integer, PCClassLevel> localMap = null; for (final Map.Entry<PCClass, Map<Integer, PCClassLevel>> pcClassMapEntry : map.entrySet()) { if (pcc.equals(pcClassMapEntry.getKey())) { localMap = pcClassMapEntry.getValue(); break; } } if (localMap == null) { throw new IllegalArgumentException( "Level cannot be returned for Class " + pcc.getKeyName() + " which is not in the PC"); } PCClassLevel classLevel = localMap.get(level); if (classLevel == null) { classLevel = pcc.getOriginalClassLevel(level); classLevel.put(ObjectKey.PARENT, pcc); localMap.put(level, classLevel); } return classLevel; } public boolean removeClass(PCClass pcc) { boolean returnValue = map.containsKey(pcc); map.remove(pcc); return returnValue; } public Set<PCClass> getClassSet() { return Collections.unmodifiableSet(map.keySet()); } public boolean isEmpty() { return map.isEmpty(); } public int classCount() { return map.size(); } public boolean containsClass(PCClass pcc) { return map.containsKey(pcc); } @Override public int hashCode() { return map.hashCode(); } @Override public boolean equals(Object o) { if (o instanceof ClassInfo) { ClassInfo other = (ClassInfo) o; return map.equals(other.map) && levelmap.equals(other.levelmap); } return false; } } @Override public void copyContents(CharID source, CharID destination) { ClassInfo info = getClassInfo(source); if (info != null) { setCache(destination, new ClassInfo(info)); } } public void addLevelChangeListener(ClassLevelChangeListener listener) { support.addLevelChangeListener(listener); } public ClassLevelChangeListener[] getLevelChangeListeners() { return support.getLevelChangeListeners(); } public void removeLevelChangeListener(ClassLevelChangeListener listener) { support.removeLevelChangeListener(listener); } public static interface ClassLevelChangeListener extends EventListener { public void levelChanged(ClassLevelChangeEvent lce); public void levelObjectChanged(ClassLevelObjectChangeEvent lce); } public static class ClassLevelChangeEvent extends EventObject { /** * The ID indicating the owning character for this ClassLevelChangeEvent */ private final CharID charID; private final PCClass pcClass; private final int oldLvl; private final int newLvl; public ClassLevelChangeEvent(CharID source, PCClass pcc, int oldLevel, int newLevel) { super(source); if (source == null) { throw new IllegalArgumentException("CharID cannot be null"); } if (pcc == null) { throw new IllegalArgumentException("PCClass cannot be null"); } charID = source; pcClass = pcc; oldLvl = oldLevel; newLvl = newLevel; } /** * Returns an identifier indicating the PlayerCharacter on which this * event occurred. * * @return A identifier indicating the PlayerCharacter on which this * event occurred. */ public CharID getCharID() { return charID; } public PCClass getPCClass() { return pcClass; } public int getOldLevel() { return oldLvl; } public int getNewLevel() { return newLvl; } } public static class ClassLevelObjectChangeEvent extends EventObject { /** * The ID indicating the owning character for this ClassLevelChangeEvent */ private final CharID charID; private final PCClass pcClass; private final PCClassLevel oldLvl; private final PCClassLevel newLvl; public ClassLevelObjectChangeEvent(CharID source, PCClass pcc, PCClassLevel oldLevel, PCClassLevel newLevel) { super(source); if (source == null) { throw new IllegalArgumentException("CharID cannot be null"); } if (pcc == null) { throw new IllegalArgumentException("PCClass cannot be null"); } if (newLevel == null) { throw new IllegalArgumentException("New Level cannot be null"); } charID = source; pcClass = pcc; oldLvl = oldLevel; newLvl = newLevel; } /** * Returns an identifier indicating the PlayerCharacter on which this * event occurred. * * @return A identifier indicating the PlayerCharacter on which this * event occurred. */ public CharID getCharID() { return charID; } public PCClass getPCClass() { return pcClass; } public PCClassLevel getOldLevel() { return oldLvl; } public PCClassLevel getNewLevel() { return newLvl; } } public static class ClassLevelChangeSupport { /** * The listeners to which LevelChangeEvents will be fired when a change * in the source ClassFacet occurs. */ private final EventListenerList listenerList = new EventListenerList(); /** * Adds a new ClassLevelChangeListener to receive LevelChangeEvents * (EdgeChangeEvent and NodeChangeEvent) from the source ClassFacet. * * @param listener * The LevelChangeListener to receive LevelChangeEvents */ public void addLevelChangeListener(ClassLevelChangeListener listener) { listenerList.add(ClassLevelChangeListener.class, listener); } /** * Returns an Array of LevelChangeListeners receiving LevelChangeEvents * from the source ClassFacet. * * Ownership of the returned Array is transferred to the calling Object. * No reference to the Array is maintained by ClassLevelChangeSupport. * However, the LevelChangeListeners contained in the Array are * (obviously!) returned BY REFERENCE, and care should be taken with * modifying those LevelChangeListeners.* * * @return An Array of LevelChangeListeners receiving LevelChangeEvents * from the source ClassFacet */ public synchronized ClassLevelChangeListener[] getLevelChangeListeners() { return listenerList.getListeners(ClassLevelChangeListener.class); } /** * Removes a LevelChangeListener so that it will no longer receive * LevelChangeEvents from the source ClassFacet. * * @param listener * The LevelChangeListener to be removed */ public void removeLevelChangeListener(ClassLevelChangeListener listener) { listenerList.remove(ClassLevelChangeListener.class, listener); } /** * Sends a NodeChangeEvent to the LevelChangeListeners that are * receiving LevelChangeEvents from the source ClassFacet. * * @param id * The CharID that has beed added to or removed from the source * ClassFacet * @param pcc * The PCClass to be added to the list of PCClass objects stored * in this AbstractListFacet for the Player Character represented * by the given CharID * @param oldLevel * The chracter's previous level * * @param newLevel * The new level specified by the user. */ protected void fireClassLevelChangeEvent(CharID id, PCClass pcc, int oldLevel, int newLevel) { if (oldLevel == newLevel) { // Nothing to do return; } ClassLevelChangeListener[] listeners = listenerList.getListeners(ClassLevelChangeListener.class); /* * This list is decremented from the end of the list to the * beginning in order to maintain consistent operation with how Java * AWT and Swing listeners are notified of Events (they are in * reverse order to how they were added to the Event-owning object). */ ClassLevelChangeEvent ccEvent = null; for (int i = listeners.length - 1; i >= 0; i--) { // Lazily create event if (ccEvent == null) { ccEvent = new ClassLevelChangeEvent(id, pcc, oldLevel, newLevel); } listeners[i].levelChanged(ccEvent); } } public void fireClassLevelObjectChangeEvent(CharID id, PCClass pcc, PCClassLevel oldLevel, PCClassLevel newLevel) { if (oldLevel == newLevel) { // Nothing to do return; } ClassLevelChangeListener[] listeners = listenerList.getListeners(ClassLevelChangeListener.class); /* * This list is decremented from the end of the list to the * beginning in order to maintain consistent operation with how Java * AWT and Swing listeners are notified of Events (they are in * reverse order to how they were added to the Event-owning object). */ ClassLevelObjectChangeEvent ccEvent = null; for (int i = listeners.length - 1; i >= 0; i--) { // Lazily create event if (ccEvent == null) { ccEvent = new ClassLevelObjectChangeEvent(id, pcc, oldLevel, newLevel); } listeners[i].levelObjectChanged(ccEvent); } } } public void init() { OutputDB.register("classes", this); } }