/* * Copyright (c) Thomas Parker, 2012. * * 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; import java.util.Collection; import java.util.Collections; import java.util.EventListener; import java.util.EventObject; import java.util.HashMap; import java.util.IdentityHashMap; import java.util.Map; import java.util.Map.Entry; import javax.swing.event.EventListenerList; import pcgen.cdom.enumeration.CharID; import pcgen.cdom.facet.base.AbstractStorageFacet; import pcgen.core.PCClass; import pcgen.core.Skill; /** * SkillRankFacet stores the number of Skill Ranks for a specific Skill for a * Player Character. * * @author Thomas Parker (thpr [at] yahoo.com) */ public class SkillRankFacet extends AbstractStorageFacet<CharID> { private SkillRankChangeSupport support = new SkillRankChangeSupport(); /** * Returns the type-safe CacheInfo for this SkillRankFacet and the given * CharID. Will return a new, empty CacheInfo if no Skill information has * been set for the given CharID. Will not return null. * * Note that this method SHOULD NOT be public. The CacheInfo object is owned * by SkillRankFacet, and since it can be modified, a reference to that * object should not be exposed to any object other than SkillRankFacet. * * @param id * The CharID for which the CacheInfo should be returned * @return The CacheInfo for the Player Character represented by the given * CharID. */ private Map<Skill, Map<PCClass, Double>> getConstructingInfo(CharID id) { Map<Skill, Map<PCClass, Double>> map = getInfo(id); if (map == null) { map = new HashMap<>(); setCache(id, map); } return map; } /** * Returns the type-safe CacheInfo for this SkillRankFacet and the given * CharID. May return null if no Skill information has been set for the * given CharID. * * Note that this method SHOULD NOT be public. The CacheInfo object is owned * by SkillRankFacet, and since it can be modified, a reference to that * object should not be exposed to any object other than SkillRankFacet. * * @param id * The CharID for which the CacheInfo should be returned * @return The CacheInfo for the Player Character represented by the given * CharID; null if no Skill information has been set for the Player * Character. */ private Map<Skill, Map<PCClass, Double>> getInfo(CharID id) { return (Map<Skill, Map<PCClass, Double>>) getCache(id); } public void set(CharID id, Skill skill, PCClass pcc, double value) { if (skill == null) { throw new IllegalArgumentException("Skill cannot be null in add"); } Float oldRank = getRank(id, skill); Map<Skill, Map<PCClass, Double>> map = getConstructingInfo(id); Map<PCClass, Double> clMap = map.get(skill); if (clMap == null) { clMap = new IdentityHashMap<>(); map.put(skill, clMap); } clMap.put(pcc, value); Float newRank = getRank(id, skill); support.fireSkillRankChangeEvent(id, skill, oldRank, newRank); } /** * Copies the contents of the SkillRankFacet from one Player Character to * another Player Character, based on the given CharIDs representing those * Player Characters. * * This is a method in SkillRankFacet in order to avoid exposing the mutable * Map object to other classes. This should not be inlined, as the Map is * internal information to SkillRankFacet and should not be exposed to other * classes. * * 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 SkillRankFacet of one * Player Character will only impact the Player Character where the * SkillRankFacet was changed). * * @param source * The CharID representing the Player Character from which the * information should be copied * @param copy * The CharID representing the Player Character to which the * information should be copied */ @Override public void copyContents(CharID source, CharID copy) { Map<Skill, Map<PCClass, Double>> map = getInfo(source); if (map != null) { for (Entry<Skill, Map<PCClass, Double>> fme : map.entrySet()) { Skill sk = fme.getKey(); for (Entry<PCClass, Double> clEntry : fme.getValue().entrySet()) { set(copy, sk, clEntry.getKey(), clEntry.getValue()); } } } } public Collection<PCClass> getClasses(CharID id, Skill sk) { Map<Skill, Map<PCClass, Double>> map = getInfo(id); if (map == null) { return Collections.emptyList(); } Map<PCClass, Double> clMap = map.get(sk); if (clMap == null) { return Collections.emptyList(); } return Collections.unmodifiableSet(clMap.keySet()); } public Double get(CharID id, Skill sk, PCClass pcc) { Map<Skill, Map<PCClass, Double>> map = getInfo(id); if (map == null) { return null; } Map<PCClass, Double> clMap = map.get(sk); if (clMap == null) { return null; } return clMap.get(pcc); } public void remove(CharID id, Skill sk, PCClass pcc) { if (sk == null) { throw new IllegalArgumentException("Skill cannot be null in remove"); } Map<Skill, Map<PCClass, Double>> map = getInfo(id); if (map != null) { Map<PCClass, Double> clMap = map.get(sk); if (clMap != null) { Float oldRank = getRank(id, sk); clMap.remove(pcc); Float newRank = getRank(id, sk); support.fireSkillRankChangeEvent(id, sk, oldRank, newRank); } } } public float getRank(CharID id, Skill sk) { double rank = 0.0; Map<Skill, Map<PCClass, Double>> map = getInfo(id); if (map != null) { Map<PCClass, Double> clMap = map.get(sk); if (clMap != null) { for (Double d : clMap.values()) { rank += d; } } } return (float) rank; } public void addSkillRankChangeListener(SkillRankChangeListener listener) { support.addLevelChangeListener(listener); } @FunctionalInterface public static interface SkillRankChangeListener extends EventListener { public void rankChanged(SkillRankChangeEvent lce); } public static class SkillRankChangeEvent extends EventObject { /** * The ID indicating the owning character for this SkillRankChangeEvent */ private final CharID charID; private final Skill skill; private final float oldRnk; private final float newRnk; public SkillRankChangeEvent(CharID source, Skill sk, float oldRank, float newRank) { super(source); if (source == null) { throw new IllegalArgumentException("CharID cannot be null"); } if (sk == null) { throw new IllegalArgumentException("PCClass cannot be null"); } charID = source; skill = sk; oldRnk = oldRank; newRnk = newRank; } /** * 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 Skill getSkill() { return skill; } public float getOldRank() { return oldRnk; } public float getNewRank() { return newRnk; } } public static class SkillRankChangeSupport { /** * The listeners to which SkillRankChangeEvents will be fired when a * change in the source SkillRankFacet occurs. */ private final EventListenerList listenerList = new EventListenerList(); /** * Adds a new SkillRankChangeListener to receive SkillRankChangeEvents * (EdgeChangeEvent and NodeChangeEvent) from the source SkillRankFacet. * * @param listener * The LevelChangeListener to receive SkillRankChangeEvents */ public void addLevelChangeListener(SkillRankChangeListener listener) { listenerList.add(SkillRankChangeListener.class, listener); } /** * Returns an Array of SkillRankChangeListeners receiving * SkillRankChangeEvents from the source SkillRankFacet. * * Ownership of the returned Array is transferred to the calling Object. * No reference to the Array is maintained by ClassLevelChangeSupport. * However, the SkillRankChangeListeners contained in the Array are * (obviously!) returned BY REFERENCE, and care should be taken with * modifying those SkillRankChangeListeners.* * * @return An Array of SkillRankChangeListeners receiving * SkillRankChangeEvents from the source SkillRankFacet */ public synchronized SkillRankChangeListener[] getLevelChangeListeners() { return listenerList.getListeners(SkillRankChangeListener.class); } /** * Removes a SkillRankChangeListener so that it will no longer receive * SkillRankChangeEvents from the source SkillRankFacet. * * @param listener * The LevelChangeListener to be removed */ public void removeLevelChangeListener(SkillRankChangeListener listener) { listenerList.remove(SkillRankChangeListener.class, listener); } /** * Sends a SkillRankChangeEvent to the SkillRankChangeListeners that are * receiving SkillRankChangeEvents from the source SkillRankFacet. * * @param id * The CharID on which the skill rank change has taken place * @param sk * The Skill to be added to the list of PCClass objects * stored in this Facet for the Player Character represented * by the given CharID * @param oldRank * The character's previous rank for the given skill * * @param newRank * The character's new rank for the given skill. */ protected void fireSkillRankChangeEvent(CharID id, Skill sk, float oldRank, float newRank) { if (oldRank == newRank) { // Nothing to do return; } SkillRankChangeListener[] listeners = listenerList.getListeners(SkillRankChangeListener.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). */ SkillRankChangeEvent ccEvent = null; for (int i = listeners.length - 1; i >= 0; i--) { // Lazily create event if (ccEvent == null) { ccEvent = new SkillRankChangeEvent(id, sk, oldRank, newRank); } listeners[i].rankChanged(ccEvent); } } } }