/*
* 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.fact;
import pcgen.base.lang.ObjectUtil;
import pcgen.cdom.base.Constants;
import pcgen.cdom.enumeration.CharID;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.enumeration.Region;
import pcgen.cdom.enumeration.SubRegion;
import pcgen.cdom.facet.base.AbstractDataFacet;
import pcgen.cdom.facet.event.DataFacetChangeEvent;
import pcgen.cdom.facet.event.DataFacetChangeListener;
import pcgen.cdom.facet.model.TemplateFacet;
import pcgen.core.PCTemplate;
/**
* RegionFacet is a Facet that tracks the Region and SubRegion of a Player
* Character. The Region and SubRegion can be set explicitly or inferred from
* the PCTemplate objects possessed by the PlayerCharacter.
*
* @author Thomas Parker (thpr [at] yahoo.com)
*/
public class RegionFacet extends AbstractDataFacet<CharID, String> implements
DataFacetChangeListener<CharID, PCTemplate>
{
/*
* TODO A LOT of this should be type-safe to Region and SubRegion objects,
* but that is currently gated by how Template returns objects.
*/
private TemplateFacet templateFacet;
/**
* Returns the type-safe RegionCacheInfo for this RegionFacet and the given
* CharID. Will return a new, empty RegionCacheInfo if no Region information
* has been set for the given CharID. Will not return null.
*
* Note that this method SHOULD NOT be public. The RegionCacheInfo object is
* owned by RegionFacet, and since it can be modified, a reference to that
* object should not be exposed to any object other than RegionFacet.
*
* @param id
* The CharID for which the RegionCacheInfo should be returned
* @return The RegionCacheInfo for the Player Character represented by the
* given CharID.
*/
private RegionCacheInfo getConstructingInfo(CharID id)
{
RegionCacheInfo rci = getInfo(id);
if (rci == null)
{
rci = new RegionCacheInfo();
setCache(id, rci);
}
return rci;
}
/**
* Returns the type-safe RegionCacheInfo for this RegionFacet and the given
* CharID. May return null if no Region information has been set for the
* given CharID.
*
* Note that this method SHOULD NOT be public. The RegionCacheInfo object is
* owned by RegionFacet, and since it can be modified, a reference to that
* object should not be exposed to any object other than RegionFacet.
*
* @param id
* The CharID for which the RegionCacheInfo should be returned
* @return The RegionCacheInfo for the Player Character represented by the
* given CharID; null if no Region information has been set for the
* Player Character.
*/
private RegionCacheInfo getInfo(CharID id)
{
return (RegionCacheInfo) getCache(id);
}
/**
* Sets the character Region for the Player Character represented by the
* given CharID to the given Region. This set Region will override any
* Region provided by a PCTemplate.
*
* @param id
* The CharID representing the Player Character for which the
* Region will be set
* @param region
* The Region for the Player Character represented by the given
* CharID
*/
public void setRegion(CharID id, Region region)
{
getConstructingInfo(id).region = region;
updateRegion(id);
}
/**
* Sets the character SubRegion for the Player Character represented by the
* given CharID to the given SubRegion. This set SubRegion will override any
* SubRegion provided by a PCTemplate.
*
* @param id
* The CharID representing the Player Character for which the
* SubRegion will be set
* @param subregion
* The SubRegion for the Player Character represented by the
* given CharID
*/
public void setSubRegion(CharID id, SubRegion subregion)
{
getConstructingInfo(id).subregion = subregion;
}
/**
* Returns a String representation of the character Region for the Player
* Character represented by the given CharID. Returns "NONE" if no character
* Region is set for the Player Character
*
* **NOTE** Unless you are analyzing (or storing) raw values for a Player
* Character, it is unlikely that you want this method. It is more likely
* that you should be using getRegion(CharID id)
*
* @see RegionFacet#getRegion(CharID)
*
* @param id
* The CharID representing the Player Character for which the
* character Region should be returned.
* @return A String representation of the character Region for the Player
* Character represented by the given CharID; "NONE" if no character
* Region is set for the Player Character
*/
public String getCharacterRegion(CharID id)
{
RegionCacheInfo rci = getInfo(id);
if (rci != null && rci.region != null)
{
return rci.region.toString();
}
return Constants.NONE;
}
/**
* Returns a String representation of the Region for the Player Character
* represented by the given CharID. Returns "NONE" if no Region is set for
* the Player Character.
*
* @param id
* The CharID representing the Player Character for which the
* Region should be returned.
* @return A String representation of the Region for the Player Character
* represented by the given CharID; "NONE" if no Region is set for
* the Player Character
*/
public String getRegion(CharID id)
{
String charRegion = getCharacterRegion(id);
if (!charRegion.equalsIgnoreCase(Constants.NONE))
{
return charRegion;
}
String region = Constants.NONE;
for (PCTemplate template : templateFacet.getSet(id))
{
String tempRegion = getTemplateRegion(template);
if (!tempRegion.equals(Constants.NONE))
{
region = tempRegion;
}
}
return region;
}
private String getTemplateRegion(PCTemplate template)
{
/*
* TODO This should be made type safe to return a Region. Will require a
* change in the REGION token to suppress load of "None" (corner case)
*/
Region sr = template.get(ObjectKey.REGION);
if (sr == null)
{
if (template.getSafe(ObjectKey.USETEMPLATENAMEFORREGION))
{
return template.getDisplayName();
}
return Constants.NONE;
}
return sr.toString();
}
/**
* Returns true if the Region of the Player Character represented by the
* given CharID matches the given Region. This method tests the Region
* (which includes Region as set by PCTemplate objects), not solely the
* character Region. This does not compare the SubRegion.
*
* @param id
* The CharID representing the Player Character for which the
* given Region will be tested to see if it matches the Player
* Character's Region.
* @param r
* The Region to be tested to determine if it matches the Player
* Character's Region
* @return true if the Region of the Player Character represented by the
* given CharID matches the given Region; false otherwise.
*/
public boolean matchesRegion(CharID id, Region r)
{
String current = getRegion(id);
/*
* TODO If this is true (null matches None), should Region prohibit the
* building of Region.NONE or we have to double test here? or two ways
* of specifying NONE?
*/
return (r == null && Constants.NONE.equals(current))
|| (r != null && r.toString().equalsIgnoreCase(current));
}
/**
* Returns a String representation of the character SubRegion for the Player
* Character represented by the given CharID. Returns "NONE" if no character
* SubRegion is set for the Player Character
*
* **NOTE** Unless you are analyzing (or storing) raw values for a Player
* Character, it is unlikely that you want this method. It is more likely
* that you should be using getSubRegion(CharID id)
*
* @see RegionFacet#getSubRegion(CharID)
*
* @param id
* The CharID representing the Player Character for which the
* character SubRegion should be returned.
* @return A String representation of the character SubRegion for the Player
* Character represented by the given CharID; "NONE" if no character
* SubRegion is set for the Player Character
*/
public String getCharacterSubRegion(CharID id)
{
RegionCacheInfo rci = getInfo(id);
// character's subregion trumps any from templates
if (rci != null && rci.subregion != null)
{
return rci.subregion.toString();
}
return Constants.NONE;
}
/**
* Returns a String representation of the SubRegion for the Player Character
* represented by the given CharID. Returns "NONE" if no SubRegion is set
* for the Player Character.
*
* @param id
* The CharID representing the Player Character for which the
* Region should be returned.
* @return A String representation of the SubRegion for the Player Character
* represented by the given CharID; "NONE" if no SubRegion is set
* for the Player Character
*/
public String getSubRegion(CharID id)
{
RegionCacheInfo rci = getInfo(id);
// character's subregion trumps any from templates
if (rci != null && rci.subregion != null)
{
return rci.subregion.toString();
}
String s = Constants.NONE;
for (PCTemplate template : templateFacet.getSet(id))
{
final String tempSubRegion = getTemplateSubRegion(template);
if (!tempSubRegion.equals(Constants.NONE))
{
s = tempSubRegion;
}
}
return s;
}
private String getTemplateSubRegion(PCTemplate template)
{
/*
* TODO This should be made type safe to return a SubRegion. Will
* require a change in the SUBREGION token to suppress load of "None"
* (corner case)
*/
SubRegion sr = template.get(ObjectKey.SUBREGION);
if (sr == null)
{
if (template.getSafe(ObjectKey.USETEMPLATENAMEFORSUBREGION))
{
return template.getDisplayName();
}
return Constants.NONE;
}
return sr.toString();
}
/**
* Returns a String representation of the full Region (Region and SubRegion)
* for the Player Character represented by the given CharID. Returns "NONE"
* if no Region is set for the Player Character.
*
* @param id
* The CharID representing the Player Character for which the
* full Region should be returned.
* @return A String representation of the full Region for the Player
* Character represented by the given CharID; "NONE" if no Region is
* set for the Player Character
*/
public String getFullRegion(CharID id)
{
final String sub = getSubRegion(id);
final StringBuilder tempRegName = new StringBuilder(40)
.append(getRegion(id));
if (!sub.equals(Constants.NONE))
{
tempRegName.append(" (").append(sub).append(')');
}
return tempRegName.toString();
}
/**
* RegionClassInfo is the data structure used by RegionFacet to store a
* Player Character's Region and SubRegion if they are directly set by a
* user.
*/
private static class RegionCacheInfo
{
public String cachedRegion;
public Region region;
public SubRegion subregion;
@Override
public String toString()
{
return region + " " + subregion + " " + cachedRegion;
}
@Override
public int hashCode()
{
return (region == null ? -1 : region.hashCode());
}
@Override
public boolean equals(Object o)
{
if (o instanceof RegionCacheInfo)
{
RegionCacheInfo other = (RegionCacheInfo) o;
return ObjectUtil.compareWithNull(region, other.region)
&& ObjectUtil.compareWithNull(subregion, other.subregion)
&& ObjectUtil.compareWithNull(cachedRegion, other.cachedRegion);
}
return false;
}
}
/**
* Copies the contents of the RegionFacet from one Player Character to
* another Player Character, based on the given CharIDs representing those
* Player Characters.
*
* This is a method in RegionFacet in order to avoid exposing the mutable
* RegionCacheInfo object to other classes. This should not be inlined, as
* RegionCacheInfo is internal information to RegionFacet and should not be
* exposed to other classes.
*
* Note also the copy is a one-time event and no Region references are
* maintained between the Player Characters represented by the given CharIDs
* (meaning once this copy takes place, any change to the Region will only
* impact the Player Character where the Region was changed).
*
* @param source
* The CharID representing the Player Character from which the
* Region information should be copied
* @param destination
* The CharID representing the Player Character to which the
* Region information should be copied
*/
@Override
public void copyContents(CharID source, CharID destination)
{
RegionCacheInfo sourceRCI = getInfo(source);
if (sourceRCI != null)
{
RegionCacheInfo destRCI = getConstructingInfo(destination);
destRCI.region = sourceRCI.region;
destRCI.subregion = sourceRCI.subregion;
}
}
/**
* Drives an update of the Region and SubRegion for a Player Character when
* a CDOMObject is added to a Player Character.
*
* Triggered when one of the Facets to which RegionFacet listens fires a
* DataFacetChangeEvent to indicate a CDOMObject was added to a Player
* Character.
*
* @param dfce
* The DataFacetChangeEvent containing the information about the
* change
*
* @see pcgen.cdom.facet.event.DataFacetChangeListener#dataAdded(pcgen.cdom.facet.event.DataFacetChangeEvent)
*/
@Override
public void dataAdded(DataFacetChangeEvent<CharID, PCTemplate> dfce)
{
updateRegion(dfce.getCharID());
}
private void updateRegion(CharID id)
{
RegionCacheInfo rci = getInfo(id);
String current = rci.cachedRegion;
String newRegion = getRegion(id);
if (current == null || !current.equals(newRegion))
{
if (current != null)
{
fireDataFacetChangeEvent(id, current,
DataFacetChangeEvent.DATA_REMOVED);
}
rci.cachedRegion = newRegion;
fireDataFacetChangeEvent(id, newRegion,
DataFacetChangeEvent.DATA_ADDED);
}
}
/**
* Drives an update of the Region and SubRegion for a Player Character when
* a CDOMObject is removed from a Player Character.
*
* Triggered when one of the Facets to which RegionFacet listens fires a
* DataFacetChangeEvent to indicate a CDOMObject was removed from a Player
* Character.
*
* @param dfce
* The DataFacetChangeEvent containing the information about the
* change
*
* @see pcgen.cdom.facet.event.DataFacetChangeListener#dataRemoved(pcgen.cdom.facet.event.DataFacetChangeEvent)
*/
@Override
public void dataRemoved(DataFacetChangeEvent<CharID, PCTemplate> dfce)
{
updateRegion(dfce.getCharID());
}
public void setTemplateFacet(TemplateFacet templateFacet)
{
this.templateFacet = templateFacet;
}
}