/* * 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; } }