/*
* PCGIOHandler.java
* Copyright 2002 (C) Thomas Behr <ravenlock@gmx.de>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*
* Created on March 11, 2002, 8:30 PM
*
* Current Ver: $Revision$
*
*/
package pcgen.io;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import pcgen.cdom.base.Constants;
import pcgen.cdom.content.CNAbility;
import pcgen.cdom.enumeration.Nature;
import pcgen.cdom.enumeration.ObjectKey;
import pcgen.cdom.inst.PCClassLevel;
import pcgen.core.AbilityCategory;
import pcgen.core.Equipment;
import pcgen.core.GameMode;
import pcgen.core.PCClass;
import pcgen.core.PlayerCharacter;
import pcgen.core.SpecialAbility;
import pcgen.core.character.EquipSet;
import pcgen.facade.core.CampaignFacade;
import pcgen.facade.core.SourceSelectionFacade;
import pcgen.system.LanguageBundle;
import pcgen.system.PCGenPropBundle;
import pcgen.system.PCGenSettings;
import pcgen.util.FileHelper;
import pcgen.util.Logging;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.Nullable;
/**
* {@code PCGIOHandler}<br>
* Reading and Writing PlayerCharacters in PCGen's own format (PCG).
*
* @author Thomas Behr 11-03-02
*/
public final class PCGIOHandler extends IOHandler
{
private final List<String> errors = new ArrayList<>();
private final List<String> warnings = new ArrayList<>();
/**
* Selector
* <p>
* <br>author: Thomas Behr 18-03-02
*
* @return a list of error messages
*/
public List<String> getErrors()
{
return errors;
}
/**
* Convenience Method
* <p>
* <br>author: Thomas Behr 18-03-02
*
* @return a list of messages
*/
public List<String> getMessages()
{
final List<String> messages = new ArrayList<>();
messages.addAll(errors);
messages.addAll(warnings);
return messages;
}
/**
* Selector
* <p>
* <br>author: Thomas Behr 15-03-02
*
* @return a list of warning messages
*/
public List<String> getWarnings()
{
return warnings;
}
public static void buildSALIST(String aChoice, List<String> aAvailable,
List<String> aBonus, final PlayerCharacter currentPC)
{
// SALIST:Smite|VAR|%|1
// SALIST:Turn ,Rebuke|VAR|%|1
String aString;
String aPost = "";
int iOffs = aChoice.indexOf('|', 7);
if (iOffs < 0)
{
aString = aChoice;
}
else
{
aString = aChoice.substring(7, iOffs);
aPost = aChoice.substring(iOffs + 1);
}
final List<String> saNames = new ArrayList<>();
final StringTokenizer aTok = new StringTokenizer(aString, ",");
while (aTok.hasMoreTokens())
{
saNames.add(aTok.nextToken());
}
final List<SpecialAbility> aSAList = currentPC.getSpecialAbilityList();
for (String name : saNames)
{
for (SpecialAbility sa : aSAList)
{
String aSA = sa.getKeyName();
if (aSA.startsWith(aString))
{
String aVar = "";
//
// Trim off variable portion of SA, and save variable name
// (eg. "Smite Evil %/day|SmiteEvil" --> aSA = "Smite Evil", aVar = "SmiteEvil")
//
iOffs = aSA.indexOf('|');
if (iOffs >= 0)
{
aVar = aSA.substring(iOffs + 1);
iOffs = aSA.indexOf('%');
if (iOffs >= 0)
{
aSA = aSA.substring(0, iOffs).trim();
}
}
if (!aAvailable.contains(aSA))
{
aAvailable.add(aSA);
//
// Check for variable substitution
//
iOffs = aPost.indexOf('%');
if (iOffs >= 0)
{
aVar =
aPost.substring(0, iOffs) + aVar
+ aPost.substring(iOffs + 1);
}
aBonus.add(aSA + "|" + aVar);
}
}
}
}
}
/**
* Reads the contents of the given PlayerCharacter from a stream
* <p>
* <br>author: Thomas Behr 11-03-02
*
* @param pcToBeRead the PlayerCharacter to store the read data
* @param in the stream to be read from
* @param validate
*/
@Override
public void read(PlayerCharacter pcToBeRead, InputStream in,
final boolean validate)
{
warnings.clear();
final List<String> lines = readPcgLines(in);
boolean isPCGVersion2 = isPCGCersion2(lines);
pcToBeRead.setImporting(true);
final String[] pcgLines = lines.toArray(new String[lines.size()]);
if (isPCGVersion2)
{
final PCGParser parser = new PCGVer2Parser(pcToBeRead);
try
{
// parse it all
parser.parsePCG(pcgLines);
}
catch (PCGParseException pcgex)
{
Logging.errorPrint("Error loading character: "
+ pcgex.getMessage() + "\n Method " + pcgex.getMethod()
+ " was unable to parse line " + pcgex.getLine());
errors.add(LanguageBundle.getFormattedString(
"in_pcgIoErrorReport", pcgex.getMessage())); //$NON-NLS-1$
}
warnings.addAll(parser.getWarnings());
// we are now all done with the import parsing, so turn off
// the Importing flag and then do some sanity checks
pcToBeRead.setImporting(false);
try
{
sanityChecks(pcToBeRead, parser);
}
catch (NumberFormatException ex)
{
errors.add(ex.getMessage() + Constants.LINE_SEPARATOR
+ "Method: sanityChecks");
}
pcToBeRead.setDirty(false);
}
else
{
errors.add("Cannot open PCG file");
}
}
private boolean isPCGCersion2(List<String> lines)
{
for (String aLine : lines)
{
if (aLine.startsWith(IOConstants.TAG_PCGVERSION))
{
return true;
}
}
return false;
}
/**
* @param in
* @return
*/
private List<String> readPcgLines(InputStream in)
{
final List<String> lines = new ArrayList<>();
// try reading in all the lines in the .pcg file
BufferedReader br = null;
try
{
br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
String aLine;
while ((aLine = br.readLine()) != null)
{
lines.add(aLine);
//isPCGVersion2 |= aLine.startsWith(IOConstants.TAG_PCGVERSION);
}
}
catch (IOException ioe)
{
Logging.errorPrint("Exception in PCGIOHandler::read", ioe);
}
finally
{
try
{
if (br != null) {
br.close();
}
}
catch (IOException e)
{
Logging.errorPrint("Couldn't close file in PCGIOHandler.read",
e);
}
}
return lines;
}
/**
* Writes the contents of the given PlayerCharacter to a stream
* <p>
* <br>author: Thomas Behr 11-03-02
*
* @deprecated The write to a file method should be used in preference as it has safe backup handling.
*
* @param pcToBeWritten the PlayerCharacter to write
* @param out the stream to be written to
*/
@Override
public void write(PlayerCharacter pcToBeWritten, GameMode mode, List<CampaignFacade> campaigns, OutputStream out)
{
final String pcgString;
pcgString = (new PCGVer2Creator(pcToBeWritten, mode, campaigns)).createPCGString();
BufferedWriter bw = null;
try
{
bw = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
bw.write(pcgString);
bw.flush();
pcToBeWritten.setDirty(false);
}
catch (IOException ioe)
{
Logging.errorPrint("Exception in PCGIOHandler::write", ioe);
}
finally
{
try
{
if (bw != null) {
bw.close();
}
}
catch (IOException e)
{
Logging.errorPrint("Couldn't close file in PCGIOHandler.write",
e);
}
}
}
/**
* Writes the contents of the given PlayerCharacter to a file. This method also includes
* safely backing up the original character file, but only once we know we have
* successfully exported the character to a string ready for writing. This means that if
* the save fails, the file system is untouched.
*
* @param pcToBeWritten the PlayerCharacter to write
* @param mode The character's game mode.
* @param campaigns The character's sources.
* @param outFile The file to write the character to.
*/
public void write(PlayerCharacter pcToBeWritten, GameMode mode, List<CampaignFacade> campaigns, File outFile)
{
final String pcgString;
pcgString = (new PCGVer2Creator(pcToBeWritten, mode, campaigns)).createPCGString();
// Do backup now that we have the character ready to save
createBackupForFile(outFile);
// Now save the character
BufferedWriter bw = null;
try
{
FileOutputStream out = new FileOutputStream(outFile);
bw = new BufferedWriter(new OutputStreamWriter(out, "UTF-8"));
bw.write(pcgString);
bw.flush();
pcToBeWritten.setDirty(false);
}
catch (IOException ioe)
{
Logging.errorPrint("Exception in PCGIOHandler::write", ioe);
}
finally
{
try
{
if (bw != null)
{
bw.close();
}
}
catch (IOException e)
{
Logging.errorPrint("Couldn't close file in PCGIOHandler.write",
e);
}
}
}
/*
* ###############################################################
* private helper methods
* ###############################################################
*/
private void sanityChecks(PlayerCharacter currentPC, PCGParser parser)
{
// Hit point sanity check
boolean fixMade = false;
resolveDuplicateEquipmentSets(currentPC);
// First make sure the "working" equipment list
// is in effect for all the bonuses it may add
currentPC.setCalcEquipmentList();
// make sure the bonuses from companions are applied
currentPC.setCalcFollowerBonus();
// pre-calculate all the bonuses
currentPC.calcActiveBonuses();
int iSides;
int iRoll;
final int oldHp = currentPC.hitPoints();
// Recalc the feat pool if required
if (parser.isCalcFeatPoolAfterLoad())
{
double baseFeatPool = parser.getBaseFeatPool();
double featPoolBonus = currentPC.getRemainingFeatPoints(true);
baseFeatPool -= featPoolBonus;
currentPC.setUserPoolBonus(AbilityCategory.FEAT, new BigDecimal(baseFeatPool));
}
for (CNAbility aFeat : currentPC.getPoolAbilities(AbilityCategory.FEAT, Nature.NORMAL))
{
if (aFeat.getAbility().getSafe(ObjectKey.MULTIPLE_ALLOWED) && !currentPC.hasAssociations(aFeat))
{
warnings.add("Multiple selection feat found with no selections ("
+ aFeat.getAbility().getDisplayName() + "). Correct on Feat tab.");
}
}
// Get templates - give it the biggest HD
// sk4p 11 Dec 2002
//PCTemplate aTemplate = null;
if (currentPC.hasClass())
{
for (PCClass pcClass : currentPC.getClassSet())
{
// Ignore if no levels
if (currentPC.getLevel(pcClass) < 1)
{
continue;
}
// Walk through the levels for this class
for (int i = 1; i <= currentPC.getLevel(pcClass); i++)
{
int baseSides = currentPC.getLevelHitDie(pcClass, i).getDie();
//TODO i-1 is strange see CODE-1925
PCClassLevel pcl = currentPC.getActiveClassLevel(pcClass, i - 1);
Integer hp = currentPC.getHP(pcl);
iRoll = hp == null ? 0 : hp;
iSides =
baseSides
+ (int) pcClass.getBonusTo("HD", "MAX", i,
currentPC);
if (iRoll > iSides)
{
currentPC.setHP(pcl, iSides);
fixMade = true;
}
}
}
}
if (fixMade)
{
final String message =
"Fixed illegal value in hit points. "
+ "Current character hit points: " + currentPC.hitPoints()
+ " not " + oldHp;
warnings.add(message);
}
// Sometimes another class, feat, item, whatever can affect
// what spells or whatever would have been available for a
// class, so this simply lets the level advancement routine
// take into account all the details known about a character
// now that the import is completed. The level isn't affected.
// merton_monk@yahoo.com 2/15/2002
//
for (PCClass pcClass : currentPC.getClassSet())
{
currentPC.calcActiveBonuses();
currentPC.calculateKnownSpellsForClassLevel(pcClass);
}
//
// need to calc the movement rates
currentPC.adjustMoveRates();
// re-calculate all the bonuses
currentPC.calcActiveBonuses();
// make sure we are not dirty
currentPC.setDirty(false);
}
/**
* Check all equipment sets to ensure there are no duplicate paths. Where a
* duplicate path is found, report it and try to move one non-container to
* a new path.
*
* @param currentPC The character being loaded.
*/
private void resolveDuplicateEquipmentSets(PlayerCharacter currentPC)
{
boolean anyMoved = false;
Iterable<EquipSet> equipSetList =
new ArrayList<>(currentPC.getDisplay().getEquipSet());
Map<String, EquipSet> idMap = new HashMap<>();
for (final EquipSet es : equipSetList)
{
String idPath = es.getIdPath();
if (idMap.containsKey(idPath))
{
EquipSet existingEs = idMap.get(idPath);
EquipSet esToBeMoved = chooseItemToBeMoved(existingEs, es);
if (esToBeMoved == null)
{
warnings.add(String.format(
"Found two equipment items equipped to the "
+ "path %s. Items were %s and %s.", idPath,
es.getItem(), existingEs.getItem()));
continue;
}
// change the item's location
currentPC.moveEquipSetToNewPath(esToBeMoved);
EquipSet esStaying = esToBeMoved == es ? existingEs : es;
// As we always move the non container, move any items it
// erroneously held to the item remaining in place
for (int j =
esToBeMoved.getItem().getContainedEquipmentCount() - 1; j >= 0; j--)
{
Equipment containedItem =
esToBeMoved.getItem().getContainedEquipment(j);
esToBeMoved.getItem().removeChild(currentPC, containedItem);
esStaying.getItem().insertChild(currentPC, containedItem);
}
Logging.log(Logging.WARNING, String.format(
"Moved item %s from path %s to %s as it "
+ "clashed with %s", esToBeMoved.getItem(), idPath,
esToBeMoved.getIdPath(),
esToBeMoved == es ? existingEs.getItem() : es.getItem()));
idMap.put(es.getIdPath(), es);
idMap.put(existingEs.getIdPath(), existingEs);
anyMoved = true;
}
else
{
idMap.put(idPath, es);
}
}
if (anyMoved)
{
warnings.add("Some equipment was moved as it was incorrectly stored."
+ " Please see the log for details.");
}
}
/**
* Pick one of two equipment sets sharing a path to be moved to a new path.
* Only non containers will be moved to avoid issues with contents.
* @param equipSet1 The first equipment set at a path.
* @param equipSet2 The second equipment set at a path.
* @return The equipment set that should be move,d or null if none are safe.
*/
private EquipSet chooseItemToBeMoved(EquipSet equipSet1, EquipSet equipSet2)
{
if (!equipSet2.getItem().isContainer())
{
return equipSet2;
}
if (!equipSet1.getItem().isContainer())
{
return equipSet1;
}
// Currently be really conservative
return null;
}
/**
* reads from the given partyFile and returns the list of
* character files for this party
* @param partyFile a .pcp party file
* @return a list of files containing the characters in this party
*/
public List<File> readCharacterFileList(File partyFile)
{
List<String> lines;
try
{
lines = FileUtils.readLines(partyFile, "UTF-8");
}
catch (IOException ex)
{
Logging.errorPrint("Exception in IOHandler::read when reading", ex);
return null;
}
if (lines.size() < 2)
{
Logging.errorPrint("Character files missing in " + partyFile.getAbsolutePath());
return null;
}
//Read and throw away version info. May change to actually use later
String versionInfo = lines.get(0);
//read character filename data
String charFiles = lines.get(1);
String[] files = charFiles.split(",");
List<File> fileList = new ArrayList<>();
for (final String fileName : files)
{
// try to find it in the party's directory
File characterFile = new File(partyFile.getParent(), fileName);
if (!characterFile.exists())
{
// try using the global pcg path
characterFile = new File(PCGenSettings.getPcgDir(), fileName);
}
if (!characterFile.exists())
{
// try it as an absolute path
characterFile = new File(fileName);
}
if (characterFile.exists())
{
fileList.add(characterFile);
}
else
{
Logging.errorPrint("Character file does not exist: " + fileName);
}
}
return fileList;
}
public static void write(File partyFile, List<File> characterFiles)
{
String versionLine = "VERSION:" + PCGenPropBundle.getVersionNumber();
String[] files = new String[characterFiles.size()];
for (int i = 0; i < files.length; i++)
{
files[i] = FileHelper.findRelativePath(partyFile, characterFiles.get(i));
}
String filesLine = StringUtils.join(files, ',');
try
{
FileUtils.writeLines(partyFile, "UTF-8", Arrays.asList(versionLine, filesLine));
}
catch (IOException ex)
{
Logging.errorPrint("Could not save the party file: " + partyFile.getAbsolutePath(), ex);
}
}
/**
* Read in the list of sources required for the character.
* @param pcgFile The character file
* @return The list of sources
*/
public SourceSelectionFacade readSources(File pcgFile)
{
InputStream in = null;
try
{
in = new FileInputStream(pcgFile);
return internalReadSources(in);
}
catch (IOException ex)
{
Logging.errorPrint("Exception in IOHandler::read when reading", ex);
}
finally
{
if (in != null)
{
try
{
in.close();
}
catch (IOException e)
{
Logging.errorPrint("Exception in IOHandler::readSources", e);
}
catch (NullPointerException e)
{
Logging.errorPrint(
"Could not create file inputStream IOHandler::readSources", e);
}
}
}
return null;
}
@Nullable
private SourceSelectionFacade internalReadSources(InputStream in)
{
// Read lines from file
final List<String> lines = readPcgLines(in);
// Verify it is ver2
boolean isPCGVersion2 = isPCGCersion2(lines);
final String[] pcgLines = lines.toArray(new String[lines.size()]);
if (isPCGVersion2)
{
//PlayerCharacter aPC = new PlayerCharacter();
final PCGParser parser = new PCGVer2Parser(null);
try
{
// Extract list of sources
return parser.parcePCGSourceOnly(pcgLines);
}
catch (PCGParseException pcgex)
{
errors.add(pcgex.getMessage() + Constants.LINE_SEPARATOR + "Method: "
+ pcgex.getMethod() + '\n' + "Line: " + pcgex.getLine());
}
warnings.addAll(parser.getWarnings());
}
else
{
errors.add("Cannot open PCG file");
}
return null;
}
}