/* * Copyright (c) 1998-2017 by Richard A. Wilkes. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, version 2.0. If a copy of the MPL was not distributed with * this file, You can obtain one at http://mozilla.org/MPL/2.0/. * * This Source Code Form is "Incompatible With Secondary Licenses", as * defined by the Mozilla Public License, version 2.0. */ package com.trollworks.gcs.character; import com.trollworks.gcs.advantage.Advantage; import com.trollworks.gcs.advantage.AdvantageColumn; import com.trollworks.gcs.equipment.Equipment; import com.trollworks.gcs.notes.Note; import com.trollworks.gcs.preferences.OutputPreferences; import com.trollworks.gcs.preferences.SheetPreferences; import com.trollworks.gcs.skill.Skill; import com.trollworks.gcs.skill.SkillColumn; import com.trollworks.gcs.skill.SkillDifficulty; import com.trollworks.gcs.spell.Spell; import com.trollworks.gcs.spell.SpellColumn; import com.trollworks.gcs.weapon.MeleeWeaponStats; import com.trollworks.gcs.weapon.RangedWeaponStats; import com.trollworks.gcs.weapon.WeaponDisplayRow; import com.trollworks.gcs.weapon.WeaponStats; import com.trollworks.gcs.widgets.outline.ListRow; import com.trollworks.toolkit.collections.FilteredIterator; import com.trollworks.toolkit.io.xml.XMLWriter; import com.trollworks.toolkit.ui.image.StdImage; import com.trollworks.toolkit.utility.FileType; import com.trollworks.toolkit.utility.PathUtils; import com.trollworks.toolkit.utility.text.Numbers; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.text.DateFormat; import java.text.MessageFormat; import java.util.Date; import java.util.function.Function; /** Provides text template output. */ @SuppressWarnings("nls") public class TextTemplate { private static String UNIDENTIFIED_KEY = "Unidentified key: '%s'"; private static final String CURRENT = "current"; private static final String ITEM = "ITEM"; private static final String KEY_ACCURACY = "ACCURACY"; private static final String KEY_ADVANTAGE_POINTS = "ADVANTAGE_POINTS"; private static final String KEY_ADVANTAGES_LOOP_END = "ADVANTAGES_LOOP_END"; private static final String KEY_ADVANTAGES_LOOP_START = "ADVANTAGES_LOOP_START"; private static final String KEY_ADVANTAGES_ONLY_LOOP_END = "ADVANTAGES_ONLY_LOOP_END"; private static final String KEY_ADVANTAGES_ONLY_LOOP_START = "ADVANTAGES_ONLY_LOOP_START"; private static final String KEY_AGE = "AGE"; private static final String KEY_ATTRIBUTE_POINTS = "ATTRIBUTE_POINTS"; private static final String KEY_BASIC_FP = "BASIC_FP"; private static final String KEY_BASIC_HP = "BASIC_HP"; private static final String KEY_BASIC_LIFT = "BASIC_LIFT"; private static final String KEY_BASIC_MOVE = "BASIC_MOVE"; private static final String KEY_BASIC_MOVE_POINTS = "BASIC_MOVE_POINTS"; private static final String KEY_BASIC_SPEED = "BASIC_SPEED"; private static final String KEY_BASIC_SPEED_POINTS = "BASIC_SPEED_POINTS"; private static final String KEY_BEST_CURRENT_BLOCK = "BEST_CURRENT_BLOCK"; private static final String KEY_BEST_CURRENT_PARRY = "BEST_CURRENT_PARRY"; private static final String KEY_BIRTHDAY = "BIRTHDAY"; private static final String KEY_BLOCK = "BLOCK"; private static final String KEY_BULK = "BULK"; private static final String KEY_CAMPAIGN = "CAMPAIGN"; private static final String KEY_CARRIED_VALUE = "CARRIED_VALUE"; private static final String KEY_CARRIED_WEIGHT = "CARRIED_WEIGHT"; private static final String KEY_CARRY_ON_BACK = "CARRY_ON_BACK"; private static final String KEY_CLASS = "CLASS"; private static final String KEY_COLLEGE = "COLLEGE"; private static final String KEY_COST = "COST"; private static final String KEY_COST_SUMMARY = "COST_SUMMARY"; private static final String KEY_CREATED_ON = "CREATED_ON"; private static final String KEY_CULTURAL_FAMILIARITIES_LOOP_END = "CULTURAL_FAMILIARITIES_LOOP_END"; private static final String KEY_CULTURAL_FAMILIARITIES_LOOP_START = "CULTURAL_FAMILIARITIES_LOOP_START"; private static final String KEY_CURRENT_DODGE = "CURRENT_DODGE"; private static final String KEY_CURRENT_MARKER = "CURRENT_MARKER"; private static final String KEY_DAMAGE = "DAMAGE"; private static final String KEY_UNMODIFIED_DAMAGE = "UNMODIFIED_DAMAGE"; private static final String KEY_DEAD = "DEAD"; private static final String KEY_DEATH_CHECK_1 = "DEATH_CHECK_1"; private static final String KEY_DEATH_CHECK_2 = "DEATH_CHECK_2"; private static final String KEY_DEATH_CHECK_3 = "DEATH_CHECK_3"; private static final String KEY_DEATH_CHECK_4 = "DEATH_CHECK_4"; private static final String KEY_DESCRIPTION = "DESCRIPTION"; private static final String KEY_DESCRIPTION_MODIFIER_NOTES = "DESCRIPTION_MODIFIER_NOTES"; private static final String KEY_DESCRIPTION_NOTES = "DESCRIPTION_NOTES"; private static final String KEY_DESCRIPTION_PRIMARY = "DESCRIPTION_PRIMARY"; private static final String KEY_DIFFICULTY = "DIFFICULTY"; private static final String KEY_DISADVANTAGE_POINTS = "DISADVANTAGE_POINTS"; private static final String KEY_DISADVANTAGES_LOOP_END = "DISADVANTAGES_LOOP_END"; private static final String KEY_DISADVANTAGES_LOOP_START = "DISADVANTAGES_LOOP_START"; private static final String KEY_DODGE = "DODGE"; private static final String KEY_DR = "DR"; private static final String KEY_DURATION = "DURATION"; private static final String KEY_DX = "DX"; private static final String KEY_DX_POINTS = "DX_POINTS"; private static final String KEY_EARNED_POINTS = "EARNED_POINTS"; private static final String KEY_ENCODING_OFF = "ENCODING_OFF"; private static final String KEY_ENCUMBRANCE_LOOP_END = "ENCUMBRANCE_LOOP_END"; private static final String KEY_ENCUMBRANCE_LOOP_START = "ENCUMBRANCE_LOOP_START"; private static final String KEY_EQUIPMENT_LOOP_END = "EQUIPMENT_LOOP_END"; private static final String KEY_EQUIPMENT_LOOP_START = "EQUIPMENT_LOOP_START"; private static final String KEY_EYES = "EYES"; private static final String KEY_FP = "FP"; private static final String KEY_FP_COLLAPSE = "FP_COLLAPSE"; private static final String KEY_FP_POINTS = "FP_POINTS"; private static final String KEY_FRIGHT_CHECK = "FRIGHT_CHECK"; private static final String KEY_GENDER = "GENDER"; private static final String KEY_GENERAL_DR = "GENERAL_DR"; private static final String KEY_HAIR = "HAIR"; private static final String KEY_HAND = "HAND"; private static final String KEY_HEARING = "HEARING"; private static final String KEY_HEIGHT = "HEIGHT"; private static final String KEY_HIT_LOCATION_LOOP_END = "HIT_LOCATION_LOOP_END"; private static final String KEY_HIT_LOCATION_LOOP_START = "HIT_LOCATION_LOOP_START"; private static final String KEY_HP = "HP"; private static final String KEY_HP_COLLAPSE = "HP_COLLAPSE"; private static final String KEY_HP_POINTS = "HP_POINTS"; private static final String KEY_HT = "HT"; private static final String KEY_HT_POINTS = "HT_POINTS"; private static final String KEY_ID = "ID"; private static final String KEY_IQ = "IQ"; private static final String KEY_IQ_POINTS = "IQ_POINTS"; private static final String KEY_LANGUAGES_LOOP_END = "LANGUAGES_LOOP_END"; private static final String KEY_LANGUAGES_LOOP_START = "LANGUAGES_LOOP_START"; private static final String KEY_LEVEL = "LEVEL"; private static final String KEY_MANA_CAST = "MANA_CAST"; private static final String KEY_MANA_MAINTAIN = "MANA_MAINTAIN"; private static final String KEY_MAX_LOAD = "MAX_LOAD"; private static final String KEY_MELEE_LOOP_END = "MELEE_LOOP_END"; private static final String KEY_MELEE_LOOP_START = "MELEE_LOOP_START"; private static final String KEY_MODIFIED_ON = "MODIFIED_ON"; private static final String KEY_MOVE = "MOVE"; private static final String KEY_NAME = "NAME"; private static final String KEY_NOTE = "NOTE"; private static final String KEY_NOTES = "NOTES"; private static final String KEY_NOTES_LOOP_END = "NOTES_LOOP_END"; private static final String KEY_NOTES_LOOP_START = "NOTES_LOOP_START"; private static final String KEY_ONE_HANDED_LIFT = "ONE_HANDED_LIFT"; private static final String KEY_PARRY = "PARRY"; private static final String KEY_PENALTY = "PENALTY"; private static final String KEY_PERCEPTION = "PERCEPTION"; private static final String KEY_PERCEPTION_POINTS = "PERCEPTION_POINTS"; private static final String KEY_PERKS_LOOP_END = "PERKS_LOOP_END"; private static final String KEY_PERKS_LOOP_START = "PERKS_LOOP_START"; private static final String KEY_PLAYER = "PLAYER"; private static final String KEY_POINTS = "POINTS"; private static final String KEY_PORTRAIT = "PORTRAIT"; private static final String KEY_PREFIX_DEPTH = "DEPTHx"; private static final String KEY_QTY = "QTY"; private static final String KEY_QUIRK_POINTS = "QUIRK_POINTS"; private static final String KEY_QUIRKS_LOOP_END = "QUIRKS_LOOP_END"; private static final String KEY_QUIRKS_LOOP_START = "QUIRKS_LOOP_START"; private static final String KEY_RACE = "RACE"; private static final String KEY_RACE_POINTS = "RACE_POINTS"; private static final String KEY_RANGE = "RANGE"; private static final String KEY_RANGED_LOOP_END = "RANGED_LOOP_END"; private static final String KEY_RANGED_LOOP_START = "RANGED_LOOP_START"; private static final String KEY_REACH = "REACH"; private static final String KEY_RECOIL = "RECOIL"; private static final String KEY_REELING = "REELING"; private static final String KEY_REF = "REF"; private static final String KEY_RELIGION = "RELIGION"; private static final String KEY_ROF = "ROF"; private static final String KEY_ROLL = "ROLL"; private static final String KEY_RSL = "RSL"; private static final String KEY_RUNNING_SHOVE = "RUNNING_SHOVE"; private static final String KEY_SATISFIED = "SATISFIED"; private static final String KEY_SHIFT_SLIGHTLY = "SHIFT_SLIGHTLY"; private static final String KEY_SHOTS = "SHOTS"; private static final String KEY_SHOVE = "SHOVE"; private static final String KEY_SIZE = "SIZE"; private static final String KEY_SKILL_POINTS = "SKILL_POINTS"; private static final String KEY_SKILLS_LOOP_END = "SKILLS_LOOP_END"; private static final String KEY_SKILLS_LOOP_START = "SKILLS_LOOP_START"; private static final String KEY_SKIN = "SKIN"; private static final String KEY_SL = "SL"; private static final String KEY_SPELL_POINTS = "SPELL_POINTS"; private static final String KEY_SPELLS_LOOP_END = "SPELLS_LOOP_END"; private static final String KEY_SPELLS_LOOP_START = "SPELLS_LOOP_START"; private static final String KEY_ST = "ST"; private static final String KEY_ST_POINTS = "ST_POINTS"; private static final String KEY_STATE = "STATE"; private static final String KEY_STYLE_INDENT_WARNING = "STYLE_INDENT_WARNING"; private static final String KEY_SUFFIX_PAREN = "_PAREN"; private static final String KEY_SWING = "SWING"; private static final String KEY_TASTE_SMELL = "TASTE_SMELL"; private static final String KEY_THRUST = "THRUST"; private static final String KEY_TIME_CAST = "TIME_CAST"; private static final String KEY_TIRED = "TIRED"; private static final String KEY_TITLE = "TITLE"; private static final String KEY_TL = "TL"; private static final String KEY_TOTAL_POINTS = "TOTAL_POINTS"; private static final String KEY_TOUCH = "TOUCH"; private static final String KEY_TWO_HANDED_LIFT = "TWO_HANDED_LIFT"; private static final String KEY_TYPE = "TYPE"; private static final String KEY_UNCONSCIOUS = "UNCONSCIOUS"; private static final String KEY_USAGE = "USAGE"; private static final String KEY_VISION = "VISION"; private static final String KEY_WEAPON_STRENGTH = "STRENGTH"; private static final String KEY_WEIGHT = "WEIGHT"; private static final String KEY_WEIGHT_SUMMARY = "WEIGHT_SUMMARY"; private static final String KEY_WHERE = "WHERE"; private static final String KEY_WILL = "WILL"; private static final String KEY_WILL_POINTS = "WILL_POINTS"; private CharacterSheet mSheet; private boolean mEncodeText = true; public static File resolveTextTemplate(File template) { if (template == null || !template.isFile() || !template.canRead()) { template = new File(OutputPreferences.getTextTemplate()); if (!template.isFile() || !template.canRead()) { template = new File(OutputPreferences.getDefaultTextTemplate()); } } return template; } public TextTemplate(CharacterSheet sheet) { mSheet = sheet; } /** * @param exportTo The file to save to. * @param template The template file to use. * @return <code>true</code> on success. */ public boolean export(File exportTo, File template) { try { char[] buffer = new char[1]; boolean lookForKeyMarker = true; StringBuilder keyBuffer = new StringBuilder(); template = resolveTextTemplate(template); try (BufferedReader in = new BufferedReader(new InputStreamReader(new FileInputStream(template)))) { try (BufferedWriter out = new BufferedWriter(new FileWriter(exportTo))) { while (in.read(buffer) != -1) { char ch = buffer[0]; if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; in.mark(1); } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); in.mark(1); } else { in.reset(); emitKey(in, out, keyBuffer.toString(), exportTo); keyBuffer.setLength(0); lookForKeyMarker = true; } } } if (keyBuffer.length() != 0) { emitKey(in, out, keyBuffer.toString(), exportTo); } } } return true; } catch (Exception exception) { return false; } } private void emitKey(BufferedReader in, BufferedWriter out, String key, File base) throws IOException { GURPSCharacter gurpsCharacter = mSheet.getCharacter(); Profile description = gurpsCharacter.getDescription(); switch (key) { case KEY_ENCODING_OFF: mEncodeText = false; break; case KEY_PORTRAIT: String fileName = PathUtils.enforceExtension(PathUtils.getLeafName(base.getName(), false), FileType.PNG_EXTENSION); StdImage.writePNG(new File(base.getParentFile(), fileName), description.getPortrait().getRetina(), 150); writeEncodedData(out, fileName); break; case KEY_NAME: writeEncodedText(out, description.getName()); break; case KEY_TITLE: writeEncodedText(out, description.getTitle()); break; case KEY_RELIGION: writeEncodedText(out, description.getReligion()); break; case KEY_PLAYER: writeEncodedText(out, description.getPlayerName()); break; case KEY_CAMPAIGN: writeEncodedText(out, description.getCampaign()); break; case KEY_CREATED_ON: Date date = new Date(gurpsCharacter.getCreatedOn()); writeEncodedText(out, DateFormat.getDateInstance(DateFormat.MEDIUM).format(date)); break; case KEY_MODIFIED_ON: writeEncodedText(out, gurpsCharacter.getLastModified()); break; case KEY_TOTAL_POINTS: writeEncodedText(out, Numbers.format(SheetPreferences.shouldIncludeUnspentPointsInTotalPointDisplay() ? gurpsCharacter.getTotalPoints() : gurpsCharacter.getSpentPoints())); break; case KEY_ATTRIBUTE_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getAttributePoints())); break; case KEY_ST_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getStrengthPoints())); break; case KEY_DX_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getDexterityPoints())); break; case KEY_IQ_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getIntelligencePoints())); break; case KEY_HT_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getHealthPoints())); break; case KEY_PERCEPTION_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getPerceptionPoints())); break; case KEY_WILL_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getWillPoints())); break; case KEY_FP_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getFatiguePointPoints())); break; case KEY_HP_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getHitPointPoints())); break; case KEY_BASIC_SPEED_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getBasicSpeedPoints())); break; case KEY_BASIC_MOVE_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getBasicMovePoints())); break; case KEY_ADVANTAGE_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getAdvantagePoints())); break; case KEY_DISADVANTAGE_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getDisadvantagePoints())); break; case KEY_QUIRK_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getQuirkPoints())); break; case KEY_SKILL_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getSkillPoints())); break; case KEY_SPELL_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getSpellPoints())); break; case KEY_RACE_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getRacePoints())); break; case KEY_EARNED_POINTS: writeEncodedText(out, Numbers.format(gurpsCharacter.getEarnedPoints())); break; case KEY_RACE: writeEncodedText(out, description.getRace()); break; case KEY_HEIGHT: writeEncodedText(out, description.getHeight().toString()); break; case KEY_HAIR: writeEncodedText(out, description.getHair()); break; case KEY_GENDER: writeEncodedText(out, description.getGender()); break; case KEY_WEIGHT: writeEncodedText(out, description.getWeight().toString()); break; case KEY_EYES: writeEncodedText(out, description.getEyeColor()); break; case KEY_AGE: writeEncodedText(out, Numbers.format(description.getAge())); break; case KEY_SIZE: writeEncodedText(out, Numbers.formatWithForcedSign(description.getSizeModifier())); break; case KEY_SKIN: writeEncodedText(out, description.getSkinColor()); break; case KEY_BIRTHDAY: writeEncodedText(out, description.getBirthday()); break; case KEY_TL: writeEncodedText(out, description.getTechLevel()); break; case KEY_HAND: writeEncodedText(out, description.getHandedness()); break; case KEY_ST: writeEncodedText(out, Numbers.format(gurpsCharacter.getStrength())); break; case KEY_DX: writeEncodedText(out, Numbers.format(gurpsCharacter.getDexterity())); break; case KEY_IQ: writeEncodedText(out, Numbers.format(gurpsCharacter.getIntelligence())); break; case KEY_HT: writeEncodedText(out, Numbers.format(gurpsCharacter.getHealth())); break; case KEY_WILL: writeEncodedText(out, Numbers.format(gurpsCharacter.getWill())); break; case KEY_FRIGHT_CHECK: writeEncodedText(out, Numbers.format(gurpsCharacter.getFrightCheck())); break; case KEY_BASIC_SPEED: writeEncodedText(out, Numbers.format(gurpsCharacter.getBasicSpeed())); break; case KEY_BASIC_MOVE: writeEncodedText(out, Numbers.format(gurpsCharacter.getBasicMove())); break; case KEY_PERCEPTION: writeEncodedText(out, Numbers.format(gurpsCharacter.getPerception())); break; case KEY_VISION: writeEncodedText(out, Numbers.format(gurpsCharacter.getVision())); break; case KEY_HEARING: writeEncodedText(out, Numbers.format(gurpsCharacter.getHearing())); break; case KEY_TASTE_SMELL: writeEncodedText(out, Numbers.format(gurpsCharacter.getTasteAndSmell())); break; case KEY_TOUCH: writeEncodedText(out, Numbers.format(gurpsCharacter.getTouch())); break; case KEY_THRUST: writeEncodedText(out, gurpsCharacter.getThrust().toString()); break; case KEY_SWING: writeEncodedText(out, gurpsCharacter.getSwing().toString()); break; case KEY_GENERAL_DR: writeEncodedText(out, Numbers.format(((Integer) gurpsCharacter.getValueForID(Armor.ID_TORSO_DR)).intValue())); break; case KEY_CURRENT_DODGE: writeEncodedText(out, Numbers.format(gurpsCharacter.getDodge(gurpsCharacter.getEncumbranceLevel()))); break; case KEY_BEST_CURRENT_PARRY: writeBestWeaponDefense(out, (weapon) -> weapon.getResolvedParry()); break; case KEY_BEST_CURRENT_BLOCK: writeBestWeaponDefense(out, (weapon) -> weapon.getResolvedBlock()); break; case KEY_FP: writeEncodedText(out, gurpsCharacter.getCurrentFatiguePoints()); break; case KEY_BASIC_FP: writeEncodedText(out, Numbers.format(gurpsCharacter.getFatiguePoints())); break; case KEY_TIRED: writeEncodedText(out, Numbers.format(gurpsCharacter.getTiredFatiguePoints())); break; case KEY_FP_COLLAPSE: writeEncodedText(out, Numbers.format(gurpsCharacter.getUnconsciousChecksFatiguePoints())); break; case KEY_UNCONSCIOUS: writeEncodedText(out, Numbers.format(gurpsCharacter.getUnconsciousFatiguePoints())); break; case KEY_HP: writeEncodedText(out, gurpsCharacter.getCurrentHitPoints()); break; case KEY_BASIC_HP: writeEncodedText(out, Numbers.format(gurpsCharacter.getHitPoints())); break; case KEY_REELING: writeEncodedText(out, Numbers.format(gurpsCharacter.getReelingHitPoints())); break; case KEY_HP_COLLAPSE: writeEncodedText(out, Numbers.format(gurpsCharacter.getUnconsciousChecksHitPoints())); break; case KEY_DEATH_CHECK_1: writeEncodedText(out, Numbers.format(gurpsCharacter.getDeathCheck1HitPoints())); break; case KEY_DEATH_CHECK_2: writeEncodedText(out, Numbers.format(gurpsCharacter.getDeathCheck2HitPoints())); break; case KEY_DEATH_CHECK_3: writeEncodedText(out, Numbers.format(gurpsCharacter.getDeathCheck3HitPoints())); break; case KEY_DEATH_CHECK_4: writeEncodedText(out, Numbers.format(gurpsCharacter.getDeathCheck4HitPoints())); break; case KEY_DEAD: writeEncodedText(out, Numbers.format(gurpsCharacter.getDeadHitPoints())); break; case KEY_BASIC_LIFT: writeEncodedText(out, gurpsCharacter.getBasicLift().toString()); break; case KEY_ONE_HANDED_LIFT: writeEncodedText(out, gurpsCharacter.getOneHandedLift().toString()); break; case KEY_TWO_HANDED_LIFT: writeEncodedText(out, gurpsCharacter.getTwoHandedLift().toString()); break; case KEY_SHOVE: writeEncodedText(out, gurpsCharacter.getShoveAndKnockOver().toString()); break; case KEY_RUNNING_SHOVE: writeEncodedText(out, gurpsCharacter.getRunningShoveAndKnockOver().toString()); break; case KEY_CARRY_ON_BACK: writeEncodedText(out, gurpsCharacter.getCarryOnBack().toString()); break; case KEY_SHIFT_SLIGHTLY: writeEncodedText(out, gurpsCharacter.getShiftSlightly().toString()); break; case KEY_CARRIED_WEIGHT: writeEncodedText(out, gurpsCharacter.getWeightCarried().toString()); break; case KEY_CARRIED_VALUE: writeEncodedText(out, "$" + Numbers.format(gurpsCharacter.getWealthCarried())); break; case KEY_NOTES: StringBuilder buffer = new StringBuilder(); for (Note note : gurpsCharacter.getNoteIterator()) { if (buffer.length() > 0) { buffer.append("\n\n"); } buffer.append(note.getDescription()); } writeEncodedText(out, buffer.toString()); break; default: if (key.startsWith(KEY_ENCUMBRANCE_LOOP_START)) { processEncumbranceLoop(out, extractUpToMarker(in, KEY_ENCUMBRANCE_LOOP_END)); } else if (key.startsWith(KEY_HIT_LOCATION_LOOP_START)) { processHitLocationLoop(out, extractUpToMarker(in, KEY_HIT_LOCATION_LOOP_END)); } else if (key.startsWith(KEY_ADVANTAGES_LOOP_START)) { processAdvantagesLoop(out, extractUpToMarker(in, KEY_ADVANTAGES_LOOP_END), AdvantagesLoopType.ALL); } else if (key.startsWith(KEY_ADVANTAGES_ONLY_LOOP_START)) { processAdvantagesLoop(out, extractUpToMarker(in, KEY_ADVANTAGES_ONLY_LOOP_END), AdvantagesLoopType.ADS); } else if (key.startsWith(KEY_DISADVANTAGES_LOOP_START)) { processAdvantagesLoop(out, extractUpToMarker(in, KEY_DISADVANTAGES_LOOP_END), AdvantagesLoopType.DISADS); } else if (key.startsWith(KEY_QUIRKS_LOOP_START)) { processAdvantagesLoop(out, extractUpToMarker(in, KEY_QUIRKS_LOOP_END), AdvantagesLoopType.QUIRKS); } else if (key.startsWith(KEY_PERKS_LOOP_START)) { processAdvantagesLoop(out, extractUpToMarker(in, KEY_PERKS_LOOP_END), AdvantagesLoopType.PERKS); } else if (key.startsWith(KEY_LANGUAGES_LOOP_START)) { processAdvantagesLoop(out, extractUpToMarker(in, KEY_LANGUAGES_LOOP_END), AdvantagesLoopType.LANGUAGES); } else if (key.startsWith(KEY_CULTURAL_FAMILIARITIES_LOOP_START)) { processAdvantagesLoop(out, extractUpToMarker(in, KEY_CULTURAL_FAMILIARITIES_LOOP_END), AdvantagesLoopType.CULTURAL_FAMILIARITIES); } else if (key.startsWith(KEY_SKILLS_LOOP_START)) { processSkillsLoop(out, extractUpToMarker(in, KEY_SKILLS_LOOP_END)); } else if (key.startsWith(KEY_SPELLS_LOOP_START)) { processSpellsLoop(out, extractUpToMarker(in, KEY_SPELLS_LOOP_END)); } else if (key.startsWith(KEY_MELEE_LOOP_START)) { processMeleeLoop(out, extractUpToMarker(in, KEY_MELEE_LOOP_END)); } else if (key.startsWith(KEY_RANGED_LOOP_START)) { processRangedLoop(out, extractUpToMarker(in, KEY_RANGED_LOOP_END)); } else if (key.startsWith(KEY_EQUIPMENT_LOOP_START)) { processEquipmentLoop(out, extractUpToMarker(in, KEY_EQUIPMENT_LOOP_END)); } else if (key.startsWith(KEY_NOTES_LOOP_START)) { processNotesLoop(out, extractUpToMarker(in, KEY_NOTES_LOOP_END)); } else { writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); } break; } } private void writeBestWeaponDefense(BufferedWriter out, Function<MeleeWeaponStats, String> resolver) throws IOException { String best = "-"; int bestValue = Integer.MIN_VALUE; for (WeaponDisplayRow row : new FilteredIterator<>(mSheet.getMeleeWeaponOutline().getModel().getRows(), WeaponDisplayRow.class)) { MeleeWeaponStats weapon = (MeleeWeaponStats) row.getWeapon(); String result = resolver.apply(weapon).trim(); if (result.length() > 0 && !"No".equals(result)) { int value = Numbers.extractInteger(result, 0, false); if (value > bestValue) { bestValue = value; best = result; } } } writeEncodedText(out, best); } private void writeEncodedData(BufferedWriter out, String text) throws IOException { out.write(mEncodeText ? XMLWriter.encodeData(text).replaceAll(" ", "%20") : text); } private void writeEncodedText(BufferedWriter out, String text) throws IOException { out.write(mEncodeText ? XMLWriter.encodeData(text).replaceAll(" ", "<br>").replaceAll("\"", """) : text); } private static String extractUpToMarker(BufferedReader in, String marker) throws IOException { char[] buffer = new char[1]; StringBuilder keyBuffer = new StringBuilder(); StringBuilder extraction = new StringBuilder(); boolean lookForKeyMarker = true; while (in.read(buffer) != -1) { char ch = buffer[0]; if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; in.mark(1); } else { extraction.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); in.mark(1); } else { String key = keyBuffer.toString(); in.reset(); if (key.equals(marker)) { return extraction.toString(); } extraction.append('@'); extraction.append(key); keyBuffer.setLength(0); lookForKeyMarker = true; } } } return extraction.toString(); } private void processEncumbranceLoop(BufferedWriter out, String contents) throws IOException { GURPSCharacter gurpsCharacter = mSheet.getCharacter(); int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; for (Encumbrance encumbrance : Encumbrance.values()) { for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; switch (key) { case KEY_CURRENT_MARKER: if (encumbrance == gurpsCharacter.getEncumbranceLevel()) { out.write(CURRENT); } break; case KEY_LEVEL: writeEncodedText(out, MessageFormat.format(encumbrance == gurpsCharacter.getEncumbranceLevel() ? EncumbrancePanel.CURRENT_ENCUMBRANCE_FORMAT : EncumbrancePanel.ENCUMBRANCE_FORMAT, encumbrance, Numbers.format(-encumbrance.getEncumbrancePenalty()))); break; case KEY_MAX_LOAD: writeEncodedText(out, gurpsCharacter.getMaximumCarry(encumbrance).toString()); break; case KEY_MOVE: writeEncodedText(out, Numbers.format(gurpsCharacter.getMove(encumbrance))); break; case KEY_DODGE: writeEncodedText(out, Numbers.format(gurpsCharacter.getDodge(encumbrance))); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } private void processHitLocationLoop(BufferedWriter out, String contents) throws IOException { GURPSCharacter gurpsCharacter = mSheet.getCharacter(); int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; HitLocationTable table = gurpsCharacter.getDescription().getHitLocationTable(); for (HitLocationTableEntry entry : table.getEntries()) { for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; switch (key) { case KEY_ROLL: writeEncodedText(out, entry.getRoll()); break; case KEY_WHERE: writeEncodedText(out, entry.getName()); break; case KEY_PENALTY: writeEncodedText(out, Numbers.format(entry.getHitPenalty())); break; case KEY_DR: writeEncodedText(out, Numbers.format(((Integer) gurpsCharacter.getValueForID(entry.getKey())).intValue())); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } private void processAdvantagesLoop(BufferedWriter out, String contents, AdvantagesLoopType loopType) throws IOException { int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; int counter = 0; for (Advantage advantage : mSheet.getCharacter().getAdvantagesIterator(false)) { if (loopType.shouldInclude(advantage)) { counter++; for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; if (!processStyleIndentWarning(key, out, advantage)) { if (!processDescription(key, out, advantage)) { switch (key) { case KEY_POINTS: writeEncodedText(out, AdvantageColumn.POINTS.getDataAsText(advantage)); break; case KEY_REF: writeEncodedText(out, AdvantageColumn.REFERENCE.getDataAsText(advantage)); break; case KEY_ID: writeEncodedText(out, Integer.toString(counter)); break; case KEY_TYPE: writeEncodedText(out, advantage.canHaveChildren() ? advantage.getContainerType().name() : ITEM); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } } } } private boolean processDescription(String key, BufferedWriter out, ListRow row) throws IOException { if (key.equals(KEY_DESCRIPTION)) { writeEncodedText(out, row.toString()); writeNote(out, row.getModifierNotes()); writeNote(out, row.getNotes()); } else if (key.equals(KEY_DESCRIPTION_PRIMARY)) { writeEncodedText(out, row.toString()); } else if (key.startsWith(KEY_DESCRIPTION_MODIFIER_NOTES)) { writeXMLTextWithOptionalParens(key, out, row.getModifierNotes()); } else if (key.startsWith(KEY_DESCRIPTION_NOTES)) { writeXMLTextWithOptionalParens(key, out, row.getNotes()); } else { return false; } return true; } private void writeXMLTextWithOptionalParens(String key, BufferedWriter out, String text) throws IOException { if (text.length() > 0) { boolean parenVersion = key.endsWith(KEY_SUFFIX_PAREN); if (parenVersion) { out.write(" ("); } writeEncodedText(out, text); if (parenVersion) { out.write(')'); } } } private void writeNote(BufferedWriter out, String notes) throws IOException { if (notes.length() > 0) { out.write("<div class=\"note\">"); writeEncodedText(out, notes); out.write("</div>"); } } private void processSkillsLoop(BufferedWriter out, String contents) throws IOException { int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; int counter = 0; for (Skill skill : mSheet.getCharacter().getSkillsIterator()) { counter++; for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; if (!processStyleIndentWarning(key, out, skill)) { if (!processDescription(key, out, skill)) { switch (key) { case KEY_SL: writeEncodedText(out, SkillColumn.LEVEL.getDataAsText(skill)); break; case KEY_RSL: writeEncodedText(out, SkillColumn.RELATIVE_LEVEL.getDataAsText(skill)); break; case KEY_DIFFICULTY: writeEncodedText(out, SkillColumn.DIFFICULTY.getDataAsText(skill)); break; case KEY_POINTS: writeEncodedText(out, SkillColumn.POINTS.getDataAsText(skill)); break; case KEY_REF: writeEncodedText(out, SkillColumn.REFERENCE.getDataAsText(skill)); break; case KEY_ID: writeEncodedText(out, Integer.toString(counter)); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } } } private static boolean processStyleIndentWarning(String key, BufferedWriter out, ListRow row) throws IOException { if (key.equals(KEY_STYLE_INDENT_WARNING)) { StringBuilder style = new StringBuilder(); int depth = row.getDepth(); if (depth > 0) { style.append(" style=\"padding-left: "); style.append(depth * 12); style.append("px;"); } if (!row.isSatisfied()) { if (style.length() == 0) { style.append(" style=\""); } style.append(" color: red;"); } if (style.length() > 0) { style.append("\" "); out.write(style.toString()); } } else if (key.startsWith(KEY_PREFIX_DEPTH)) { int amt = Numbers.extractInteger(key.substring(6), 1, false); out.write("" + amt * row.getDepth()); } else if (key.equals(KEY_SATISFIED)) { out.write(row.isSatisfied() ? "Y" : "N"); } else { return false; } return true; } private void processSpellsLoop(BufferedWriter out, String contents) throws IOException { int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; int counter = 0; for (Spell spell : mSheet.getCharacter().getSpellsIterator()) { counter++; for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; if (!processStyleIndentWarning(key, out, spell)) { if (!processDescription(key, out, spell)) { switch (key) { case KEY_CLASS: writeEncodedText(out, spell.getSpellClass()); break; case KEY_COLLEGE: writeEncodedText(out, spell.getCollege()); break; case KEY_MANA_CAST: writeEncodedText(out, spell.getCastingCost()); break; case KEY_MANA_MAINTAIN: writeEncodedText(out, spell.getMaintenance()); break; case KEY_TIME_CAST: writeEncodedText(out, spell.getCastingTime()); break; case KEY_DURATION: writeEncodedText(out, spell.getDuration()); break; case KEY_SL: writeEncodedText(out, SpellColumn.LEVEL.getDataAsText(spell)); break; case KEY_RSL: writeEncodedText(out, SpellColumn.RELATIVE_LEVEL.getDataAsText(spell)); break; case KEY_DIFFICULTY: writeEncodedText(out, (spell.isVeryHard() ? SkillDifficulty.VH : SkillDifficulty.H).toString()); break; case KEY_POINTS: writeEncodedText(out, SpellColumn.POINTS.getDataAsText(spell)); break; case KEY_REF: writeEncodedText(out, SpellColumn.REFERENCE.getDataAsText(spell)); break; case KEY_ID: writeEncodedText(out, Integer.toString(counter)); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } } } private void processMeleeLoop(BufferedWriter out, String contents) throws IOException { int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; int counter = 0; for (WeaponDisplayRow row : new FilteredIterator<>(mSheet.getMeleeWeaponOutline().getModel().getRows(), WeaponDisplayRow.class)) { counter++; MeleeWeaponStats weapon = (MeleeWeaponStats) row.getWeapon(); for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; if (!processDescription(key, out, weapon)) { switch (key) { case KEY_USAGE: writeEncodedText(out, weapon.getUsage()); break; case KEY_LEVEL: writeEncodedText(out, Numbers.format(weapon.getSkillLevel())); break; case KEY_PARRY: writeEncodedText(out, weapon.getResolvedParry()); break; case KEY_BLOCK: writeEncodedText(out, weapon.getResolvedBlock()); break; case KEY_DAMAGE: writeEncodedText(out, weapon.getResolvedDamage()); break; case KEY_UNMODIFIED_DAMAGE: writeEncodedText(out, weapon.getDamage()); break; case KEY_REACH: writeEncodedText(out, weapon.getReach()); break; case KEY_WEAPON_STRENGTH: writeEncodedText(out, weapon.getStrength()); break; case KEY_ID: writeEncodedText(out, Integer.toString(counter)); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } } private boolean processDescription(String key, BufferedWriter out, WeaponStats stats) throws IOException { if (key.equals(KEY_DESCRIPTION)) { writeEncodedText(out, stats.toString()); writeNote(out, stats.getNotes()); } else if (key.equals(KEY_DESCRIPTION_PRIMARY)) { writeEncodedText(out, stats.toString()); } else if (key.startsWith(KEY_DESCRIPTION_NOTES)) { writeXMLTextWithOptionalParens(key, out, stats.getNotes()); } else { return false; } return true; } private void processRangedLoop(BufferedWriter out, String contents) throws IOException { int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; int counter = 0; for (WeaponDisplayRow row : new FilteredIterator<>(mSheet.getRangedWeaponOutline().getModel().getRows(), WeaponDisplayRow.class)) { counter++; RangedWeaponStats weapon = (RangedWeaponStats) row.getWeapon(); for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; if (!processDescription(key, out, weapon)) { switch (key) { case KEY_USAGE: writeEncodedText(out, weapon.getUsage()); break; case KEY_LEVEL: writeEncodedText(out, Numbers.format(weapon.getSkillLevel())); break; case KEY_ACCURACY: writeEncodedText(out, weapon.getAccuracy()); break; case KEY_DAMAGE: writeEncodedText(out, weapon.getResolvedDamage()); break; case KEY_UNMODIFIED_DAMAGE: writeEncodedText(out, weapon.getDamage()); break; case KEY_RANGE: writeEncodedText(out, weapon.getResolvedRange()); break; case KEY_ROF: writeEncodedText(out, weapon.getRateOfFire()); break; case KEY_SHOTS: writeEncodedText(out, weapon.getShots()); break; case KEY_BULK: writeEncodedText(out, weapon.getBulk()); break; case KEY_RECOIL: writeEncodedText(out, weapon.getRecoil()); break; case KEY_WEAPON_STRENGTH: writeEncodedText(out, weapon.getStrength()); break; case KEY_ID: writeEncodedText(out, Integer.toString(counter)); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } } private void processEquipmentLoop(BufferedWriter out, String contents) throws IOException { int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; int counter = 0; for (Equipment equipment : mSheet.getCharacter().getEquipmentIterator()) { counter++; for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; if (!processStyleIndentWarning(key, out, equipment)) { if (!processDescription(key, out, equipment)) { switch (key) { case KEY_STATE: out.write(equipment.getState().toShortName()); break; case KEY_QTY: writeEncodedText(out, Numbers.format(equipment.getQuantity())); break; case KEY_COST: writeEncodedText(out, Numbers.format(equipment.getValue())); break; case KEY_WEIGHT: writeEncodedText(out, equipment.getWeight().toString()); break; case KEY_COST_SUMMARY: writeEncodedText(out, Numbers.format(equipment.getExtendedValue())); break; case KEY_WEIGHT_SUMMARY: writeEncodedText(out, equipment.getExtendedWeight().toString()); break; case KEY_REF: writeEncodedText(out, equipment.getReference()); break; case KEY_ID: writeEncodedText(out, Integer.toString(counter)); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } } } private void processNotesLoop(BufferedWriter out, String contents) throws IOException { int length = contents.length(); StringBuilder keyBuffer = new StringBuilder(); boolean lookForKeyMarker = true; int counter = 0; for (Note note : mSheet.getCharacter().getNoteIterator()) { counter++; for (int i = 0; i < length; i++) { char ch = contents.charAt(i); if (lookForKeyMarker) { if (ch == '@') { lookForKeyMarker = false; } else { out.append(ch); } } else { if (ch == '_' || Character.isLetterOrDigit(ch)) { keyBuffer.append(ch); } else { String key = keyBuffer.toString(); i--; keyBuffer.setLength(0); lookForKeyMarker = true; if (!processStyleIndentWarning(key, out, note)) { switch (key) { case KEY_NOTE: writeEncodedText(out, note.getDescription()); break; case KEY_ID: writeEncodedText(out, Integer.toString(counter)); break; default: writeEncodedText(out, String.format(UNIDENTIFIED_KEY, key)); break; } } } } } } } private enum AdvantagesLoopType { ALL { @Override public boolean shouldInclude(Advantage advantage) { return true; } }, ADS { @Override public boolean shouldInclude(Advantage advantage) { return advantage.getAdjustedPoints() > 1; } }, DISADS { @Override public boolean shouldInclude(Advantage advantage) { return advantage.getAdjustedPoints() < -1; } }, PERKS { @Override public boolean shouldInclude(Advantage advantage) { return advantage.getAdjustedPoints() == 1; } }, QUIRKS { @Override public boolean shouldInclude(Advantage advantage) { return advantage.getAdjustedPoints() == -1; } }, LANGUAGES { @Override public boolean shouldInclude(Advantage advantage) { return advantage.getCategories().contains("Language"); } }, CULTURAL_FAMILIARITIES { @Override public boolean shouldInclude(Advantage advantage) { return advantage.getName().startsWith("Cultural Familiarity ("); } }; public abstract boolean shouldInclude(Advantage advantage); } }